diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-09-07 01:15:44 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-09-07 01:15:44 +0000 |
commit | 4f883d02ffa29881b6a439d17540374838469702 (patch) | |
tree | 9e8035c4d2478e788183dff96c4861e8594a6097 | |
parent | 724a4990bf96018acc8ea8be7ce03f9bf2230183 (diff) | |
parent | b07ae071ca91900fc506ed287803a7df8b07a5c6 (diff) | |
download | robolectric-android14-d2-release.tar.gz |
Snap for 10771014 from b07ae071ca91900fc506ed287803a7df8b07a5c6 to udc-d2-releaseandroid-14.0.0_r45android-14.0.0_r44android-14.0.0_r43android-14.0.0_r42android-14.0.0_r41android-14.0.0_r40android-14.0.0_r39android-14.0.0_r38android14-d2-s5-releaseandroid14-d2-s4-releaseandroid14-d2-s3-releaseandroid14-d2-s2-releaseandroid14-d2-s1-releaseandroid14-d2-release
Change-Id: Ida5ef5d7ab7f84a235b8e2ea185de24f5072b4fb
198 files changed, 6412 insertions, 906 deletions
diff --git a/.github/workflows/check_code_formatting.yml b/.github/workflows/check_code_formatting.yml index e99374582..205901db6 100644 --- a/.github/workflows/check_code_formatting.yml +++ b/.github/workflows/check_code_formatting.yml @@ -16,7 +16,7 @@ permissions: jobs: check_code_formatting: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml index 9762b9e51..2494a8355 100644 --- a/.github/workflows/gradle_tasks_validation.yml +++ b/.github/workflows/gradle_tasks_validation.yml @@ -16,7 +16,7 @@ permissions: jobs: run_checkForApiChanges: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -33,7 +33,7 @@ jobs: run: ./gradlew checkForApiChanges run_aggregateDocs: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -49,8 +49,25 @@ jobs: - name: Run aggregateDocs run: ./gradlew clean aggregateDocs + run_javadocJar: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 11 + + - uses: gradle/gradle-build-action@v2 + + - name: Run javadocJar + run: ./gradlew clean javadocJar + run_instrumentAll: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/gradle_wrapper_validation.yml b/.github/workflows/gradle_wrapper_validation.yml index b7e100892..04388fcec 100644 --- a/.github/workflows/gradle_wrapper_validation.yml +++ b/.github/workflows/gradle_wrapper_validation.yml @@ -17,7 +17,7 @@ permissions: jobs: validation: name: Validation - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/graphics_tests.yml b/.github/workflows/graphics_tests.yml index e0d385a16..f2d27c84a 100644 --- a/.github/workflows/graphics_tests.yml +++ b/.github/workflows/graphics_tests.yml @@ -16,7 +16,10 @@ permissions: jobs: graphics_tests: - runs-on: self-hosted + strategy: + matrix: + device: [ macos-12, ubuntu-22.04, self-hosted ] + runs-on: ${{ matrix.device }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b47f37522..7d6c33f88 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ env: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -38,7 +38,7 @@ jobs: ./gradlew clean assemble testClasses --parallel --stacktrace --no-watch-fs unit-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: build strategy: fail-fast: false @@ -149,7 +149,7 @@ jobs: **/build/outputs/*/connected/* publish-to-snapshots: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: SONATYPE_LOGIN: ${{ secrets.SONATYPE_LOGIN }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} diff --git a/.github/workflows/validate_commit_message.yml b/.github/workflows/validate_commit_message.yml new file mode 100644 index 000000000..1851822bd --- /dev/null +++ b/.github/workflows/validate_commit_message.yml @@ -0,0 +1,42 @@ +name: Validate commit message + +on: + pull_request: + branches: [ master, google ] + +permissions: + contents: read + +jobs: + validate_commit_message: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Validate commit title + run: | + # Check that the commit title isn't excessively long. + commit_title="$(git log -1 --pretty=format:'%s')" + if [ "${#commit_title}" -gt 120 ]; then + echo "Error: The title of commit is too long" + exit 1 + fi + + lowercase_title="$(echo $commit_title | awk '{print tolower($0)}')" + # Check that the commit title isn't 'internal' (ignore case) + if [ "$lowercase_title" = "internal" ]; then + echo "Error: '$commit_title' is not a valid commit title" + exit 1 + fi + + - name: Validate commit body + run: | + # Check that the commit has a body + commit_body="$(git log -1 --pretty=format:'%b' | grep -v 'PiperOrigin-RevId')" + if [ -z "$commit_body" ]; then + echo "Error: The commit message should have a descriptive body" + exit 1 + fi diff --git a/Android.bp b/Android.bp index 135e7c151..a3e54dced 100644 --- a/Android.bp +++ b/Android.bp @@ -16,6 +16,8 @@ package { default_visibility: [ "//external/robolectric:__subpackages__", "//test/robolectric-extensions:__subpackages__", + "//frameworks/base/packages/SettingsLib/tests/robotests:__subpackages__", + "//packages/apps/Settings/tests/robotests:__subpackages__" ], default_applicable_licenses: ["external_robolectric_license"], } @@ -60,8 +62,9 @@ robolectric_build_props { name: "robolectric_build_props_upstream", } -java_genrule_host { +java_genrule { name: "robolectric_framework_res_upstream", + host_supported: true, tools: ["zip2zip"], srcs: [":framework-res"], out: ["robolectric_framework_res_upstream.jar"], @@ -118,46 +121,16 @@ java_library_host { ":__subpackages__", "//prebuilts/misc/common/robolectric", "//test/robolectric-extensions:__subpackages__", + "//frameworks/base/packages/SettingsLib/tests/robotests:__subpackages__", ], } -//############################################# -// Assemble Robolectric_all -//############################################# - -// This is a hack and should be removed with proper resource merging a la maven-shaded-plugin -// -// In order to use AndroidXTest APIs in Robolectric (e.g. ActivityScenario), it -// is necessary to define the service metadata at the top-level. The classes -// themselves (e.g. LocalUiController) do not use `@AutoService` at the moment. -// When they are migrated to `@AutoService` the AndroidXTest service metadata -// can be removed. -java_genrule_host { - name: "robolectric_meta_service_file", - out: ["robolectric_meta_service_file.jar"], - tools: ["soong_zip"], - cmd: "mkdir -p $(genDir)/META-INF/services/ && " + - "echo -e 'org.robolectric.Shadows\norg.robolectric.shadows.httpclient.Shadows\norg.robolectric.shadows.multidex.Shadows' > " + - "$(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider &&" + - "echo org.robolectric.android.internal.LocalUiController > " + - "$(genDir)/META-INF/services/androidx.test.platform.ui.UiController &&" + - "echo org.robolectric.android.internal.LocalActivityInvoker > " + - "$(genDir)/META-INF/services/androidx.test.internal.platform.app.ActivityInvoker &&" + - "echo org.robolectric.android.internal.LocalPermissionGranter > " + - "$(genDir)/META-INF/services/androidx.test.internal.platform.content.PermissionGranter &&" + - "echo org.robolectric.android.internal.NoOpThreadChecker > " + - "$(genDir)/META-INF/services/androidx.test.internal.platform.ThreadChecker &&" + - "echo org.robolectric.android.internal.LocalControlledLooper > " + - "$(genDir)/META-INF/services/androidx.test.internal.platform.os.ControlledLooper &&" + - "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)/META-INF/services/", -} java_library_host { name: "Robolectric_all_upstream", static_libs: [ "Robolectric-aosp-plugins", - "robolectric_meta_service_file", "Robolectric_shadows_httpclient_upstream", "Robolectric_shadows_framework_upstream", "Robolectric_shadows_multidex_upstream", @@ -209,12 +182,13 @@ java_host_for_device { "//external/mobile-data-download/javatests:__pkg__", "//frameworks/base/services/robotests:__pkg__", "//frameworks/base/services/robotests/backup:__pkg__", - "//frameworks/base/packages/SettingsLib/tests/robotests:__pkg__", + "//frameworks/base/packages/SettingsLib/tests/robotests:__subpackages__", "//frameworks/base/packages/SystemUI:__pkg__", "//frameworks/opt/car/setupwizard/library/main/tests/robotests:__pkg__", "//frameworks/opt/localepicker/tests:__pkg__", "//frameworks/opt/wear/signaldetector/robotests:__pkg__", "//frameworks/opt/wear/robotests:__pkg__", + "//packages/modules/Bluetooth/service:__pkg__", "//packages/modules/Connectivity/nearby/tests/multidevices/clients/test_support/snippet_helper/tests:__pkg__", "//packages/modules/Connectivity/nearby/tests/robotests:__pkg__", "//packages/modules/DeviceLock/DeviceLockController/tests/robolectric:__pkg__", diff --git a/METADATA b/METADATA new file mode 100644 index 000000000..43c94c633 --- /dev/null +++ b/METADATA @@ -0,0 +1,15 @@ +name: "robolectric" +description: "Android Unit Testing Framework" +third_party { + url { + type: GIT + value: "https://github.com/robolectric/robolectric" + } + version: "68ec9953ea0ac7c47588db540145a728a1f00ba8" + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 5 + day: 24 + } +} @@ -79,3 +79,17 @@ Run compatibility test suites on opening Emulator: ./gradlew connectedCheck +### Using Snapshots + +If you would like to live on the bleeding edge, you can try running against a snapshot build. Keep in mind that snapshots represent the most recent changes on master and may contain bugs. + +#### build.gradle: + +```groovy +repositories { + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } +} +dependencies { + testImplementation "org.robolectric:robolectric:4.11-SNAPSHOT" +} +``` diff --git a/annotations/src/main/java/org/robolectric/annotation/LooperMode.java b/annotations/src/main/java/org/robolectric/annotation/LooperMode.java index 264d6cc9d..5849daab5 100644 --- a/annotations/src/main/java/org/robolectric/annotation/LooperMode.java +++ b/annotations/src/main/java/org/robolectric/annotation/LooperMode.java @@ -118,6 +118,20 @@ public @interface LooperMode { PAUSED, /** + * A mode that simulates an android instrumentation test threading model, which has a separate + * test thread distinct from the main looper thread. + * + * <p>Otherwise it is quite similar to PAUSED mode. The clock time is still fixed, and you can + * use shadowLooper methods to pause, unpause, and wait for any looper to be idle. + * + * <p>It is recommended to use this mode in tests that mostly use androidx.test APIs, which will + * support being called directly on the main thread or on the test thread. Most org.robolectric + * APIs that interact with the android UI (e.g. ActivityController) will raise an exception if + * called off the main thread. + */ + INSTRUMENTATION_TEST, + + /** * Currently not supported. * * <p>In future, will have free running threads with an automatically increasing clock. diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy index bcc79e34a..d65f71cba 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/AndroidProjectConfigPlugin.groovy @@ -28,6 +28,19 @@ public class AndroidProjectConfigPlugin implements Plugin<Project> { .findAll { k,v -> k.startsWith("robolectric.") } .collect { k,v -> "-D$k=$v" } jvmArgs = forwardedSystemProperties + jvmArgs += [ + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.net=ALL-UNNAMED', + '--add-opens=java.base/java.security=ALL-UNNAMED', + '--add-opens=java.base/java.text=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.desktop/java.awt.font=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + ] doFirst { if (!forwardedSystemProperties.isEmpty()) { diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy index deb97c994..adffa3810 100644 --- a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy @@ -71,6 +71,19 @@ class RoboJavaModulePlugin implements Plugin<Project> { .findAll { k,v -> k.startsWith("robolectric.") } .collect { k,v -> "-D$k=$v" } jvmArgs = forwardedSystemProperties + jvmArgs += [ + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.net=ALL-UNNAMED', + '--add-opens=java.base/java.security=ALL-UNNAMED', + '--add-opens=java.base/java.text=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.desktop/java.awt.font=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + ] doFirst { if (!forwardedSystemProperties.isEmpty()) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b43dfa1f..46bd35ef9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ android-gradle = "7.4.2" conscrypt = "2.5.2" # https://github.com/bcgit/bc-java/tags -bouncycastle = "1.73" +bouncycastle = "1.76" # https://github.com/findbugsproject/findbugs/tags findbugs-jsr305 = "3.0.2" @@ -21,17 +21,20 @@ hamcrest = "2.0.0.0" aggregate-javadocs-gradle = "3.0.1" # https://github.com/google/error-prone/releases -error-prone = "2.19.1" +error-prone = "2.20.0" error-prone-javac = "9+181-r4173-1" # https://github.com/tbroyer/gradle-errorprone-plugin/releases error-prone-gradle = "3.1.0" # https://kotlinlang.org/docs/releases.html#release-details -kotlin = "1.8.10" +kotlin = "1.9.0" + +# https://github.com/Kotlin/kotlinx.coroutines/releases/ +kotlinx-coroutines = '1.7.3' # https://github.com/diffplug/spotless/blob/main/CHANGES.md -spotless-gradle = "6.18.0" +spotless-gradle = "6.20.0" # https://hc.apache.org/news.html apache-http-core = "4.0.1" @@ -41,23 +44,23 @@ apache-http-client = "4.0.3" asm = "9.5" # https://github.com/google/auto/releases -auto-common = "1.2.1" -auto-service = "1.0.1" -auto-value = "1.10.1" +auto-common = "1.2.2" +auto-service = "1.1.1" +auto-value = "1.10.2" compile-testing = "0.21.0" # https://github.com/google/guava/releases -guava-jre = "31.1-jre" +guava-jre = "32.0.1-jre" # https://github.com/google/gson/releases gson = "2.10.1" # https://github.com/google/truth/releases -truth = "1.1.3" +truth = "1.1.5" # https://github.com/unicode-org/icu/releases -icu4j = "73.1" +icu4j = "73.2" jacoco = "0.8.10" @@ -73,7 +76,7 @@ jetbrains-annotations = "24.0.1" junit4 = "4.13.2" # https://github.com/google/libphonenumber/releases -libphonenumber = "8.13.11" +libphonenumber = "8.13.17" # https://github.com/mockito/mockito/releases mockito = "4.11.0" @@ -92,11 +95,12 @@ sqlite4java = "1.0.392" # https://developer.android.com/jetpack/androidx/versions androidx-annotation = "1.3.0" androidx-appcompat = "1.6.1" +androidx-biometric = "1.1.0" androidx-constraintlayout = "2.1.4" androidx-core = "1.10.1" -androidx-fragment = "1.5.7" +androidx-fragment = "1.6.1" androidx-multidex = "2.0.1" -androidx-window = "1.0.0" +androidx-window = "1.1.0" # https://github.com/android/android-test/tags androidx-test-annotation = "1.0.1" @@ -119,6 +123,7 @@ kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version. spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless-gradle" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines"} auto-common = { module = "com.google.auto:auto-common", version.ref = "auto-common" } auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "auto-service" } @@ -195,6 +200,7 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "androidx-biometric" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } androidx-fragment = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment" } diff --git a/integration_tests/androidx/build.gradle b/integration_tests/androidx/build.gradle index 10cc8c650..2eac30a44 100644 --- a/integration_tests/androidx/build.gradle +++ b/integration_tests/androidx/build.gradle @@ -26,6 +26,7 @@ android { } dependencies { + implementation libs.kotlinx.coroutines.android implementation libs.androidx.appcompat implementation libs.androidx.window diff --git a/integration_tests/androidx_test/build.gradle b/integration_tests/androidx_test/build.gradle index d07ef2ed6..e7dfe9304 100644 --- a/integration_tests/androidx_test/build.gradle +++ b/integration_tests/androidx_test/build.gradle @@ -55,6 +55,7 @@ dependencies { testImplementation libs.androidx.test.espresso.core testImplementation libs.androidx.test.ext.truth testImplementation libs.androidx.test.core + testImplementation libs.androidx.biometric testImplementation libs.androidx.fragment testImplementation libs.androidx.fragment.testing testImplementation libs.androidx.test.ext.junit diff --git a/integration_tests/androidx_test/src/test/AndroidManifest.xml b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml index e79dbdb80..7f2d38666 100644 --- a/integration_tests/androidx_test/src/test/AndroidManifest.xml +++ b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityScenario.xml @@ -9,16 +9,6 @@ <application> <activity - android:name="org.robolectric.integrationtests.axt.ActivityTestRuleTest$TranscriptActivity" - android:exported="true"/> - <activity - android:name="org.robolectric.integrationtests.axt.EspressoActivity" - android:exported="true"/> - <activity - android:name="org.robolectric.integrationtests.axt.EspressoScrollingActivity" - android:exported="true"/> - - <activity android:name="org.robolectric.integrationtests.axt.ActivityScenarioTest$LifecycleOwnerActivity" android:exported="true"/> <activity @@ -27,9 +17,6 @@ <activity-alias android:name="org.robolectric.integrationtests.axt.ActivityScenarioTestAlias" android:targetActivity="org.robolectric.integrationtests.axt.ActivityScenarioTest$TranscriptActivity" /> - <activity - android:name="org.robolectric.integrationtests.axt.IntentsTest$ResultCapturingActivity" - android:exported = "true"/> </application> <instrumentation diff --git a/integration_tests/androidx_test/src/test/AndroidManifest-ActivityTestRule.xml b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityTestRule.xml new file mode 100644 index 000000000..a71754e6b --- /dev/null +++ b/integration_tests/androidx_test/src/test/AndroidManifest-ActivityTestRule.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="org.robolectric.integrationtests.axt"> + + <uses-sdk + android:minSdkVersion="14" + android:targetSdkVersion="27"/> + + <application> + <activity + android:name="org.robolectric.integrationtests.axt.ActivityTestRuleTest$TranscriptActivity" + android:exported="true"/> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="org.robolectric.integration.axt"/> + +</manifest> diff --git a/integration_tests/androidx_test/src/test/AndroidManifest-Intents.xml b/integration_tests/androidx_test/src/test/AndroidManifest-Intents.xml new file mode 100644 index 000000000..86f9e36cf --- /dev/null +++ b/integration_tests/androidx_test/src/test/AndroidManifest-Intents.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="org.robolectric.integrationtests.axt"> + + <uses-sdk + android:minSdkVersion="14" + android:targetSdkVersion="27"/> + + <application> + <activity + android:name="org.robolectric.integrationtests.axt.IntentsTest$ResultCapturingActivity" + android:exported = "true"/> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="org.robolectric.integration.axt"/> + +</manifest> diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java index acbd67a5f..72031be74 100644 --- a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java +++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java @@ -2,6 +2,7 @@ package org.robolectric.integrationtests.axt; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.app.Activity; import android.app.UiAutomation; @@ -106,18 +107,19 @@ public class ActivityScenarioTest { @Test public void launch_callbackSequence() { - ActivityScenario<TranscriptActivity> activityScenario = - ActivityScenario.launch(TranscriptActivity.class); + try (ActivityScenario<TranscriptActivity> activityScenario = + ActivityScenario.launch(TranscriptActivity.class)) { assertThat(activityScenario).isNotNull(); assertThat(callbacks) .containsExactly( "onCreate", "onStart", "onPostCreate", "onResume", "onWindowFocusChanged true"); + } } @Test public void launch_pauseAndResume_callbackSequence() { - ActivityScenario<TranscriptActivity> activityScenario = - ActivityScenario.launch(TranscriptActivity.class); + try (ActivityScenario<TranscriptActivity> activityScenario = + ActivityScenario.launch(TranscriptActivity.class)) { assertThat(activityScenario).isNotNull(); activityScenario.moveToState(State.STARTED); activityScenario.moveToState(State.RESUMED); @@ -125,12 +127,13 @@ public class ActivityScenarioTest { .containsExactly( "onCreate", "onStart", "onPostCreate", "onResume", "onWindowFocusChanged true", "onPause", "onResume"); + } } @Test public void launch_stopAndResume_callbackSequence() { - ActivityScenario<TranscriptActivity> activityScenario = - ActivityScenario.launch(TranscriptActivity.class); + try (ActivityScenario<TranscriptActivity> activityScenario = + ActivityScenario.launch(TranscriptActivity.class)) { assertThat(activityScenario).isNotNull(); activityScenario.moveToState(State.CREATED); activityScenario.moveToState(State.RESUMED); @@ -146,16 +149,17 @@ public class ActivityScenarioTest { "onRestart", "onStart", "onResume"); + } } @Test public void launchAlias_createTargetAndCallbackSequence() { Context context = ApplicationProvider.getApplicationContext(); - ActivityScenario<Activity> activityScenario = + try (ActivityScenario<Activity> activityScenario = ActivityScenario.launch( new Intent() .setClassName( - context, "org.robolectric.integrationtests.axt.ActivityScenarioTestAlias")); + context, "org.robolectric.integrationtests.axt.ActivityScenarioTestAlias"))) { assertThat(activityScenario).isNotNull(); activityScenario.onActivity( @@ -163,12 +167,13 @@ public class ActivityScenarioTest { assertThat(callbacks) .containsExactly( "onCreate", "onStart", "onPostCreate", "onResume", "onWindowFocusChanged true"); + } } @Test public void launch_lifecycleOwnerActivity() { - ActivityScenario<LifecycleOwnerActivity> activityScenario = - ActivityScenario.launch(LifecycleOwnerActivity.class); + try (ActivityScenario<LifecycleOwnerActivity> activityScenario = + ActivityScenario.launch(LifecycleOwnerActivity.class)) { assertThat(activityScenario).isNotNull(); activityScenario.onActivity( activity -> assertThat(activity.getLifecycle().getCurrentState()).isEqualTo(State.RESUMED)); @@ -178,14 +183,15 @@ public class ActivityScenarioTest { activityScenario.moveToState(State.CREATED); activityScenario.onActivity( activity -> assertThat(activity.getLifecycle().getCurrentState()).isEqualTo(State.CREATED)); + } } @Test public void recreate_retainFragmentHostingActivity() { Fragment fragment = new Fragment(); fragment.setRetainInstance(true); - ActivityScenario<LifecycleOwnerActivity> activityScenario = - ActivityScenario.launch(LifecycleOwnerActivity.class); + try (ActivityScenario<LifecycleOwnerActivity> activityScenario = + ActivityScenario.launch(LifecycleOwnerActivity.class)) { assertThat(activityScenario).isNotNull(); activityScenario.onActivity( activity -> { @@ -202,14 +208,15 @@ public class ActivityScenarioTest { activity -> assertThat(activity.getSupportFragmentManager().findFragmentById(android.R.id.content)) .isSameInstanceAs(fragment)); + } } @Test public void recreate_nonRetainFragmentHostingActivity() { Fragment fragment = new Fragment(); fragment.setRetainInstance(false); - ActivityScenario<LifecycleOwnerActivity> activityScenario = - ActivityScenario.launch(LifecycleOwnerActivity.class); + try (ActivityScenario<LifecycleOwnerActivity> activityScenario = + ActivityScenario.launch(LifecycleOwnerActivity.class)) { assertThat(activityScenario).isNotNull(); activityScenario.onActivity( activity -> { @@ -226,6 +233,7 @@ public class ActivityScenarioTest { activity -> assertThat(activity.getSupportFragmentManager().findFragmentById(android.R.id.content)) .isNotSameInstanceAs(fragment)); + } } @Test @@ -242,13 +250,14 @@ public class ActivityScenarioTest { @Test public void setRotation_recreatesActivity() { UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); - try (ActivityScenario<?> scenario = ActivityScenario.launch(TranscriptActivity.class)) { + try (ActivityScenario<TranscriptActivity> activityScenario = + ActivityScenario.launch(TranscriptActivity.class)) { AtomicReference<Activity> originalActivity = new AtomicReference<>(); - scenario.onActivity(originalActivity::set); + activityScenario.onActivity(originalActivity::set); uiAutomation.setRotation(UiAutomation.ROTATION_FREEZE_90); - scenario.onActivity( + activityScenario.onActivity( activity -> { assertThat(activity.getResources().getConfiguration().orientation) .isEqualTo(Configuration.ORIENTATION_LANDSCAPE); @@ -256,4 +265,18 @@ public class ActivityScenarioTest { }); } } + + @Test + public void onActivityExceptionPropagated() { + try (ActivityScenario<TranscriptActivity> activityScenario = + ActivityScenario.launch(TranscriptActivity.class)) { + assertThrows( + IllegalStateException.class, + () -> + activityScenario.onActivity( + activity -> { + throw new IllegalStateException("test"); + })); + } + } } diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/CryptoObjectTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/CryptoObjectTest.java new file mode 100644 index 000000000..2ece12427 --- /dev/null +++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/CryptoObjectTest.java @@ -0,0 +1,65 @@ +package org.robolectric.integrationtests.axt; + +import static org.junit.Assert.fail; + +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; +import androidx.biometric.BiometricPrompt.PromptInfo; +import androidx.fragment.app.FragmentActivity; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.Executor; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.android.controller.ActivityController; + +/** Test intercepting classes not present in OpenJDK. */ +@RunWith(AndroidJUnit4.class) +public class CryptoObjectTest { + + private FragmentActivity fragmentActivity; + + @Before + public void setUp() { + fragmentActivity = + ActivityController.of(new FragmentActivity()).create().resume().start().get(); + } + + @Test + public void biometricPromptAuthenticateShouldNotCrashWithNoSuchMethodError() + throws NoSuchPaddingException, NoSuchAlgorithmException { + BiometricPrompt biometricPrompt = + new BiometricPrompt( + fragmentActivity, + new Executor() { + @Override + public void execute(Runnable command) {} + }, + new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {} + + @Override + public void onAuthenticationSucceeded( + @NonNull BiometricPrompt.AuthenticationResult result) {} + + @Override + public void onAuthenticationFailed() {} + }); + + PromptInfo promptInfo = + new PromptInfo.Builder() + .setTitle("Set and not empty") + .setNegativeButtonText("Set and not empty") + .build(); + try { + biometricPrompt.authenticate( + promptInfo, new BiometricPrompt.CryptoObject(Cipher.getInstance("RSA"))); + } catch (NoSuchMethodError e) { + fail(); + } + } +} diff --git a/integration_tests/compat-target28/build.gradle b/integration_tests/compat-target28/build.gradle index 1fc7485ae..bbcad7220 100644 --- a/integration_tests/compat-target28/build.gradle +++ b/integration_tests/compat-target28/build.gradle @@ -27,6 +27,10 @@ android { targetCompatibility = '1.8' } + kotlinOptions { + jvmTarget = '1.8' + } + testOptions.unitTests.includeAndroidResources true } diff --git a/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java b/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java index 12c992f6c..0c2823fe3 100644 --- a/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java +++ b/integration_tests/ctesque/src/sharedTest/java/android/app/InstrumentationTest.java @@ -2,6 +2,7 @@ package android.app; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.robolectric.annotation.LooperMode.Mode.PAUSED; import android.os.Handler; @@ -48,4 +49,16 @@ public final class InstrumentationTest { assertThat(events).containsExactly("before runOnMainSync", "in runOnMainSync").inOrder(); } + + @Test + public void runOnMainSync_propagatesException() { + assertThrows( + IllegalStateException.class, + () -> + getInstrumentation() + .runOnMainSync( + () -> { + throw new IllegalStateException("test"); + })); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowColorTest.java b/integration_tests/ctesque/src/sharedTest/java/android/graphics/ColorTest.java index 46387d653..308410b44 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowColorTest.java +++ b/integration_tests/ctesque/src/sharedTest/java/android/graphics/ColorTest.java @@ -1,15 +1,16 @@ -package org.robolectric.shadows; +package android.graphics; import static com.google.common.truth.Truth.assertThat; -import android.graphics.Color; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.annotation.internal.DoNotInstrument; +/** Compatibility tests for {@link Color} */ +@DoNotInstrument @RunWith(AndroidJUnit4.class) -public class ShadowColorTest { - +public class ColorTest { @Test public void testRgb() { int color = Color.rgb(160, 160, 160); @@ -69,9 +70,21 @@ public class ShadowColorTest { @Test public void HSVToColorShouldReverseColorToHSV() { - float[] hsv = new float[3]; - Color.colorToHSV(Color.RED, hsv); + float[] hsv = new float[3]; + Color.colorToHSV(Color.RED, hsv); + + assertThat(Color.HSVToColor(hsv)).isEqualTo(Color.RED); + } - assertThat(Color.HSVToColor(hsv)).isEqualTo(Color.RED); + @Test + public void HSVToColorValueShouldBePinned() { + assertThat(Color.HSVToColor(new float[] {0f, 0f, -1.0f})).isEqualTo(Color.BLACK); + assertThat(Color.HSVToColor(new float[] {0f, 0f, 2.0f})).isEqualTo(Color.WHITE); + } + + @Test + public void HSVToColorSaturationShouldBePinned() { + assertThat(Color.HSVToColor(new float[] {0f, -1.0f, 0.5f})).isEqualTo(0xff808080); + assertThat(Color.HSVToColor(new float[] {0f, 2.0f, 0.5f})).isEqualTo(0xff800000); } } diff --git a/integration_tests/ctesque/src/sharedTest/java/android/os/LooperTest.java b/integration_tests/ctesque/src/sharedTest/java/android/os/LooperTest.java new file mode 100644 index 000000000..14a4a7d79 --- /dev/null +++ b/integration_tests/ctesque/src/sharedTest/java/android/os/LooperTest.java @@ -0,0 +1,26 @@ +package android.os; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertTrue; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.CountDownLatch; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; +import org.robolectric.annotation.internal.DoNotInstrument; + +/** Tests to verify INSTRUMENTATION_TEST mode Looper behaves like a looping Looper. */ +@DoNotInstrument +@RunWith(AndroidJUnit4.class) +public class LooperTest { + + @Test + @LooperMode(Mode.INSTRUMENTATION_TEST) + public void postAndWait() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + new Handler(Looper.getMainLooper()).post(latch::countDown); + assertTrue(latch.await(1, SECONDS)); + } +} diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle index fd52d973d..a8bf910c8 100644 --- a/integration_tests/kotlin/build.gradle +++ b/integration_tests/kotlin/build.gradle @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.robolectric.gradle.RoboJavaModulePlugin apply plugin: RoboJavaModulePlugin @@ -12,7 +13,11 @@ spotless { } compileKotlin { - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 +} + +compileTestKotlin { + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } dependencies { diff --git a/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java b/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java index ec7ca8bf9..3e990d133 100644 --- a/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java +++ b/integration_tests/memoryleaks/src/test/java/org/robolectric/integrationtests/memoryleaks/BaseMemoryLeaksTest.java @@ -3,7 +3,9 @@ package org.robolectric.integrationtests.memoryleaks; import static org.robolectric.Shadows.shadowOf; import android.app.Activity; +import android.content.Context; import android.content.res.Configuration; +import android.content.res.TypedArray; import android.os.Looper; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -14,8 +16,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; /** * A test that verifies that activities and fragments become GC candidates after being destroyed, or @@ -126,5 +130,15 @@ public abstract class BaseMemoryLeaksTest { } } + @Test + public void typedArrayData() { + assertNotLeaking( + () -> { + Context context = RuntimeEnvironment.getApplication(); + TypedArray typedArray = context.obtainStyledAttributes(new int[] {}); + return ReflectionHelpers.getField(typedArray, "mData"); + }); + } + public abstract <T> void assertNotLeaking(Callable<T> potentiallyLeakingCallable); } diff --git a/integration_tests/mockito-kotlin/build.gradle b/integration_tests/mockito-kotlin/build.gradle index 776f33bd2..22df04b0f 100644 --- a/integration_tests/mockito-kotlin/build.gradle +++ b/integration_tests/mockito-kotlin/build.gradle @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.robolectric.gradle.RoboJavaModulePlugin apply plugin: RoboJavaModulePlugin @@ -11,6 +12,14 @@ spotless { } } +compileKotlin { + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 +} + +compileTestKotlin { + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 +} + dependencies { api project(":robolectric") compileOnly AndroidSdk.MAX_SDK.coordinates diff --git a/integration_tests/nativegraphics/config/robolectric.properties b/integration_tests/nativegraphics/config/robolectric.properties new file mode 100644 index 000000000..0d16e6b27 --- /dev/null +++ b/integration_tests/nativegraphics/config/robolectric.properties @@ -0,0 +1,15 @@ +# Copyright (C) 2022 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +sdk=NEWEST_SDK diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java index 77f548373..b029f01f5 100644 --- a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java +++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java @@ -15,6 +15,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowDrawable; @@ -71,4 +72,51 @@ public class ShadowNativeAnimatedVectorDrawableTest { assertEquals( R.drawable.animation_vector_drawable_grouping_1, shadowDrawable.getCreatedFromResId()); } + + @Test + public void start_isRunning_returnsTrue() throws Exception { + // Setup AnimatedVectorDrawable from xml file + XmlPullParser parser = resources.getXml(RES_ID); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Empty loop + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + AnimatedVectorDrawable drawable = new AnimatedVectorDrawable(); + drawable.inflate(resources, parser, attrs); + + drawable.start(); + + assertEquals(true, Shadows.shadowOf(drawable).isStartInitiated()); + } + + @Test + public void stop_returnsFalse() throws Exception { + // Setup AnimatedVectorDrawable from xml file + XmlPullParser parser = resources.getXml(RES_ID); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Empty loop + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + AnimatedVectorDrawable drawable = new AnimatedVectorDrawable(); + drawable.inflate(resources, parser, attrs); + + drawable.start(); + drawable.stop(); + + assertEquals(false, Shadows.shadowOf(drawable).isStartInitiated()); + } } diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle index 1e4ba1ddf..c8b4c5a0e 100644 --- a/integration_tests/sparsearray/build.gradle +++ b/integration_tests/sparsearray/build.gradle @@ -26,11 +26,13 @@ android { targetCompatibility = '1.8' } - android { - testOptions { - unitTests { - includeAndroidResources = true - } + kotlinOptions { + jvmTarget = '1.8' + } + + testOptions { + unitTests { + includeAndroidResources = true } } } diff --git a/nativeruntime/src/test/resources/resources.ap_ b/nativeruntime/src/test/resources/resources.ap_ Binary files differindex bc05da2ad..13cc837ec 100644 --- a/nativeruntime/src/test/resources/resources.ap_ +++ b/nativeruntime/src/test/resources/resources.ap_ diff --git a/plugins/maven-dependency-resolver/build.gradle b/plugins/maven-dependency-resolver/build.gradle index 2aa33d9f5..acbff55b6 100644 --- a/plugins/maven-dependency-resolver/build.gradle +++ b/plugins/maven-dependency-resolver/build.gradle @@ -21,29 +21,11 @@ tasks.withType(GenerateModuleMetadata).configureEach { } compileKotlin { - // Use java/main classes directory to replace default kotlin/main to - // avoid d8 error when dexing & desugaring kotlin classes with non-exist - // kotlin/main directory because this module doesn't have kotlin code - // in production. If utils module starts to add Kotlin code in main source - // set, we can remove this destinationDirectory modification. - destinationDirectory = file("${projectDir}/build/classes/java/main") compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } -afterEvaluate { - configurations { - runtimeElements { - attributes { - // We should add artifactType with jar to ensure standard runtimeElements variant - // has a max priority selection sequence than other variants that brought by - // kotlin plugin. - attribute( - Attribute.of("artifactType", String.class), - ArtifactTypeDefinition.JAR_TYPE - ) - } - } - } +compileTestKotlin { + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } dependencies { diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java index bb5604d80..91d2af75e 100644 --- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java +++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java @@ -35,7 +35,7 @@ import org.xml.sax.SAXException; * client library here could create conflicts with the ones in the Android system. * * @see <a href="https://maven.apache.org/ant-tasks/">maven-ant-tasks</a> - * @see <a href="https://maven.apache.org/resolver/index.html">Maven Resolver</a></a> + * @see <a href="https://maven.apache.org/resolver/index.html">Maven Resolver</a> */ public class MavenDependencyResolver implements DependencyResolver { diff --git a/preinstrumented/build.gradle b/preinstrumented/build.gradle index 8ffb5bbf2..dc1e5d0d5 100644 --- a/preinstrumented/build.gradle +++ b/preinstrumented/build.gradle @@ -20,6 +20,9 @@ dependencies { implementation libs.guava implementation project(":sandbox") implementation project(":shadows:versioning") + + testImplementation libs.junit4 + testImplementation libs.mockito } tasks.register('instrumentAll') { diff --git a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java index 753c1670d..1410c33d5 100644 --- a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java +++ b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java @@ -1,5 +1,6 @@ package org.robolectric.preinstrumented; +import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteStreams; import java.io.BufferedOutputStream; import java.io.File; @@ -19,6 +20,7 @@ import org.robolectric.internal.bytecode.ClassInstrumentor; import org.robolectric.internal.bytecode.ClassNodeProvider; import org.robolectric.internal.bytecode.InstrumentationConfiguration; import org.robolectric.internal.bytecode.Interceptors; +import org.robolectric.internal.bytecode.NativeCallHandler; import org.robolectric.util.inject.Injector; import org.robolectric.versioning.AndroidVersionInitTools; import org.robolectric.versioning.AndroidVersions.AndroidRelease; @@ -33,6 +35,10 @@ public class JarInstrumentor { private final ClassInstrumentor classInstrumentor; private final InstrumentationConfiguration instrumentationConfiguration; + public static void main(String[] args) throws IOException, ClassNotFoundException { + new JarInstrumentor().processCommandLine(args); + } + public JarInstrumentor() { AndroidConfigurer androidConfigurer = INJECTOR.getInstance(AndroidConfigurer.class); classInstrumentor = INJECTOR.getInstance(ClassInstrumentor.class); @@ -43,18 +49,61 @@ public class JarInstrumentor { instrumentationConfiguration = builder.build(); } - public static void main(String[] args) throws IOException, ClassNotFoundException { - if (args.length != 2) { - System.err.println("Usage: JarInstrumentor <source jar> <dest jar>"); - System.exit(1); + @VisibleForTesting + void processCommandLine(String[] args) throws IOException, ClassNotFoundException { + if (args.length >= 2) { + File sourceFile = new File(args[0]); + File destJarFile = new File(args[1]); + + File destNativesFile = null; + boolean throwOnNatives = false; + boolean parseError = false; + for (int i = 2; i < args.length; i++) { + if (args[i].startsWith("--write-natives=")) { + destNativesFile = new File(args[i].substring("--write-natives=".length())); + } else if (args[i].equals("--throw-on-natives")) { + throwOnNatives = true; + } else { + System.err.println("Unknown argument: " + args[i]); + parseError = true; + break; + } + } + + if (!parseError) { + instrumentJar(sourceFile, destJarFile, destNativesFile, throwOnNatives); + return; + } } - new JarInstrumentor().instrumentJar(new File(args[0]), new File(args[1])); + + System.err.println( + "Usage: JarInstrumentor <source jar> <dest jar> " + + "[--write-natives=<file>] " + + "[--throw-on-natives]"); + exit(1); + } + + /** Calls {@link System#exit(int)}. Overridden during tests to avoid exiting during tests. */ + @VisibleForTesting + protected void exit(int status) { + System.exit(status); } - private void instrumentJar(File sourceFile, File destFile) + /** + * Performs the JAR instrumentation. + * + * @param sourceJarFile The source JAR to process. + * @param destJarFile The destination JAR with the instrumented method calls. + * @param destNativesFile Optional file to write native calls signature. Null to disable. + * @param throwOnNatives Whether native calls should be instrumented as throwing a dedicated + * exception (true) or no-op (false). + */ + @VisibleForTesting + protected void instrumentJar( + File sourceJarFile, File destJarFile, File destNativesFile, boolean throwOnNatives) throws IOException, ClassNotFoundException { long startNs = System.nanoTime(); - JarFile jarFile = new JarFile(sourceFile); + JarFile jarFile = new JarFile(sourceJarFile); ClassNodeProvider classNodeProvider = new ClassNodeProvider() { @Override @@ -63,6 +112,23 @@ public class JarInstrumentor { } }; + NativeCallHandler nativeCallHandler; + final boolean writeNativesFile = destNativesFile != null; + + if (destNativesFile == null) { + destNativesFile = + new File( + sourceJarFile.getParentFile(), + sourceJarFile.getName().replace(".jar", "-natives.txt")); + } + + try { + nativeCallHandler = new NativeCallHandler(destNativesFile, writeNativesFile, throwOnNatives); + classInstrumentor.setNativeCallHandler(nativeCallHandler); + } catch (IOException e) { + throw new AssertionError("Unable to load native exemptions file", e); + } + int nonClassCount = 0; int classCount = 0; @@ -70,11 +136,11 @@ public class JarInstrumentor { try { classInstrumentor.setAndroidJarSDKVersion(getJarAndroidSDKVersion(jarFile)); } catch (Exception e) { - throw new AssertionError("Unable to get Android SDK version from Jar File", e); + throw new AssertionError("Unable to get Android SDK version from Jar file", e); } try (JarOutputStream jarOut = - new JarOutputStream(new BufferedOutputStream(new FileOutputStream(destFile), ONE_MB))) { + new JarOutputStream(new BufferedOutputStream(new FileOutputStream(destJarFile), ONE_MB))) { Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); @@ -109,6 +175,11 @@ public class JarInstrumentor { } } } + + if (writeNativesFile) { + nativeCallHandler.writeExemptionsList(); + } + long elapsedNs = System.nanoTime() - startNs; System.out.println( String.format( diff --git a/preinstrumented/src/test/java/org/robolectric/preinstrumented/JarInstrumentorTest.java b/preinstrumented/src/test/java/org/robolectric/preinstrumented/JarInstrumentorTest.java new file mode 100644 index 000000000..6b6d169e1 --- /dev/null +++ b/preinstrumented/src/test/java/org/robolectric/preinstrumented/JarInstrumentorTest.java @@ -0,0 +1,71 @@ +package org.robolectric.preinstrumented; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.io.File; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test for {@link JarInstrumentor}. */ +@RunWith(JUnit4.class) +public class JarInstrumentorTest { + + private JarInstrumentor spyDummyInstrumentor; + + @Before + public void setUp() throws Exception { + JarInstrumentor dummyInstrumentor = + new JarInstrumentor() { + @Override + protected void instrumentJar( + File sourceJarFile, File destJarFile, File destNativesFile, boolean throwOnNatives) { + // No-op. We only want to test the command line processing. Stub the actual + // instrumention. + } + + @Override + protected void exit(int status) { + // No-op. Tests should never call system.exit(). + } + }; + spyDummyInstrumentor = spy(dummyInstrumentor); + } + + @Test + public void processCommandLine_legacyUsage() throws Exception { + spyDummyInstrumentor.processCommandLine(new String[] {"source.jar", "dest.jar"}); + verify(spyDummyInstrumentor) + .instrumentJar(new File("source.jar"), new File("dest.jar"), null, false); + } + + @Test + public void processCommandLine_throwOnNatives() throws Exception { + spyDummyInstrumentor.processCommandLine( + new String[] {"source.jar", "dest.jar", "--throw-on-natives"}); + verify(spyDummyInstrumentor) + .instrumentJar(new File("source.jar"), new File("dest.jar"), null, true); + } + + @Test + public void processCommandLine_writeNativesExemptionFile() throws Exception { + spyDummyInstrumentor.processCommandLine( + new String[] {"source.jar", "dest.jar", "--write-natives=natives.txt"}); + verify(spyDummyInstrumentor) + .instrumentJar( + new File("source.jar"), new File("dest.jar"), new File("natives.txt"), false); + } + + @Test + public void processCommandLine_unknownArguments() throws Exception { + spyDummyInstrumentor.processCommandLine(new String[] {"source.jar", "dest.jar", "--some-flag"}); + verify(spyDummyInstrumentor, never()) + .instrumentJar(any(File.class), any(File.class), any(File.class), anyBoolean()); + verify(spyDummyInstrumentor).exit(1); + } +} diff --git a/processor/Android.bp b/processor/Android.bp index 90a49815e..607486ba7 100644 --- a/processor/Android.bp +++ b/processor/Android.bp @@ -24,6 +24,7 @@ java_library_host { static_libs: [ "Robolectric_annotations_upstream", "Robolectric_shadowapi_upstream", + "Robolectric_shadows_versioning_upstream", "auto_service_annotations", "asm-commons-9.2", "guava", diff --git a/processor/build.gradle b/processor/build.gradle index 9185d088a..c2f00d77d 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -34,6 +34,7 @@ tasks['classes'].dependsOn(generateSdksFile) dependencies { api project(":annotations") api project(":shadowapi") + implementation project(":shadows:versioning") compileOnly libs.findbugs.jsr305 api libs.asm diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java index 05822d783..e08b3acd1 100644 --- a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java +++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.jar.JarFile; @@ -40,6 +39,7 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; import org.robolectric.annotation.Implementation; +import org.robolectric.versioning.AndroidVersionInitTools; /** Encapsulates a collection of Android framework jars. */ public class SdkStore { @@ -248,26 +248,14 @@ public class SdkStore { /** * Determine the API level for this SDK jar by inspecting its {@code build.prop} file. * - * <p>If the {@code ro.build.version.codename} value isn't {@code REL}, this is an unreleased - * SDK, which is represented as 10000 (see {@link - * android.os.Build.VERSION_CODES#CUR_DEVELOPMENT}. - * - * @return the API level, or 10000 + * @return the API level */ private int readSdkInt() { - Properties properties = new Properties(); - try (InputStream inputStream = jarFile.getInputStream(jarFile.getJarEntry("build.prop"))) { - properties.load(inputStream); + try { + return AndroidVersionInitTools.computeReleaseVersion(jarFile).getSdkInt(); } catch (IOException e) { throw new RuntimeException("failed to read build.prop from " + path); } - int sdkInt = Integer.parseInt(properties.getProperty("ro.build.version.sdk")); - String codename = properties.getProperty("ro.build.version.codename"); - if (!"REL".equals(codename)) { - sdkInt = 10000; - } - - return sdkInt; } private JarFile ensureJar() { diff --git a/resources/src/main/java/org/robolectric/res/android/ResTable.java b/resources/src/main/java/org/robolectric/res/android/ResTable.java index edc1a0c86..9a696b17e 100644 --- a/resources/src/main/java/org/robolectric/res/android/ResTable.java +++ b/resources/src/main/java/org/robolectric/res/android/ResTable.java @@ -1268,13 +1268,30 @@ public class ResTable { } int findEntry(PackageGroup group, int typeIndex, String name, Ref<Integer> outTypeSpecFlags) { + // const TypeList& typeList = group->types[typeIndex]; List<Type> typeList = getOrDefault(group.types, typeIndex, Collections.emptyList()); + // const size_t typeCount = typeList.size(); + // for (size_t i = 0; i < typeCount; i++) { for (Type type : typeList) { + // const Type* t = typeList[i]; + // const base::expected<size_t, NullOrIOError> ei = + // t->package->keyStrings.indexOfString(name, nameLen); int ei = type._package_.keyStrings.indexOfString(name); + // if (!ei.has_value()) { if (ei < 0) { continue; } + // const size_t configCount = t->configs.size(); + // for (size_t j = 0; j < configCount; j++) { for (ResTable_type resTableType : type.configs) { + // const TypeVariant tv(t->configs[j]); + // for (TypeVariant::iterator iter = tv.beginEntries(); + // iter != tv.endEntries(); + // iter++) { + // const ResTable_entry* entry = *iter; + // if (entry == NULL) { + // continue; + // } int entryIndex = resTableType.findEntryByResName(ei); if (entryIndex >= 0) { int resId = Res_MAKEID(group.id - 1, typeIndex, entryIndex); diff --git a/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java b/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java index 17c9dd91c..bab7f9670 100644 --- a/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java +++ b/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java @@ -8,6 +8,7 @@ import static org.robolectric.res.android.Util.SIZEOF_INT; import static org.robolectric.res.android.Util.SIZEOF_SHORT; import static org.robolectric.res.android.Util.dtohl; import static org.robolectric.res.android.Util.dtohs; +import static org.robolectric.res.android.Util.isTruthy; import java.nio.Buffer; import java.nio.ByteBuffer; @@ -1213,14 +1214,23 @@ public static class ResTable_ref int entryOffset(int entryIndex) { ByteBuffer byteBuffer = myBuf(); int offset = myOffset(); - boolean isOffset16 = (flags & ResTable_type.FLAG_OFFSET16) == ResTable_type.FLAG_OFFSET16; - if (isOffset16) { + if (isTruthy(flags & ResTable_type.FLAG_OFFSET16)) { short off16 = byteBuffer.getShort(offset + header.headerSize + entryIndex * 2); if (off16 == -1) { return -1; } // Check for no entry (0xffff short) - return dtohs(off16) == 0xffff ? ResTable_type.NO_ENTRY : dtohs(off16) * 4; + return dtohs(off16) == -1 ? ResTable_type.NO_ENTRY : dtohs(off16) * 4; + } else if (isTruthy(flags & ResTable_type.FLAG_SPARSE)) { + ResTable_sparseTypeEntry sparse_entry = + new ResTable_sparseTypeEntry( + myBuf(), myOffset() + entryIndex * ResTable_sparseTypeEntry.SIZEOF); + // if (!sparse_entry) { + // return base::unexpected(IOError::PAGES_MISSING); + // } + // TODO: implement above + // offset = dtohs(sparse_entry->offset) * 4u; + return dtohs(sparse_entry.offset) * 4; } else { return byteBuffer.getInt(offset + header.headerSize + entryIndex * 4); } @@ -1236,11 +1246,13 @@ public static class ResTable_ref int offset = myOffset(); // from ResTable cpp: -// const uint32_t* const eindex = reinterpret_cast<const uint32_t*>( -// reinterpret_cast<const uint8_t*>(thisType) + dtohs(thisType->header.headerSize)); -// -// uint32_t thisOffset = dtohl(eindex[realEntryIndex]); - int entryOffset = byteBuffer.getInt(offset + header.headerSize + entryIndex * 4); + // const uint32_t* const eindex = reinterpret_cast<const uint32_t*>( + // reinterpret_cast<const uint8_t*>(thisType) + + // dtohs(thisType->header.headerSize)); + // + // uint32_t thisOffset = dtohl(eindex[realEntryIndex]); + + int entryOffset = entryOffset(entryIndex); if (entryOffset == -1) { return -1; } diff --git a/robolectric/Android.bp b/robolectric/Android.bp index e690895a3..822c32288 100644 --- a/robolectric/Android.bp +++ b/robolectric/Android.bp @@ -63,6 +63,7 @@ java_test_host { "Robolectric_shadows_framework_upstream", "Robolectric_annotations_upstream", "Robolectric_shadowapi_upstream", + "Robolectric_shadows_versioning_upstream", "Robolectric_resources_upstream", "Robolectric_sandbox_upstream", "Robolectric_junit_upstream", diff --git a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java index 519f42c09..d9dbbd46d 100644 --- a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java +++ b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java @@ -76,6 +76,10 @@ public class RobolectricTestRunner extends SandboxTestRunner { new SecureRandom(); // Fixes an issue using AWT-backed graphics shadows when using X11 forwarding. System.setProperty("java.awt.headless", "true"); + // Fixes a performance regression in caused by the addition of RSA modulus + // validation introduced in Bouncy Castle 1.71. + // https://github.com/bcgit/bc-java/issues/1144 + System.setProperty("org.bouncycastle.rsa.max_mr_tests", "0"); } protected static Injector.Builder defaultInjector() { diff --git a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java index f250381f4..2f66c7d88 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java @@ -86,6 +86,7 @@ import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowPackageManager; import org.robolectric.shadows.ShadowPackageParser; import org.robolectric.shadows.ShadowPackageParser._Package_; +import org.robolectric.shadows.ShadowPausedLooper; import org.robolectric.shadows.ShadowView; import org.robolectric.util.Logger; import org.robolectric.util.PerfStatsCollector; @@ -189,13 +190,13 @@ public class AndroidTestEnvironment implements TestEnvironment { : androidConfiguration.locale; Locale.setDefault(locale); - // Looper needs to be prepared before the activity thread is created - if (Looper.myLooper() == null) { - Looper.prepareMainLooper(); - } if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { + if (Looper.myLooper() == null) { + Looper.prepareMainLooper(); + } ShadowLooper.getShadowMainLooper().resetScheduler(); } else { + ShadowPausedLooper.resetLoopers(); RuntimeEnvironment.setMasterScheduler(new LooperDelegatingScheduler(Looper.getMainLooper())); } @@ -203,9 +204,6 @@ public class AndroidTestEnvironment implements TestEnvironment { RuntimeEnvironment.setAndroidFrameworkJarPath(sdkJarPath); Bootstrap.setDisplayConfiguration(androidConfiguration, displayMetrics); - RuntimeEnvironment.setActivityThread(ReflectionHelpers.callConstructor(ActivityThread.class)); - ReflectionHelpers.setStaticField( - ActivityThread.class, "sMainThreadHandler", new Handler(Looper.myLooper())); Instrumentation instrumentation = createInstrumentation(); InstrumentationRegistry.registerInstance(instrumentation, new Bundle()); @@ -284,6 +282,9 @@ public class AndroidTestEnvironment implements TestEnvironment { Package parsedPackage = loadAppPackage(config, appManifest); ApplicationInfo applicationInfo = parsedPackage.applicationInfo; + Class<? extends Application> applicationClass = + getApplicationClass(appManifest, config, applicationInfo); + applicationInfo.className = applicationClass.getName(); ComponentName actualComponentName = new ComponentName( @@ -311,8 +312,8 @@ public class AndroidTestEnvironment implements TestEnvironment { Bootstrap.setUpDisplay(); activityThread.applyConfigurationToResources(androidConfiguration); - Application application = createApplication(appManifest, config, applicationInfo); - RuntimeEnvironment.setConfiguredApplicationClass(application.getClass()); + Application application = ReflectionHelpers.callConstructor(applicationClass); + RuntimeEnvironment.setConfiguredApplicationClass(applicationClass); RuntimeEnvironment.application = application; @@ -479,13 +480,7 @@ public class AndroidTestEnvironment implements TestEnvironment { } @VisibleForTesting - static Application createApplication( - AndroidManifest appManifest, Config config, ApplicationInfo applicationInfo) { - return ReflectionHelpers.callConstructor( - getApplicationClass(appManifest, config, applicationInfo)); - } - - private static Class<? extends Application> getApplicationClass( + static Class<? extends Application> getApplicationClass( AndroidManifest appManifest, Config config, ApplicationInfo applicationInfo) { Class<? extends Application> applicationClass = null; if (config != null && !Config.Builder.isDefaultApplication(config.application())) { @@ -554,35 +549,39 @@ public class AndroidTestEnvironment implements TestEnvironment { } private Instrumentation createInstrumentation() { - final ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread(); - final _ActivityThread_ activityThreadReflector = - reflector(_ActivityThread_.class, activityThread); - Instrumentation androidInstrumentation = new RoboMonitoringInstrumentation(); - activityThreadReflector.setInstrumentation(androidInstrumentation); - - Application dummyInitialApplication = new Application(); - final ComponentName dummyInitialComponent = - new ComponentName("", androidInstrumentation.getClass().getSimpleName()); - // TODO Move the API check into a helper method inside ShadowInstrumentation - if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.JELLY_BEAN_MR1) { - reflector(_Instrumentation_.class, androidInstrumentation) - .init( - activityThread, - dummyInitialApplication, - dummyInitialApplication, - dummyInitialComponent, - null); - } else { - reflector(_Instrumentation_.class, androidInstrumentation) - .init( - activityThread, - dummyInitialApplication, - dummyInitialApplication, - dummyInitialComponent, - null, - null); - } + androidInstrumentation.runOnMainSync( + () -> { + ActivityThread activityThread = ReflectionHelpers.callConstructor(ActivityThread.class); + ReflectionHelpers.setStaticField( + ActivityThread.class, "sMainThreadHandler", new Handler(Looper.getMainLooper())); + reflector(_ActivityThread_.class, activityThread) + .setInstrumentation(androidInstrumentation); + RuntimeEnvironment.setActivityThread(activityThread); + + Application dummyInitialApplication = new Application(); + final ComponentName dummyInitialComponent = + new ComponentName("", androidInstrumentation.getClass().getSimpleName()); + // TODO Move the API check into a helper method inside ShadowInstrumentation + if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.JELLY_BEAN_MR1) { + reflector(_Instrumentation_.class, androidInstrumentation) + .init( + activityThread, + dummyInitialApplication, + dummyInitialApplication, + dummyInitialComponent, + null); + } else { + reflector(_Instrumentation_.class, androidInstrumentation) + .init( + activityThread, + dummyInitialApplication, + dummyInitialApplication, + dummyInitialComponent, + null, + null); + } + }); androidInstrumentation.onCreate(new Bundle()); return androidInstrumentation; @@ -604,7 +603,7 @@ public class AndroidTestEnvironment implements TestEnvironment { @Override public void tearDownApplication() { if (RuntimeEnvironment.application != null) { - RuntimeEnvironment.application.onTerminate(); + ShadowInstrumentation.runOnMainSyncNoIdle(RuntimeEnvironment.getApplication()::onTerminate); ShadowInstrumentation.getInstrumentation().finish(1, new Bundle()); } } diff --git a/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java index 29de83dec..4329e8c3e 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java @@ -2,6 +2,7 @@ package org.robolectric.android.internal; import static com.google.common.base.Preconditions.checkNotNull; +import androidx.test.internal.platform.util.TestOutputEmitter; import com.google.common.annotations.Beta; import java.util.List; import java.util.Locale; @@ -15,10 +16,12 @@ import java.util.Locale; * <p>Note: This API may be removed in the future in favor of using espresso's exception directly. */ @Beta +@SuppressWarnings("RestrictTo") public final class IdlingResourceTimeoutException extends RuntimeException { public IdlingResourceTimeoutException(List<String> resourceNames) { super( String.format( Locale.ROOT, "Wait for %s to become idle timed out", checkNotNull(resourceNames))); + TestOutputEmitter.dumpThreadStates("ThreadState-IdlingResTimeoutExcep.txt"); } } diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java index fc10913d8..1bc207383 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalActivityInvoker.java @@ -17,6 +17,7 @@ import javax.annotation.Nullable; import org.robolectric.android.controller.ActivityController; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowActivity; +import org.robolectric.shadows.ShadowInstrumentation; /** * An {@link ActivityInvoker} that drives {@link Activity} lifecycles manually. @@ -69,89 +70,105 @@ public class LocalActivityInvoker implements ActivityInvoker { public void resumeActivity(Activity activity) { checkNotNull(controller); checkState(controller.get() == activity); - Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); - switch (stage) { - case RESUMED: - return; - case PAUSED: - controller.resume().topActivityResumed(true); - return; - case STOPPED: - controller.restart().resume().topActivityResumed(true); - return; - default: - throw new IllegalStateException( - String.format( - "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage)); - } + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + Stage stage = + ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); + switch (stage) { + case RESUMED: + return; + case PAUSED: + controller.resume().topActivityResumed(true); + return; + case STOPPED: + controller.restart().resume().topActivityResumed(true); + return; + default: + throw new IllegalStateException( + String.format( + "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage)); + } + }); } @Override public void pauseActivity(Activity activity) { checkNotNull(controller); checkState(controller.get() == activity); - Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); - switch (stage) { - case RESUMED: - controller.topActivityResumed(false).pause(); - return; - case PAUSED: - return; - default: - throw new IllegalStateException( - String.format("Activity's stage must be RESUMED or PAUSED but was %s.", stage)); - } + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + Stage stage = + ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); + switch (stage) { + case RESUMED: + controller.topActivityResumed(false).pause(); + return; + case PAUSED: + return; + default: + throw new IllegalStateException( + String.format("Activity's stage must be RESUMED or PAUSED but was %s.", stage)); + } + }); } @Override public void stopActivity(Activity activity) { checkNotNull(controller); checkState(controller.get() == activity); - Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); - switch (stage) { - case RESUMED: - controller.topActivityResumed(false).pause().stop(); - return; - case PAUSED: - controller.stop(); - return; - case STOPPED: - return; - default: - throw new IllegalStateException( - String.format( - "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage)); - } + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + Stage stage = + ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); + switch (stage) { + case RESUMED: + controller.topActivityResumed(false).pause().stop(); + return; + case PAUSED: + controller.stop(); + return; + case STOPPED: + return; + default: + throw new IllegalStateException( + String.format( + "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage)); + } + }); } @Override public void recreateActivity(Activity activity) { checkNotNull(controller); checkState(controller.get() == activity); - controller.recreate(); + ShadowInstrumentation.runOnMainSyncNoIdle(() -> controller.recreate()); } @Override public void finishActivity(Activity activity) { checkNotNull(controller); checkState(controller.get() == activity); - activity.finish(); - Stage stage = ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); - switch (stage) { - case RESUMED: - controller.topActivityResumed(false).pause().stop().destroy(); - return; - case PAUSED: - controller.stop().destroy(); - return; - case STOPPED: - controller.destroy(); - return; - default: - throw new IllegalStateException( - String.format( - "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage)); - } + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + activity.finish(); + Stage stage = + ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); + switch (stage) { + case RESUMED: + controller.topActivityResumed(false).pause().stop().destroy(); + return; + case PAUSED: + controller.stop().destroy(); + return; + case STOPPED: + controller.destroy(); + return; + default: + throw new IllegalStateException( + String.format( + "Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.", stage)); + } + }); } // This implementation makes sure, that the activity you are trying to launch exists diff --git a/robolectric/src/main/java/org/robolectric/android/internal/NoOpThreadChecker.java b/robolectric/src/main/java/org/robolectric/android/internal/NoOpThreadChecker.java deleted file mode 100644 index 1f7f7c37c..000000000 --- a/robolectric/src/main/java/org/robolectric/android/internal/NoOpThreadChecker.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.robolectric.android.internal; - -import androidx.test.internal.platform.ThreadChecker; - -/** - * In Robolectric environment, everything is executed on the main thread except for when you - * manually create and run your code on worker thread. - */ -@SuppressWarnings("RestrictTo") -public class NoOpThreadChecker implements ThreadChecker { - @Override - public void checkMainThread() {} - - @Override - public void checkNotMainThread() {} -} diff --git a/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java b/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java index 0d8d15be2..b41f01504 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/RoboMonitoringInstrumentation.java @@ -2,7 +2,6 @@ package org.robolectric.android.internal; import static org.robolectric.Shadows.shadowOf; import static org.robolectric.shadow.api.Shadow.extract; -import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import android.app.Activity; import android.app.Application; @@ -33,11 +32,18 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.android.controller.ActivityController; +import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowActivity; +import org.robolectric.shadows.ShadowInstrumentation; +import org.robolectric.shadows.ShadowLooper; /** * A Robolectric instrumentation that acts like a slimmed down {@link @@ -48,7 +54,6 @@ public class RoboMonitoringInstrumentation extends Instrumentation { private static final String TAG = "RoboInstrumentation"; - private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); private final ActivityLifecycleMonitorImpl lifecycleMonitor = new ActivityLifecycleMonitorImpl(); private final ApplicationLifecycleMonitorImpl applicationMonitor = new ApplicationLifecycleMonitorImpl(); @@ -79,7 +84,7 @@ public class RoboMonitoringInstrumentation extends Instrumentation { @Override public void waitForIdleSync() { - shadowMainLooper().idle(); + shadowOf(Looper.getMainLooper()).idle(); } @Override @@ -108,22 +113,28 @@ public class RoboMonitoringInstrumentation extends Instrumentation { } catch (ClassNotFoundException e) { throw new RuntimeException("Could not load activity " + ai.name, e); } - - ActivityController<? extends Activity> controller = - Robolectric.buildActivity(activityClass, intent, activityOptions).create(); - if (controller.get().isFinishing()) { - controller.destroy(); - } else { - createdActivities.add(controller); - controller - .start() - .postCreate(null) - .resume() - .visible() - .windowFocusChanged(true) - .topActivityResumed(true); - } - return controller; + AtomicReference<ActivityController<? extends Activity>> activityControllerReference = + new AtomicReference<>(); + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + ActivityController<? extends Activity> controller = + Robolectric.buildActivity(activityClass, intent, activityOptions); + activityControllerReference.set(controller); + controller.create(); + if (controller.get().isFinishing()) { + controller.destroy(); + } else { + createdActivities.add(controller); + controller + .start() + .postCreate(null) + .resume() + .visible() + .windowFocusChanged(true) + .topActivityResumed(true); + } + }); + return activityControllerReference.get(); } @Override @@ -136,10 +147,47 @@ public class RoboMonitoringInstrumentation extends Instrumentation { applicationMonitor.signalLifecycleChange(app, ApplicationStage.CREATED); } + /** + * Executes a runnable on the main thread, blocking until it is complete. + * + * <p>When in INSTUMENTATION_TEST Looper mode, the runnable is posted to the main handler and the + * caller's thread blocks until that runnable has finished. When a Throwable is thrown in the + * runnable, the exception is propagated back to the caller's thread. If it is an unchecked + * throwable, it will be rethrown as is. If it is a checked exception, it will be rethrown as a + * {@link RuntimeException}. + * + * <p>For other Looper modes, the main looper is idled and then the runnable is executed in the + * caller's thread. + * + * @param runnable a runnable to be executed on the main thread + */ @Override - public void runOnMainSync(Runnable runner) { - shadowMainLooper().idle(); - runner.run(); + public void runOnMainSync(Runnable runnable) { + if (ShadowLooper.looperMode() != LooperMode.Mode.INSTRUMENTATION_TEST) { + waitForIdleSync(); + runnable.run(); + return; + } + + FutureTask<Void> wrappedRunnable = new FutureTask<>(runnable, null); + new Handler(Looper.getMainLooper()).post(wrappedRunnable); + while (!wrappedRunnable.isDone()) { + ShadowLooper.runMainLooperToNextTask(); + } + + try { + wrappedRunnable.get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } + throw new RuntimeException(cause); + } } /** {@inheritDoc} */ @@ -218,21 +266,48 @@ public class RoboMonitoringInstrumentation extends Instrumentation { private void postDispatchActivityResult( ShadowActivity shadowActivity, String target, int requestCode, ActivityResult ar) { - mainThreadHandler.post( - new Runnable() { - @Override - public void run() { - shadowActivity.internalCallDispatchActivityResult( - target, requestCode, ar.getResultCode(), ar.getResultData()); - } - }); + new Handler(Looper.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + shadowActivity.internalCallDispatchActivityResult( + target, requestCode, ar.getResultCode(), ar.getResultData()); + } + }); } private ActivityResult stubResultFor(Intent intent) { - if (IntentStubberRegistry.isLoaded()) { - return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent); + if (!IntentStubberRegistry.isLoaded()) { + return null; + } + + FutureTask<ActivityResult> task = + new FutureTask<ActivityResult>( + new Callable<ActivityResult>() { + @Override + public ActivityResult call() throws Exception { + return IntentStubberRegistry.getInstance().getActivityResultForIntent(intent); + } + }); + ShadowInstrumentation.runOnMainSyncNoIdle(task); + + try { + return task.get(); + } catch (ExecutionException e) { + String msg = String.format("Could not retrieve stub result for intent %s", intent); + // Preserve original exception + if (e.getCause() instanceof RuntimeException) { + Log.w(TAG, msg, e); + throw (RuntimeException) e.getCause(); + } else if (e.getCause() != null) { + throw new RuntimeException(msg, e.getCause()); + } else { + throw new RuntimeException(msg, e); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); } - return null; } /** {@inheritDoc} */ diff --git a/robolectric/src/main/java/org/robolectric/android/internal/RobolectricThreadChecker.java b/robolectric/src/main/java/org/robolectric/android/internal/RobolectricThreadChecker.java new file mode 100644 index 000000000..b11e3d8cb --- /dev/null +++ b/robolectric/src/main/java/org/robolectric/android/internal/RobolectricThreadChecker.java @@ -0,0 +1,38 @@ +package org.robolectric.android.internal; + +import static com.google.common.base.Preconditions.checkState; + +import android.os.Looper; +import androidx.test.internal.platform.ThreadChecker; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowLooper; + +/** + * Performs thread checking when in INSTRUMENTAION_TEST Looper Mode where the test thread is + * distinct from the main thread. No-op for other modes because everything is executed on the main + * thread (except for manually created worker threads). + */ +@SuppressWarnings("RestrictTo") +public class RobolectricThreadChecker implements ThreadChecker { + @Override + public void checkMainThread() { + if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) { + checkState( + Thread.currentThread().equals(Looper.getMainLooper().getThread()), + "Method cannot be called off the main application thread (on: %s) when running in" + + " LooperMode.INSTRUMENTATION_TEST", + Thread.currentThread().getName()); + } + } + + @Override + public void checkNotMainThread() { + if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) { + checkState( + !Thread.currentThread().equals(Looper.getMainLooper().getThread()), + "Method cannot be called on the main application thread (on: %s) when running in" + + " LooperMode.INSTRUMENTATION_TEST", + Thread.currentThread().getName()); + } + } +} diff --git a/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java b/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java index 1a1b7e1f8..359cb554e 100644 --- a/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java +++ b/robolectric/src/main/java/org/robolectric/junit/rules/BackgroundTestRule.java @@ -33,7 +33,10 @@ import org.robolectric.android.util.concurrent.BackgroundExecutor; * assertThat(Looper.myLooper()).isEqualTo(Looper.getMainLooper()); * } * </pre> + * + * @deprecated use LooperMode.Mode.INSTRUMENTATION_TEST instead */ +@Deprecated public final class BackgroundTestRule implements TestRule { /** Annotation for test methods that need to be executed in a background thread. */ diff --git a/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker b/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker index 55104eac2..341f4d97c 100644 --- a/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker +++ b/robolectric/src/main/resources/META-INF/services/androidx.test.internal.platform.ThreadChecker @@ -1 +1 @@ -org.robolectric.android.internal.NoOpThreadChecker +org.robolectric.android.internal.RobolectricThreadChecker diff --git a/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java b/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java index 1837a7678..0ed5f056a 100644 --- a/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java +++ b/robolectric/src/test/java/org/robolectric/AttributeSetBuilderTest.java @@ -3,6 +3,7 @@ package org.robolectric; import static com.google.common.truth.Truth.assertThat; import static java.util.Arrays.asList; import static org.junit.Assert.fail; +import static org.robolectric.annotation.Config.NEWEST_SDK; import static org.robolectric.res.AttributeResource.ANDROID_NS; import static org.robolectric.res.AttributeResource.ANDROID_RES_NS_PREFIX; import static org.robolectric.res.AttributeResource.RES_AUTO_NS_URI; @@ -13,6 +14,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; import org.robolectric.res.AttributeResource; /** Tests for {@link Robolectric#buildAttributeSet()} */ @@ -50,7 +52,9 @@ public class AttributeSetBuilderTest { .addAttribute(android.R.attr.text, AttributeResource.NULL_VALUE) .build(); - assertThat(roboAttributeSet.getAttributeResourceValue(ANDROID_RES_NS_PREFIX + "com.some.namespace", "text", 0)) + assertThat( + roboAttributeSet.getAttributeResourceValue( + ANDROID_RES_NS_PREFIX + "com.some.namespace", "text", 0)) .isEqualTo(0); } @@ -60,7 +64,9 @@ public class AttributeSetBuilderTest { .addAttribute(android.R.attr.id, "@+id/text1") .build(); - assertThat(roboAttributeSet.getAttributeResourceValue(ANDROID_RES_NS_PREFIX + "com.some.other.namespace", "id", 0)) + assertThat( + roboAttributeSet.getAttributeResourceValue( + ANDROID_RES_NS_PREFIX + "com.some.other.namespace", "id", 0)) .isEqualTo(0); } @@ -128,7 +134,9 @@ public class AttributeSetBuilderTest { AttributeSet roboAttributeSet = Robolectric.buildAttributeSet() .build(); - assertThat(roboAttributeSet.getAttributeBooleanValue(ANDROID_RES_NS_PREFIX + "com.some.namespace", "isSugary", true)) + assertThat( + roboAttributeSet.getAttributeBooleanValue( + ANDROID_RES_NS_PREFIX + "com.some.namespace", "isSugary", true)) .isTrue(); } @@ -403,4 +411,15 @@ public class AttributeSetBuilderTest { } } + @Test + // buildAttributeSet always uses resource table from latest SDK + @Config(sdk = NEWEST_SDK) + public void attrWithIconReference() { + AttributeSet roboAttributeSet = + Robolectric.buildAttributeSet() + .addAttribute(R.attr.loaderIcon, "@android:drawable/ic_menu_save") + .build(); + + assertThat(roboAttributeSet.getAttributeNameResource(0)).isEqualTo(R.attr.loaderIcon); + } } diff --git a/robolectric/src/test/java/org/robolectric/R.java b/robolectric/src/test/java/org/robolectric/R.java index 880955aac..9f3708e69 100644 --- a/robolectric/src/test/java/org/robolectric/R.java +++ b/robolectric/src/test/java/org/robolectric/R.java @@ -36,7 +36,7 @@ public final class R { or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". <p>May be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character. */ - public static final int altTitle=0x7f010025; + public static final int altTitle=0x7f010026; /** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>" or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". */ @@ -71,7 +71,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), in (inches), mm (millimeters). */ - public static final int averageSheepWidth=0x7f01001f; + public static final int averageSheepWidth=0x7f010020; /** <p>Must be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character. <p>This may also be a reference to a resource (in the form "<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or @@ -115,7 +115,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". <p>May be a boolean value, either "<code>true</code>" or "<code>false</code>". */ - public static final int isSugary=0x7f010020; + public static final int isSugary=0x7f010021; /** <p>Must be one of the following constant values.</p> <table> <colgroup align="left" /> @@ -138,13 +138,17 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na </table> */ public static final int keycode=0x7f01000f; + /** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>" +or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". + */ + public static final int loaderIcon=0x7f01001e; /** <p>May be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>" or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". <p>May be a dimension value, which is a floating point number appended with a unit such as "<code>14.5sp</code>". Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), in (inches), mm (millimeters). */ - public static final int logoHeight=0x7f010021; + public static final int logoHeight=0x7f010022; /** <p>Must be a dimension value, which is a floating point number appended with a unit such as "<code>14.5sp</code>". Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), in (inches), mm (millimeters). @@ -154,7 +158,7 @@ theme attribute (in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>") containing a value of this type. */ - public static final int logoWidth=0x7f010022; + public static final int logoWidth=0x7f010023; /** <p>Must be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character. <p>This may also be a reference to a resource (in the form "<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or @@ -228,7 +232,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na /** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>" or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". */ - public static final int snail=0x7f010024; + public static final int snail=0x7f010025; /** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>" or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". */ @@ -244,7 +248,7 @@ theme attribute (in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>") containing a value of this type. */ - public static final int stateFoo=0x7f01001e; + public static final int stateFoo=0x7f01001f; /** <p>Must be a string value, using '\\;' to escape characters such as '\\n' or '\\uxxxx' for a unicode character. <p>This may also be a reference to a resource (in the form "<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or @@ -280,7 +284,7 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na /** <p>Must be a reference to another resource, in the form "<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>" or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>". */ - public static final int styleReference=0x7f010023; + public static final int styleReference=0x7f010024; /** <p>Must be an integer value, such as "<code>100</code>". <p>This may also be a reference to a resource (in the form "<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>") or @@ -644,7 +648,7 @@ containing a value of this type. @see #CustomStateView_stateFoo */ public static final int[] CustomStateView = { - 0x7f01001e + 0x7f01001f }; /** <p>This symbol is the offset where the {@link org.robolectric.R.attr#stateFoo} @@ -966,8 +970,8 @@ containing a value of this type. @see #Theme_AnotherTheme_Attributes_styleReference */ public static final int[] Theme_AnotherTheme_Attributes = { - 0x7f01001f, 0x7f010020, 0x7f010021, 0x7f010022, - 0x7f010023, 0x7f010024, 0x7f010025 + 0x7f010020, 0x7f010021, 0x7f010022, 0x7f010023, + 0x7f010024, 0x7f010025, 0x7f010026 }; /** <p>This symbol is the offset where the {@link org.robolectric.R.attr#altTitle} diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java index 737af4849..db4058d11 100644 --- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java +++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java @@ -257,8 +257,8 @@ public class RobolectricTestRunnerTest { RobolectricTestRunner runner = new SingleSdkRobolectricTestRunner( TestWithTwoMethods.class, - RobolectricTestRunner.defaultInjector() - .bind(PerfStatsReporter[].class, new PerfStatsReporter[]{reporter}) + SingleSdkRobolectricTestRunner.defaultInjector() + .bind(PerfStatsReporter[].class, new PerfStatsReporter[] {reporter}) .build()); runner.run(notifier); @@ -275,7 +275,7 @@ public class RobolectricTestRunnerTest { RobolectricTestRunner runner = new SingleSdkRobolectricTestRunner( TestThatFails.class, - RobolectricTestRunner.defaultInjector() + SingleSdkRobolectricTestRunner.defaultInjector() .bind(PerfStatsReporter[].class, new PerfStatsReporter[] {reporter}) .build()); diff --git a/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java b/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java index 9a7fb0ddc..f2395b788 100644 --- a/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java +++ b/robolectric/src/test/java/org/robolectric/android/DeviceConfigTest.java @@ -7,6 +7,7 @@ import android.content.res.Configuration; import android.os.Build.VERSION_CODES; import android.util.DisplayMetrics; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.Locale; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -34,61 +35,100 @@ public class DeviceConfigTest { : ""; } - @Test @Config(minSdk = VERSION_CODES.JELLY_BEAN_MR1) - public void applyToConfiguration() throws Exception { + @Test + @Config(minSdk = VERSION_CODES.JELLY_BEAN_MR1) + public void applyToConfiguration() { applyQualifiers("en-rUS-w400dp-h800dp-notround"); assertThat(asQualifierString()) .isEqualTo("en-rUS-ldltr-w400dp-h800dp-notround"); } @Test - public void applyToConfiguration_isCumulative() throws Exception { - applyQualifiers("en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + public void applyToConfiguration_isCumulative() { + applyQualifiers( + "en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); applyQualifiers("fr-land"); assertThat(asQualifierString()) - .isEqualTo("fr-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "fr-ldltr-sw400dp-w400dp-h800dp-normal-notlong-notround-" + + optsForO + + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); applyQualifiers("w500dp-large-television-night-xxhdpi-notouch-keyshidden"); assertThat(asQualifierString()) - .isEqualTo("fr-ldltr-sw400dp-w500dp-large-notlong-notround-" + optsForO + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav"); + .isEqualTo( + "fr-ldltr-sw400dp-w500dp-large-notlong-notround-" + + optsForO + + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav"); applyQualifiers("long"); assertThat(asQualifierString()) - .isEqualTo("fr-ldltr-sw400dp-w500dp-large-long-notround-" + optsForO + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav"); + .isEqualTo( + "fr-ldltr-sw400dp-w500dp-large-long-notround-" + + optsForO + + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav"); applyQualifiers("round"); assertThat(asQualifierString()) - .isEqualTo("fr-ldltr-sw400dp-w500dp-large-long-round-" + optsForO + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav"); + .isEqualTo( + "fr-ldltr-sw400dp-w500dp-large-long-round-" + + optsForO + + "land-television-night-xxhdpi-notouch-keyshidden-nokeys-navhidden-nonav"); } @Test - public void applyRules_defaults() throws Exception { + public void applyRules_defaults() { DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } // todo: this fails on JELLY_BEAN and LOLLIPOP through M... why? - @Test @Config(minSdk = VERSION_CODES.N) - public void applyRules_rtlScript() throws Exception { - applyQualifiers("he"); + @Test + @Config(minSdk = VERSION_CODES.N) + public void applyRules_rtlScript() { + String language = "he"; + applyQualifiers(language); DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); - + // Locale's constructor has always converted three language codes to their earlier, obsoleted + // forms: he maps to iw, yi maps to ji, and id maps to in. Since Java SE 17, this is no longer + // the case. Each language maps to its new form; iw maps to he, ji maps to yi, and in maps to + // id. + // See + // https://stackoverflow.com/questions/8202406/locale-code-for-hebrew-reference-to-other-locale-codes/70882234#70882234, + // and https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Locale.html. + // To make sure this test can work with different JDK versions, using the following workaround. + Locale locale = new Locale(language); assertThat(asQualifierString()) - .isEqualTo("iw-ldrtl-sw320dp-w320dp-h470dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + locale.getLanguage() + + "-ldrtl-sw320dp-w320dp-h470dp-normal-notlong-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } @Test - public void applyRules_heightWidth() throws Exception { + public void applyRules_heightWidth() { applyQualifiers("w800dp-h400dp"); DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw400dp-w800dp-h400dp-normal-long-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw400dp-w800dp-h400dp-normal-long-notround-" + + optsForO + + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } @Test @@ -97,51 +137,70 @@ public class DeviceConfigTest { DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw400dp-w400dp-h800dp-normal-long-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } @Test - public void applyRules_sizeToDimens() throws Exception { + public void applyRules_sizeToDimens() { applyQualifiers("large-land"); DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw480dp-w640dp-h480dp-large-notlong-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw480dp-w640dp-h480dp-large-notlong-notround-" + + optsForO + + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } @Test - public void applyRules_sizeFromDimens() throws Exception { + public void applyRules_sizeFromDimens() { applyQualifiers("w800dp-h640dp"); DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw640dp-w800dp-h640dp-large-notlong-notround-" + optsForO + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw640dp-w800dp-h640dp-large-notlong-notround-" + + optsForO + + "land-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } @Test - public void applyRules_longIncreasesHeight() throws Exception { + public void applyRules_longIncreasesHeight() { applyQualifiers("long"); DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h587dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw320dp-w320dp-h587dp-normal-long-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } @Test - public void applyRules_greatHeightTriggersLong() throws Exception { + public void applyRules_greatHeightTriggersLong() { applyQualifiers("h590dp"); DeviceConfig.applyRules(configuration, displayMetrics, apiLevel); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } - @Ignore("consider how to reset uiMode type") @Test - public void shouldParseButNotDisplayNormal() throws Exception { + @Ignore("consider how to reset uiMode type") + @Test + public void shouldParseButNotDisplayNormal() { applyQualifiers("car"); applyQualifiers("+normal"); assertThat(asQualifierString()) - .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); + .isEqualTo( + "en-rUS-ldltr-sw320dp-w320dp-h590dp-normal-long-notround-" + + optsForO + + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav"); } @Test diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentApplicationInfoTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentApplicationInfoTest.java new file mode 100644 index 000000000..be5870b31 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentApplicationInfoTest.java @@ -0,0 +1,24 @@ +package org.robolectric.android.internal; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +@Config(application = AndroidTestEnvironmentApplicationInfoTest.ThisApplication.class) +@RunWith(AndroidJUnit4.class) +public final class AndroidTestEnvironmentApplicationInfoTest { + + @Test + public void testApplicationInfoIncludesConfiguredAppClass() { + Application app = ApplicationProvider.getApplicationContext(); + assertThat(app).isInstanceOf(ThisApplication.class); + assertThat(app.getApplicationInfo().className).isEqualTo(ThisApplication.class.getName()); + } + + public static final class ThisApplication extends Application {} +} diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java index dc6ac3138..0e6874c65 100644 --- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java +++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java @@ -26,6 +26,7 @@ import org.robolectric.annotation.Config; import org.robolectric.manifest.AndroidManifest; import org.robolectric.shadows.ShadowApplication; import org.robolectric.shadows.testing.TestApplication; +import org.robolectric.util.ReflectionHelpers; @RunWith(AndroidJUnit4.class) public class AndroidTestEnvironmentCreateApplicationTest { @@ -37,7 +38,7 @@ public class AndroidTestEnvironmentCreateApplicationTest { assertThrows( RuntimeException.class, () -> - AndroidTestEnvironment.createApplication( + createApplication( newConfigWith( "<application android:name=\"org.robolectric.BogusTestApplication\"/>)"), null, @@ -47,18 +48,18 @@ public class AndroidTestEnvironmentCreateApplicationTest { @Test public void shouldReturnDefaultAndroidApplicationWhenManifestDeclaresNoAppName() throws Exception { - Application application = AndroidTestEnvironment.createApplication(newConfigWith(""), null, - new ApplicationInfo()); + Application application = createApplication(newConfigWith(""), null, new ApplicationInfo()); assertThat(application.getClass()).isEqualTo(Application.class); } @Test public void shouldReturnSpecifiedApplicationWhenManifestDeclaresAppName() throws Exception { Application application = - AndroidTestEnvironment.createApplication( + createApplication( newConfigWith( "<application android:name=\"org.robolectric.shadows.testing.TestApplication\"/>"), - null, null); + null, + null); assertThat(application.getClass()).isEqualTo(TestApplication.class); } @@ -85,8 +86,7 @@ public class AndroidTestEnvironmentCreateApplicationTest { + " </intent-filter>" + " </receiver>" + "</application>"); - Application application = AndroidTestEnvironment.createApplication(appManifest, null, - new ApplicationInfo()); + Application application = createApplication(appManifest, null, new ApplicationInfo()); shadowOf(application).callAttach(RuntimeEnvironment.systemContext); registerBroadcastReceivers(application, appManifest, null); @@ -109,27 +109,30 @@ public class AndroidTestEnvironmentCreateApplicationTest { @Test public void shouldLoadConfigApplicationIfSpecified() throws Exception { Application application = - AndroidTestEnvironment.createApplication( + createApplication( newConfigWith("<application android:name=\"" + "ClassNameToIgnore" + "\"/>"), - new Config.Builder().setApplication(TestFakeApp.class).build(), null); + new Config.Builder().setApplication(TestFakeApp.class).build(), + null); assertThat(application.getClass()).isEqualTo(TestFakeApp.class); } @Test public void shouldLoadConfigInnerClassApplication() throws Exception { Application application = - AndroidTestEnvironment.createApplication( + createApplication( newConfigWith("<application android:name=\"" + "ClassNameToIgnore" + "\"/>"), - new Config.Builder().setApplication(TestFakeAppInner.class).build(), null); + new Config.Builder().setApplication(TestFakeAppInner.class).build(), + null); assertThat(application.getClass()).isEqualTo(TestFakeAppInner.class); } @Test public void shouldLoadTestApplicationIfClassIsPresent() throws Exception { Application application = - AndroidTestEnvironment.createApplication( + createApplication( newConfigWith("<application android:name=\"" + FakeApp.class.getName() + "\"/>"), - null, null); + null, + null); assertThat(application.getClass()).isEqualTo(TestFakeApp.class); } @@ -137,7 +140,7 @@ public class AndroidTestEnvironmentCreateApplicationTest { public void shouldLoadPackageApplicationIfClassIsPresent() { final ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.className = TestApplication.class.getCanonicalName(); - Application application = AndroidTestEnvironment.createApplication(null, null, applicationInfo); + Application application = createApplication(null, null, applicationInfo); assertThat(application.getClass()).isEqualTo(TestApplication.class); } @@ -145,7 +148,7 @@ public class AndroidTestEnvironmentCreateApplicationTest { public void shouldLoadTestPackageApplicationIfClassIsPresent() { final ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.className = FakeApp.class.getCanonicalName(); - Application application = AndroidTestEnvironment.createApplication(null, null, applicationInfo); + Application application = createApplication(null, null, applicationInfo); assertThat(application.getClass()).isEqualTo(TestFakeApp.class); } @@ -154,15 +157,15 @@ public class AndroidTestEnvironmentCreateApplicationTest { try { final ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.className = "org.robolectric.BogusTestApplication"; - AndroidTestEnvironment.createApplication(null, null, applicationInfo); + createApplication(null, null, applicationInfo); fail(); - } catch (RuntimeException expected) { } + } catch (RuntimeException expected) { + } } @Test public void whenNoAppManifestPresent_shouldCreateGenericApplication() { - Application application = AndroidTestEnvironment.createApplication(null, null, - new ApplicationInfo()); + Application application = createApplication(null, null, new ApplicationInfo()); assertThat(application.getClass()).isEqualTo(Application.class); } @@ -190,4 +193,10 @@ public class AndroidTestEnvironmentCreateApplicationTest { } public static class TestFakeAppInner extends Application {} + + private static Application createApplication( + AndroidManifest appManifest, Config config, ApplicationInfo applicationInfo) { + return ReflectionHelpers.callConstructor( + AndroidTestEnvironment.getApplicationClass(appManifest, config, applicationInfo)); + } } diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java index 375699417..54828ea5d 100644 --- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java +++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java @@ -121,7 +121,6 @@ public class AndroidTestEnvironmentTest { @ConscryptMode(ON) public void testWhenConscryptModeOn_ConscryptInstalled() throws CertificateException, NoSuchAlgorithmException { - bootstrapWrapper.callSetUpApplicationState(); CertificateFactory factory = CertificateFactory.getInstance("X.509"); assertThat(factory.getProvider().getName()).isEqualTo("Conscrypt"); @@ -142,7 +141,6 @@ public class AndroidTestEnvironmentTest { @ConscryptMode(OFF) public void testWhenConscryptModeOff_ConscryptNotInstalled() throws CertificateException, NoSuchAlgorithmException { - bootstrapWrapper.callSetUpApplicationState(); CertificateFactory factory = CertificateFactory.getInstance("X.509"); assertThat(factory.getProvider().getName()).isNotEqualTo("Conscrypt"); @@ -154,12 +152,12 @@ public class AndroidTestEnvironmentTest { @Test @ConscryptMode(OFF) public void testWhenConscryptModeOff_BouncyCastleInstalled() throws GeneralSecurityException { - bootstrapWrapper.callSetUpApplicationState(); - MessageDigest digest = MessageDigest.getInstance("SHA256"); + MessageDigest digest = MessageDigest.getInstance("SHA256", BouncyCastleProvider.PROVIDER_NAME); assertThat(digest.getProvider().getName()).isEqualTo(BouncyCastleProvider.PROVIDER_NAME); - Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); + Cipher aesCipher = + Cipher.getInstance("AES/CBC/PKCS7Padding", BouncyCastleProvider.PROVIDER_NAME); assertThat(aesCipher.getProvider().getName()).isEqualTo(BouncyCastleProvider.PROVIDER_NAME); } diff --git a/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java index b1d848968..49dd13dbc 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/AssociationInfoBuilderTest.java @@ -9,6 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; @RunWith(AndroidJUnit4.class) public final class AssociationInfoBuilderTest { @@ -22,10 +23,20 @@ public final class AssociationInfoBuilderTest { private static final boolean NOTIFY_ON_DEVICE_NEARBY = true; private static final long APPROVED_MS = 1234L; private static final long LAST_TIME_CONNECTED_MS = 5678L; + private static final int SYSTEM_DATA_SYNC_FALGS = 7; @Test @Config(minSdk = VERSION_CODES.TIRAMISU) public void obtain() { + Object associatedDeviceValue = null; + if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) { + try { + Class<?> associatedDeviceClazz = Class.forName("android.companion.AssociatedDevice"); + associatedDeviceValue = ReflectionHelpers.newInstance(associatedDeviceClazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + } AssociationInfo info = AssociationInfoBuilder.newBuilder() .setId(ID) @@ -33,11 +44,13 @@ public final class AssociationInfoBuilderTest { .setPackageName(PACKAGE_NAME) .setDeviceMacAddress(DEVICE_MAC_ADDRESS) .setDisplayName(DISPLAY_NAME) + .setAssociatedDevice(associatedDeviceValue) .setDeviceProfile(DEVICE_PROFILE) .setSelfManaged(SELF_MANAGED) .setNotifyOnDeviceNearby(NOTIFY_ON_DEVICE_NEARBY) .setApprovedMs(APPROVED_MS) .setLastTimeConnectedMs(LAST_TIME_CONNECTED_MS) + .setSystemDataSyncFlags(SYSTEM_DATA_SYNC_FALGS) .build(); assertThat(info.getId()).isEqualTo(ID); @@ -50,5 +63,13 @@ public final class AssociationInfoBuilderTest { assertThat(info.isNotifyOnDeviceNearby()).isEqualTo(NOTIFY_ON_DEVICE_NEARBY); assertThat(info.getTimeApprovedMs()).isEqualTo(APPROVED_MS); assertThat(info.getLastTimeConnectedMs()).isEqualTo(LAST_TIME_CONNECTED_MS); + + if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) { + Object associatedDevice = ReflectionHelpers.callInstanceMethod(info, "getAssociatedDevice"); + assertThat(associatedDevice).isEqualTo(associatedDeviceValue); + int systemDataSyncFlags = + ReflectionHelpers.callInstanceMethod(info, "getSystemDataSyncFlags"); + assertThat(systemDataSyncFlags).isEqualTo(SYSTEM_DATA_SYNC_FALGS); + } } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java index 06769c66e..f57b4842b 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java @@ -166,12 +166,39 @@ public class MediaCodecInfoBuilderTest { @Test @Config(minSdk = Q) + public void canCreateVideoEncoderCapabilities_supportedFormatResolutionRangeIsSet() { + MediaFormat formatWithResolutionRange = AVC_MEDIA_FORMAT; + + final int kMinDimension = 64; + formatWithResolutionRange.setInteger(MediaFormat.KEY_WIDTH, kMinDimension); + formatWithResolutionRange.setInteger(MediaFormat.KEY_HEIGHT, kMinDimension); + formatWithResolutionRange.setInteger(MediaFormat.KEY_MAX_WIDTH, WIDTH); + formatWithResolutionRange.setInteger(MediaFormat.KEY_MAX_HEIGHT, HEIGHT); + + CodecCapabilities codecCapabilities = + MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() + .setMediaFormat(formatWithResolutionRange) + .setIsEncoder(true) + .setProfileLevels(AVC_PROFILE_LEVELS) + .setColorFormats(AVC_COLOR_FORMATS) + .build(); + + assertThat(codecCapabilities.getVideoCapabilities()).isNotNull(); + assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths()) + .isEqualTo(new Range<>(kMinDimension, WIDTH)); + assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights()) + .isEqualTo(new Range<>(kMinDimension, HEIGHT)); + } + + @Test + @Config(minSdk = Q) public void canCreateVideoDecoderCapabilities() { CodecCapabilities codecCapabilities = MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() .setMediaFormat(VP9_MEDIA_FORMAT) .setProfileLevels(VP9_PROFILE_LEVELS) .setColorFormats(VP9_COLOR_FORMATS) + .setRequiredFeatures(new String[] {CodecCapabilities.FEATURE_SecurePlayback}) .build(); assertThat(codecCapabilities.getMimeType()).isEqualTo(MIMETYPE_VIDEO_VP9); @@ -180,6 +207,8 @@ public class MediaCodecInfoBuilderTest { assertThat(codecCapabilities.getEncoderCapabilities()).isNull(); assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback)) .isTrue(); + assertThat(codecCapabilities.isFeatureRequired(CodecCapabilities.FEATURE_SecurePlayback)) + .isTrue(); assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_MultipleFrames)) .isTrue(); assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_DynamicTimestamp)) diff --git a/robolectric/src/test/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilderTest.java new file mode 100644 index 000000000..0944ae35f --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilderTest.java @@ -0,0 +1,122 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static com.google.common.truth.Truth.assertThat; + +import android.telephony.CellIdentity; +import android.telephony.DataSpecificRegistrationInfo; +import android.telephony.NetworkRegistrationInfo; +import android.telephony.VoiceSpecificRegistrationInfo; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +/** Test for {@link NetworkRegistrationInfoTestBuilder}. */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = Q) +public class NetworkRegistrationInfoTestBuilderTest { + + private final List<Integer> intList = new ArrayList<>(); + @Mock protected CellIdentity cellIdentity; + + @Test + public void testSetAccessNetworkTechnology_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setAccessNetworkTechnology(10).build(); + assertThat(networkRegistrationInfo.getAccessNetworkTechnology()).isEqualTo(10); + } + + @Test + public void testSetAvailableServices_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setAvailableServices(intList).build(); + assertThat(networkRegistrationInfo.getAvailableServices()).isEqualTo(intList); + } + + @Test + public void testSetCellIdentity_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setCellIdentity(cellIdentity).build(); + assertThat(networkRegistrationInfo.getCellIdentity()).isEqualTo(cellIdentity); + } + + @Test + public void testSetDataSpecificInfo_isSetInResultingObject() { + DataSpecificRegistrationInfo dataSpecificRegistrationInfo = + ReflectionHelpers.callConstructor(DataSpecificRegistrationInfo.class); + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder() + .setDataSpecificInfo(dataSpecificRegistrationInfo) + .build(); + assertThat(networkRegistrationInfo.getDataSpecificInfo()) + .isEqualTo(dataSpecificRegistrationInfo); + } + + @Test + public void testSetDomain_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setDomain(10).build(); + assertThat(networkRegistrationInfo.getDomain()).isEqualTo(10); + } + + @Test + public void testSetEmergencyOnly_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setEmergencyOnly(true).build(); + assertThat(networkRegistrationInfo.isEmergencyEnabled()).isEqualTo(true); + } + + @Test + public void testSetRegistrationState_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setRegistrationState(10).build(); + assertThat(networkRegistrationInfo.getRegistrationState()).isEqualTo(10); + } + + @Test + public void testSetRejectCause_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setRejectCause(10).build(); + assertThat(networkRegistrationInfo.getRejectCause()).isEqualTo(10); + } + + @Test + public void testSetRoamingType_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setRoamingType(10).build(); + assertThat(networkRegistrationInfo.getRoamingType()).isEqualTo(10); + } + + @Test + @Config(minSdk = R) + public void testSetRegisteredPlmn_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setRegisteredPlmn("string").build(); + assertThat(networkRegistrationInfo.getRegisteredPlmn()).isEqualTo("string"); + } + + @Test + public void testSetTransportType_isSetInResultingObject() { + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder().setTransportType(10).build(); + assertThat(networkRegistrationInfo.getTransportType()).isEqualTo(10); + } + + @Test + public void testSetVoiceSpecificInfo_isSetInResultingObject() { + VoiceSpecificRegistrationInfo voiceSpecificRegistrationInfo = + ReflectionHelpers.callConstructor(VoiceSpecificRegistrationInfo.class); + NetworkRegistrationInfo networkRegistrationInfo = + NetworkRegistrationInfoTestBuilder.newBuilder() + .setVoiceSpecificInfo(voiceSpecificRegistrationInfo) + .build(); + assertThat(networkRegistrationInfo.getVoiceSpecificInfo()) + .isEqualTo(voiceSpecificRegistrationInfo); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java index 26d92bc5d..45541f5b4 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/PreciseDataConnectionStateBuilderTest.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN; import static com.google.common.truth.Truth.assertThat; import android.os.Build; @@ -14,7 +15,7 @@ import org.robolectric.annotation.Config; /** Test for {@link PreciseDataConnectionStateBuilder} */ @RunWith(AndroidJUnit4.class) -@Config(minSdk = Build.VERSION_CODES.R) +@Config(minSdk = Build.VERSION_CODES.S) public class PreciseDataConnectionStateBuilderTest { @Test public void build_preciseDataConnectionState() { @@ -23,6 +24,7 @@ public class PreciseDataConnectionStateBuilderTest { PreciseDataConnectionStateBuilder.newBuilder() .setDataState(TelephonyManager.DATA_DISCONNECTED) .setNetworkType(TelephonyManager.NETWORK_TYPE_LTE) + .setTransportType(TRANSPORT_TYPE_WWAN) .setApnSetting(apnSetting) .setDataFailCause(DataFailCause.IMEI_NOT_ACCEPTED) .build(); @@ -30,6 +32,7 @@ public class PreciseDataConnectionStateBuilderTest { assertThat(state).isNotNull(); assertThat(state.getState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED); assertThat(state.getNetworkType()).isEqualTo(TelephonyManager.NETWORK_TYPE_LTE); + assertThat(state.getTransportType()).isEqualTo(TRANSPORT_TYPE_WWAN); assertThat(state.getLastCauseCode()).isEqualTo(DataFailCause.IMEI_NOT_ACCEPTED); assertThat(state.getApnSetting()).isEqualTo(apnSetting); } @@ -40,12 +43,14 @@ public class PreciseDataConnectionStateBuilderTest { PreciseDataConnectionStateBuilder.newBuilder() .setDataState(TelephonyManager.DATA_DISCONNECTED) .setNetworkType(TelephonyManager.NETWORK_TYPE_LTE) + .setTransportType(TRANSPORT_TYPE_WWAN) .setDataFailCause(DataFailCause.IMEI_NOT_ACCEPTED) .build(); assertThat(state).isNotNull(); assertThat(state.getState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED); assertThat(state.getNetworkType()).isEqualTo(TelephonyManager.NETWORK_TYPE_LTE); + assertThat(state.getTransportType()).isEqualTo(TRANSPORT_TYPE_WWAN); assertThat(state.getLastCauseCode()).isEqualTo(DataFailCause.IMEI_NOT_ACCEPTED); assertThat(state.getApnSetting()).isNull(); } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ServiceStateBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/ServiceStateBuilderTest.java new file mode 100644 index 000000000..620ce51a4 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ServiceStateBuilderTest.java @@ -0,0 +1,126 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static com.google.common.truth.Truth.assertThat; + +import android.telephony.NetworkRegistrationInfo; +import android.telephony.ServiceState; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +/** Test for {@link ShadowServiceState}. */ +@RunWith(AndroidJUnit4.class) +@Config(minSdk = P) +public class ServiceStateBuilderTest { + + static final int[] INT_ARRAY = {1, 2, 3}; + + @Test + public void testServiceStateBuilder_setVoiceRegStateAndBuild_isSetInResultingObject() { + // These public APIs expected to be available in all SDKs in range. + ServiceState serviceState = ServiceStateBuilder.newBuilder().setVoiceRegState(10).build(); + assertThat(serviceState.getVoiceRegState()).isEqualTo(10); + } + + @Test + public void testServiceStateBuilder_setDataRegStateAndBuild_isSetInResultingObject() { + ServiceState serviceState = ServiceStateBuilder.newBuilder().setDataRegState(10).build(); + assertThat(serviceState.getDataRegState()).isEqualTo(10); + } + + @Test + public void testServiceStateBuilder_setIsManualSelectionAndBuild_isSetInResultingObject() { + ServiceState serviceState = ServiceStateBuilder.newBuilder().setIsManualSelection(true).build(); + assertThat(serviceState.getIsManualSelection()).isTrue(); + } + + @Test + public void testServiceStateBuilder_setRoamingAndBuild_isSetInResultingObject() { + ServiceState serviceState = ServiceStateBuilder.newBuilder().setRoaming(true).build(); + assertThat(serviceState.getRoaming()).isTrue(); + } + + @Test + public void testServiceStateBuilder_setEmergencyOnlyAndBuild_isSetInResultingObject() { + ServiceState serviceState = ServiceStateBuilder.newBuilder().setEmergencyOnly(true).build(); + assertThat(serviceState.isEmergencyOnly()).isTrue(); + } + + @Test + public void testServiceStateBuilder_setChannelNumberAndBuild_isSetInResultingObject() { + ServiceState serviceState = ServiceStateBuilder.newBuilder().setChannelNumber(10).build(); + assertThat(serviceState.getChannelNumber()).isEqualTo(10); + } + + @Test + public void testServiceStateBuilder_setCellBandwidthsAndBuild_isSetInResultingObject() { + ServiceState serviceState = + ServiceStateBuilder.newBuilder().setCellBandwidths(INT_ARRAY).build(); + assertThat(serviceState.getCellBandwidths()).isEqualTo(INT_ARRAY); + } + + @Config(minSdk = Q) + @Test + public void testServiceStateBuilder_setNrFrequencyRangeAndBuild_isSetInResultingObject() { + ServiceState serviceState = ServiceStateBuilder.newBuilder().setNrFrequencyRange(10).build(); + assertThat(serviceState.getNrFrequencyRange()).isEqualTo(10); + } + + @Config(minSdk = R) + @Test + public void testServiceStateBuilder_setOperatorNameAndBuild_isSetInResultingObject() { + ServiceState serviceState = + ServiceStateBuilder.newBuilder().setOperatorName("string", "string", "string").build(); + assertThat(serviceState.getOperatorAlphaLong()).isEqualTo("string"); + assertThat(serviceState.getOperatorAlphaShort()).isEqualTo("string"); + assertThat(serviceState.getOperatorNumeric()).isEqualTo("string"); + } + + @Config(minSdk = R) + @Test + public void testServiceStateBuilder_setIwlanPreferredAndBuild_isSetInResultingObjectField() { + ServiceState serviceState = ServiceStateBuilder.newBuilder().setIwlanPreferred(true).build(); + assertThat((boolean) ReflectionHelpers.getField(serviceState, "mIsIwlanPreferred")).isTrue(); + } + + @Config(minSdk = R) + @Test + public void + testServiceStateBuilder_setDataRoamingFromRegistrationAndBuild_isSetInResultingObjectField() { + ServiceState serviceState = + ServiceStateBuilder.newBuilder().setDataRoamingFromRegistration(true).build(); + assertThat((boolean) ReflectionHelpers.getField(serviceState, "mIsDataRoamingFromRegistration")) + .isTrue(); + } + + @Config(sdk = {P}) + @Test + public void + testServiceStateBuilder_setIsUsingCarrierAggregationAndBuild_isSetInResultingObjectField() { + ServiceState serviceState = + ServiceStateBuilder.newBuilder().setIsUsingCarrierAggregation(true).build(); + assertThat((boolean) ReflectionHelpers.getField(serviceState, "mIsUsingCarrierAggregation")) + .isTrue(); + } + + @Config(minSdk = Q) + @Test + public void + testServiceStateBuilder_setNetworkRegistrationInfoListAndBuild_isSetInResultingObjectField() { + List<NetworkRegistrationInfo> networkRegistrationInfoList = new ArrayList<>(); + networkRegistrationInfoList.add(new NetworkRegistrationInfo.Builder().build()); + ServiceState serviceState = + ServiceStateBuilder.newBuilder() + .setNetworkRegistrationInfoList(networkRegistrationInfoList) + .build(); + assertThat(serviceState.getNetworkRegistrationInfoList()) + .isEqualTo(networkRegistrationInfoList); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataInputTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataInputTest.java new file mode 100644 index 000000000..4b0359800 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataInputTest.java @@ -0,0 +1,188 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataInputStream; +import android.os.Build.VERSION_CODES; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +@RunWith(AndroidJUnit4.class) +@Config(minSdk = VERSION_CODES.LOLLIPOP) +public final class ShadowBackupDataInputTest { + + private static final String TEST_KEY_1 = "key_1"; + private static final byte[] TEST_DATA_1 = {1, 2, 3, 4}; + private static final String TEST_KEY_2 = "key_2"; + private static final byte[] TEST_DATA_2 = {5, 6, 7, 8}; + + private final BackupDataInput backupDataInput = + BackupDataInputBuilder.newBuilder() + .addEntity(BackupDataEntity.create(TEST_KEY_1, TEST_DATA_1)) + .addEntity(BackupDataEntity.create(TEST_KEY_2, TEST_DATA_2)) + .build(); + + @Test + public void readNextHeader_onFirstItem_returnsTrue() throws IOException { + boolean result = backupDataInput.readNextHeader(); + + assertThat(result).isTrue(); + } + + @Test + public void readNextHeader_afterReadData_returnsTrue() throws IOException { + backupDataInput.readNextHeader(); + backupDataInput.readEntityData(new byte[TEST_DATA_1.length], 0, TEST_DATA_1.length); + + boolean result = backupDataInput.readNextHeader(); + + assertThat(result).isTrue(); + } + + @Test + public void readNextHeader_afterSkipData_returnsTrue() throws IOException { + backupDataInput.readNextHeader(); + backupDataInput.skipEntityData(); + + boolean result = backupDataInput.readNextHeader(); + + assertThat(result).isTrue(); + } + + @Test + public void readNextHeader_afterLastItem_returnsFalse() throws IOException { + backupDataInput.readNextHeader(); + backupDataInput.skipEntityData(); + backupDataInput.readNextHeader(); + backupDataInput.skipEntityData(); + + boolean result = backupDataInput.readNextHeader(); + + assertThat(result).isFalse(); + } + + @Test + public void readNextHeader_withoutReadOrSkipData_throwsException() throws IOException { + backupDataInput.readNextHeader(); + + assertThrows(IOException.class, backupDataInput::readNextHeader); + } + + @Test + public void getKey_beforeReadNextHeader_throwsException() { + assertThrows(IllegalStateException.class, backupDataInput::getKey); + } + + @Test + public void getKey_afterReadNextHeader_returnsKey() throws IOException { + backupDataInput.readNextHeader(); + + String key = backupDataInput.getKey(); + + assertThat(key).isEqualTo(TEST_KEY_1); + } + + @Test + public void getDataSize_beforeReadNextHeader_throwsException() { + assertThrows(IllegalStateException.class, backupDataInput::getDataSize); + } + + @Test + public void getDataSize_afterReadNextHeader_returnsArrayLength() throws IOException { + backupDataInput.readNextHeader(); + + int dataSize = backupDataInput.getDataSize(); + + assertThat(dataSize).isEqualTo(TEST_DATA_1.length); + } + + @Test + public void readEntityData_afterLastItem_throwsException() throws IOException { + backupDataInput.readNextHeader(); + backupDataInput.skipEntityData(); + backupDataInput.readNextHeader(); + backupDataInput.skipEntityData(); + backupDataInput.readNextHeader(); + + assertThrows( + IllegalStateException.class, + () -> backupDataInput.readEntityData(TEST_DATA_1, 0, TEST_DATA_1.length)); + } + + @Test + public void readEntityData_afterAllBytesRead_returns0() throws IOException { + backupDataInput.readNextHeader(); + backupDataInput.readEntityData(new byte[TEST_DATA_1.length], 0, TEST_DATA_1.length); + + int result = + backupDataInput.readEntityData(new byte[TEST_DATA_1.length], 0, TEST_DATA_1.length); + + assertThat(result).isEqualTo(0); + } + + @Test + public void readEntityData_withSizeGreaterThanDestination_throwsException() throws IOException { + backupDataInput.readNextHeader(); + + assertThrows( + IOException.class, + () -> backupDataInput.readEntityData(new byte[2], 0, TEST_DATA_1.length)); + } + + @Test + public void readEntityData_withFullDataRead_copiesSourceData() throws IOException { + backupDataInput.readNextHeader(); + byte[] data = new byte[TEST_DATA_1.length]; + + int result = backupDataInput.readEntityData(data, 0, data.length); + + assertThat(result).isEqualTo(TEST_DATA_1.length); + assertThat(data).isEqualTo(TEST_DATA_1); + } + + @Test + public void readEntityData_withPartialDataRead_copiesSourceData() throws IOException { + backupDataInput.readNextHeader(); + byte[] data1 = new byte[2]; + byte[] data2 = new byte[2]; + + int result1 = backupDataInput.readEntityData(data1, 0, 2); + int result2 = backupDataInput.readEntityData(data2, 0, 2); + + assertThat(result1).isEqualTo(2); + assertThat(result2).isEqualTo(2); + assertThat(data1).isEqualTo(new byte[] {1, 2}); + assertThat(data2).isEqualTo(new byte[] {3, 4}); + } + + @Test + public void readEntityData_usingBackupDataInputStream_matchesSourceData() throws IOException { + backupDataInput.readNextHeader(); + InputStream inputStream = + new BufferedInputStream( + reflector(BackupDataInputStreamReflector.class).newInstance(backupDataInput), 1); + byte[] data = new byte[TEST_DATA_1.length]; + + int result = inputStream.read(data); + + assertThat(result).isEqualTo(TEST_DATA_1.length); + assertThat(data).isEqualTo(TEST_DATA_1); + } + + @ForType(BackupDataInputStream.class) + private interface BackupDataInputStreamReflector { + + @Constructor + BackupDataInputStream newInstance(BackupDataInput backupDataInput); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataOutputTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataOutputTest.java new file mode 100644 index 000000000..ac222a899 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBackupDataOutputTest.java @@ -0,0 +1,103 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.backup.BackupDataOutput; +import android.os.Build.VERSION_CODES; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.util.reflector.ForType; + +@RunWith(AndroidJUnit4.class) +@Config(minSdk = VERSION_CODES.LOLLIPOP) +public final class ShadowBackupDataOutputTest { + + private static final String TEST_PREFIX = "prefix"; + private static final String TEST_KEY = "key"; + private static final byte[] TEST_DATA = {1, 2, 3, 4}; + + private final BackupDataOutput backupDataOutput = BackupDataOutputFactory.newInstance(); + + @Test + public void writeEntityHeader_withUnfinishedData_throwsException() throws IOException { + backupDataOutput.writeEntityHeader(TEST_KEY, TEST_DATA.length); + + assertThrows(IOException.class, () -> backupDataOutput.writeEntityHeader("key_2", 5)); + } + + @Test + public void writeEntityData_withKeyPrefix_hasPrefixInEntityKey() throws IOException { + reflector(BackupDataOutputReflector.class, backupDataOutput).setKeyPrefix(TEST_PREFIX); + backupDataOutput.writeEntityHeader(TEST_KEY, 0); + + assertThat(shadowOf(backupDataOutput).getEntities()) + .containsExactly( + BackupDataEntity.create( + TEST_PREFIX + ShadowBackupDataOutput.KEY_PREFIX_JOINER + TEST_KEY, new byte[0])); + } + + @Test + public void writeEntityData_withNonNegativeDataSize_addsEntityOfSize() throws IOException { + backupDataOutput.writeEntityHeader(TEST_KEY, 5); + + assertThat(shadowOf(backupDataOutput).getEntities()) + .containsExactly(BackupDataEntity.create(TEST_KEY, new byte[5])); + } + + @Test + public void writeEntityData_withNegativeDataSize_addsDeletedEntity() throws IOException { + backupDataOutput.writeEntityHeader(TEST_KEY, -1); + + assertThat(shadowOf(backupDataOutput).getEntities()) + .containsExactly(BackupDataEntity.createDeletedEntity(TEST_KEY)); + } + + @Test + public void writeEntityData_withGreaterSizeThanSource_throwsException() throws IOException { + backupDataOutput.writeEntityHeader(TEST_KEY, 10); + + assertThrows(IOException.class, () -> backupDataOutput.writeEntityData(new byte[5], 10)); + } + + @Test + public void writeEntityData_withGreaterSizeThanDestination_throwsException() throws IOException { + backupDataOutput.writeEntityHeader(TEST_KEY, 2); + + assertThrows( + IOException.class, () -> backupDataOutput.writeEntityData(TEST_DATA, TEST_DATA.length)); + } + + @Test + public void writeEntityData_withFullDataWrite_addsCorrectEntityData() throws IOException { + backupDataOutput.writeEntityHeader(TEST_KEY, TEST_DATA.length); + + backupDataOutput.writeEntityData(TEST_DATA, TEST_DATA.length); + + assertThat(shadowOf(backupDataOutput).getEntities()) + .containsExactly(BackupDataEntity.create(TEST_KEY, TEST_DATA)); + } + + @Test + public void writeEntityData_withPartialDataWrites_addsCorrectEntityData() throws IOException { + backupDataOutput.writeEntityHeader(TEST_KEY, 4); + + backupDataOutput.writeEntityData(Arrays.copyOfRange(TEST_DATA, 0, 2), 2); + backupDataOutput.writeEntityData(Arrays.copyOfRange(TEST_DATA, 2, 4), 2); + + assertThat(shadowOf(backupDataOutput).getEntities()) + .containsExactly(BackupDataEntity.create(TEST_KEY, TEST_DATA)); + } + + @ForType(BackupDataOutput.class) + private interface BackupDataOutputReflector { + + void setKeyPrefix(String prefix); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java index 9fa2ef2b9..b94d94e94 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java @@ -13,32 +13,42 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.robolectric.Shadows.shadowOf; +import android.Manifest.permission; import android.app.Application; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothProfile.ServiceListener; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothStatusCodes; import android.bluetooth.le.BluetoothLeScanner; import android.content.Intent; +import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.time.Duration; import java.util.Set; import java.util.UUID; +import java.util.concurrent.LinkedBlockingQueue; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.versioning.AndroidVersions.U; /** Unit tests for {@link ShadowBluetoothAdapter} */ @RunWith(AndroidJUnit4.class) @@ -600,7 +610,7 @@ public class ShadowBluetoothAdapterTest { getApplicationContext(), /* requestCode= */ 0, new Intent("com.dummy.action.DUMMY_ACTION") - .setPackage(getApplicationContext().getPackageName()), + .setPackage(getApplicationContext().getPackageName()), /* flags= */ PendingIntent.FLAG_MUTABLE))); } @@ -777,6 +787,76 @@ public class ShadowBluetoothAdapterTest { .isEqualTo(BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED); } + @Config(minSdk = U.SDK_INT) + @Test + public void getProfileProxy_serviceListenerInvoked() throws Exception { + shadowOf((Application) getApplicationContext()).grantPermissions(permission.BLUETOOTH); + bluetoothAdapter.enable(); + LinkedBlockingQueue<Integer> profileQueue = new LinkedBlockingQueue<>(); + LinkedBlockingQueue<BluetoothProfile> proxyQueue = new LinkedBlockingQueue<>(); + BluetoothProfile.ServiceListener listener = + new ServiceListener() { + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + profileQueue.add(profile); + proxyQueue.add(proxy); + } + + @Override + public void onServiceDisconnected(int profile) {} + }; + + assertThat( + bluetoothAdapter.getProfileProxy( + getApplicationContext(), listener, BluetoothProfile.HEADSET)) + .isTrue(); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(profileQueue.take()).isEqualTo(BluetoothProfile.HEADSET); + assertThat(proxyQueue.take()).isInstanceOf(BluetoothHeadset.class); + } + + @Config(minSdk = U.SDK_INT) + @Test + public void getProfileProxy_adapterDisabled_serviceListenerNotInvoked() { + shadowOf((Application) getApplicationContext()).grantPermissions(permission.BLUETOOTH); + BluetoothProfile.ServiceListener listener = + Mockito.mock(BluetoothProfile.ServiceListener.class); + + bluetoothAdapter.getProfileProxy(getApplicationContext(), listener, BluetoothProfile.HEADSET); + shadowOf(Looper.getMainLooper()).idle(); + + verify(listener, never()).onServiceConnected(anyInt(), any(BluetoothProfile.class)); + } + + @Config(minSdk = U.SDK_INT) + @Test + public void disconnectProfileProxy_serviceListenerInvoked() throws Exception { + shadowOf((Application) getApplicationContext()).grantPermissions(permission.BLUETOOTH); + bluetoothAdapter.enable(); + LinkedBlockingQueue<Integer> profileQueue = new LinkedBlockingQueue<>(); + LinkedBlockingQueue<BluetoothHeadset> headsetProxies = new LinkedBlockingQueue<>(); + BluetoothProfile.ServiceListener listener = + new ServiceListener() { + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + headsetProxies.add((BluetoothHeadset) proxy); + } + + @Override + public void onServiceDisconnected(int profile) { + profileQueue.add(profile); + } + }; + + bluetoothAdapter.getProfileProxy(getApplicationContext(), listener, BluetoothProfile.HEADSET); + shadowOf(Looper.getMainLooper()).idle(); + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, headsetProxies.take()); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(profileQueue.take()).isEqualTo(BluetoothProfile.HEADSET); + } + private PendingIntent createTestPendingIntent(Intent intent) { return PendingIntent.getBroadcast( getApplicationContext(), /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java index 1216595c7..cdac21f20 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattServerTest.java @@ -109,6 +109,16 @@ public class ShadowBluetoothGattServerTest { } @Test + public void test_getResponses_acceptsNull() { + shadowOf(server).sendResponse(device, 0, 0, 0, RESPONSE_VALUE1); + assertThat(shadowOf(server).getResponses()).hasSize(1); + shadowOf(server).sendResponse(device, 0, 0, 0, null); + assertThat(shadowOf(server).getResponses()).hasSize(2); + assertThat(shadowOf(server).getResponses().get(0)).isEqualTo(RESPONSE_VALUE1); + assertThat(shadowOf(server).getResponses().get(1)).isEqualTo(null); + } + + @Test public void test_isConnectedToDevice_initially() { assertThat(shadowOf(server).isConnectedToDevice(device)).isFalse(); } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java index 6586ad668..88ee76029 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBuildTest.java @@ -17,6 +17,12 @@ import org.robolectric.annotation.Config; public class ShadowBuildTest { @Test + public void setBoard() { + ShadowBuild.setBoard("test_board"); + assertThat(Build.BOARD).isEqualTo("test_board"); + } + + @Test public void setDevice() { ShadowBuild.setDevice("test_device"); assertThat(Build.DEVICE).isEqualTo("test_device"); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java index e718ab2fb..88ff63cea 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java @@ -1,6 +1,7 @@ package org.robolectric.shadows; import static android.content.ClipboardManager.OnPrimaryClipChangedListener; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; @@ -10,8 +11,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.os.SystemClock; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.time.Duration; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -126,4 +129,27 @@ public class ShadowClipboardManagerTest { verify(listener).onPrimaryClipChanged(); } + + @Test + @Config(minSdk = O) + public void shouldSetTimestampForClip() { + long currentUptimeMs = SystemClock.uptimeMillis(); + ShadowSystemClock.advanceBy(Duration.ofSeconds(47)); + ClipData clip = ClipData.newPlainText(null, "BLARG?"); + clipboardManager.setPrimaryClip(clip); + assertThat(clipboardManager.getPrimaryClipDescription()).isNotNull(); + assertThat(clipboardManager.getPrimaryClipDescription().getTimestamp()) + .isEqualTo(currentUptimeMs + 47 * 1000); + } + + @Test + @Config(minSdk = O) + public void shouldSetTimestampForText() { + long currentUptimeMs = SystemClock.uptimeMillis(); + ShadowSystemClock.advanceBy(Duration.ofSeconds(42)); + clipboardManager.setText("BLARG!!!"); + assertThat(clipboardManager.getPrimaryClipDescription()).isNotNull(); + assertThat(clipboardManager.getPrimaryClipDescription().getTimestamp()) + .isEqualTo(currentUptimeMs + 42 * 1000); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java index e49cfcdc7..f70af70e7 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowCompanionDeviceManagerTest.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES; import static android.os.Build.VERSION_CODES.O; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; @@ -7,10 +8,10 @@ import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.app.Application; -import android.companion.AssociatedDevice; import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; +import android.companion.DeviceNotAssociatedException; import android.content.ComponentName; import android.content.IntentSender; import android.net.MacAddress; @@ -30,16 +31,18 @@ import org.robolectric.util.ReflectionHelpers.ClassParameter; public class ShadowCompanionDeviceManagerTest { private static final String MAC_ADDRESS = "AA:BB:CC:DD:FF:EE"; + private static final String PACKAGE_NAME = "org.robolectric"; + private final Application application = getApplicationContext(); private CompanionDeviceManager companionDeviceManager; private ShadowCompanionDeviceManager shadowCompanionDeviceManager; private ComponentName componentName; @Before public void setUp() throws Exception { - companionDeviceManager = getApplicationContext().getSystemService(CompanionDeviceManager.class); + companionDeviceManager = application.getSystemService(CompanionDeviceManager.class); shadowCompanionDeviceManager = shadowOf(companionDeviceManager); - componentName = new ComponentName(getApplicationContext(), Application.class); + componentName = new ComponentName(application, Application.class); } @Test @@ -121,22 +124,67 @@ public class ShadowCompanionDeviceManagerTest { @Test @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testAddAssociation_byAssociationInfo_defaultValue() { + AssociationInfoBuilder infoBuilder = + AssociationInfoBuilder.newBuilder() + .setId(1) + .setUserId(1) + .setDeviceMacAddress(MAC_ADDRESS) + .setDisplayName("displayName") + .setSystemDataSyncFlags(-1); + AssociationInfo info = infoBuilder.build(); + + AssociationInfoBuilder expectedInfoBuilder = + AssociationInfoBuilder.newBuilder() + .setId(1) + .setUserId(1) + .setDeviceMacAddress(MAC_ADDRESS) + .setDisplayName("displayName") + .setSelfManaged(false) + .setNotifyOnDeviceNearby(false) + .setApprovedMs(0) + .setLastTimeConnectedMs(0) + .setSystemDataSyncFlags(-1); + AssociationInfo expectedInfo = expectedInfoBuilder.build(); + assertThat(companionDeviceManager.getAssociations()).isEmpty(); + shadowCompanionDeviceManager.addAssociation(info); + assertThat(companionDeviceManager.getMyAssociations()).contains(expectedInfo); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) public void testAddAssociation_byAssociationInfo() { - AssociationInfo info = - new AssociationInfo( - /* id= */ 1, - /* userId= */ 1, - "packageName", - MacAddress.fromString(MAC_ADDRESS), - "displayName", - "deviceProfile", - /* AssociatedDevice*/ null, - /* selfManaged= */ false, - /* notifyOnDeviceNearby= */ false, - /* revoked */ false, - /* timeApprovedMs= */ 0, - /* lastTimeConnectedMs= */ 0, - /* systemDataSyncFlags= */ -1); + AssociationInfoBuilder infoBuilder = + AssociationInfoBuilder.newBuilder() + .setId(1) + .setUserId(1) + .setPackageName("packageName") + .setDeviceMacAddress(MAC_ADDRESS) + .setDisplayName("displayName") + .setSelfManaged(false) + .setNotifyOnDeviceNearby(false) + .setApprovedMs(0) + .setLastTimeConnectedMs(0); + Object associatedDeviceValue = null; + if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) { + try { + Class<?> associatedDeviceClazz = Class.forName("android.companion.AssociatedDevice"); + associatedDeviceValue = ReflectionHelpers.newInstance(associatedDeviceClazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + infoBuilder = infoBuilder.setAssociatedDevice(associatedDeviceValue); + } + int systemDataSyncFlagsValue = 1; + if (ReflectionHelpers.hasField(AssociationInfo.class, "mSystemDataSyncFlags")) { + infoBuilder = infoBuilder.setSystemDataSyncFlags(systemDataSyncFlagsValue); + } + AssociationInfo info = infoBuilder.build(); + if (ReflectionHelpers.hasField(AssociationInfo.class, "mSystemDataSyncFlags")) { + int systemDataSyncFlags = + ReflectionHelpers.callInstanceMethod(info, "getSystemDataSyncFlags"); + assertThat(systemDataSyncFlags).isEqualTo(systemDataSyncFlagsValue); + } assertThat(companionDeviceManager.getAssociations()).isEmpty(); shadowCompanionDeviceManager.addAssociation(info); assertThat(companionDeviceManager.getMyAssociations()).contains(info); @@ -201,6 +249,102 @@ public class ShadowCompanionDeviceManagerTest { companionDeviceManager, "notifyDeviceAppeared", ClassParameter.from(int.class, 1)); } + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testStartObservingDevicePresence_deviceNotAssociated_throwsException() { + assertThrows( + DeviceNotAssociatedException.class, + () -> companionDeviceManager.startObservingDevicePresence(MAC_ADDRESS)); + assertThat(shadowCompanionDeviceManager.getLastObservingDevicePresenceDeviceAddress()) + .isEqualTo(MAC_ADDRESS); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testStartObservingDevicePresence_deviceAssociated_presenceObserved() { + shadowCompanionDeviceManager.addAssociation(MAC_ADDRESS); + + companionDeviceManager.startObservingDevicePresence(MAC_ADDRESS); + assertThat(shadowCompanionDeviceManager.getLastObservingDevicePresenceDeviceAddress()) + .isEqualTo(MAC_ADDRESS); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void + testGetLastObservingDevicePresenceDeviceAddress_startObservingDevicePresenceNotCalled_returnsNull() { + assertThat(shadowCompanionDeviceManager.getLastObservingDevicePresenceDeviceAddress()).isNull(); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testAssociate_systemApi_deviceAssociated() { + MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS); + shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES); + + companionDeviceManager.associate(PACKAGE_NAME, macAddress, new byte[] {0x01}); + assertThat(companionDeviceManager.getAssociations()).contains(macAddress.toString()); + assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress()) + .isEqualTo(macAddress); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testGetLastSystemApiAssociationMacAddress_associateCalled_returnsLastMacAddress() { + MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS); + shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES); + + companionDeviceManager.associate(PACKAGE_NAME, macAddress, new byte[] {0x01}); + assertThat(companionDeviceManager.getAssociations()).contains(macAddress.toString()); + assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress()) + .isEqualTo(macAddress); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testGetLastSystemApiAssociationMacAddress_associateNotCalled_returnsNull() { + assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress()).isNull(); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testAssociate_systemApi_permissionDeniedDeviceNotAssociated() { + MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS); + shadowOf(application).denyPermissions(ASSOCIATE_COMPANION_DEVICES); + + assertThrows( + SecurityException.class, + () -> companionDeviceManager.associate(PACKAGE_NAME, macAddress, new byte[] {0x01})); + assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress()) + .isEqualTo(macAddress); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testAssociate_systemApi_badPackageNameDeviceNotAssociated() { + MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS); + shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES); + + assertThrows( + SecurityException.class, + () -> companionDeviceManager.associate("some.package", macAddress, new byte[] {0x01})); + assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress()) + .isEqualTo(macAddress); + } + + @Test + @Config(minSdk = VERSION_CODES.TIRAMISU) + public void testAssociate_systemApi_badCertificateDeviceNotAssociated() { + MacAddress macAddress = MacAddress.fromString(MAC_ADDRESS); + shadowOf(application).grantPermissions(ASSOCIATE_COMPANION_DEVICES); + + assertThrows( + SecurityException.class, + () -> companionDeviceManager.associate(PACKAGE_NAME, macAddress, null)); + assertThat(shadowCompanionDeviceManager.getLastSystemApiAssociationMacAddress()) + .isEqualTo(macAddress); + } + private CompanionDeviceManager.Callback createCallback() { return new CompanionDeviceManager.Callback() { @Override @@ -210,4 +354,39 @@ public class ShadowCompanionDeviceManagerTest { public void onFailure(CharSequence error) {} }; } + + /** Create {@link AssociationInfo}. */ + private AssociationInfo createDefaultAssociationInfo() { + AssociationInfoBuilder aiBuilder = AssociationInfoBuilder.newBuilder() + .setId(1) + .setUserId(1) + .setPackageName("packageName") + .setDeviceMacAddress(MAC_ADDRESS) + .setDisplayName("displayName") + .setDeviceProfile("deviceProfile") + .setSelfManaged(false) + .setNotifyOnDeviceNearby(false) + .setApprovedMs(0) + .setLastTimeConnectedMs(0); + + if (ReflectionHelpers.hasField(AssociationInfo.class, "mTag")) { + ReflectionHelpers.callInstanceMethod( + aiBuilder, "setTag", ClassParameter.from(String.class, "tag")); + } + if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) { + ReflectionHelpers.callInstanceMethod( + aiBuilder, + "setAssociatedDevice", + ClassParameter.from(Object.class, null)); + ReflectionHelpers.callInstanceMethod( + aiBuilder, + "setSystemDataSyncFlags", + ClassParameter.from(int.class, -1)); + } + if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) { + ReflectionHelpers.callInstanceMethod( + aiBuilder, "setRevoked", ClassParameter.from(boolean.class, false)); + } + return aiBuilder.build(); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java index f9721e464..1459a2362 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDateIntervalFormatTest.java @@ -1,6 +1,5 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static com.google.common.truth.Truth.assertThat; import android.icu.text.DateFormat; @@ -16,9 +15,10 @@ import android.text.format.DateIntervalFormat; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import org.robolectric.versioning.AndroidVersions.U; @RunWith(AndroidJUnit4.class) -@Config(minSdk = UPSIDE_DOWN_CAKE) +@Config(minSdk = U.SDK_INT) public class ShadowDateIntervalFormatTest { @Test public void testDateInterval_FormatDateRange() throws ParseException { diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java index 9d8a03881..7dcc6ad10 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDisplayHashManagerTest.java @@ -1,6 +1,7 @@ package org.robolectric.shadows; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.content.Context; import android.graphics.Rect; @@ -10,6 +11,8 @@ import android.view.displayhash.DisplayHashManager; import android.view.displayhash.VerifiedDisplayHash; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableSet; +import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,11 +34,26 @@ public final class ShadowDisplayHashManagerTest { @Test public void getSupportedHashAlgorithms() { + // Default value is PHASH assertThat(displayHashManager.getSupportedHashAlgorithms()).containsExactly("PHASH"); + + ShadowDisplayHashManager.setSupportedHashAlgorithms(ImmutableSet.of("TESTHASH")); + assertThat(displayHashManager.getSupportedHashAlgorithms()).containsExactly("TESTHASH"); } @Test public void verifyDisplayHash() { + DisplayHash displayHash = createDisplayHash(); + + assertThat(displayHashManager.verifyDisplayHash(displayHash)).isNull(); + + VerifiedDisplayHash verifiedDisplayHash = + new VerifiedDisplayHash(54321L, new Rect(0, 0, 100, 100), "PHASH", new byte[8]); + ShadowDisplayHashManager.setVerifyDisplayHashResult(verifiedDisplayHash); + assertThat(displayHashManager.verifyDisplayHash(displayHash)).isEqualTo(verifiedDisplayHash); + } + + private DisplayHash createDisplayHash() { Parcel parcel = Parcel.obtain(); parcel.writeLong(12345L); parcel.writeTypedObject(new Rect(0, 0, 100, 100), 0); @@ -43,13 +61,17 @@ public final class ShadowDisplayHashManagerTest { parcel.writeByteArray(new byte[15]); parcel.writeByteArray(new byte[21]); parcel.setDataPosition(0); - DisplayHash displayHash = DisplayHash.CREATOR.createFromParcel(parcel); + return DisplayHash.CREATOR.createFromParcel(parcel); + } - assertThat(displayHashManager.verifyDisplayHash(displayHash)).isNull(); + @Test + public void testSetSupportedHashAlgorithmsToNull() { + Set<String> previousSupportedHashAlgorithms = displayHashManager.getSupportedHashAlgorithms(); + ShadowDisplayHashManager.setSupportedHashAlgorithms(previousSupportedHashAlgorithms); + ShadowDisplayHashManager.setSupportedHashAlgorithms(null); + assertThrows(NullPointerException.class, () -> displayHashManager.getSupportedHashAlgorithms()); - VerifiedDisplayHash verifiedDisplayHash = - new VerifiedDisplayHash(54321L, new Rect(0, 0, 100, 100), "PHASH", new byte[8]); - ShadowDisplayHashManager.setVerifyDisplayHashResult(verifiedDisplayHash); - assertThat(displayHashManager.verifyDisplayHash(displayHash)).isEqualTo(verifiedDisplayHash); + // Restore previous value + ShadowDisplayHashManager.setSupportedHashAlgorithms(previousSupportedHashAlgorithms); } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java index 3cb4ae992..ad8d1c1d0 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowEnvironmentTest.java @@ -6,6 +6,7 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -38,6 +39,30 @@ public class ShadowEnvironmentTest { } @Test + @Config(minSdk = R) + public void getStorageDirectory_storageDirectoryUnset_shouldReturnDefaultDirectory() { + assertThat(Environment.getStorageDirectory().getAbsolutePath()).isEqualTo("/storage"); + } + + @Test + @Config(minSdk = R) + public void setStorageDirectory_shouldReturnDirectory() { + // state prior to override + File defaultDir = Environment.getStorageDirectory(); + // override + Path expectedPath = FileSystems.getDefault().getPath("/tmp", "foo"); + ShadowEnvironment.setStorageDirectory(expectedPath); + File override = Environment.getStorageDirectory(); + assertThat(override.getAbsolutePath()).isEqualTo(expectedPath.toAbsolutePath().toString()); + + // restore default value by supplying {@code null} + ShadowEnvironment.setStorageDirectory(null); + + // verify default + assertThat(defaultDir).isEqualTo(Environment.getStorageDirectory()); + } + + @Test public void getExternalStorageDirectory_shouldReturnDirectory() { assertThat(Environment.getExternalStorageDirectory().exists()).isTrue(); } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowImageDecoderTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowImageDecoderTest.java new file mode 100644 index 000000000..a4e4a5c49 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowImageDecoderTest.java @@ -0,0 +1,76 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.ImageDecoder; +import android.os.Build.VERSION_CODES; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.R; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.GraphicsMode; +import org.robolectric.annotation.GraphicsMode.Mode; + +@RunWith(AndroidJUnit4.class) +@Config(minSdk = VERSION_CODES.P) +@GraphicsMode(Mode.LEGACY) // Non-legacy native mode is tested in ShadowNativeImageDecoderTest +public class ShadowImageDecoderTest { + + private final Context context = RuntimeEnvironment.getApplication(); + + @Test + public void mimeType_png_returnsPng() throws Exception { + Bitmap bmp = + ImageDecoder.decodeBitmap( + ImageDecoder.createSource(context.getResources(), R.drawable.an_image), + (imageDecoder, imageInfo, source) -> { + assertThat(imageInfo.getSize().getHeight()).isEqualTo(53); + assertThat(imageInfo.getSize().getWidth()).isEqualTo(64); + assertThat(imageInfo.getMimeType()).isEqualTo("image/png"); + }); + assertThat(bmp).isNotNull(); + } + + @Test + public void mimeType_jpg_returnsJpg() throws Exception { + Bitmap bmp = + ImageDecoder.decodeBitmap( + ImageDecoder.createSource(context.getResources(), R.drawable.test_jpeg), + (imageDecoder, imageInfo, source) -> { + assertThat(imageInfo.getSize().getHeight()).isEqualTo(50); + assertThat(imageInfo.getSize().getWidth()).isEqualTo(50); + assertThat(imageInfo.getMimeType()).isEqualTo("image/jpeg"); + }); + assertThat(bmp).isNotNull(); + } + + @Test + public void mimeType_gif_returnsGif() throws Exception { + Bitmap bmp = + ImageDecoder.decodeBitmap( + ImageDecoder.createSource(context.getResources(), R.drawable.an_other_image), + (imageDecoder, imageInfo, source) -> { + assertThat(imageInfo.getSize().getWidth()).isEqualTo(32); + assertThat(imageInfo.getSize().getHeight()).isEqualTo(18); + assertThat(imageInfo.getMimeType()).isEqualTo("image/gif"); + }); + assertThat(bmp).isNotNull(); + } + + @Test + public void mimeType_webp_returnsWebp() throws Exception { + Bitmap bmp = + ImageDecoder.decodeBitmap( + ImageDecoder.createSource(context.getResources(), R.drawable.test_webp), + (imageDecoder, imageInfo, source) -> { + // The Legacy decoder doesn't support WebP images so it'll return unknown MIME type. + assertEquals("image/unknown", imageInfo.getMimeType()); + }); + assertThat(bmp).isNotNull(); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java index f0196218f..cffc091b9 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.annotation.SuppressLint; import android.os.Build.VERSION_CODES; +import android.telephony.AccessNetworkConstants; import android.telephony.ims.ImsException; import android.telephony.ims.ImsMmTelManager; import android.telephony.ims.ImsMmTelManager.CapabilityCallback; @@ -17,22 +18,28 @@ import android.telephony.ims.RegistrationManager; import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities; import android.telephony.ims.stub.ImsRegistrationImplBase; import android.util.ArraySet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; /** Tests for {@link ShadowImsMmTelManager} */ @RunWith(RobolectricTestRunner.class) @Config(minSdk = VERSION_CODES.Q) public class ShadowImsMmTelManagerTest { + private static final int SUBSCRIPTION_ID = 5; + private ShadowImsMmTelManager shadowImsMmTelManager; @Before public void setup() { - shadowImsMmTelManager = new ShadowImsMmTelManager(); + shadowImsMmTelManager = + Shadow.extract(ImsMmTelManager.createForSubscriptionId(SUBSCRIPTION_ID)); } @Test @@ -241,6 +248,47 @@ public class ShadowImsMmTelManagerTest { } @Test + public void getRegistrationState_setAsRegistered_returnsRegistrationStateRegistered() { + AtomicInteger registrationState = new AtomicInteger(); + Consumer<Integer> stateCallback = registrationState::set; + ShadowImsMmTelManager.setRegistrationState( + SUBSCRIPTION_ID, RegistrationManager.REGISTRATION_STATE_REGISTERED); + + shadowImsMmTelManager.getRegistrationState(Runnable::run, stateCallback); + + assertThat(registrationState.intValue()) + .isEqualTo(RegistrationManager.REGISTRATION_STATE_REGISTERED); + } + + @Test + public void getRegistrationStateCallback() { + Consumer<Integer> stateCallback = state -> {}; + shadowImsMmTelManager.getRegistrationState(Runnable::run, stateCallback); + assertThat(shadowImsMmTelManager.getRegistrationStateCallback()).isEqualTo(stateCallback); + } + + @Test + public void getRegistrationTransportType_setAsWlan_returnsTransportTypeWlan() { + AtomicInteger registrationTransportType = new AtomicInteger(); + Consumer<Integer> stateCallback = registrationTransportType::set; + ShadowImsMmTelManager.setRegistrationTransportType( + SUBSCRIPTION_ID, AccessNetworkConstants.TRANSPORT_TYPE_WLAN); + + shadowImsMmTelManager.getRegistrationTransportType(Runnable::run, stateCallback); + + assertThat(registrationTransportType.intValue()) + .isEqualTo(AccessNetworkConstants.TRANSPORT_TYPE_WLAN); + } + + @Test + public void getRegistrationTransportTypeCallback() { + Consumer<Integer> transportTypeCallback = state -> {}; + shadowImsMmTelManager.getRegistrationTransportType(Runnable::run, transportTypeCallback); + assertThat(shadowImsMmTelManager.getRegistrationTransportTypeCallback()) + .isEqualTo(transportTypeCallback); + } + + @Test public void registerMmTelCapabilityCallback_imsRegistered_availabilityChange_onCapabilitiesStatusChangedInvoked() throws ImsException { @@ -474,15 +522,40 @@ public class ShadowImsMmTelManagerTest { assertThat(imsMmTelManager1).isEqualTo(ShadowImsMmTelManager.createForSubscriptionId(1)); assertThat(imsMmTelManager2).isEqualTo(ShadowImsMmTelManager.createForSubscriptionId(2)); - ShadowImsMmTelManager.clearExistingInstances(); + ShadowImsMmTelManager.clearExistingInstancesAndStates(); assertThat(imsMmTelManager1).isNotEqualTo(ShadowImsMmTelManager.createForSubscriptionId(1)); assertThat(imsMmTelManager2).isNotEqualTo(ShadowImsMmTelManager.createForSubscriptionId(2)); } @Test + public void clearExistingInstancesAndStates_statesAreCleared() { + AtomicInteger registrationState = new AtomicInteger(); + Consumer<Integer> stateCallback = registrationState::set; + ShadowImsMmTelManager.setRegistrationState( + SUBSCRIPTION_ID, RegistrationManager.REGISTRATION_STATE_REGISTERED); + + ShadowImsMmTelManager.clearExistingInstancesAndStates(); + shadowImsMmTelManager.getRegistrationState(Runnable::run, stateCallback); + + assertThat(registrationState.intValue()).isEqualTo(0); + } + + @Test + public void clearExistingInstancesAndStates_typesAreCleared() { + AtomicInteger registrationTransportType = new AtomicInteger(); + Consumer<Integer> stateCallback = registrationTransportType::set; + ShadowImsMmTelManager.setRegistrationTransportType( + SUBSCRIPTION_ID, AccessNetworkConstants.TRANSPORT_TYPE_WLAN); + + ShadowImsMmTelManager.clearExistingInstancesAndStates(); + shadowImsMmTelManager.getRegistrationTransportType(Runnable::run, stateCallback); + + assertThat(registrationTransportType.intValue()).isEqualTo(0); + } + + @Test public void getSubscriptionId() { - shadowImsMmTelManager.__constructor__(5); - assertThat(shadowImsMmTelManager.getSubscriptionId()).isEqualTo(5); + assertThat(shadowImsMmTelManager.getSubscriptionId()).isEqualTo(SUBSCRIPTION_ID); } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java index daff15fda..a707463d1 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInCallServiceTest.java @@ -86,9 +86,7 @@ public class ShadowInCallServiceTest { @Test @TargetApi(P) - @Config( - minSdk = P, - shadows = {ShadowBluetoothDevice.class}) + @Config(minSdk = P) public void requestBluetoothAudio_getBluetoothAudio() { BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance("00:11:22:33:AA:BB"); ShadowInCallService shadowInCallService = shadowOf(inCallService); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java new file mode 100644 index 000000000..a9fb74699 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInstrumentationTestLooperTest.java @@ -0,0 +1,111 @@ +package org.robolectric.shadows; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.robolectric.Shadows.shadowOf; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; + +@LooperMode(Mode.INSTRUMENTATION_TEST) +@RunWith(RobolectricTestRunner.class) +public class ShadowInstrumentationTestLooperTest { + + @Test + @Config(minSdk = 18) + public void testThreadIsNotMainThread() { + assertFalse(Looper.getMainLooper().isCurrentThread()); + } + + @Test + public void idle() throws InterruptedException { + ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + + AtomicBoolean hasRun = new AtomicBoolean(false); + mainHandler.post(() -> hasRun.set(true)); + shadowMainLooper.idle(); + assertTrue(hasRun.get()); + } + + @Test + public void pauseMainLooper() { + ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + + shadowMainLooper.pause(); + AtomicBoolean hasRun = new AtomicBoolean(false); + mainHandler.post(() -> hasRun.set(true)); + assertFalse(hasRun.get()); + shadowMainLooper.idle(); + assertTrue(hasRun.get()); + } + + @Test + public void unpauseMainLooper() throws InterruptedException { + ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + + shadowMainLooper.pause(); + CountDownLatch hasRun = new CountDownLatch(1); + mainHandler.post(hasRun::countDown); + assertEquals(1, hasRun.getCount()); + shadowMainLooper.unPause(); + assertTrue(hasRun.await(2, SECONDS)); + } + + @Test + public void idleFor() { + ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + + AtomicBoolean hasRun = new AtomicBoolean(false); + mainHandler.postDelayed(() -> hasRun.set(true), 99); + assertFalse(hasRun.get()); + shadowMainLooper.idleFor(Duration.ofMillis(100)); + assertTrue(hasRun.get()); + } + + @Test + public void exceptionOnMainThreadPropagated() throws InterruptedException { + ShadowLooper shadowMainLooper = shadowOf(Looper.getMainLooper()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + + mainHandler.post( + () -> { + throw new RuntimeException("Exception should be propagated!"); + }); + assertThrows(RuntimeException.class, () -> shadowMainLooper.idle()); + + // Restore main looper and main thread to avoid error at tear down + Looper.getMainLooper().getThread().join(); + ShadowPausedLooper.resetLoopers(); + } + + @Test + public void backgroundLooperCrash() throws InterruptedException { + HandlerThread ht = new HandlerThread("backgroundLooperCrash"); + ht.start(); + Handler handler = new Handler(ht.getLooper()); + handler.post( + () -> { + throw new RuntimeException(); + }); + ht.join(); + + assertThrows(IllegalStateException.class, () -> handler.post(() -> {})); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java index 8fca386d0..2e549b727 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowJobServiceTest.java @@ -18,25 +18,40 @@ import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) @Config(minSdk = LOLLIPOP) public class ShadowJobServiceTest { - private JobService jobService; - @Mock - private JobParameters params; + @Mock private JobParameters params; @Before public void setUp() { MockitoAnnotations.initMocks(this); - jobService = new JobService() { - @Override - public boolean onStartJob(JobParameters params) { - return false; - } - - @Override - public boolean onStopJob(JobParameters params) { - return false; - } - }; + jobService = + new JobService() { + @Override + public boolean onStartJob(JobParameters params) { + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + }; + } + + @Test + @Config(minSdk = 34) + public void updateEstimatedNetworkBytes() { + shadowOf(jobService) + .updateEstimatedNetworkBytes(params, /* downloadBytes= */ 10L, /* uploadBytes= */ 0L); + // If we make it here, the call above did not throw + } + + @Test + @Config(minSdk = 34) + public void updateTransferredNetworkBytes() { + shadowOf(jobService) + .updateEstimatedNetworkBytes(params, /* downloadBytes= */ 1000L, /* uploadBytes= */ 0L); + // If we make it here, the call above did not throw } @Test @@ -57,4 +72,4 @@ public class ShadowJobServiceTest { assertThat(shadow.getIsRescheduleNeeded()).isTrue(); assertThat(shadow.getIsJobFinished()).isTrue(); } -}
\ No newline at end of file +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java index 47c16b4f4..340cbaab0 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java @@ -401,7 +401,10 @@ public class ShadowLauncherAppsTest { ClassParameter.from(UserHandle.class, userHandle)); } else if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) { LauncherActivityInfoInternal launcherActivityInfoInternal = - new LauncherActivityInfoInternal(info, null, userHandle); + ReflectionHelpers.callConstructor( + LauncherActivityInfoInternal.class, + ClassParameter.from(ActivityInfo.class, info), + ClassParameter.from(IncrementalStatesInfo.class, null)); return ReflectionHelpers.callConstructor( LauncherActivityInfo.class, diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java index bd99b4c83..18d4527fc 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java @@ -153,6 +153,35 @@ public class ShadowMatrixTest { } @Test + public void testGetSetValues_withLargeArray() { + final Matrix matrix = new Matrix(); + final float[] values = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f}; + matrix.setValues(values); + final float[] matrixValues = new float[10]; + matrix.getValues(matrixValues); + // First 9 elements should match. + for (int i = 0; i < 9; i++) { + assertThat(matrixValues[i]).isEqualTo(values[i]); + } + // The last element should not have been set. + assertThat(matrixValues[9]).isEqualTo(0); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testGetValues_withSmallArray() { + final Matrix matrix = new Matrix(); + final float[] matrixValues = new float[8]; + matrix.getValues(matrixValues); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testSetValues_withSmallArray() { + final Matrix matrix = new Matrix(); + final float[] values = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f}; + matrix.setValues(values); + } + + @Test public void testSet() { final Matrix matrix1 = new Matrix(); matrix1.postScale(2.0f, 2.0f); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java index a359e1e16..d40ce2540 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaStoreTest.java @@ -1,15 +1,23 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static android.provider.MediaStore.Images; import static android.provider.MediaStore.Video; import static com.google.common.truth.Truth.assertThat; +import android.provider.MediaStore; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) public class ShadowMediaStoreTest { + private static final String AUTHORITY = "authority"; + private static final String INCORRECT_AUTHORITY = "incorrect_authority"; + private static final String CURRENT_MEDIA_COLLECTION_ID = "media_collection_id"; + @Test public void shouldInitializeFields() { assertThat(Images.Media.EXTERNAL_CONTENT_URI.toString()) @@ -21,4 +29,45 @@ public class ShadowMediaStoreTest { assertThat(Video.Media.INTERNAL_CONTENT_URI.toString()) .isEqualTo("content://media/internal/video/media"); } + + @Test + @Config(minSdk = TIRAMISU) + public void notifyCloudMediaChangedEvent_storesCloudMediaChangedEvent() { + MediaStore.notifyCloudMediaChangedEvent(null, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID); + + ImmutableList<ShadowMediaStore.CloudMediaChangedEvent> cloudMediaChangedEventList = + ShadowMediaStore.getCloudMediaChangedEvents(); + assertThat(cloudMediaChangedEventList).hasSize(1); + assertThat(cloudMediaChangedEventList.get(0).authority()).isEqualTo(AUTHORITY); + assertThat(cloudMediaChangedEventList.get(0).currentMediaCollectionId()) + .isEqualTo(CURRENT_MEDIA_COLLECTION_ID); + } + + @Test + @Config(minSdk = TIRAMISU) + public void clearCloudMediaChangedEventList_clearsCloudMediaChangedEventList() { + MediaStore.notifyCloudMediaChangedEvent(null, AUTHORITY, CURRENT_MEDIA_COLLECTION_ID); + assertThat(ShadowMediaStore.getCloudMediaChangedEvents()).isNotEmpty(); + + ShadowMediaStore.clearCloudMediaChangedEventList(); + + assertThat(ShadowMediaStore.getCloudMediaChangedEvents()).isEmpty(); + } + + @Test + @Config(minSdk = TIRAMISU) + public void isCurrentCloudMediaProviderAuthority_withCorrectAuthority_returnsTrue() { + ShadowMediaStore.setCurrentCloudMediaProviderAuthority(AUTHORITY); + + assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(null, AUTHORITY)).isTrue(); + } + + @Test + @Config(minSdk = TIRAMISU) + public void isCurrentCloudMediaProviderAuthority_withIncorrectAuthority_returnsFalse() { + ShadowMediaStore.setCurrentCloudMediaProviderAuthority(AUTHORITY); + + assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(null, INCORRECT_AUTHORITY)) + .isFalse(); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java index 36386f599..47ed360b2 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMimeTypeMapTest.java @@ -24,13 +24,15 @@ public class ShadowMimeTypeMapTest { @Test public void shouldResetStaticStateBetweenTests() { assertFalse(MimeTypeMap.getSingleton().hasExtension(VIDEO_EXTENSION)); - shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); + shadowOf(MimeTypeMap.getSingleton()) + .addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); } @Test public void shouldResetStaticStateBetweenTests_anotherTime() { assertFalse(MimeTypeMap.getSingleton().hasExtension(VIDEO_EXTENSION)); - shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); + shadowOf(MimeTypeMap.getSingleton()) + .addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); } @Test @@ -50,8 +52,8 @@ public class ShadowMimeTypeMapTest { @Test public void addingMappingShouldWorkCorrectly() { ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton()); - shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); - shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); assertTrue(MimeTypeMap.getSingleton().hasExtension(VIDEO_EXTENSION)); assertTrue(MimeTypeMap.getSingleton().hasExtension(IMAGE_EXTENSION)); @@ -68,8 +70,8 @@ public class ShadowMimeTypeMapTest { @Test public void clearMappingsShouldRemoveAllMappings() { ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton()); - shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); - shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); shadowMimeTypeMap.clearMappings(); @@ -82,8 +84,8 @@ public class ShadowMimeTypeMapTest { @Test public void unknownExtensionShouldProvideNothing() { ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton()); - shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); - shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); assertFalse(MimeTypeMap.getSingleton().hasExtension("foo")); assertNull(MimeTypeMap.getSingleton().getMimeTypeFromExtension("foo")); @@ -92,8 +94,8 @@ public class ShadowMimeTypeMapTest { @Test public void unknownMimeTypeShouldProvideNothing() { ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton()); - shadowMimeTypeMap.addExtensionMimeTypMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); - shadowMimeTypeMap.addExtensionMimeTypMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(VIDEO_EXTENSION, VIDEO_MIMETYPE); + shadowMimeTypeMap.addExtensionMimeTypeMapping(IMAGE_EXTENSION, IMAGE_MIMETYPE); assertFalse(MimeTypeMap.getSingleton().hasMimeType("foo/bar")); assertNull(MimeTypeMap.getSingleton().getExtensionFromMimeType("foo/bar")); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java index d913c9927..bd64a107f 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java @@ -1,12 +1,10 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.TIRAMISU; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.util.reflector.Reflector.reflector; import android.app.Activity; import android.app.Application; @@ -16,7 +14,6 @@ import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.nfc.Tag; import android.os.Build; -import android.os.Bundle; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; import org.junit.Rule; @@ -26,7 +23,6 @@ import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.util.reflector.ForType; @RunWith(AndroidJUnit4.class) public class ShadowNfcAdapterTest { @@ -205,13 +201,9 @@ public class ShadowNfcAdapterTest { callback, NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, /* extras= */ null); - Tag tag = createMockTag(); + Tag tag = ShadowNfcAdapter.createMockTag(); shadowOf(adapter).dispatchTagDiscovered(tag); verify(callback).onTagDiscovered(same(tag)); } - - private static Tag createMockTag() { - return Tag.createMockTag(new byte[0], new int[0], new Bundle[0], 0L); - } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java index 7744b9507..77710bc3f 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java @@ -145,4 +145,36 @@ public class ShadowPaintTest { Paint paint = new Paint(); assertThat(paint.getTextScaleX()).isEqualTo(1f); } + + @Test + public void testSetFilterBitmapFlag() { + Paint paint = new Paint(); + paint.setFlags(paint.getFlags() | Paint.FILTER_BITMAP_FLAG); + assertThat(paint.isFilterBitmap()).isTrue(); + assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isNotEqualTo(0); + } + + @Test + public void testClearFilterBitmapFlag() { + Paint paint = new Paint(); + paint.setFlags(paint.getFlags() & ~Paint.FILTER_BITMAP_FLAG); + assertThat(paint.isFilterBitmap()).isFalse(); + assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isEqualTo(0); + } + + @Test + public void testSetFilterBitmap() { + Paint paint = new Paint(); + paint.setFilterBitmap(true); + assertThat(paint.isFilterBitmap()).isTrue(); + assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isNotEqualTo(0); + } + + @Test + public void testClearFilterBitmap() { + Paint paint = new Paint(); + paint.setFilterBitmap(false); + assertThat(paint.isFilterBitmap()).isFalse(); + assertThat(paint.getFlags() & Paint.FILTER_BITMAP_FLAG).isEqualTo(0); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java index d61a56504..eb2dd4850 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowParcelFileDescriptorTest.java @@ -1,21 +1,27 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.KITKAT; +import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE; import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.Charset.defaultCharset; import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeThat; import static org.robolectric.Shadows.shadowOf; import android.os.Handler; import android.os.HandlerThread; +import android.os.Parcel; import android.os.ParcelFileDescriptor; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.io.Files; +import java.io.BufferedReader; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.util.concurrent.atomic.AtomicBoolean; import org.hamcrest.Matchers; import org.junit.After; @@ -23,6 +29,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowParcelFileDescriptor.FileDescriptorFromParcelUnavailableException; import org.robolectric.util.ReflectionHelpers; @RunWith(AndroidJUnit4.class) @@ -344,4 +351,87 @@ public class ShadowParcelFileDescriptorTest { assertThrows(IllegalStateException.class, () -> pfd.getFd()); } + + @Test + public void testCanMarshalUnmarshal_closedByFlagUponWrite() throws Exception { + Files.asCharSink(file, defaultCharset()).write("foo"); + pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE); + assertThat(file.delete()).isTrue(); + ParcelFileDescriptor clone = dupViaParcel(pfd, PARCELABLE_WRITE_RETURN_VALUE); + assertThat(pfd.getFileDescriptor().valid()).isFalse(); + assertThat(readLine(clone.getFileDescriptor())).isEqualTo("foo"); + } + + @Test + public void testCanMarshalUnmarshal_closedAfterWrite() throws Exception { + Files.asCharSink(file, defaultCharset()).write("foo"); + pfd = ParcelFileDescriptor.open(file, 0); + assertThat(file.delete()).isTrue(); + ParcelFileDescriptor clone = dupViaParcel(pfd, 0); + assertThat(pfd.getFileDescriptor().valid()).isTrue(); + pfd.close(); + assertThat(readLine(clone.getFileDescriptor())).isEqualTo("foo"); + } + + @Test + public void testCanMarshalUnmarshal_canClosePendingDup() throws Exception { + Files.asCharSink(file, defaultCharset()).write("foo"); + pfd = ParcelFileDescriptor.open(file, 0); + assertThat(file.delete()).isTrue(); + ParcelFileDescriptor clone = dupViaParcel(pfd, 0); + clone.close(); + assertThat(readLine(pfd.getFileDescriptor())).isEqualTo("foo"); + } + + @Test + public void testCanMarshalUnmarshal_marshalTwice() throws Exception { + Files.asCharSink(file, defaultCharset()).write("bar"); + pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE); + assertThat(file.delete()).isTrue(); + Parcel parcel = Parcel.obtain(); + pfd.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParcelFileDescriptor clone1 = ParcelFileDescriptor.CREATOR.createFromParcel(parcel); + parcel.setDataPosition(0); + ParcelFileDescriptor clone2 = ParcelFileDescriptor.CREATOR.createFromParcel(parcel); + pfd.close(); + assertThat(readLine(clone1.getFileDescriptor())).isEqualTo("bar"); + assertThrows( + FileDescriptorFromParcelUnavailableException.class, () -> clone2.getFileDescriptor()); + parcel.recycle(); + } + + @Test + public void testCanMarshalUnmarshal_chained() throws Exception { + Files.asCharSink(file, defaultCharset()).write("foo"); + pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE); + assertThat(file.delete()).isTrue(); + + ParcelFileDescriptor pfd2 = dupViaParcel(pfd, 0); + ParcelFileDescriptor pfd3 = dupViaParcel(pfd2, 0); + + pfd.close(); // Makes our data available to anyone downstream on the chain. + + assertThat(readLine(pfd3.getFileDescriptor())).isEqualTo("foo"); + assertThrows( + FileDescriptorFromParcelUnavailableException.class, () -> pfd2.getFileDescriptor()); + } + + private static String readLine(FileDescriptor fd) throws IOException { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(new FileInputStream(fd), defaultCharset()))) { + return reader.readLine(); + } + } + + private static ParcelFileDescriptor dupViaParcel(ParcelFileDescriptor src, int flags) { + Parcel parcel = Parcel.obtain(); + try { + src.writeToParcel(parcel, flags); + parcel.setDataPosition(0); + return ParcelFileDescriptor.CREATOR.createFromParcel(parcel); + } finally { + parcel.recycle(); + } + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java index f0fe96f72..9098c6b50 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedMessageQueueTest.java @@ -92,6 +92,21 @@ public class ShadowPausedMessageQueueTest { assertMainQueueEmptyAndAdd(); } + @Test + public void drainQueue_withMultipleMsg() { + Message msg1 = Message.obtain(new Handler(), 1); + shadowQueue.doEnqueueMessage(msg1, 1); + Message msg3 = Message.obtain(new Handler(), 3); + shadowQueue.doEnqueueMessage(msg3, 3); + + shadowQueue.drainQueue(input -> true); + + Message msg2 = Message.obtain(new Handler(), 2); + shadowQueue.doEnqueueMessage(msg2, 2); + + assertThat(shadowQueue.getNextIgnoringWhen().what).isEqualTo(2); + } + private void assertMainQueueEmptyAndAdd() { MessageQueue mainQueue = Looper.getMainLooper().getQueue(); ShadowPausedMessageQueue shadowPausedMessageQueue = Shadow.extract(mainQueue); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java index 02219fe24..58d219cf7 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPendingIntentTest.java @@ -34,6 +34,7 @@ import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.annotation.Config; +@SuppressWarnings("deprecation") @RunWith(AndroidJUnit4.class) public class ShadowPendingIntentTest { @@ -47,7 +48,7 @@ public class ShadowPendingIntentTest { @Test public void getBroadcast_shouldCreateIntentForBroadcast() { Intent intent = new Intent(); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 0); ShadowPendingIntent shadow = shadowOf(pendingIntent); assertThat(shadow.isActivityIntent()).isFalse(); @@ -57,7 +58,7 @@ public class ShadowPendingIntentTest { assertThat(intent).isEqualTo(shadow.getSavedIntent()); assertThat(context).isEqualTo(shadow.getSavedContext()); assertThat(shadow.getRequestCode()).isEqualTo(99); - assertThat(shadow.getFlags()).isEqualTo(100); + assertThat(shadow.getFlags()).isEqualTo(0); } @Test @@ -66,7 +67,7 @@ public class ShadowPendingIntentTest { Bundle bundle = new Bundle(); bundle.putInt("weight", 741); bundle.putString("name", "Ada"); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100, bundle); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0, bundle); ShadowPendingIntent shadow = shadowOf(pendingIntent); assertThat(shadow.isActivityIntent()).isTrue(); @@ -76,7 +77,7 @@ public class ShadowPendingIntentTest { assertThat(intent).isEqualTo(shadow.getSavedIntent()); assertThat(context).isEqualTo(shadow.getSavedContext()); assertThat(shadow.getRequestCode()).isEqualTo(99); - assertThat(shadow.getFlags()).isEqualTo(100); + assertThat(shadow.getFlags()).isEqualTo(0); assertThat(shadow.getOptions().getInt("weight")).isEqualTo(741); assertThat(shadow.getOptions().getString("name")).isEqualTo("Ada"); } @@ -84,7 +85,7 @@ public class ShadowPendingIntentTest { @Test public void getActivities_shouldCreateIntentForBroadcast() throws Exception { Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)}; - PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0); ShadowPendingIntent shadow = shadowOf(pendingIntent); assertThat(shadow.getSavedIntents()).isEqualTo(intents); @@ -102,7 +103,7 @@ public class ShadowPendingIntentTest { Bundle bundle = new Bundle(); bundle.putInt("weight", 741); bundle.putString("name", "Ada"); - PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100, bundle); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0, bundle); ShadowPendingIntent shadow = shadowOf(pendingIntent); assertThat(shadow.getSavedIntents()).isEqualTo(intents); @@ -119,7 +120,7 @@ public class ShadowPendingIntentTest { @Test public void getService_shouldCreateIntentForBroadcast() { Intent intent = new Intent().setPackage("dummy.package"); - PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 0); ShadowPendingIntent shadow = shadowOf(pendingIntent); assertThat(shadow.isActivityIntent()).isFalse(); @@ -129,14 +130,14 @@ public class ShadowPendingIntentTest { assertThat(intent).isEqualTo(shadow.getSavedIntent()); assertThat(context).isEqualTo(shadow.getSavedContext()); assertThat(shadow.getRequestCode()).isEqualTo(99); - assertThat(shadow.getFlags()).isEqualTo(100); + assertThat(shadow.getFlags()).isEqualTo(0); } @Test @Config(minSdk = Build.VERSION_CODES.O) public void getForegroundService_shouldCreateIntentForBroadcast() { Intent intent = new Intent().setPackage("dummy.package"); - PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 0); ShadowPendingIntent shadow = shadowOf(pendingIntent); assertThat(shadow.isActivityIntent()).isFalse(); @@ -146,13 +147,13 @@ public class ShadowPendingIntentTest { assertThat(intent).isEqualTo(shadow.getSavedIntent()); assertThat(context).isEqualTo(shadow.getSavedContext()); assertThat(shadow.getRequestCode()).isEqualTo(99); - assertThat(shadow.getFlags()).isEqualTo(100); + assertThat(shadow.getFlags()).isEqualTo(0); } @Test public void getActivities_nullIntent() { try { - PendingIntent.getActivities(context, 99, null, 100); + PendingIntent.getActivities(context, 99, null, 0); fail("Expected NullPointerException when creating PendingIntent with null Intent[]"); } catch (NullPointerException ignore) { // expected @@ -162,7 +163,7 @@ public class ShadowPendingIntentTest { @Test public void getActivities_withBundle_nullIntent() { try { - PendingIntent.getActivities(context, 99, null, 100, Bundle.EMPTY); + PendingIntent.getActivities(context, 99, null, 0, Bundle.EMPTY); fail("Expected NullPointerException when creating PendingIntent with null Intent[]"); } catch (NullPointerException ignore) { // expected @@ -173,7 +174,7 @@ public class ShadowPendingIntentTest { public void send_shouldFillInIntentData() throws Exception { Intent intent = new Intent("action"); Context context = Robolectric.setupActivity(Activity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0); Activity otherContext = Robolectric.setupActivity(Activity.class); Intent fillIntent = new Intent().putExtra("TEST", 23); @@ -189,7 +190,7 @@ public class ShadowPendingIntentTest { public void send_shouldNotReusePreviouslyFilledInIntentData() throws Exception { Intent intent = new Intent("action"); Context context = Robolectric.setupActivity(Activity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0); Activity otherContext = Robolectric.setupActivity(Activity.class); Intent firstFillIntent = new Intent().putExtra("KEY1", 23).putExtra("KEY2", 24); @@ -212,7 +213,7 @@ public class ShadowPendingIntentTest { public void send_shouldFillInLastIntentData() throws Exception { Intent[] intents = {new Intent("first"), new Intent("second")}; Context context = Robolectric.setupActivity(Activity.class); - PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0); Activity otherContext = Robolectric.setupActivity(Activity.class); Intent fillIntent = new Intent(); @@ -232,10 +233,62 @@ public class ShadowPendingIntentTest { } @Test + @Config(minSdk = Build.VERSION_CODES.S) + public void send_shouldFillInIntentData_immutable() throws Exception { + Intent[] intents = {new Intent("first")}; + Context context = Robolectric.setupActivity(Activity.class); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, FLAG_IMMUTABLE); + + Activity otherContext = Robolectric.setupActivity(Activity.class); + Intent fillIntent = new Intent(); + fillIntent.putExtra("TEST", 23); + pendingIntent.send(otherContext, 0, fillIntent); + + ShadowActivity shadowActivity = shadowOf(otherContext); + Intent first = shadowActivity.getNextStartedActivity(); + assertThat(first).isNotNull(); + assertThat(first.filterEquals(intents[0])).isTrue(); // Ignore extras. + assertThat(first.hasExtra("TEST")).isFalse(); + } + + @Test + public void send_shouldFillInIntentData_mutable_alreadySet() throws Exception { + Intent[] intents = {new Intent("first")}; + Context context = Robolectric.setupActivity(Activity.class); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0); + + Activity otherContext = Robolectric.setupActivity(Activity.class); + Intent fillIntent = new Intent("first_and"); + pendingIntent.send(otherContext, 0, fillIntent); + + ShadowActivity shadowActivity = shadowOf(otherContext); + Intent first = shadowActivity.getNextStartedActivity(); + assertThat(first).isNotNull(); + assertThat(first.getAction()).isEqualTo("first"); + } + + @Test + public void send_shouldFillInIntentData_mutable_alreadySet_fillIn() throws Exception { + Intent[] intents = {new Intent("first")}; + Context context = Robolectric.setupActivity(Activity.class); + PendingIntent pendingIntent = + PendingIntent.getActivities(context, 99, intents, Intent.FILL_IN_ACTION); + + Activity otherContext = Robolectric.setupActivity(Activity.class); + Intent fillIntent = new Intent("first_and"); + pendingIntent.send(otherContext, 0, fillIntent); + + ShadowActivity shadowActivity = shadowOf(otherContext); + Intent first = shadowActivity.getNextStartedActivity(); + assertThat(first).isNotNull(); + assertThat(first.getAction()).isEqualTo("first_and"); + } + + @Test public void send_shouldNotUsePreviouslyFilledInLastIntentData() throws Exception { Intent[] intents = {new Intent("first"), new Intent("second")}; Context context = Robolectric.setupActivity(Activity.class); - PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0); Activity otherContext = Robolectric.setupActivity(Activity.class); Intent firstFillIntent = new Intent(); @@ -305,7 +358,7 @@ public class ShadowPendingIntentTest { @Test public void getActivity_withFlagNoCreate_shouldReturnExistingIntent() { Intent intent = new Intent(); - PendingIntent.getActivity(context, 99, intent, 100); + PendingIntent.getActivity(context, 99, intent, 0); Intent identical = new Intent(); PendingIntent saved = PendingIntent.getActivity(context, 99, identical, FLAG_NO_CREATE); @@ -316,7 +369,7 @@ public class ShadowPendingIntentTest { @Test public void getActivity_withNoFlags_shouldReturnExistingIntent() { Intent intent = new Intent(); - PendingIntent.getActivity(context, 99, intent, 100); + PendingIntent.getActivity(context, 99, intent, 0); Intent updated = new Intent(); PendingIntent saved = PendingIntent.getActivity(context, 99, updated, 0); @@ -341,7 +394,7 @@ public class ShadowPendingIntentTest { @Test public void getActivities_withFlagNoCreate_shouldReturnExistingIntent() { Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)}; - PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 100); + PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 0); Intent[] identicalIntents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)}; PendingIntent saved = @@ -353,7 +406,7 @@ public class ShadowPendingIntentTest { @Test public void getActivities_withNoFlags_shouldReturnExistingIntent() { Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)}; - PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 100); + PendingIntent.getActivities(ApplicationProvider.getApplicationContext(), 99, intents, 0); Intent[] identicalIntents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)}; PendingIntent saved = PendingIntent.getActivities(context, 99, identicalIntents, 0); @@ -378,7 +431,7 @@ public class ShadowPendingIntentTest { @Test public void getBroadcast_withFlagNoCreate_shouldReturnExistingIntent() { Intent intent = new Intent(); - PendingIntent.getBroadcast(context, 99, intent, 100); + PendingIntent.getBroadcast(context, 99, intent, 0); Intent identical = new Intent(); PendingIntent saved = PendingIntent.getBroadcast(context, 99, identical, FLAG_NO_CREATE); @@ -389,7 +442,7 @@ public class ShadowPendingIntentTest { @Test public void getBroadcast_withNoFlags_shouldReturnExistingIntent() { Intent intent = new Intent(); - PendingIntent.getBroadcast(context, 99, intent, 100); + PendingIntent.getBroadcast(context, 99, intent, 0); Intent identical = new Intent(); PendingIntent saved = PendingIntent.getBroadcast(context, 99, identical, 0); @@ -414,7 +467,7 @@ public class ShadowPendingIntentTest { @Test public void getService_withFlagNoCreate_shouldReturnExistingIntent() { Intent intent = new Intent().setPackage("dummy.package"); - PendingIntent.getService(context, 99, intent, 100); + PendingIntent.getService(context, 99, intent, 0); Intent identical = new Intent().setPackage("dummy.package"); PendingIntent saved = PendingIntent.getService(context, 99, identical, FLAG_NO_CREATE); @@ -425,7 +478,7 @@ public class ShadowPendingIntentTest { @Test public void getService_withNoFlags_shouldReturnExistingIntent() { Intent intent = new Intent().setPackage("dummy.package"); - PendingIntent.getService(context, 99, intent, 100); + PendingIntent.getService(context, 99, intent, 0); Intent identical = new Intent().setPackage("dummy.package"); PendingIntent saved = PendingIntent.getService(context, 99, identical, 0); @@ -454,7 +507,7 @@ public class ShadowPendingIntentTest { @Config(minSdk = Build.VERSION_CODES.O) public void getForegroundService_withFlagNoCreate_shouldReturnExistingIntent() { Intent intent = new Intent(); - PendingIntent.getForegroundService(context, 99, intent, 100); + PendingIntent.getForegroundService(context, 99, intent, 0); Intent identical = new Intent(); PendingIntent saved = @@ -467,7 +520,7 @@ public class ShadowPendingIntentTest { @Config(minSdk = Build.VERSION_CODES.O) public void getForegroundService_withNoFlags_shouldReturnExistingIntent() { Intent intent = new Intent().setPackage("dummy.package"); - PendingIntent.getForegroundService(context, 99, intent, 100); + PendingIntent.getForegroundService(context, 99, intent, 0); Intent identical = new Intent().setPackage("dummy.package"); PendingIntent saved = PendingIntent.getForegroundService(context, 99, identical, 0); @@ -478,7 +531,7 @@ public class ShadowPendingIntentTest { @Test public void cancel_shouldRemovePendingIntentForBroadcast() { Intent intent = new Intent(); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, intent, 0); assertThat(pendingIntent).isNotNull(); pendingIntent.cancel(); @@ -488,7 +541,7 @@ public class ShadowPendingIntentTest { @Test public void cancel_shouldRemovePendingIntentForActivity() { Intent intent = new Intent(); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent, 0); assertThat(pendingIntent).isNotNull(); pendingIntent.cancel(); @@ -498,7 +551,7 @@ public class ShadowPendingIntentTest { @Test public void cancel_shouldRemovePendingIntentForActivities() { Intent[] intents = {new Intent(Intent.ACTION_VIEW), new Intent(Intent.ACTION_PICK)}; - PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0); assertThat(pendingIntent).isNotNull(); pendingIntent.cancel(); @@ -508,7 +561,7 @@ public class ShadowPendingIntentTest { @Test public void cancel_shouldRemovePendingIntentForService() { Intent intent = new Intent().setPackage("dummy.package"); - PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getService(context, 99, intent, 0); assertThat(pendingIntent).isNotNull(); pendingIntent.cancel(); @@ -519,7 +572,7 @@ public class ShadowPendingIntentTest { @Config(minSdk = Build.VERSION_CODES.O) public void cancel_shouldRemovePendingIntentForForegroundService() { Intent intent = new Intent(); - PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 100); + PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 99, intent, 0); assertThat(pendingIntent).isNotNull(); pendingIntent.cancel(); @@ -530,35 +583,35 @@ public class ShadowPendingIntentTest { @Config(minSdk = Build.VERSION_CODES.S) public void isActivity_activityPendingIntent_returnsTrue() { Intent intent = new Intent(); - assertThat(PendingIntent.getActivity(context, 99, intent, 100).isActivity()).isTrue(); + assertThat(PendingIntent.getActivity(context, 99, intent, 0).isActivity()).isTrue(); } @Test @Config(minSdk = Build.VERSION_CODES.S) public void isActivity_broadcastPendingIntent_returnsFalse() { Intent intent = new Intent(); - assertThat(PendingIntent.getBroadcast(context, 99, intent, 100).isActivity()).isFalse(); + assertThat(PendingIntent.getBroadcast(context, 99, intent, 0).isActivity()).isFalse(); } @Test @Config(minSdk = Build.VERSION_CODES.S) public void isBroadcast_broadcastPendingIntent_returnsTrue() { Intent intent = new Intent(); - assertThat(PendingIntent.getBroadcast(context, 99, intent, 100).isBroadcast()).isTrue(); + assertThat(PendingIntent.getBroadcast(context, 99, intent, 0).isBroadcast()).isTrue(); } @Test @Config(minSdk = Build.VERSION_CODES.S) public void isBroadcast_activityPendingIntent_returnsFalse() { Intent intent = new Intent(); - assertThat(PendingIntent.getActivity(context, 99, intent, 100).isBroadcast()).isFalse(); + assertThat(PendingIntent.getActivity(context, 99, intent, 0).isBroadcast()).isFalse(); } @Test @Config(minSdk = Build.VERSION_CODES.S) public void isForegroundService_foregroundServicePendingIntent_returnsTrue() { Intent intent = new Intent(); - assertThat(PendingIntent.getForegroundService(context, 99, intent, 100).isForegroundService()) + assertThat(PendingIntent.getForegroundService(context, 99, intent, 0).isForegroundService()) .isTrue(); } @@ -566,21 +619,21 @@ public class ShadowPendingIntentTest { @Config(minSdk = Build.VERSION_CODES.S) public void isForegroundService_normalServicePendingIntent_returnsFalse() { Intent intent = new Intent(); - assertThat(PendingIntent.getService(context, 99, intent, 100).isForegroundService()).isFalse(); + assertThat(PendingIntent.getService(context, 99, intent, 0).isForegroundService()).isFalse(); } @Test @Config(minSdk = Build.VERSION_CODES.S) public void isService_servicePendingIntent_returnsTrue() { Intent intent = new Intent(); - assertThat(PendingIntent.getService(context, 99, intent, 100).isService()).isTrue(); + assertThat(PendingIntent.getService(context, 99, intent, 0).isService()).isTrue(); } @Test @Config(minSdk = Build.VERSION_CODES.S) public void isService_foregroundServicePendingIntent_returnsFalse() { Intent intent = new Intent(); - assertThat(PendingIntent.getForegroundService(context, 99, intent, 100).isService()).isFalse(); + assertThat(PendingIntent.getForegroundService(context, 99, intent, 0).isService()).isFalse(); } @Test @@ -603,7 +656,7 @@ public class ShadowPendingIntentTest { @Test public void send_canceledPendingIntent_throwsCanceledException() throws CanceledException { Intent intent = new Intent().setPackage("dummy.package"); - PendingIntent canceled = PendingIntent.getService(context, 99, intent, 100); + PendingIntent canceled = PendingIntent.getService(context, 99, intent, 0); assertThat(canceled).isNotNull(); // Cancel the existing PendingIntent and create a new one in its place. @@ -798,8 +851,7 @@ public class ShadowPendingIntentTest { @Test public void testEquals() { - PendingIntent pendingIntent = - PendingIntent.getActivity(context, 99, new Intent("activity"), 100); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0); // Same type, requestCode and Intent action implies equality. assertThat(PendingIntent.getActivity(context, 99, new Intent("activity"), FLAG_NO_CREATE)) @@ -823,7 +875,7 @@ public class ShadowPendingIntentTest { @Test public void testEquals_getActivities() { Intent[] intents = {new Intent("activity"), new Intent("activity2")}; - PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 100); + PendingIntent pendingIntent = PendingIntent.getActivities(context, 99, intents, 0); Intent[] forward = {new Intent("activity"), new Intent("activity2")}; assertThat(PendingIntent.getActivities(context, 99, forward, FLAG_NO_CREATE)) @@ -844,8 +896,7 @@ public class ShadowPendingIntentTest { @Test @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1) public void testGetCreatorPackage_nothingSet() { - PendingIntent pendingIntent = - PendingIntent.getActivity(context, 99, new Intent("activity"), 100); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0); assertThat(pendingIntent.getCreatorPackage()).isEqualTo(context.getPackageName()); assertThat(pendingIntent.getTargetPackage()).isEqualTo(context.getPackageName()); } @@ -854,8 +905,7 @@ public class ShadowPendingIntentTest { @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1) public void testGetCreatorPackage_explicitlySetPackage() { String fakePackage = "some.fake.package"; - PendingIntent pendingIntent = - PendingIntent.getActivity(context, 99, new Intent("activity"), 100); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0); shadowOf(pendingIntent).setCreatorPackage(fakePackage); assertThat(pendingIntent.getCreatorPackage()).isEqualTo(fakePackage); assertThat(pendingIntent.getTargetPackage()).isEqualTo(fakePackage); @@ -865,8 +915,7 @@ public class ShadowPendingIntentTest { @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1) public void testGetCreatorUid() { int fakeUid = 123; - PendingIntent pendingIntent = - PendingIntent.getActivity(context, 99, new Intent("activity"), 100); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, new Intent("activity"), 0); shadowOf(pendingIntent).setCreatorUid(fakeUid); assertThat(pendingIntent.getCreatorUid()).isEqualTo(fakeUid); @@ -875,16 +924,16 @@ public class ShadowPendingIntentTest { @Test public void testHashCode() { Context ctx = ApplicationProvider.getApplicationContext(); - PendingIntent pendingIntent1 = PendingIntent.getActivity(ctx, 99, new Intent("activity"), 100); + PendingIntent pendingIntent1 = PendingIntent.getActivity(ctx, 99, new Intent("activity"), 0); assertThat(pendingIntent1.hashCode()) - .isEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity"), 100).hashCode()); + .isEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity"), 0).hashCode()); assertThat(pendingIntent1.hashCode()) - .isNotEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity2"), 100).hashCode()); + .isNotEqualTo(PendingIntent.getActivity(ctx, 99, new Intent("activity2"), 0).hashCode()); assertThat(pendingIntent1.hashCode()) - .isNotEqualTo(PendingIntent.getActivity(ctx, 999, new Intent("activity"), 100).hashCode()); + .isNotEqualTo(PendingIntent.getActivity(ctx, 999, new Intent("activity"), 0).hashCode()); } @Test diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java index 96ef647ed..3c5781720 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java @@ -266,10 +266,12 @@ public class ShadowPowerManagerTest { String rebootReason = "reason"; powerManager.reboot(rebootReason); + powerManager.reboot(null); - assertThat(shadowOf(powerManager).getTimesRebooted()).isEqualTo(1); - assertThat(shadowOf(powerManager).getRebootReasons()).hasSize(1); - assertThat(shadowOf(powerManager).getRebootReasons()).contains(rebootReason); + assertThat(shadowOf(powerManager).getTimesRebooted()).isEqualTo(2); + assertThat(shadowOf(powerManager).getRebootReasons()) + .containsExactly(rebootReason, null) + .inOrder(); } @Test diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowStatsManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowStatsManagerTest.java new file mode 100644 index 000000000..1c895bb27 --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowStatsManagerTest.java @@ -0,0 +1,80 @@ +package org.robolectric.shadows; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.StatsManager; +import android.content.Context; +import android.os.Build; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link ShadowStatsManager} */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = Build.VERSION_CODES.P) +public final class ShadowStatsManagerTest { + + @Test + public void testGetMetadata() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + StatsManager statsManager = context.getSystemService(StatsManager.class); + byte[] metadataBytes = new byte[] {1, 2, 3, 4, 5}; + ShadowStatsManager.setStatsMetadata(metadataBytes); + + assertThat(statsManager.getMetadata()).isEqualTo(metadataBytes); + } + + @Test + public void testGetReports_multipleReports() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + StatsManager statsManager = context.getSystemService(StatsManager.class); + long reportId1 = 1L; + long reportId2 = 2L; + byte[] report1Bytes = new byte[] {1, 2, 3, 4, 5}; + byte[] report2Bytes = new byte[] {1, 2, 3}; + ShadowStatsManager.addReportData(reportId1, report1Bytes); + ShadowStatsManager.addReportData(reportId2, report2Bytes); + + assertThat(statsManager.getReports(reportId1)).isEqualTo(report1Bytes); + assertThat(statsManager.getReports(reportId2)).isEqualTo(report2Bytes); + } + + @Test + public void testGetReports_clearsExistingReport() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + StatsManager statsManager = context.getSystemService(StatsManager.class); + long reportId1 = 1L; + byte[] report1Bytes = new byte[] {1, 2, 3, 4, 5}; + ShadowStatsManager.addReportData(reportId1, report1Bytes); + + assertThat(statsManager.getReports(reportId1)).isEqualTo(report1Bytes); + assertThat(statsManager.getReports(reportId1)).isEqualTo(new byte[] {}); + } + + @Test + public void testReset_clearsReports() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + StatsManager statsManager = context.getSystemService(StatsManager.class); + long reportId1 = 1L; + byte[] report1Bytes = new byte[] {1, 2, 3, 4, 5}; + ShadowStatsManager.addReportData(reportId1, report1Bytes); + + ShadowStatsManager.reset(); + + assertThat(statsManager.getReports(reportId1)).isEqualTo(new byte[] {}); + } + + @Test + public void testReset_clearsMetadata() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + StatsManager statsManager = context.getSystemService(StatsManager.class); + byte[] metadataBytes = new byte[] {1, 2, 3, 4, 5}; + ShadowStatsManager.setStatsMetadata(metadataBytes); + + ShadowStatsManager.reset(); + + assertThat(statsManager.getMetadata()).isEqualTo(new byte[] {}); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java index a527ec98f..e3fd11c48 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java @@ -3,6 +3,7 @@ package org.robolectric.shadows; import static android.content.Context.TELEPHONY_SUBSCRIPTION_SERVICE; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.TIRAMISU; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; @@ -156,6 +157,16 @@ public class ShadowSubscriptionManagerTest { } @Test + public void getActiveSubscriptionInfo_shouldThrowExceptionWhenNoPermissions() { + shadowOf(subscriptionManager).setReadPhoneStatePermission(false); + assertThrows( + SecurityException.class, + () -> + shadowOf(subscriptionManager) + .getActiveSubscriptionInfo(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)); + } + + @Test public void getActiveSubscriptionInfoList_shouldReturnInfoList() { SubscriptionInfo expectedSubscriptionInfo = SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo(); @@ -373,6 +384,17 @@ public class ShadowSubscriptionManagerTest { @Test @Config(minSdk = TIRAMISU) + public void getPhoneNumber_shouldThrowExceptionWhenNoPermissions() { + shadowOf(subscriptionManager).setReadPhoneNumbersPermission(false); + assertThrows( + SecurityException.class, + () -> + shadowOf(subscriptionManager) + .getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)); + } + + @Test + @Config(minSdk = TIRAMISU) public void getPhoneNumberWithSource_phoneNumberNotSet_returnsEmptyString() { assertThat( subscriptionManager.getPhoneNumber( @@ -413,6 +435,28 @@ public class ShadowSubscriptionManagerTest { .isEqualTo("123"); } + @Test + @Config(minSdk = Q) + public void setIsOpportunistic_shouldReturnFalse() { + assertThat( + ShadowSubscriptionManager.SubscriptionInfoBuilder.newBuilder() + .setIsOpportunistic(false) + .buildSubscriptionInfo() + .isOpportunistic()) + .isFalse(); + } + + @Test + @Config(minSdk = Q) + public void setIsOpportunistic_shouldReturnTrue() { + assertThat( + ShadowSubscriptionManager.SubscriptionInfoBuilder.newBuilder() + .setIsOpportunistic(true) + .buildSubscriptionInfo() + .isOpportunistic()) + .isTrue(); + } + private static class DummySubscriptionsChangedListener extends SubscriptionManager.OnSubscriptionsChangedListener { private int subscriptionChangedCount; diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java index 67e4746f5..58d77d3c8 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelecomManagerTest.java @@ -8,6 +8,7 @@ import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -93,6 +94,14 @@ public class ShadowTelecomManagerTest { } @Test + public void getPhoneAccount_noPermission_throwsSecurityException() { + shadowOf(telecomService).setReadPhoneStatePermission(false); + + PhoneAccountHandle handler = createHandle("id"); + assertThrows(SecurityException.class, () -> telecomService.getPhoneAccount(handler)); + } + + @Test public void clearAccounts() { PhoneAccountHandle anotherPackageHandle = createHandle("some.other.package", "OtherConnectionService", "id"); @@ -156,6 +165,14 @@ public class ShadowTelecomManagerTest { } @Test + @Config(minSdk = M) + public void getCallCapablePhoneAccounts_noPermission_throwsSecurityException() { + shadowOf(telecomService).setReadPhoneStatePermission(false); + + assertThrows(SecurityException.class, () -> telecomService.getCallCapablePhoneAccounts()); + } + + @Test @Config(minSdk = O) public void getSelfManagedPhoneAccounts() { PhoneAccountHandle selfManagedPhoneAccountHandle = createHandle("id1"); @@ -266,6 +283,19 @@ public class ShadowTelecomManagerTest { @Test @Config(minSdk = M) + public void testPlaceCall_noPermission_throwsSecurityException() { + shadowOf(telecomService).setCallPhonePermission(false); + + Bundle extras = new Bundle(); + extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, createHandle("id")); + + assertThrows( + SecurityException.class, + () -> telecomService.placeCall(Uri.parse("tel:+1-201-555-0123"), extras)); + } + + @Test + @Config(minSdk = M) public void testAllowPlaceCall() { shadowOf(telecomService).setCallRequestMode(CallRequestMode.ALLOW_ALL); @@ -414,6 +444,13 @@ public class ShadowTelecomManagerTest { } @Test + public void setTtySupported_noPermission_throwsSecurityException() { + shadowOf(telecomService).setReadPhoneStatePermission(false); + + assertThrows(SecurityException.class, () -> telecomService.isTtySupported()); + } + + @Test public void canSetAndGetIsInCall() { shadowOf(telecomService).setIsInCall(true); assertThat(telecomService.isInCall()).isTrue(); @@ -532,6 +569,59 @@ public class ShadowTelecomManagerTest { assertThat(telecomService.getVoiceMailNumber(phoneAccountHandle)).isNull(); } + @Test + @Config(minSdk = LOLLIPOP_MR1) + public void getLine1Number() { + // Check initial state + PhoneAccountHandle phoneAccountHandle = createHandle("id1"); + assertThat(telecomService.getLine1Number(phoneAccountHandle)).isNull(); + + // After setting + shadowOf(telecomService).setLine1Number(phoneAccountHandle, "123"); + assertThat(telecomService.getLine1Number(phoneAccountHandle)).isEqualTo("123"); + + // After reset + shadowOf(telecomService).setLine1Number(phoneAccountHandle, null); + assertThat(telecomService.getLine1Number(phoneAccountHandle)).isNull(); + } + + @Test + @Config(minSdk = LOLLIPOP_MR1) + public void getLine1Number_noPermission_throwsSecurityException() { + shadowOf(telecomService).setReadPhoneStatePermission(false); + + PhoneAccountHandle phoneAccountHandle = createHandle("id1"); + assertThrows(SecurityException.class, () -> telecomService.getLine1Number(phoneAccountHandle)); + } + + @Test + public void handleMmi_defaultValueFalse() { + assertThat(telecomService.handleMmi("123")).isFalse(); + } + + @Test + public void handleMmi() { + shadowOf(telecomService).setHandleMmiValue(true); + + assertThat(telecomService.handleMmi("123")).isTrue(); + } + + @Test + @Config(minSdk = M) + public void handleMmiWithHandle_defaultValueFalse() { + PhoneAccountHandle phoneAccountHandle = createHandle("id1"); + assertThat(telecomService.handleMmi("123", phoneAccountHandle)).isFalse(); + } + + @Test + @Config(minSdk = M) + public void handleMmiWithHandle() { + shadowOf(telecomService).setHandleMmiValue(true); + PhoneAccountHandle phoneAccountHandle = createHandle("id1"); + + assertThat(telecomService.handleMmi("123", phoneAccountHandle)).isTrue(); + } + private static PhoneAccountHandle createHandle(String id) { return new PhoneAccountHandle( new ComponentName(ApplicationProvider.getApplicationContext(), TestConnectionService.class), diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java index ad3adebcf..90c74d105 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java @@ -58,6 +58,7 @@ import android.os.PersistableBundle; import android.telecom.PhoneAccountHandle; import android.telephony.CellInfo; import android.telephony.CellLocation; +import android.telephony.PhoneCapability; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.SignalStrength; @@ -172,6 +173,13 @@ public class ShadowTelephonyManagerTest { } @Test + public void shouldGiveDeviceSoftwareVersion() { + String testSoftwareVersion = "getDeviceSoftwareVersion"; + shadowOf(telephonyManager).setDeviceSoftwareVersion(testSoftwareVersion); + assertEquals(testSoftwareVersion, telephonyManager.getDeviceSoftwareVersion()); + } + + @Test @Config(minSdk = O) public void getImei() { String testImei = "4test imei"; @@ -326,6 +334,13 @@ public class ShadowTelephonyManagerTest { } @Test + @Config(minSdk = O) + public void shouldGiveNetworkSpecifier() { + shadowOf(telephonyManager).setNetworkSpecifier("SomeSpecifier"); + assertEquals("SomeSpecifier", telephonyManager.getNetworkSpecifier()); + } + + @Test public void shouldGiveLine1Number() { shadowOf(telephonyManager).setLine1Number("123-244-2222"); assertEquals("123-244-2222", telephonyManager.getLine1Number()); @@ -346,6 +361,23 @@ public class ShadowTelephonyManagerTest { } @Test + @Config(minSdk = M) + public void + getDeviceIdForSlot_shouldThrowSecurityExceptionWhenReadPhoneStatePermissionNotGranted() + throws Exception { + shadowOf(telephonyManager).setReadPhoneStatePermission(false); + assertThrows(SecurityException.class, () -> telephonyManager.getDeviceId(1)); + } + + @Test + public void + getDeviceSoftwareVersion_shouldThrowSecurityExceptionWhenReadPhoneStatePermissionNotGranted() + throws Exception { + shadowOf(telephonyManager).setReadPhoneStatePermission(false); + assertThrows(SecurityException.class, () -> telephonyManager.getDeviceSoftwareVersion()); + } + + @Test public void shouldGivePhoneType() { shadowOf(telephonyManager).setPhoneType(TelephonyManager.PHONE_TYPE_CDMA); assertEquals(TelephonyManager.PHONE_TYPE_CDMA, telephonyManager.getPhoneType()); @@ -515,7 +547,7 @@ public class ShadowTelephonyManagerTest { PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle( new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); - Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment = */ null); + Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment= */ null); shadowOf(telephonyManager).setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri); @@ -528,7 +560,7 @@ public class ShadowTelephonyManagerTest { PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle( new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); - Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment = */ null); + Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment= */ null); // Note: Using the real manager to set, instead of the shadow. telephonyManager.setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri); @@ -849,6 +881,15 @@ public class ShadowTelephonyManagerTest { } @Test + public void setDataActivityChangesDataActivity() { + assertThat(telephonyManager.getDataActivity()).isEqualTo(TelephonyManager.DATA_ACTIVITY_NONE); + shadowOf(telephonyManager).setDataActivity(TelephonyManager.DATA_ACTIVITY_IN); + assertThat(telephonyManager.getDataActivity()).isEqualTo(TelephonyManager.DATA_ACTIVITY_IN); + shadowOf(telephonyManager).setDataActivity(TelephonyManager.DATA_ACTIVITY_OUT); + assertThat(telephonyManager.getDataActivity()).isEqualTo(TelephonyManager.DATA_ACTIVITY_OUT); + } + + @Test @Config(minSdk = Q) public void setRttSupportedChangesIsRttSupported() { shadowOf(telephonyManager).setRttSupported(false); @@ -858,6 +899,33 @@ public class ShadowTelephonyManagerTest { } @Test + @Config(minSdk = M) + public void setTtyModeSupportedChangesIsTtyModeSupported() { + shadowOf(telephonyManager).setTtyModeSupported(false); + assertThat(telephonyManager.isTtyModeSupported()).isFalse(); + shadowOf(telephonyManager).setTtyModeSupported(true); + assertThat(telephonyManager.isTtyModeSupported()).isTrue(); + } + + @Test + @Config(minSdk = M) + public void + isTtyModeSupported_shouldThrowSecurityExceptionWhenReadPhoneStatePermissionNotGranted() + throws Exception { + shadowOf(telephonyManager).setReadPhoneStatePermission(false); + assertThrows(SecurityException.class, () -> telephonyManager.isTtyModeSupported()); + } + + @Test + @Config(minSdk = N) + public void hasCarrierPrivilegesWithSubId() { + int subId = 3; + assertThat(telephonyManager.hasCarrierPrivileges(subId)).isFalse(); + shadowOf(telephonyManager).setHasCarrierPrivileges(subId, true); + assertThat(telephonyManager.hasCarrierPrivileges(subId)).isTrue(); + } + + @Test @Config(minSdk = O) public void sendDialerSpecialCode() { shadowOf(telephonyManager).sendDialerSpecialCode("1234"); @@ -1034,6 +1102,16 @@ public class ShadowTelephonyManagerTest { } @Test + @Config(minSdk = S) + public void setPhoneCapability_returnsPhoneCapability() { + PhoneCapability phoneCapability = PhoneCapabilityFactory.create(2, 1, false, new int[0]); + + shadowTelephonyManager.setPhoneCapability(phoneCapability); + + assertThat(telephonyManager.getPhoneCapability()).isEqualTo(phoneCapability); + } + + @Test @Config(minSdk = O) public void sendVisualVoicemailSms_shouldStoreLastSendSmsParameters() { telephonyManager.sendVisualVoicemailSms("destAddress", 0, "message", null); @@ -1137,4 +1215,16 @@ public class ShadowTelephonyManagerTest { ImmutableMap.of(0, ImmutableList.of(emergencyNumber))); assertThat(telephonyManager.getEmergencyNumberList().get(0)).containsExactly(emergencyNumber); } + + @Test + @Config(minSdk = R) + public void getSubscriptionIdForPhoneAccountHandle() { + int subscriptionId = 123; + PhoneAccountHandle phoneAccountHandle = + new PhoneAccountHandle( + new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); + shadowOf(telephonyManager) + .setPhoneAccountHandleSubscriptionId(phoneAccountHandle, subscriptionId); + assertEquals(subscriptionId, telephonyManager.getSubscriptionId(phoneAccountHandle)); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java index 9da4c416c..14aa24297 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTimeManagerTest.java @@ -15,7 +15,7 @@ import org.robolectric.annotation.Config; /** Tests for {@link ShadowTimeManager} */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Build.VERSION_CODES.S) +@Config(minSdk = Build.VERSION_CODES.S) public final class ShadowTimeManagerTest { @Test diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java index 5f19aafef..66f40b933 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUiAutomationTest.java @@ -17,6 +17,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; /** Test for {@link ShadowUiAutomation}. */ @Config(minSdk = JELLY_BEAN_MR2) @@ -102,4 +103,16 @@ public class ShadowUiAutomationTest { assertThat(Resources.getSystem().getConfiguration().orientation) .isEqualTo(Configuration.ORIENTATION_PORTRAIT); } + + @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) + @Test + public void setAnimationScale_zero_instrumentationTestLooperMode() throws Exception { + setAnimationScale_zero(); + } + + @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) + @Test + public void setRotation_freeze90_rotatesToLandscape_instrumentationTestLooperMode() { + setRotation_freeze90_rotatesToLandscape(); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java index 18b3e92b6..39f8b6d5e 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVoiceInteractionSessionTest.java @@ -5,6 +5,7 @@ import static android.os.Build.VERSION_CODES.Q; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; +import android.app.VoiceInteractor.CommandRequest; import android.content.Intent; import android.os.Bundle; import android.service.voice.VoiceInteractionSession; @@ -46,6 +47,16 @@ public class ShadowVoiceInteractionSessionTest { } @Test + @Config(minSdk = 34) + public void isWindowShowing_afterShowSdk34_returnsTrue() { + shadowSession.create(); + + session.show(new Bundle(), /* flags= */ 0); + + assertThat(shadowSession.isWindowShowing()).isTrue(); + } + + @Test public void isWindowShowing_afterShowThenHide_returnsFalse() { shadowSession.create(); @@ -159,4 +170,71 @@ public class ShadowVoiceInteractionSessionTest { public void isUiEnabled_belowAndroidO_throws() { shadowSession.isUiEnabled(); } + + @Test + public void sendCommandRequest_cancel_requestCanceled() { + TestCommandRequest commandRequest = new TestCommandRequest("test_command", new Bundle()); + VoiceInteractionSession.CommandRequest receivedCommandRequest = + shadowSession.sendCommandRequest(commandRequest, "test_package", 123); + + assertThat(receivedCommandRequest.isActive()).isTrue(); + assertThat(receivedCommandRequest.getCommand()).isEqualTo("test_command"); + + receivedCommandRequest.cancel(); + + assertThat(commandRequest.isCancelled).isTrue(); + } + + @Test + public void sendCommandRequest_sendIntermediateResult_requestRemainsActive() { + TestCommandRequest commandRequest = new TestCommandRequest("test_command", new Bundle()); + VoiceInteractionSession.CommandRequest receivedCommandRequest = + shadowSession.sendCommandRequest(commandRequest, "test_package", 123); + + assertThat(receivedCommandRequest.isActive()).isTrue(); + assertThat(receivedCommandRequest.getCommand()).isEqualTo("test_command"); + + Bundle result = new Bundle(); + result.putBoolean("intermediate", true); + receivedCommandRequest.sendIntermediateResult(result); + assertThat(commandRequest.isCompleted).isFalse(); + assertThat(commandRequest.result).isEqualTo(result); + } + + @Test + public void sendCommandRequest_sendFinalResult_requestCompleted() { + TestCommandRequest commandRequest = new TestCommandRequest("test_command", new Bundle()); + VoiceInteractionSession.CommandRequest receivedCommandRequest = + shadowSession.sendCommandRequest(commandRequest, "test_package", 123); + + assertThat(receivedCommandRequest.isActive()).isTrue(); + assertThat(receivedCommandRequest.getCommand()).isEqualTo("test_command"); + + Bundle result = new Bundle(); + result.putBoolean("final", true); + receivedCommandRequest.sendResult(result); + assertThat(commandRequest.isCompleted).isTrue(); + assertThat(commandRequest.result).isEqualTo(result); + } + + private static class TestCommandRequest extends CommandRequest { + public boolean isCancelled = false; + public boolean isCompleted = false; + public Bundle result = null; + + public TestCommandRequest(String command, Bundle args) { + super(command, args); + } + + @Override + public void onCommandResult(boolean isCompleted, Bundle result) { + this.isCompleted = isCompleted; + this.result = result; + } + + @Override + public void onCancel() { + this.isCancelled = true; + } + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java index fc5a5e5da..26acedbb8 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java @@ -642,6 +642,38 @@ public class ShadowWifiManagerTest { @Test @Config(minSdk = Q) + public void isWpa3SaeSupportedAndConfigurable() { + assertThat(wifiManager.isWpa3SaeSupported()).isFalse(); + shadowOf(wifiManager).setWpa3SaeSupported(true); + assertThat(wifiManager.isWpa3SaeSupported()).isTrue(); + } + + @Test + @Config(minSdk = S) + public void isWpa3SaePublicKeySupportedAndConfigurable() { + assertThat(wifiManager.isWpa3SaePublicKeySupported()).isFalse(); + shadowOf(wifiManager).setWpa3SaePublicKeySupported(true); + assertThat(wifiManager.isWpa3SaePublicKeySupported()).isTrue(); + } + + @Test + @Config(minSdk = S) + public void isWpa3SaeH2eSupportedAndConfigurable() { + assertThat(wifiManager.isWpa3SaeH2eSupported()).isFalse(); + shadowOf(wifiManager).setWpa3SaeH2eSupported(true); + assertThat(wifiManager.isWpa3SaeH2eSupported()).isTrue(); + } + + @Test + @Config(minSdk = Q) + public void isWpa3SuiteBSupportedAndConfigurable() { + assertThat(wifiManager.isWpa3SuiteBSupported()).isFalse(); + shadowOf(wifiManager).setWpa3SuiteBSupported(true); + assertThat(wifiManager.isWpa3SuiteBSupported()).isTrue(); + } + + @Test + @Config(minSdk = Q) public void testAddOnWifiUsabilityStatsListener() { // GIVEN WifiManager.OnWifiUsabilityStatsListener mockListener = diff --git a/robolectric/src/test/resources/res/values/attrs.xml b/robolectric/src/test/resources/res/values/attrs.xml index b96d70613..aea211d93 100644 --- a/robolectric/src/test/resources/res/values/attrs.xml +++ b/robolectric/src/test/resources/res/values/attrs.xml @@ -65,6 +65,7 @@ <attr name="attributeReferencingAnAttribute" format="reference"/> <attr name="circularReference" format="reference"/> <attr name="title" format="string"/> + <attr name="loaderIcon" format="reference"/> <declare-styleable name="CustomStateView"> <attr name="stateFoo" format="boolean" /> diff --git a/robolectric/src/test/resources/resources.ap_ b/robolectric/src/test/resources/resources.ap_ Binary files differindex 52d9aeaf6..b34ec69fd 100644 --- a/robolectric/src/test/resources/resources.ap_ +++ b/robolectric/src/test/resources/resources.ap_ diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java index 53d4e5724..5c77b9964 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java @@ -38,6 +38,7 @@ import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; +import org.robolectric.sandbox.NativeMethodNotFoundException; import org.robolectric.util.PerfStatsCollector; /** @@ -53,6 +54,7 @@ public class ClassInstrumentor { protected static final Type OBJECT_TYPE = Type.getType(Object.class); private static final ShadowImpl SHADOW_IMPL = new ShadowImpl(); final Decorator decorator; + private NativeCallHandler nativeCallHandler; static { String className = Type.getInternalName(InvokeDynamicSupport.class); @@ -538,6 +540,18 @@ public class ClassInstrumentor { method.access = method.access & ~Opcodes.ACC_NATIVE; RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method); + + if (nativeCallHandler != null) { + String descriptor = + String.format("%s#%s%s", mutableClass.getName(), method.name, method.desc); + nativeCallHandler.logNativeCall(descriptor); + if (nativeCallHandler.shouldThrow(descriptor)) { + String message = + nativeCallHandler.getExceptionMessage(descriptor, mutableClass.getName(), method.name); + generator.throwException(Type.getType(NativeMethodNotFoundException.class), message); + } + } + Type returnType = generator.getReturnType(); generator.pushDefaultReturnValueToStack(returnType); generator.returnValue(); @@ -739,6 +753,10 @@ public class ClassInstrumentor { return -1; } + public void setNativeCallHandler(NativeCallHandler nativeCallHandler) { + this.nativeCallHandler = nativeCallHandler; + } + public interface Decorator { void decorate(MutableClass mutableClass); } diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java new file mode 100644 index 000000000..89034d63f --- /dev/null +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java @@ -0,0 +1,137 @@ +package org.robolectric.internal.bytecode; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Set; +import java.util.TreeSet; +import javax.annotation.Nonnull; + +/** + * Handler for native calls instrumented by ClassInstrumentor. + * + * <p>Native Calls can either be instrumented as no-op calls (returning a default value or 0 or + * null) or throw an exception. This helper class helps maintain a list of exemptions to indicates + * which native calls should be no-op and never throw. + */ +public class NativeCallHandler { + + private final File exemptionsFile; + private final boolean writeExemptions; + private final boolean throwOnNatives; + private final Set<String> descriptors = new TreeSet<>(); + + /** + * Initializes the native calls handler. + * + * @param exemptionsFile The exemptions file to read from and/or to generate. + * @param writeExemptions When true, native calls are added to the exemption list. + * @param throwOnNatives Whether native calls should throw by default unless their signature is + * listed in the exemption list. When false, all native calls become no-op. + * @throws IOException if there's an issue reading an existing exemption list. + */ + public NativeCallHandler( + @Nonnull File exemptionsFile, boolean writeExemptions, boolean throwOnNatives) + throws IOException { + this.exemptionsFile = exemptionsFile; + this.writeExemptions = writeExemptions; + this.throwOnNatives = throwOnNatives; + + if (exemptionsFile.exists()) { + readExemptionsList(exemptionsFile); + } + } + + private String getExemptionFileName() { + return exemptionsFile.getName(); + } + + private void readExemptionsList(File exemptionsFile) throws IOException { + try (BufferedReader reader = + new BufferedReader(new FileReader(exemptionsFile.getPath(), UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + // Sanitize input. Ignore empty lines and commented lines starting with #. + line = sanitize(line.trim()); + if (line.isEmpty() || line.charAt(0) == '#') { + continue; + } + descriptors.add(line); + } + } + System.out.println( + "Loaded " + descriptors.size() + " exemptions from " + exemptionsFile.getPath()); + } + + public void writeExemptionsList() throws IOException { + try (BufferedWriter writer = + new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { + for (String descriptor : descriptors) { + writer.write(descriptor); + writer.write('\n'); + } + } + System.out.println( + "Wrote " + descriptors.size() + " exemptions to " + exemptionsFile.getPath()); + } + + /** + * Adds the method description to the native call exemption list if {@link #writeExemptions} is + * set. + */ + public void logNativeCall(@Nonnull String descriptor) { + if (!writeExemptions) { + return; + } + descriptors.add(sanitize(descriptor)); + } + + /** Returns whether the ClassInstrumentor should generate an exception or a no-op bytecode. */ + public boolean shouldThrow(@Nonnull String descriptor) { + return throwOnNatives && !descriptors.contains(sanitize(descriptor)); + } + + private String sanitize(String descriptor) { + // Post-processing of the exemptions files is made complicated by the presence of $ signs + // in the FQCN. Instead of escaping them, just replace them by another unused character + // that is not so sensitive to shell or make mangling. + return descriptor.replace('$', '^'); + } + + /** + * Returns the detailed message to be used by the ClassInstrumentor in the generated bytecode. + * + * @param descriptor The ASM descriptor as it should be written in the exemption file. + * @param className The fully qualified class name, used for the user description. + * @param methodName The method name, used for the user description. + */ + public String getExceptionMessage( + @Nonnull String descriptor, @Nonnull String className, @Nonnull String methodName) { + // The shadow message is merely a hint based on the last component of the FQCN, which is + // typically the pattern used for shadow classes. + String shadowHint = + "Shadow" + className.replaceAll("[^.]+\\.", "").replaceAll("\\$.*", "") + ".java"; + // The message below tries to educate the user that shadow overrides are not necessarily + // needed nor desired for trivial cases that are better covered by a no-op return operation. + return "Unexpected Robolectric native method call to '" + + className + + "#" + + methodName + + "()'.\n" + + "Option 1: If customizing this method is useful, add an implementation in " + + shadowHint + + ".\n" + + "Option 2: If this method just needs to trivially return 0 or null, please add an" + + " exemption entry for\n" + + " " + + sanitize(descriptor) + + "\n" + + "to exemption file " + + getExemptionFileName(); + } +} diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java index 51953fdc9..30ee4f563 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java @@ -23,6 +23,7 @@ import javax.annotation.Nonnull; import javax.annotation.Priority; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.ReflectorObject; +import org.robolectric.sandbox.NativeMethodNotFoundException; import org.robolectric.sandbox.ShadowMatcher; import org.robolectric.util.Function; import org.robolectric.util.PerfStatsCollector; @@ -182,7 +183,21 @@ public class ShadowWrangler implements ClassHandler { } else { RobolectricInternals.performStaticInitialization(clazz); } - } catch (InvocationTargetException | IllegalAccessException e) { + } catch (InvocationTargetException e) { + // Note: target exception originates from the sandbox classloader. + // "instanceof" does not check class equality across classloaders (since they differ). + // A simple workaround is to check the class FQCN instead. + String nativeMethodNotFoundException = NativeMethodNotFoundException.class.getName(); + + for (Throwable t = e.getTargetException(); t != null; ) { + if (nativeMethodNotFoundException.equals(t.getClass().getName())) { + throw (RuntimeException) t; + } + + t = t.getCause(); + } + throw new RuntimeException(e); + } catch (IllegalAccessException e) { throw new RuntimeException(e); } } diff --git a/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java b/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java new file mode 100644 index 000000000..ad04d959f --- /dev/null +++ b/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java @@ -0,0 +1,18 @@ +package org.robolectric.sandbox; + +/** + * Thrown when a particular Robolectric native method cannot be found. + * + * <p>Instrumented native methods throw this exception when the NativeCallHandler is set to + * throw-on-native and that the dedicated method signature has not been exempted. + */ +public class NativeMethodNotFoundException extends RuntimeException { + + public NativeMethodNotFoundException() { + super(); + } + + public NativeMethodNotFoundException(String message) { + super(message); + } +} diff --git a/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java b/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java new file mode 100644 index 000000000..fa208d95d --- /dev/null +++ b/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java @@ -0,0 +1,158 @@ +package org.robolectric.internal.bytecode; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +/** Test for {@link ClassInstrumentor}. */ +@RunWith(JUnit4.class) +public class ClassInstrumentorTest { + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + private ClassInstrumentor instrumentor; + private ClassNodeProvider classNodeProvider; + + @Before + public void setUp() throws Exception { + instrumentor = new ClassInstrumentor(); + classNodeProvider = + new ClassNodeProvider() { + @Override + protected byte[] getClassBytes(String className) { + return new byte[0]; + } + }; + } + + @Test + public void instrumentNativeMethod_legacy() { + ClassNode classNode = new ClassNode(); + classNode.name = "org/example/MyClass"; + + MethodNode methodNode = new MethodNode(); + methodNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_NATIVE; + methodNode.name = "someFunction"; + methodNode.desc = "()I"; + methodNode.signature = "()"; + methodNode.exceptions = ImmutableList.of(); + methodNode.visibleAnnotations = ImmutableList.of(); + + classNode.methods.add(methodNode); + + MutableClass clazz = + new MutableClass( + classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider); + instrumentor.instrument(clazz); + + // Side effect: original method has been made private. + assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0); + // Side effect: original method has been renamed to a robolectric delegate + assertThat(methodNode.name).isEqualTo("$$robo$$org_example_MyClass$someFunction"); + // Side effect: instructions have been rewritten to return 0. + assertThat(methodNode.instructions.size()).isEqualTo(2); + assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.ICONST_0); + assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.IRETURN); + } + + @Test + public void instrumentNativeMethod_withoutExemption_generatesThrowException() throws IOException { + File exemptionsFile = tempFolder.newFile("natives.txt"); + try (BufferedWriter writer = + new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { + writer.write("org.example.MyClass#someOtherMethod()V\n"); + } + + NativeCallHandler nativeCallHandler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); + instrumentor.setNativeCallHandler(nativeCallHandler); + + ClassNode classNode = new ClassNode(); + classNode.name = "org/example/MyClass"; + + MethodNode methodNode = new MethodNode(); + methodNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_NATIVE; + methodNode.name = "someFunction"; + methodNode.desc = "()I"; + methodNode.signature = "()"; + methodNode.exceptions = ImmutableList.of(); + methodNode.visibleAnnotations = ImmutableList.of(); + + classNode.methods.add(methodNode); + + MutableClass clazz = + new MutableClass( + classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider); + instrumentor.instrument(clazz); + + // Side effect: original method has been made private. + assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0); + // Side effect: original method has been renamed to a robolectric delegate + assertThat(methodNode.name).isEqualTo("$$robo$$org_example_MyClass$someFunction"); + // Side effect: instructions have been rewritten to throw and return. + assertThat(methodNode.instructions.size()).isEqualTo(7); + assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.NEW); + assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.DUP); + assertThat(methodNode.instructions.get(2).getOpcode()).isEqualTo(Opcodes.LDC); + assertThat(methodNode.instructions.get(3).getOpcode()).isEqualTo(Opcodes.INVOKESPECIAL); + assertThat(methodNode.instructions.get(4).getOpcode()).isEqualTo(Opcodes.ATHROW); + assertThat(methodNode.instructions.get(5).getOpcode()).isEqualTo(Opcodes.ICONST_0); + assertThat(methodNode.instructions.get(6).getOpcode()).isEqualTo(Opcodes.IRETURN); + } + + @Test + public void instrumentNativeMethod_withExemption_generatesNoOpReturn() throws IOException { + File exemptionsFile = tempFolder.newFile("natives.txt"); + try (BufferedWriter writer = + new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { + writer.write("org.example.MyClass#someOtherMethod()V\n"); + writer.write("org.example.MyClass#someFunction()I\n"); + } + + NativeCallHandler nativeCallHandler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); + instrumentor.setNativeCallHandler(nativeCallHandler); + + ClassNode classNode = new ClassNode(); + classNode.name = "org/example/MyClass"; + + MethodNode methodNode = new MethodNode(); + methodNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_NATIVE; + methodNode.name = "someFunction"; + methodNode.desc = "()I"; + methodNode.signature = "()"; + methodNode.exceptions = ImmutableList.of(); + methodNode.visibleAnnotations = ImmutableList.of(); + + classNode.methods.add(methodNode); + + MutableClass clazz = + new MutableClass( + classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider); + instrumentor.instrument(clazz); + + // Side effect: original method has been made private. + assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0); + // Side effect: original method has been renamed to a robolectric delegate + assertThat(methodNode.name).isEqualTo("$$robo$$org_example_MyClass$someFunction"); + // Side effect: instructions have been rewritten to return 0. + assertThat(methodNode.instructions.size()).isEqualTo(2); + assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.ICONST_0); + assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.IRETURN); + } +} diff --git a/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java b/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java new file mode 100644 index 000000000..04035346e --- /dev/null +++ b/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java @@ -0,0 +1,218 @@ +package org.robolectric.internal.bytecode; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.io.Files; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test for {@link NativeCallHandler}. */ +@RunWith(JUnit4.class) +public class NativeCallHandlerTest { + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void jarInstrumentorLegacyUsage() throws IOException { + // CUJ: Legacy jarInstrumentor usage; there is no exemption file, native methods do not throw. + + File exemptionsFile = tempFolder.newFile("natives.txt"); + assertThat(exemptionsFile.delete()).isTrue(); + + // Create handler, which loads exemptions from file. It's fine for the file to be missing. + NativeCallHandler handler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ false); + + // No method descriptor should throw. + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isFalse(); + } + + @Test + public void jarInstrumentorUsage_throwOnNativesEnabled() throws IOException { + // CUJ: jarInstrumentor usage with an exemption list and non-exempted native methods should + // throw. + + File exemptionsFile = tempFolder.newFile("natives.txt"); + try (BufferedWriter writer = + new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { + writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n"); + writer.write("libcore.io.Linux#chmod(Ljava/lang/String;I)V\n"); + writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n"); + writer.write("android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n"); + writer.write("org.example.MyClass#someOtherMethod()V\n"); + // empty or white-space lines are ignored + writer.write("\n"); + writer.write(" \t \n"); + // A # prefix denotes a comment and is ignored too + writer.write("# org.example.Ignored#comment()V\n"); + writer.write(" # org.example.Ignored#thisIsACommentToo()V \n"); + } + + // Create handler, which loads exemptions from file. ThrowOnNatives is enabled. + NativeCallHandler handler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); + + // Test exempted methods + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); + + // Test non-exempted methods + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isTrue(); + + // Empty lines and comments are ignored and not present in the exemption list. + assertThat(handler.shouldThrow("")).isTrue(); + assertThat(handler.shouldThrow(" \t ")).isTrue(); + assertThat(handler.shouldThrow("# org.example.Ignored#comment()V")).isTrue(); + assertThat(handler.shouldThrow(" # org.example.Ignored#thisIsACommentToo()V ")).isTrue(); + } + + @Test + public void jarInstrumentorUsage_throwOnNativesDisabled() throws IOException { + // CUJ: jarInstrumentor usage with an exemption list and non-exempted native methods should + // throw. + + File exemptionsFile = tempFolder.newFile("natives.txt"); + try (BufferedWriter writer = + new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { + writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n"); + writer.write("libcore.io.Linux#chmod(Ljava/lang/String;I)V\n"); + writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n"); + writer.write("android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n"); + writer.write("org.example.MyClass#someOtherMethod()V\n"); + } + + // Create handler, which loads exemptions from file. ThrowOnNatives is disabled. + NativeCallHandler handler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ false); + + // Test exempted methods + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); + + // Test non-exempted methods + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isFalse(); + } + + @Test + public void jarInstrumentorUsage_logNativeCall_ignored() throws IOException { + // When not writing the exemption list, logNativeCall calls are no-op. + + File exemptionsFile = tempFolder.newFile("natives.txt"); + + // Create handler, which loads exemptions from file. ThrowOnNatives is enabled. + NativeCallHandler handler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); + + // No methods are exempted -- initial list is empty. + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue(); + assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue(); + + handler.logNativeCall("org.example.MyClass#someOtherMethod()V"); + handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V"); + + // LogNativeCall did not capture. These methods are still not exempted. + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue(); + assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue(); + } + + @Test + public void exemptionListGeneratorUsage_logNativeCall_capturesCalls() throws IOException { + // CUJ: jarInstrumentor called to generate the exemption list. + + File exemptionsFile = tempFolder.newFile("natives.txt"); + + // Create handler, which loads exemptions from file. ThrowOnNatives is enabled. + NativeCallHandler handler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ true, /* throwOnNatives= */ true); + + // No methods are exempted -- initial list is empty. + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue(); + assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue(); + + handler.logNativeCall("org.example.MyClass#someOtherMethod()V"); + handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V"); + + // These methods are now exempted. + assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); + assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isFalse(); + } + + @Test + public void exemptionListGeneratorUsage_writeExemptionFile() throws IOException { + // CUJ: jarInstrumentor called to generate the exemption list. + + File exemptionsFile = tempFolder.newFile("natives.txt"); + try (BufferedWriter writer = + new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { + writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n"); + writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n"); + } + + // Create handler, which loads exemptions from file. ThrowOnNatives is disabled. + NativeCallHandler handler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ true, /* throwOnNatives= */ false); + + handler.logNativeCall("org.example.MyClass#someOtherMethod()V"); + // Multiple calls with same value are idempotent. + handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V"); + handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V"); + handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V"); + handler.logNativeCall("org.example.MyClass#someOtherMethod(II)V"); + handler.logNativeCall("libcore.io.Linux#chmod(Ljava/lang/String;I)V"); + // Case of a nested class with $ in the FQCN. + handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V"); + + handler.writeExemptionsList(); + + // Note: due to how the generated files are manipulated in the shell/makefile build system, + // '$' characters are a problem and would need to be escaped (and potentially differently for + // shell vs makefiles). The workaround is to have '$' rewritten as '^'. + + assertThat(Files.asCharSource(exemptionsFile, UTF_8).read()) + .isEqualTo( + "android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n" + // Font$Builder gets written as Font^Builder. + + "android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n" + + "libcore.io.Linux#chmod(Ljava/lang/String;I)V\n" + + "libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n" + + "org.example.MyClass#someOtherMethod()V\n" + + "org.example.MyClass#someOtherMethod(I)V\n" + + "org.example.MyClass#someOtherMethod(II)V\n"); + } + + @Test + public void getExceptionMessage() throws IOException { + File exemptionsFile = tempFolder.newFile("natives.txt"); + NativeCallHandler handler = + new NativeCallHandler( + exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); + + // Test generated exception message for non-exempted methods. + assertThat( + handler.getExceptionMessage( + "org.example.MyClass$1#someOtherMethod(II)V", + "org.example.MyClass$1", + "someOtherMethod")) + .isEqualTo( + "Unexpected Robolectric native method call to" + + " 'org.example.MyClass$1#someOtherMethod()'.\n" + + "Option 1: If customizing this method is useful, add an implementation in" + + " ShadowMyClass.java.\n" + + "Option 2: If this method just needs to trivially return 0 or null, please add an" + + " exemption entry for\n" + + " org.example.MyClass^1#someOtherMethod(II)V\n" + + "to exemption file natives.txt"); + } +} diff --git a/shadows/framework/Android.bp b/shadows/framework/Android.bp index 04ba8ff71..8c050b589 100644 --- a/shadows/framework/Android.bp +++ b/shadows/framework/Android.bp @@ -35,6 +35,7 @@ java_library_host { "Robolectric_pluginapi_upstream", "Robolectric_sandbox_upstream", "Robolectric_shadowapi_upstream", + "Robolectric_shadows_versioning_upstream", "Robolectric_utils_upstream", "Robolectric_utils_reflector_upstream", "auto_value_annotations", diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle index a2230b0fe..57d2679d6 100644 --- a/shadows/framework/build.gradle +++ b/shadows/framework/build.gradle @@ -17,7 +17,7 @@ configurations { def sqlite4javaVersion = libs.versions.sqlite4java.get() -task copySqliteNatives(type: Copy) { +tasks.register('copySqliteNatives', Copy) { from project.configurations.sqlite4java { include '**/*.dll' include '**/*.so' @@ -40,6 +40,10 @@ jar { dependsOn copySqliteNatives } +javadoc { + dependsOn copySqliteNatives +} + dependencies { api project(":annotations") api project(":nativeruntime") diff --git a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java index 0385636a8..a58fd7069 100644 --- a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java +++ b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java @@ -21,6 +21,7 @@ import org.robolectric.android.Bootstrap; import org.robolectric.android.ConfigurationV25; import org.robolectric.res.ResourceTable; import org.robolectric.shadows.ShadowDisplayManager; +import org.robolectric.shadows.ShadowInstrumentation; import org.robolectric.shadows.ShadowView; import org.robolectric.util.Scheduler; import org.robolectric.util.TempDirectory; @@ -39,10 +40,10 @@ public class RuntimeEnvironment { * incompatible with {@link org.robolectric.annotation.experimental.LazyApplication} and * Robolectric makes no guarantees if a test *modifies* this field during execution. */ - @Deprecated public static Application application; + @Deprecated public static volatile Application application; private static volatile Thread mainThread; - private static Object activityThread; + private static volatile Object activityThread; private static int apiLevel; private static Scheduler masterScheduler; private static ResourceTable systemResourceTable; @@ -76,7 +77,7 @@ public class RuntimeEnvironment { if (application == null) { synchronized (supplierLock) { if (applicationSupplier != null) { - application = applicationSupplier.get(); + ShadowInstrumentation.runOnMainSyncNoIdle(() -> application = applicationSupplier.get()); } } } diff --git a/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java b/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java index 09f737979..367ca62e1 100644 --- a/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java +++ b/shadows/framework/src/main/java/org/robolectric/android/controller/ComponentController.java @@ -20,7 +20,6 @@ public abstract class ComponentController<C extends ComponentController<C, T>, T protected boolean attached; - @SuppressWarnings("unchecked") public ComponentController(T component, Intent intent) { this(component); this.intent = intent; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java index e2b8f0df3..ceca51e0e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java @@ -7,6 +7,7 @@ import android.net.MacAddress; import org.robolectric.RuntimeEnvironment; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.versioning.AndroidVersions.U; /** Builder for {@link AssociationInfo}. */ public class AssociationInfoBuilder { @@ -16,10 +17,12 @@ public class AssociationInfoBuilder { private String deviceMacAddress; private CharSequence displayName; private String deviceProfile; + private Object associatedDevice; private boolean selfManaged; private boolean notifyOnDeviceNearby; private long approvedMs; private long lastTimeConnectedMs; + private int systemDataSyncFlags; private AssociationInfoBuilder() {} @@ -57,6 +60,11 @@ public class AssociationInfoBuilder { return this; } + public AssociationInfoBuilder setAssociatedDevice(Object associatedDevice) { + this.associatedDevice = associatedDevice; + return this; + } + public AssociationInfoBuilder setSelfManaged(boolean selfManaged) { this.selfManaged = selfManaged; return this; @@ -77,8 +85,15 @@ public class AssociationInfoBuilder { return this; } + public AssociationInfoBuilder setSystemDataSyncFlags(int systemDataSyncFlags) { + this.systemDataSyncFlags = systemDataSyncFlags; + return this; + } + public AssociationInfo build() { try { + MacAddress macAddress = + deviceMacAddress == null ? null : MacAddress.fromString(deviceMacAddress); if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) { // We have two different constructors for AssociationInfo across // T branches. aosp has the constructor that takes a new "revoked" parameter. @@ -92,7 +107,7 @@ public class AssociationInfoBuilder { ClassParameter.from(int.class, id), ClassParameter.from(int.class, userId), ClassParameter.from(String.class, packageName), - ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)), + ClassParameter.from(MacAddress.class, macAddress), ClassParameter.from(CharSequence.class, displayName), ClassParameter.from(String.class, deviceProfile), ClassParameter.from(boolean.class, selfManaged), @@ -106,11 +121,45 @@ public class AssociationInfoBuilder { ClassParameter.from(int.class, id), ClassParameter.from(int.class, userId), ClassParameter.from(String.class, packageName), - ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)), + ClassParameter.from(MacAddress.class, macAddress), + ClassParameter.from(CharSequence.class, displayName), + ClassParameter.from(String.class, deviceProfile), + ClassParameter.from(boolean.class, selfManaged), + ClassParameter.from(boolean.class, notifyOnDeviceNearby), + ClassParameter.from(long.class, approvedMs), + ClassParameter.from(long.class, lastTimeConnectedMs)); + } + } else if (RuntimeEnvironment.getApiLevel() <= U.SDK_INT) { + // AOSP does not yet contains the new fields - mAssociatedDevice & mSystemDataSyncFlags yet + if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) { + return ReflectionHelpers.callConstructor( + AssociationInfo.class, + ClassParameter.from(int.class, id), + ClassParameter.from(int.class, userId), + ClassParameter.from(String.class, packageName), + ClassParameter.from(MacAddress.class, macAddress), + ClassParameter.from(CharSequence.class, displayName), + ClassParameter.from(String.class, deviceProfile), + ClassParameter.from( + Class.forName("android.companion.AssociatedDevice"), associatedDevice), + ClassParameter.from(boolean.class, selfManaged), + ClassParameter.from(boolean.class, notifyOnDeviceNearby), + ClassParameter.from(boolean.class, false /*revoked*/), + ClassParameter.from(long.class, approvedMs), + ClassParameter.from(long.class, lastTimeConnectedMs), + ClassParameter.from(int.class, systemDataSyncFlags)); + } else { + return ReflectionHelpers.callConstructor( + AssociationInfo.class, + ClassParameter.from(int.class, id), + ClassParameter.from(int.class, userId), + ClassParameter.from(String.class, packageName), + ClassParameter.from(MacAddress.class, macAddress), ClassParameter.from(CharSequence.class, displayName), ClassParameter.from(String.class, deviceProfile), ClassParameter.from(boolean.class, selfManaged), ClassParameter.from(boolean.class, notifyOnDeviceNearby), + ClassParameter.from(boolean.class, false /*revoked*/), ClassParameter.from(long.class, approvedMs), ClassParameter.from(long.class, lastTimeConnectedMs)); } @@ -120,16 +169,18 @@ public class AssociationInfoBuilder { ClassParameter.from(int.class, id), ClassParameter.from(int.class, userId), ClassParameter.from(String.class, packageName), - ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)), + ClassParameter.from(String.class, null /* tag */), + ClassParameter.from(MacAddress.class, macAddress), ClassParameter.from(CharSequence.class, displayName), ClassParameter.from(String.class, deviceProfile), - ClassParameter.from(Class.forName("android.companion.AssociatedDevice"), null), + ClassParameter.from( + Class.forName("android.companion.AssociatedDevice"), associatedDevice), ClassParameter.from(boolean.class, selfManaged), ClassParameter.from(boolean.class, notifyOnDeviceNearby), ClassParameter.from(boolean.class, false /*revoked*/), ClassParameter.from(long.class, approvedMs), ClassParameter.from(long.class, lastTimeConnectedMs), - ClassParameter.from(int.class, 0 /*systemDataSyncFlags*/)); + ClassParameter.from(int.class, systemDataSyncFlags)); } } catch (ClassNotFoundException e) { throw new RuntimeException(e); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java index 54cccf067..c13b68678 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java @@ -30,8 +30,7 @@ public class AudioDeviceInfoBuilder { * Sets the device type. * * @param type The device type. The possible values are the constants defined as <a - * href=https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/AudioDeviceInfo.java?q=AudioDeviceType> - * {@code AudioDeviceInfo.AudioDeviceType}</a> + * href="https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/AudioDeviceInfo.java?q=AudioDeviceType">AudioDeviceInfo.AudioDeviceType</a> */ public AudioDeviceInfoBuilder setType(int type) { this.type = type; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataEntity.java b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataEntity.java new file mode 100644 index 000000000..38bf3f0cb --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataEntity.java @@ -0,0 +1,58 @@ +package org.robolectric.shadows; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.auto.value.AutoValue; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Represents a key value pair in {@link ShadowBackupDataInput} and {@link ShadowBackupDataOutput}. + */ +@AutoValue +public abstract class BackupDataEntity { + + /** The header key for a backup entity. */ + public abstract String key(); + + /** The size of data in a backup entity. */ + public abstract int dataSize(); + + /** The byte array of data in a backup entity. */ + @SuppressWarnings("mutable") + public abstract byte[] data(); + + /** + * Constructs a new entity with the given key but a negative size. This represents a deleted pair. + */ + public static BackupDataEntity createDeletedEntity(String key) { + return new AutoValue_BackupDataEntity( + checkNotNull(key), /* dataSize= */ -1, /* data= */ new byte[0]); + } + + /** + * Constructs a pair with a string value. The value will be converted to a byte array in {@link + * StandardCharsets#UTF_8}. + */ + public static BackupDataEntity create(String key, String data) { + return create(key, data.getBytes(UTF_8)); + } + + /** Constructs a new entity where the size of the value is the entire array. */ + public static BackupDataEntity create(String key, byte[] data) { + return create(key, data, data.length); + } + + /** + * Constructs a new entity. + * + * @param key the key of the pair + * @param data the value to associate with the key + * @param dataSize the length of the value in bytes + */ + public static BackupDataEntity create(String key, byte[] data, int dataSize) { + return new AutoValue_BackupDataEntity( + checkNotNull(key), dataSize, Arrays.copyOf(data, dataSize)); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataInputBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataInputBuilder.java new file mode 100644 index 000000000..8b91f6a73 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataInputBuilder.java @@ -0,0 +1,46 @@ +package org.robolectric.shadows; + +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.backup.BackupDataInput; +import com.google.common.collect.ImmutableList; +import java.io.FileDescriptor; +import java.util.ArrayList; +import java.util.List; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Builder for a {@link BackupDataInput} object. */ +public class BackupDataInputBuilder { + + private final List<BackupDataEntity> entities = new ArrayList<>(); + + private BackupDataInputBuilder() {} + + /** Creates a new builder for {@link BackupDataInput}. */ + public static BackupDataInputBuilder newBuilder() { + return new BackupDataInputBuilder(); + } + + /** Adds the given entity to the input. */ + public BackupDataInputBuilder addEntity(BackupDataEntity entity) { + entities.add(entity); + return this; + } + + /** Builds the {@link BackupDataInput} instance with the added entities. */ + public BackupDataInput build() { + BackupDataInput data = + reflector(BackupDataInputReflector.class).newBackupDataInput(new FileDescriptor()); + shadowOf(data).setEntities(ImmutableList.copyOf(entities)); + return data; + } + + @ForType(BackupDataInput.class) + private interface BackupDataInputReflector { + + @Constructor + BackupDataInput newBackupDataInput(FileDescriptor fd); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataOutputFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataOutputFactory.java new file mode 100644 index 000000000..eb8c888a0 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/BackupDataOutputFactory.java @@ -0,0 +1,48 @@ +package org.robolectric.shadows; + +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.backup.BackupDataOutput; +import android.os.Build.VERSION_CODES; +import androidx.annotation.RequiresApi; +import java.io.FileDescriptor; +import org.robolectric.util.reflector.Constructor; +import org.robolectric.util.reflector.ForType; + +/** Factory for instances of {@link BackupDataOutput}. */ +public class BackupDataOutputFactory { + + private BackupDataOutputFactory() {} + + /** Returns a new instance of {@link BackupDataOutput}. */ + public static BackupDataOutput newInstance() { + return reflector(BackupDataOutputReflector.class).newBackupDataOutput(new FileDescriptor()); + } + + /** Returns a new instance of {@link BackupDataOutput}. */ + @RequiresApi(VERSION_CODES.O) + public static BackupDataOutput newInstance(long quota) { + return reflector(BackupDataOutputReflector.class) + .newBackupDataOutput(new FileDescriptor(), quota); + } + + /** Returns a new instance of {@link BackupDataOutput}. */ + @RequiresApi(VERSION_CODES.P) + public static BackupDataOutput newInstance(long quota, int transportFlags) { + return reflector(BackupDataOutputReflector.class) + .newBackupDataOutput(new FileDescriptor(), quota, transportFlags); + } + + @ForType(BackupDataOutput.class) + private interface BackupDataOutputReflector { + + @Constructor + BackupDataOutput newBackupDataOutput(FileDescriptor fd); + + @Constructor + BackupDataOutput newBackupDataOutput(FileDescriptor fd, long quota); + + @Constructor + BackupDataOutput newBackupDataOutput(FileDescriptor fd, long quota, int transportFlags); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java index c9a723ca2..14c3a224a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Iterator; +import java.util.Locale; import javax.imageio.IIOException; import javax.imageio.IIOImage; import javax.imageio.ImageIO; @@ -33,7 +34,30 @@ public class ImageUtil { private static final String FORMAT_NAME_PNG = "png"; private static boolean initialized; + /** Image information descriptor. */ + public static class ImageInfo { + + public final int width; + public final int height; + public final String mimeType; + + ImageInfo(int width, int height, String mimeType) { + this.width = width; + this.height = height; + this.mimeType = mimeType; + } + } + static Point getImageSizeFromStream(InputStream is) { + ImageInfo info = getImageInfoFromStream(is); + if (info == null) { + return null; + } else { + return new Point(info.width, info.height); + } + } + + static ImageInfo getImageInfoFromStream(InputStream is) { if (!initialized) { // Stops ImageIO from creating temp files when reading images // from input stream. @@ -49,7 +73,10 @@ public class ImageUtil { ImageReader reader = readers.next(); try { reader.setInput(imageStream); - return new Point(reader.getWidth(0), reader.getHeight(0)); + return new ImageInfo( + reader.getWidth(0), + reader.getHeight(0), + "image/" + reader.getFormatName().toLowerCase(Locale.US)); } finally { reader.dispose(); } @@ -84,7 +111,7 @@ public class ImageUtil { format = reader.getFormatName(); int minIndex = reader.getMinIndex(); BufferedImage image = reader.read(minIndex); - return RobolectricBufferedImage.create(image, ("image/" + format).toLowerCase()); + return RobolectricBufferedImage.create(image, ("image/" + format).toLowerCase(Locale.US)); } finally { reader.dispose(); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java index 5c12213af..56a8fc9ee 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/LooperShadowPicker.java @@ -1,6 +1,5 @@ package org.robolectric.shadows; -import org.robolectric.annotation.LooperMode; import org.robolectric.shadow.api.ShadowPicker; public class LooperShadowPicker<T> implements ShadowPicker<T> { @@ -15,11 +14,15 @@ public class LooperShadowPicker<T> implements ShadowPicker<T> { } @Override + @SuppressWarnings("deprecation") // This is Robolectric library code public Class<? extends T> pickShadowClass() { - if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) { - return pausedShadowClass; - } else { - return legacyShadowClass; + switch (ShadowLooper.looperMode()) { + case LEGACY: + return legacyShadowClass; + case PAUSED: + case INSTRUMENTATION_TEST: + return pausedShadowClass; } + throw new UnsupportedOperationException("Unrecognized looperMode " + ShadowLooper.looperMode()); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java index 459340273..95c079190 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java @@ -2,6 +2,7 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.Q; +import static java.util.Arrays.asList; import android.media.MediaCodecInfo; import android.media.MediaCodecInfo.AudioCapabilities; @@ -12,6 +13,7 @@ import android.media.MediaCodecInfo.VideoCapabilities; import android.media.MediaFormat; import android.util.Range; import com.google.common.base.Preconditions; +import java.util.HashSet; import org.robolectric.RuntimeEnvironment; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; @@ -178,6 +180,7 @@ public class MediaCodecInfoBuilder { private boolean isEncoder; private CodecProfileLevel[] profileLevels = new CodecProfileLevel[0]; private int[] colorFormats; + private String[] requiredFeatures = new String[0]; private CodecCapabilitiesBuilder() {} @@ -190,7 +193,12 @@ public class MediaCodecInfoBuilder { * Sets media format. * * @param mediaFormat a {@link MediaFormat} supported by the codec. It is a requirement for - * mediaFormat to have {@link MediaFormat.KEY_MIME} set. Other keys are optional. + * mediaFormat to have {@link MediaFormat.KEY_MIME} set. Other keys are optional. Setting + * {@link MediaFormat.KEY_WIDTH}, {@link MediaFormat.KEY_MAX_WIDTH} and {@link + * MediaFormat.KEY_HEIGHT}, {@link MediaFormat.KEY_MAX_HEIGHT} will set the minimum and + * maximum width, height respectively. For backwards compatibility, setting only {@link + * MediaFormat.KEY_WIDTH}, {@link MediaFormat.KEY_HEIGHT} will only set the maximum width, + * height respectively. * @throws {@link NullPointerException} if mediaFormat is null. * @throws {@link IllegalArgumentException} if mediaFormat does not have {@link * MediaFormat.KEY_MIME}. @@ -205,6 +213,16 @@ public class MediaCodecInfoBuilder { } /** + * Sets required features. + * + * @param requiredFeatures An array of {@link CodecCapabilities} FEATURE strings. + */ + public CodecCapabilitiesBuilder setRequiredFeatures(String[] requiredFeatures) { + this.requiredFeatures = requiredFeatures; + return this; + } + + /** * Sets codec role. * * @param isEncoder a boolean to indicate whether the codec is an encoder or a decoder. Default @@ -220,7 +238,7 @@ public class MediaCodecInfoBuilder { * * @param profileLevels an array of {@link MediaCodecInfo.CodecProfileLevel} supported by the * codec. - * @throws {@link NullPointerException} if profileLevels is null. + * @throws NullPointerException if profileLevels is null. */ public CodecCapabilitiesBuilder setProfileLevels(CodecProfileLevel[] profileLevels) { this.profileLevels = Preconditions.checkNotNull(profileLevels); @@ -265,6 +283,9 @@ public class MediaCodecInfoBuilder { @Accessor("mFlagsSupported") void setFlagsSupported(int flagsSupported); + + @Accessor("mFlagsRequired") + void setFlagsRequired(int flagsRequired); } /** Accessor interface for {@link VideoCapabilities}'s internals. */ @@ -312,14 +333,27 @@ public class MediaCodecInfoBuilder { VideoCapabilities videoCaps = createDefaultVideoCapabilities(caps, mediaFormat); VideoCapabilitiesReflector videoCapsReflector = Reflector.reflector(VideoCapabilitiesReflector.class, videoCaps); - if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) { + if (mediaFormat.containsKey(MediaFormat.KEY_MAX_WIDTH) + && mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) { + videoCapsReflector.setWidthRange( + new Range<>( + mediaFormat.getInteger(MediaFormat.KEY_WIDTH), + mediaFormat.getInteger(MediaFormat.KEY_MAX_WIDTH))); + } else if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) { videoCapsReflector.setWidthRange( new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_WIDTH))); } - if (mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) { + if (mediaFormat.containsKey(MediaFormat.KEY_MAX_HEIGHT) + && mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) { + videoCapsReflector.setHeightRange( + new Range<>( + mediaFormat.getInteger(MediaFormat.KEY_HEIGHT), + mediaFormat.getInteger(MediaFormat.KEY_MAX_HEIGHT))); + } else if (mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) { videoCapsReflector.setHeightRange( new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_HEIGHT))); } + capsReflector.setVideoCaps(videoCaps); } else { AudioCapabilities audioCaps = createDefaultAudioCapabilities(caps, mediaFormat); @@ -334,6 +368,9 @@ public class MediaCodecInfoBuilder { if (RuntimeEnvironment.getApiLevel() >= Q) { int flagsSupported = getSupportedFeatures(caps, mediaFormat); capsReflector.setFlagsSupported(flagsSupported); + + int flagsRequired = getRequiredFeatures(caps, requiredFeatures); + capsReflector.setFlagsRequired(flagsRequired); } return caps; @@ -386,5 +423,23 @@ public class MediaCodecInfoBuilder { } return flagsSupported; } + + /** + * Read codec features from a given array of feature strings and convert them to values + * recognized by {@link CodecCapabilities}. + */ + private static int getRequiredFeatures(CodecCapabilities parent, String[] requiredFeatures) { + int flagsRequired = 0; + Object[] validFeatures = ReflectionHelpers.callInstanceMethod(parent, "getValidFeatures"); + HashSet<String> requiredFeaturesSet = new HashSet<>(asList(requiredFeatures)); + for (Object validFeature : validFeatures) { + String featureName = (String) ReflectionHelpers.getField(validFeature, "mName"); + int featureValue = (int) ReflectionHelpers.getField(validFeature, "mValue"); + if (requiredFeaturesSet.contains(featureName)) { + flagsRequired |= featureValue; + } + } + return flagsRequired; + } } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java b/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java index d04421c9b..d136be83d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/NativeInput.java @@ -34,8 +34,10 @@ import org.robolectric.res.android.Ref; * Java representation of framework native input Transliterated from oreo-mr1 (SDK 27) * frameworks/native/include/input/Input.h and libs/input/Input.cpp * - * @see <a href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/include/input/Input.h">include/input/Input.h</a> - * @see <a href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/libs/input/Input.cpp>libs/input/Input.cpp</a> + * @see <a + * href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/include/input/Input.h">include/input/Input.h</a> + * @see <a + * href="https://android.googlesource.com/platform/frameworks/native/+/oreo-mr1-release/libs/input/Input.cpp">libs/input/Input.cpp</a> */ public class NativeInput { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilder.java new file mode 100644 index 000000000..44706fba2 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/NetworkRegistrationInfoTestBuilder.java @@ -0,0 +1,138 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build.VERSION; +import android.telephony.CellIdentity; +import android.telephony.DataSpecificRegistrationInfo; +import android.telephony.NetworkRegistrationInfo; +import android.telephony.VoiceSpecificRegistrationInfo; +import androidx.annotation.RequiresApi; +import java.util.List; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; + +/** + * Builder class to create instance of {@link NetworkRegistrationInfo}. + * + * <p>NRI was first made a @SystemApi in Q then finally exposed as public in R. + * + * <p>This builder class does not extend {@link NetworkRegistrationInfo.Builder}. It uses {@link + * NetworkRegistrationInfo.Builder} and some additional APIs to set NRI private fields. + */ +@RequiresApi(Q) +public class NetworkRegistrationInfoTestBuilder { + + private final NetworkRegistrationInfo.Builder buider = new NetworkRegistrationInfo.Builder(); + + private VoiceSpecificRegistrationInfo voiceSpecificInfo; + private DataSpecificRegistrationInfo dataSpecificInfo; + private int roamingType; + + public static NetworkRegistrationInfoTestBuilder newBuilder() { + return new NetworkRegistrationInfoTestBuilder(); + } + + public NetworkRegistrationInfo build() { + NetworkRegistrationInfo networkRegistrationInfo = buider.build(); + if (VERSION.SDK_INT < Q) { + throw new IllegalStateException( + "NetworkRegistrationInfo not available on SDK : " + RuntimeEnvironment.getApiLevel()); + } else if (VERSION.SDK_INT < TIRAMISU) { + reflector(NetworkRegistrationInfoReflector.class, networkRegistrationInfo) + .setVoiceSpecificInfo(voiceSpecificInfo); + reflector(NetworkRegistrationInfoReflector.class, networkRegistrationInfo) + .setDataSpecificInfo(dataSpecificInfo); + } + networkRegistrationInfo.setRoamingType(roamingType); + return networkRegistrationInfo; + } + + public NetworkRegistrationInfoTestBuilder setAccessNetworkTechnology(int value) { + buider.setAccessNetworkTechnology(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setAvailableServices(List<Integer> value) { + buider.setAvailableServices(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setCellIdentity(CellIdentity value) { + buider.setCellIdentity(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setDomain(int value) { + buider.setDomain(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setEmergencyOnly(boolean value) { + buider.setEmergencyOnly(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setRegisteredPlmn(String value) { + if (VERSION.SDK_INT == Q) { + throw new IllegalStateException( + "Registered PLMN is not available on SDK : " + RuntimeEnvironment.getApiLevel()); + } else { + buider.setRegisteredPlmn(value); + } + return this; + } + + public NetworkRegistrationInfoTestBuilder setRegistrationState(int value) { + buider.setRegistrationState(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setRejectCause(int value) { + buider.setRejectCause(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setTransportType(int value) { + buider.setTransportType(value); + return this; + } + + public NetworkRegistrationInfoTestBuilder setDataSpecificInfo( + DataSpecificRegistrationInfo value) { + if (VERSION.SDK_INT >= TIRAMISU) { + buider.setDataSpecificInfo(value); + } else { + dataSpecificInfo = value; + } + return this; + } + + public NetworkRegistrationInfoTestBuilder setVoiceSpecificInfo( + VoiceSpecificRegistrationInfo value) { + if (VERSION.SDK_INT >= TIRAMISU) { + buider.setVoiceSpecificInfo(value); + } else { + voiceSpecificInfo = value; + } + return this; + } + + public NetworkRegistrationInfoTestBuilder setRoamingType(int value) { + roamingType = value; + return this; + } + + @ForType(NetworkRegistrationInfo.class) + private interface NetworkRegistrationInfoReflector { + + @Accessor("mDataSpecificInfo") + public void setDataSpecificInfo(DataSpecificRegistrationInfo value); + + @Accessor("mVoiceSpecificInfo") + public void setVoiceSpecificInfo(VoiceSpecificRegistrationInfo value); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/PhoneCapabilityFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/PhoneCapabilityFactory.java new file mode 100644 index 000000000..0252c12c0 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/PhoneCapabilityFactory.java @@ -0,0 +1,26 @@ +package org.robolectric.shadows; + +import android.telephony.PhoneCapability; +import java.util.ArrayList; + +/** Factory to create PhoneCapability. */ +public final class PhoneCapabilityFactory { + + /** Creates PhoneCapability. */ + public static PhoneCapability create( + int maxActiveVoiceSubscriptions, + int maxActiveDataSubscriptions, + boolean networkValidationBeforeSwitchSupported, + int[] deviceNrCapabilities) { + return new PhoneCapability( + maxActiveVoiceSubscriptions, + maxActiveDataSubscriptions, + // Since ModemInfo is an @hide object, there is no reason for an external object to be able + // to declare it, using an empty ArrayList as the parameter here. + /* List<ModemInfo> */ new ArrayList<>(), + networkValidationBeforeSwitchSupported, + deviceNrCapabilities); + } + + private PhoneCapabilityFactory() {} +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java index 1fa638b3b..ddabcaf66 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/PreciseDataConnectionStateBuilder.java @@ -36,7 +36,7 @@ public class PreciseDataConnectionStateBuilder { } public PreciseDataConnectionStateBuilder setTransportType(int transportType) { - this.transportType = networkType; + this.transportType = transportType; return this; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ServiceStateBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ServiceStateBuilder.java new file mode 100644 index 000000000..2417bf8a7 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ServiceStateBuilder.java @@ -0,0 +1,141 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.os.Build.VERSION; +import android.telephony.NetworkRegistrationInfo; +import android.telephony.ServiceState; +import androidx.annotation.RequiresApi; +import java.util.List; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; + +/** Builder class to create instance of {@link ServiceState}. */ +public class ServiceStateBuilder { + private ServiceState serviceState = new ServiceState(); + + public static ServiceStateBuilder newBuilder() { + return new ServiceStateBuilder(); + } + + public static ServiceStateBuilder newBuilder(ServiceState serviceState) { + ServiceStateBuilder builder = new ServiceStateBuilder(); + builder.serviceState = serviceState; + return builder; + } + + public ServiceState build() { + return serviceState; + } + + public ServiceStateBuilder setVoiceRegState(int value) { + serviceState.setVoiceRegState(value); + return this; + } + + public ServiceStateBuilder setDataRegState(int value) { + serviceState.setDataRegState(value); + return this; + } + + public ServiceStateBuilder setNrFrequencyRange(int value) { + assertIsAtLeast(Q); + serviceState.setNrFrequencyRange(value); + return this; + } + + public ServiceStateBuilder setIsManualSelection(boolean value) { + serviceState.setIsManualSelection(value); + return this; + } + + public ServiceStateBuilder setOperatorName(String longName, String shortName, String numeric) { + assertIsAtLeast(R); + serviceState.setOperatorName(longName, shortName, numeric); + return this; + } + + public ServiceStateBuilder setIwlanPreferred(boolean value) { + assertIsAtLeast(R); + serviceState.setIwlanPreferred(value); + return this; + } + + public ServiceStateBuilder setEmergencyOnly(boolean value) { + serviceState.setEmergencyOnly(value); + return this; + } + + public ServiceStateBuilder setDataRoamingFromRegistration(boolean value) { + assertIsAtLeast(R); + serviceState.setDataRoamingFromRegistration(value); + return this; + } + + /** + * Use this method to control return value of {@link ServiceState#isUsingCarrierAggregation()} (up + * to P). On APIs > P, use {@link ServiceStateBuilder#setNetworkRegistrationInfoList()}. + */ + public ServiceStateBuilder setIsUsingCarrierAggregation(boolean value) { + assertIsAtLeast(P); + // {@link NetworkRegistrationInfo} was first made a @SystemApi in Q then finally exposed as + // public in R. For SDK later than Q, call + // {@link ServiceStateBuilder#setNetworkRegistrationInfoList} to set this value. Downstream test + // code will have to specify NRIs in the builder to set this value. But the "actual" + // implementation code under test would still be looking at the non-NRI getters + // on {@link ServiceState}, assuming it's restricted to only public APIs. + if (VERSION.SDK_INT >= Q) { + throw new UnsupportedOperationException( + "Newer SDKs must specify carrier aggregation by constructing an appropriate " + + "NetworkRegistrationInfo and calling #setNetworkRegistrationInfoList instead"); + } else { + reflector(ServiceStateReflector.class, serviceState).setIsUsingCarrierAggregation(value); + } + return this; + } + + @RequiresApi(Q) + public ServiceStateBuilder setNetworkRegistrationInfoList(List<NetworkRegistrationInfo> value) { + assertIsAtLeast(Q); + reflector(ServiceStateReflector.class, serviceState).setNetworkRegistrationInfos(value); + return this; + } + + public ServiceStateBuilder setRoaming(boolean value) { + serviceState.setRoaming(value); + return this; + } + + public ServiceStateBuilder setChannelNumber(int value) { + serviceState.setChannelNumber(value); + return this; + } + + public ServiceStateBuilder setCellBandwidths(int[] value) { + serviceState.setCellBandwidths(value); + return this; + } + + // TODO Find a proper way to set radio tech values. + + private void assertIsAtLeast(int sdk) { + if (VERSION.SDK_INT < sdk) { + throw new IllegalStateException( + "This method is not available on SDK : " + RuntimeEnvironment.getApiLevel()); + } + } + + @ForType(ServiceState.class) + private interface ServiceStateReflector { + + @Accessor("mIsUsingCarrierAggregation") + public void setIsUsingCarrierAggregation(boolean value); + + @Accessor("mNetworkRegistrationInfos") + public void setNetworkRegistrationInfos(List<NetworkRegistrationInfo> value); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java index c28bdc44f..bd588217a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityManager.java @@ -207,7 +207,7 @@ public class ShadowActivityManager { * Sets the values to be returned by {@link #getAppTasks()}. * * @see #getAppTasks() - * @param tasks List of app tasks. + * @param appTasks List of app tasks. */ public void setAppTasks(List<ActivityManager.AppTask> appTasks) { this.appTasks.clear(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java index 852919a4d..8b55be124 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java @@ -14,10 +14,12 @@ import android.app.ActivityThread.ActivityClientRecord; import android.app.Application; import android.app.Instrumentation; import android.app.ResultInfo; +import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ComponentInfoFlags; import android.content.res.Configuration; import android.os.IBinder; import com.android.internal.content.ReferrerIntent; @@ -79,12 +81,29 @@ public class ShadowActivityThread { } else if (method.getName().equals("notifyPackageUse")) { return null; } else if (method.getName().equals("getPackageInstaller")) { - return null; + try { + Class<?> iPackageInstallerClass = + classLoader.loadClass("android.content.pm.IPackageInstaller"); + return ReflectionHelpers.createNullProxy(iPackageInstallerClass); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } } else if (method.getName().equals("hasSystemFeature")) { String featureName = (String) args[0]; return RuntimeEnvironment.getApplication() .getPackageManager() .hasSystemFeature(featureName); + } else if (method.getName().equals("getServiceInfo")) { + ComponentName componentName = (ComponentName) args[0]; + if (args[1] instanceof ComponentInfoFlags) { + return RuntimeEnvironment.getApplication() + .getPackageManager() + .getServiceInfo(componentName, (ComponentInfoFlags) args[1]); + } else { + return RuntimeEnvironment.getApplication() + .getPackageManager() + .getServiceInfo(componentName, ((Number) args[1]).intValue()); + } } throw new UnsupportedOperationException("sorry, not supporting " + method + " yet!"); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java index 8771d6adb..d0e187fa7 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java @@ -5,13 +5,15 @@ import android.annotation.Nullable; import android.content.res.AssetManager; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.versioning.AndroidVersions.U; + // TODO: update path to released version. // transliterated from // https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_rXX/core/jni/android_util_AssetManager.cpp @Implements( value = AssetManager.class, - minSdk = ShadowBuild.UPSIDE_DOWN_CAKE, + minSdk = U.SDK_INT, shadowPicker = ShadowAssetManager.Picker.class) @SuppressWarnings("NewApi") public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 { @@ -25,7 +27,7 @@ public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 { // jint smallest_screen_width_dp, jint screen_width_dp, // jint screen_height_dp, jint screen_layout, jint ui_mode, // jint color_mode, jint major_version) { - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected static void nativeSetConfiguration( long ptr, int mcc, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java index 7ce0540ae..17357d66e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAsyncTaskLoader.java @@ -6,8 +6,10 @@ import org.robolectric.annotation.Implements; /** * The shadow API for {@link AsyncTaskLoader}. * - * Different shadow implementations will be used based on the current {@link LooperMode.Mode}. - * @see ShadowLegacyAsyncTaskLoader, ShadowPausedAsyncTaskLoader + * <p>Different shadow implementations will be used based on the current {@link LooperMode.Mode}. + * + * @see ShadowLegacyAsyncTaskLoader + * @see ShadowPausedAsyncTaskLoader */ @Implements(value = AsyncTaskLoader.class, shadowPicker = ShadowAsyncTaskLoader.Picker.class) public abstract class ShadowAsyncTaskLoader<D> { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java index 6408ce87c..ecf5f16ea 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java @@ -37,6 +37,7 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; +import org.robolectric.versioning.AndroidVersions.U; /** * Implementation of a couple methods in {@link AudioTrack}. Only a couple methods are supported, @@ -245,7 +246,7 @@ public class ShadowAudioTrack { return AudioTrack.SUCCESS; } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected int native_setup( Object /*WeakReference<AudioTrack>*/ audioTrack, Object /*AudioAttributes*/ attributes, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataInput.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataInput.java new file mode 100644 index 000000000..eb9445101 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataInput.java @@ -0,0 +1,107 @@ +package org.robolectric.shadows; + +import static java.lang.Math.min; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.app.backup.BackupDataInput; +import android.os.Build.VERSION_CODES; +import com.google.common.collect.ImmutableList; +import java.io.FileDescriptor; +import java.util.ArrayList; +import java.util.List; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; + +/** Shadow for BackupDataInput. */ +@Implements(value = BackupDataInput.class, minSdk = VERSION_CODES.LOLLIPOP, looseSignatures = true) +public class ShadowBackupDataInput { + + private List<BackupDataEntity> entities = new ArrayList<>(); + private int currentEntityIndex = -1; + private int currentBytesRead = 0; + private int currentBytesToRead = 0; + + /** + * Sets the entities to return when reading from this {@link BackupDataInput}. Use {@link + * org.robolectric.shadows.BackupDataInputBuilder} to get a populated instance. + */ + void setEntities(ImmutableList<BackupDataEntity> entities) { + this.entities = entities; + } + + @Implementation + protected static long ctor(FileDescriptor fd) { + // Return value greater than 0 to indicate successful allocation of backup reader. The real + // implementation would return an allocated pointer address, but since the methods are not + // static, we do not need a native object in this shadow implementation, and can return a fixed + // value instead. + return 1; + } + + // Using loose signature because EntityHeader is a private nested class. + @Implementation + protected int readNextHeader_native(Object backupReader, Object entity) { + if (currentBytesRead < currentBytesToRead) { + // Return failure to read header due to unread data bytes. + return -1; + } + + currentEntityIndex++; + + if (currentEntityIndex >= entities.size()) { + // Return end of backup input data. + return 1; + } + + BackupDataEntity shadowEntity = entities.get(currentEntityIndex); + + currentBytesRead = 0; + currentBytesToRead = shadowEntity.dataSize(); + + // Accessing using reflection because EntityHeader is a private nested class. + reflector(EntityHeaderReflector.class, entity).setKey(shadowEntity.key()); + reflector(EntityHeaderReflector.class, entity).setDataSize(shadowEntity.dataSize()); + return 0; + } + + @Implementation + protected int readEntityData_native(long backupReader, byte[] data, int offset, int size) { + if (currentEntityIndex >= entities.size() || currentBytesRead >= currentBytesToRead) { + // Return end of data. + return 0; + } + + if (offset + size > data.length) { + // Return error reading data. + return -1; + } + + byte[] shadowData = entities.get(currentEntityIndex).data(); + int remainingBytes = currentBytesToRead - currentBytesRead; + int bytesToRead = min(size, remainingBytes); + + System.arraycopy(shadowData, currentBytesRead, data, offset, bytesToRead); + currentBytesRead += bytesToRead; + return bytesToRead; + } + + @Implementation + protected int skipEntityData_native(long backupReader) { + if (currentEntityIndex < entities.size()) { + currentBytesRead = entities.get(currentEntityIndex).dataSize(); + } + return 0; + } + + @ForType(className = "android.app.backup.BackupDataInput$EntityHeader") + private interface EntityHeaderReflector { + + @Accessor("key") + void setKey(String key); + + @Accessor("dataSize") + void setDataSize(int dataSize); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataOutput.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataOutput.java new file mode 100644 index 000000000..9c48306f2 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackupDataOutput.java @@ -0,0 +1,109 @@ +package org.robolectric.shadows; + +import android.app.backup.BackupDataOutput; +import android.os.Build.VERSION_CODES; +import androidx.annotation.Nullable; +import com.google.common.collect.ImmutableList; +import java.io.FileDescriptor; +import java.util.ArrayList; +import java.util.List; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.ReflectorObject; +import org.robolectric.res.android.NativeObjRegistry; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; + +/** Shadow for BackupDataOutput. */ +@Implements(value = BackupDataOutput.class, minSdk = VERSION_CODES.LOLLIPOP) +public class ShadowBackupDataOutput { + + protected static final String KEY_PREFIX_JOINER = ":"; + + private static final NativeObjRegistry<NativeBackupDataOutput> nativeObjectRegistry = + new NativeObjRegistry<>(NativeBackupDataOutput.class); + + @ReflectorObject private BackupDataOutputReflector backupDataOutputReflector; + + /** Gets a list of all data written to the {@link BackupDataOutput}. */ + public ImmutableList<BackupDataEntity> getEntities() { + return ImmutableList.copyOf( + nativeObjectRegistry.getNativeObject(backupDataOutputReflector.getBackupWriter()).entities); + } + + @Implementation + protected static int writeEntityHeader_native(long mBackupWriter, String key, int dataSize) { + NativeBackupDataOutput nativeObject = nativeObjectRegistry.getNativeObject(mBackupWriter); + + if (nativeObject.currentEntity != null + && nativeObject.currentBytesWritten < nativeObject.currentEntity.dataSize()) { + // Return failed due to write due to unfinished previous record. + return -1; + } + + String prefixedKey = + nativeObject.keyPrefix != null ? nativeObject.keyPrefix + KEY_PREFIX_JOINER + key : key; + if (dataSize >= 0) { + nativeObject.currentEntity = BackupDataEntity.create(prefixedKey, new byte[dataSize]); + } else { + nativeObject.currentEntity = BackupDataEntity.createDeletedEntity(prefixedKey); + } + nativeObject.currentBytesWritten = 0; + + nativeObject.entities.add(nativeObject.currentEntity); + + // Return bytes written (1 byte per char in key plus 1 for the size int). + return key.length() + 1; + } + + @Implementation + protected static int writeEntityData_native(long mBackupWriter, byte[] data, int size) { + NativeBackupDataOutput nativeObject = nativeObjectRegistry.getNativeObject(mBackupWriter); + + if (nativeObject.currentEntity == null) { + // Return error writing due to missing header. + return -1; + } + + if (size > data.length + || nativeObject.currentBytesWritten + size > nativeObject.currentEntity.dataSize()) { + // Return error writing due to size exceeding of one of the arrays. + return -1; + } + + System.arraycopy( + data, 0, nativeObject.currentEntity.data(), nativeObject.currentBytesWritten, size); + nativeObject.currentBytesWritten += size; + + return size; + } + + @Implementation + protected static void setKeyPrefix_native(long mBackupWriter, String keyPrefix) { + nativeObjectRegistry.getNativeObject(mBackupWriter).keyPrefix = keyPrefix; + } + + @Implementation + protected static long ctor(FileDescriptor fd) { + return nativeObjectRegistry.register(new NativeBackupDataOutput()); + } + + @Implementation + protected static void dtor(long mBackupWriter) { + nativeObjectRegistry.unregister(mBackupWriter); + } + + @ForType(BackupDataOutput.class) + private interface BackupDataOutputReflector { + + @Accessor("mBackupWriter") + long getBackupWriter(); + } + + private static final class NativeBackupDataOutput { + final List<BackupDataEntity> entities = new ArrayList<>(); + @Nullable String keyPrefix = null; + @Nullable BackupDataEntity currentEntity = null; + int currentBytesWritten = 0; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java index 6dabdf569..21e7d51f3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBiometricManager.java @@ -110,7 +110,7 @@ public class ShadowBiometricManager { * * @param type to set the authenticatorType * @see <a - * href="https://developer.android.com/reference/android/hardware/biometrics/BiometricManager#canAuthenticate(int)" + * href="https://developer.android.com/reference/android/hardware/biometrics/BiometricManager#canAuthenticate(int)">BiometricManager#canAuthenticate(int)</a> */ public void setAuthenticatorType(int type) { authenticatorType = type; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java index 74133081c..ca55a0657 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java @@ -1,16 +1,27 @@ package org.robolectric.shadows; +import static org.robolectric.util.reflector.Reflector.reflector; + import android.graphics.Bitmap; import android.graphics.Matrix; +import com.google.common.base.Preconditions; import java.io.InputStream; +import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowBitmap.Picker; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Direct; +import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.U; /** Base class for {@link Bitmap} shadows. */ -@Implements(value = Bitmap.class, shadowPicker = Picker.class) +@Implements(value = Bitmap.class, shadowPicker = Picker.class, looseSignatures = true) public abstract class ShadowBitmap { + @RealObject Bitmap realBitmap; + /** * Returns a textual representation of the appearance of the object. * @@ -124,6 +135,33 @@ public abstract class ShadowBitmap { public abstract void setDescription(String s); + @Implementation(minSdk = U.SDK_INT) + protected void setGainmap(Object gainmap) { + Preconditions.checkState(!realBitmap.isRecycled(), "Bitmap is recycled"); + reflector(BitmapReflector.class, realBitmap).setGainmap(gainmap); + } + + @Implementation(minSdk = U.SDK_INT) + protected boolean hasGainmap() { + Preconditions.checkState(!realBitmap.isRecycled(), "Bitmap is recycled"); + return reflector(BitmapReflector.class, realBitmap).getGainmap() != null; + } + + /** Reflector for {@link Bitmap}. */ + @ForType(Bitmap.class) + protected interface BitmapReflector { + void checkRecycled(String errorMessage); + + @Accessor("mNativePtr") + long getNativePtr(); + + @Accessor("mGainmap") + void setGainmap(Object gainmap); + + @Direct + Object getGainmap(); + } + /** Shadow picker for {@link Bitmap}. */ public static final class Picker extends GraphicsShadowPicker<Object> { public Picker() { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java index f8725068e..4dbf20915 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java @@ -22,6 +22,7 @@ import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothStatusCodes; import android.bluetooth.IBluetoothManager; +import android.bluetooth.IBluetoothProfileServiceConnection; import android.bluetooth.le.BluetoothLeAdvertiser; import android.bluetooth.le.BluetoothLeScanner; import android.content.AttributionSource; @@ -29,6 +30,7 @@ import android.content.Context; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.ParcelUuid; +import android.os.RemoteException; import android.provider.Settings; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -129,7 +131,9 @@ public class ShadowBluetoothAdapter { /** Requires LooseSignatures because of {@link AttributionSource} parameter */ @Implementation(minSdk = VERSION_CODES.TIRAMISU) protected static Object createAdapter(Object attributionSource) { - IBluetoothManager service = ReflectionHelpers.createNullProxy(IBluetoothManager.class); + IBluetoothManager service = + ReflectionHelpers.createDelegatingProxy( + IBluetoothManager.class, new BluetoothManagerDelegate()); return ReflectionHelpers.callConstructor( BluetoothAdapter.class, ClassParameter.from(IBluetoothManager.class, service), @@ -304,6 +308,11 @@ public class ShadowBluetoothAdapter { } @Implementation + protected boolean disable(boolean persist) { + return disable(); + } + + @Implementation protected String getAddress() { return this.address; } @@ -738,4 +747,39 @@ public class ShadowBluetoothAdapter { @Static void setSBluetoothLeScanner(BluetoothLeScanner scanner); } + + // Any BluetoothAdapter calls which need to invoke BluetoothManager methods can delegate those + // calls to this class. The default behavior for any methods not defined in this class is a no-op. + @SuppressWarnings("unused") + private static class BluetoothManagerDelegate { + /** + * Allows the internal BluetoothProfileConnector associated with a {@link BluetoothProfile} to + * automatically invoke the service connected callback. + */ + public boolean bindBluetoothProfileService( + int bluetoothProfile, String serviceName, IBluetoothProfileServiceConnection proxy) { + if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { + return false; + } + try { + proxy.onServiceConnected(null, null); + } catch (RemoteException e) { + return false; + } + return true; + } + + /** + * Allows the internal BluetoothProfileConnector associated with a {@link BluetoothProfile} to + * automatically invoke the service disconnected callback. + */ + public void unbindBluetoothProfileService( + int bluetoothProfile, IBluetoothProfileServiceConnection proxy) { + try { + proxy.onServiceDisconnected(null); + } catch (RemoteException e) { + // nothing to do + } + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java index de2e76e40..7927da22c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGattServer.java @@ -116,7 +116,11 @@ public class ShadowBluetoothGattServer { public List<byte[]> getResponses() { List<byte[]> responsesCopy = new ArrayList<>(); for (byte[] response : this.responses) { - responsesCopy.add(response.clone()); + if (response != null) { + responsesCopy.add(response.clone()); + } else { + responsesCopy.add(null); + } } return responsesCopy; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java index c1c887acc..b6b1b8309 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java @@ -23,10 +23,13 @@ public class ShadowBuild { private static String serialOverride = Build.UNKNOWN; /** - * Temporary constant for VERSION_CODES.UPSIDE_DOWN_CAKE. Will be removed and replaced once the - * constant is available upstream. + * Sets the value of the {@link Build#BOARD} field. + * + * <p>It will be reset for the next test. */ - public static final int UPSIDE_DOWN_CAKE = 34; + public static void setBoard(String board) { + ReflectionHelpers.setStaticField(Build.class, "BOARD", board); + } /** * Sets the value of the {@link Build#DEVICE} field. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java index 19be93acc..1c8d78ba5 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java @@ -32,6 +32,7 @@ import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow class for {@link CameraManager} */ @Implements(value = CameraManager.class, minSdk = VERSION_CODES.LOLLIPOP) @@ -77,7 +78,7 @@ public class ShadowCameraManager { cameraTorches.put(cameraId, enabled); } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected CameraDevice openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java index 39b942857..9370a7ed2 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java @@ -6,10 +6,12 @@ import static android.os.Build.VERSION_CODES.Q; import android.annotation.Nullable; import android.os.PersistableBundle; import android.telephony.CarrierConfigManager; +import com.google.common.base.Preconditions; import java.util.HashMap; import org.robolectric.annotation.HiddenApi; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.versioning.AndroidVersions.U; @Implements(value = CarrierConfigManager.class, minSdk = M) public class ShadowCarrierConfigManager { @@ -35,6 +37,19 @@ public class ShadowCarrierConfigManager { return new PersistableBundle(); } + /** + * @see #getConfigForSubId(int). Currently the 'keys' parameter is ignored. + */ + @Implementation(minSdk = U.SDK_INT) + protected PersistableBundle getConfigForSubId(int subId, String... keys) { + // TODO: consider implementing the logic in telephony service + // CarrierConfigLoader#getConfigSubsetForSubIdWithFeature + Preconditions.checkNotNull(keys); + Preconditions.checkArgument( + keys.length == 0, "filtering by keys is not currently supported in Robolectric"); + return getConfigForSubId(subId); + } + public void setReadPhoneStatePermission(boolean readPhoneStatePermission) { this.readPhoneStatePermission = readPhoneStatePermission; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java index 0d13c918d..1ff824105 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java @@ -2,6 +2,7 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; import static android.os.Build.VERSION_CODES.N; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.robolectric.RuntimeEnvironment.getApiLevel; import static org.robolectric.util.reflector.Reflector.reflector; @@ -10,12 +11,14 @@ import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; import android.content.ClipboardManager.OnPrimaryClipChangedListener; +import android.os.SystemClock; import java.util.Collection; import java.util.concurrent.CopyOnWriteArrayList; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.reflector.ForType; @SuppressWarnings("UnusedDeclaration") @@ -28,6 +31,19 @@ public class ShadowClipboardManager { @Implementation protected void setPrimaryClip(ClipData clip) { + if (getApiLevel() >= O) { + if (clip != null) { + final ClipDescription description = clip.getDescription(); + if (description != null) { + final long currentTimeMillis = SystemClock.uptimeMillis(); + ReflectionHelpers.callInstanceMethod( + ClipDescription.class, + description, + "setTimestamp", + ClassParameter.from(long.class, currentTimeMillis)); + } + } + } if (getApiLevel() >= N) { if (clip != null) { clip.prepareToLeaveProcess(true); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java index 89609cc52..f18cf22ba 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowColor.java @@ -29,7 +29,17 @@ public class ShadowColor { @Implementation protected static int HSVToColor(int alpha, float hsv[]) { - int rgb = java.awt.Color.HSBtoRGB(hsv[0] / 360, hsv[1], hsv[2]); + int rgb = java.awt.Color.HSBtoRGB(hsv[0] / 360, pin(hsv[1]), pin(hsv[2])); return Color.argb(alpha, Color.red(rgb), Color.green(rgb), Color.blue(rgb)); } + + private static float pin(float value) { + if (value < 0.0f) { + return 0.0f; + } + if (value > 1.0) { + return 1.0f; + } + return value; + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java index 6201869ae..5c45d1f4a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCompanionDeviceManager.java @@ -1,14 +1,15 @@ package org.robolectric.shadows; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toList; -import android.annotation.NonNull; -import android.annotation.UserIdInt; -import android.companion.AssociatedDevice; +import android.Manifest.permission; +import android.app.ActivityThread; import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; +import android.companion.DeviceNotAssociatedException; import android.content.ComponentName; import android.net.MacAddress; import android.os.Build.VERSION_CODES; @@ -22,12 +23,12 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -import org.robolectric.util.reflector.ForType; -import org.robolectric.util.reflector.Accessor; -import org.robolectric.util.reflector.Reflector; - +import org.robolectric.shadow.api.Shadow; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; /** Shadow for CompanionDeviceManager. */ @Implements(value = CompanionDeviceManager.class, minSdk = VERSION_CODES.O) @@ -37,7 +38,11 @@ public class ShadowCompanionDeviceManager { protected final Set<ComponentName> hasNotificationAccess = new HashSet<>(); protected ComponentName lastRequestedNotificationAccess; protected AssociationRequest lastAssociationRequest; + protected MacAddress lastSystemApiAssociationMacAddress; protected CompanionDeviceManager.Callback lastAssociationCallback; + protected String lastObservingDevicePresenceDeviceAddress; + + private static final int DEFAULT_SYSTEMDATASYNCFLAGS = -1; @Implementation @SuppressWarnings("JdkCollectors") // toImmutableList is only supported in Java 8+. @@ -109,18 +114,95 @@ public class ShadowCompanionDeviceManager { associate(request, callback, /* handler= */ null); } + @Implementation(minSdk = VERSION_CODES.TIRAMISU) + protected void associate(String packageName, MacAddress macAddress, byte[] certificate) { + lastSystemApiAssociationMacAddress = macAddress; + if (!checkPermission(permission.ASSOCIATE_COMPANION_DEVICES)) { + throw new SecurityException("Permission ASSOCIATE_COMPANION_DEVICES not granted"); + } + if (!RuntimeEnvironment.getApplication().getPackageName().equals(packageName)) { + throw new SecurityException("Calling application package does not equal packageName"); + } + if (certificate == null) { + // Check the null case for now as {@link PackageManager#hasSigningCertificate} is not yet + // supported. + throw new SecurityException("Certificate is null"); + } + associations.add( + RoboAssociationInfo.builder().setDeviceMacAddress(macAddress.toString()).build()); + } + + @Implementation(minSdk = VERSION_CODES.TIRAMISU) + protected void startObservingDevicePresence(String deviceAddress) { + lastObservingDevicePresenceDeviceAddress = deviceAddress; + for (RoboAssociationInfo association : associations) { + if (Ascii.equalsIgnoreCase(deviceAddress, association.deviceMacAddress())) { + return; + } + } + throw new DeviceNotAssociatedException("Association does not exist"); + } + + /** + * This method will return the last {@link AssociationRequest} passed to {@code + * CompanionDeviceManager#associate(AssociationRequest, CompanionDeviceManager.Callback, Handler)} + * or {@code CompanionDeviceManager#associate(AssociationRequest, Executor, + * CompanionDeviceManager.Callback, Handler)}. + * + * <p>Note that the value returned is only changed when calling {@code associate} and will be set + * if that method throws an exception. Moreover, this value will unchanged if disassociate is + * called. + */ public AssociationRequest getLastAssociationRequest() { return lastAssociationRequest; } + /** + * This method will return the last {@link CompanionDeviceManager.Callback} passed to {@code + * CompanionDeviceManager#associate(AssociationRequest, CompanionDeviceManager.Callback, Handler)} + * or {@code CompanionDeviceManager#associate(AssociationRequest, Executor, + * CompanionDeviceManager.Callback, Handler)}. + * + * <p>Note that the value returned is only changed when calling {@code associate} and will be set + * if that method throws an exception. Moreover, this value will unchanged if disassociate is + * called. + */ public CompanionDeviceManager.Callback getLastAssociationCallback() { return lastAssociationCallback; } + /** + * If an association is set, this method will return the last {@link ComponentName} passed to + * {@code CompanionDeviceManager#requestNotificationAccess(ComponentName)}. + */ public ComponentName getLastRequestedNotificationAccess() { return lastRequestedNotificationAccess; } + /** + * Returns the last {@link MacAddress} passed to systemApi {@code associate}. + * + * <p>Note that the value returned is only changed when calling {@code associate} and will be set + * if that method throws an exception. Moreover, this value will unchanged if disassociate is + * called. + */ + public MacAddress getLastSystemApiAssociationMacAddress() { + return lastSystemApiAssociationMacAddress; + } + + /** + * Returns the last device address passed to {@link + * CompanionDeviceManager#startObservingDevicePresence(String)}. + * + * <p>Note that the value returned is only changed when calling {@link + * CompanionDeviceManager#startObservingDevicePresence(String)} and will still be set in the event + * that this method throws an exception. Moreover, this value will unchanged if disassociate is + * called. + */ + public String getLastObservingDevicePresenceDeviceAddress() { + return lastObservingDevicePresenceDeviceAddress; + } + private void checkHasAssociation() { if (associations.isEmpty()) { throw new IllegalStateException("App must have an association before calling this API"); @@ -136,45 +218,79 @@ public class ShadowCompanionDeviceManager { /** Convert {@link RoboAssociationInfo} to actual {@link AssociationInfo}. */ private AssociationInfo createAssociationInfo(RoboAssociationInfo info) { - return new AssociationInfo( - info.id(), - info.userId(), - info.packageName(), - MacAddress.fromString(info.deviceMacAddress()), - info.displayName(), - info.deviceProfile(), - info.associatedDevice(), - info.selfManaged(), - info.notifyOnDeviceNearby(), - info.revoked(), - info.timeApprovedMs(), - info.lastTimeConnectedMs(), - info.systemDataSyncFlags()); + AssociationInfoBuilder aiBuilder = AssociationInfoBuilder.newBuilder() + .setId(info.id()) + .setUserId(info.userId()) + .setPackageName(info.packageName()) + .setDeviceMacAddress(info.deviceMacAddress()) + .setDisplayName(info.displayName()) + .setDeviceProfile(info.deviceProfile()) + .setAssociatedDevice(info.associatedDevice()) + .setSelfManaged(info.selfManaged()) + .setNotifyOnDeviceNearby(info.notifyOnDeviceNearby()) + .setApprovedMs(info.timeApprovedMs()) + .setLastTimeConnectedMs(info.lastTimeConnectedMs()); + + if (ReflectionHelpers.hasField(AssociationInfo.class, "mTag")) { + ReflectionHelpers.callInstanceMethod( + aiBuilder, "setTag", ClassParameter.from(String.class, info.tag())); + } + if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) { + ReflectionHelpers.callInstanceMethod( + aiBuilder, + "setAssociatedDevice", + ClassParameter.from(Object.class, info.associatedDevice())); + ReflectionHelpers.callInstanceMethod( + aiBuilder, + "setSystemDataSyncFlags", + ClassParameter.from(int.class, info.systemDataSyncFlags())); + } + if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) { + ReflectionHelpers.callInstanceMethod( + aiBuilder, "setRevoked", ClassParameter.from(boolean.class, info.revoked())); + } + return aiBuilder.build(); } private RoboAssociationInfo createShadowAssociationInfo(AssociationInfo info) { - var ref_info = Reflector.reflector(_AssociationInfo_.class, info); + Object associatedDevice = null; + int systemDataSyncFlags = DEFAULT_SYSTEMDATASYNCFLAGS; + if (ReflectionHelpers.hasField(AssociationInfo.class, "mAssociatedDevice")) { + associatedDevice = ReflectionHelpers.callInstanceMethod(info, "getAssociatedDevice"); + systemDataSyncFlags = ReflectionHelpers.callInstanceMethod(info, "getSystemDataSyncFlags"); + } + boolean revoked = false; + if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) { + revoked = ReflectionHelpers.callInstanceMethod(info, "revoked"); + } + String tag = ""; + if (ReflectionHelpers.hasField(AssociationInfo.class, "mTag")) { + tag = ReflectionHelpers.callInstanceMethod(info, "getTag"); + } return RoboAssociationInfo.create( info.getId(), info.getUserId(), info.getPackageName(), - info.getDeviceMacAddress().toString(), + tag, + info.getDeviceMacAddress() == null ? null : info.getDeviceMacAddress().toString(), info.getDisplayName(), info.getDeviceProfile(), - ref_info.getFullAssociatedDevice(), + associatedDevice, info.isSelfManaged(), info.isNotifyOnDeviceNearby(), - info.isRevoked(), + revoked, info.getTimeApprovedMs(), info.getLastTimeConnectedMs(), - info.getSystemDataSyncFlags()); + systemDataSyncFlags); } - @ForType(AssociationInfo.class) - public interface _AssociationInfo_ { - - @Accessor("mAssociatedDevice") - AssociatedDevice getFullAssociatedDevice(); + private static boolean checkPermission(String permission) { + ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread(); + ShadowInstrumentation shadowInstrumentation = + Shadow.extract(activityThread.getInstrumentation()); + return shadowInstrumentation.checkPermission( + permission, android.os.Process.myPid(), android.os.Process.myUid()) + == PERMISSION_GRANTED; } /** @@ -191,6 +307,9 @@ public class ShadowCompanionDeviceManager { public abstract String packageName(); @Nullable + public abstract String tag(); + + @Nullable public abstract String deviceMacAddress(); @Nullable @@ -199,7 +318,8 @@ public class ShadowCompanionDeviceManager { @Nullable public abstract String deviceProfile(); - public abstract AssociatedDevice associatedDevice(); + @Nullable + public abstract Object associatedDevice(); public abstract boolean selfManaged(); @@ -221,17 +341,18 @@ public class ShadowCompanionDeviceManager { .setNotifyOnDeviceNearby(false) .setTimeApprovedMs(0) .setLastTimeConnectedMs(0) - .setSystemDataSyncFlags(-1); + .setSystemDataSyncFlags(DEFAULT_SYSTEMDATASYNCFLAGS); } public static RoboAssociationInfo create( int id, int userId, String packageName, + String tag, String deviceMacAddress, CharSequence displayName, String deviceProfile, - AssociatedDevice associatedDevice, + Object associatedDevice, boolean selfManaged, boolean notifyOnDeviceNearby, boolean revoked, @@ -242,14 +363,15 @@ public class ShadowCompanionDeviceManager { .setId(id) .setUserId(userId) .setPackageName(packageName) + .setTag(tag) .setDeviceMacAddress(deviceMacAddress) .setDisplayName(displayName) .setDeviceProfile(deviceProfile) - .setAssociatedDevice(associatedDevice) + .setAssociatedDevice(associatedDevice) .setSelfManaged(selfManaged) .setNotifyOnDeviceNearby(notifyOnDeviceNearby) .setTimeApprovedMs(timeApprovedMs) - .setRevoked(revoked) + .setRevoked(revoked) .setLastTimeConnectedMs(lastTimeConnectedMs) .setSystemDataSyncFlags(systemDataSyncFlags) .build(); @@ -264,6 +386,8 @@ public class ShadowCompanionDeviceManager { public abstract Builder setPackageName(String packageName); + public abstract Builder setTag(String tag); + public abstract Builder setDeviceMacAddress(String deviceMacAddress); public abstract Builder setDisplayName(CharSequence displayName); @@ -272,7 +396,7 @@ public class ShadowCompanionDeviceManager { public abstract Builder setSelfManaged(boolean selfManaged); - public abstract Builder setAssociatedDevice(AssociatedDevice device); + public abstract Builder setAssociatedDevice(Object device); public abstract Builder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCryptoObject.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCryptoObject.java new file mode 100644 index 000000000..d338d98cb --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCryptoObject.java @@ -0,0 +1,31 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.P; + +import android.hardware.biometrics.CryptoObject; +import org.robolectric.annotation.HiddenApi; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@SuppressWarnings("UnusedDeclaration") +@Implements(value = CryptoObject.class, isInAndroidSdk = false, minSdk = P) +public class ShadowCryptoObject { + + /** + * The shadow method of CryptoObject#getOpId. + * + * <p>The CryptoObject#getOpId implementation in AOSP calls javax.crypto.CipherSpi#getCurrentSpi + * to retrieve javax.crypto.Cipher, but this API is added by Android JDK implementation, and not + * supported by OpenJDK. To avoid this issue, we shadow CryptoObject#getOpId to intercept + * call-chain early. Related issue: <a + * href="https://github.com/robolectric/robolectric/issues/8242">java.lang.NoSuchMethodError: + * 'javax.crypto.CipherSpi javax.crypto.Cipher.getCurrentSpi()</a>. + * + * @return 0L as default value. + */ + @Implementation + @HiddenApi + protected final long getOpId() { + return 0L; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java index ac435c46f..b36762501 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDateIntervalFormatU.java @@ -1,7 +1,6 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; -import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import org.robolectric.versioning.AndroidVersions.U; import java.text.FieldPosition; import java.util.HashMap; @@ -11,7 +10,7 @@ import android.text.format.DateIntervalFormat; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -@Implements(value = DateIntervalFormat.class, isInAndroidSdk = false, minSdk = UPSIDE_DOWN_CAKE) +@Implements(value = DateIntervalFormat.class, isInAndroidSdk = false, minSdk = U.SDK_INT) public class ShadowDateIntervalFormatU { private static long address; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java index 00bb9558c..145a37736 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java @@ -35,6 +35,7 @@ import org.robolectric.util.reflector.Constructor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.WithType; +import org.robolectric.versioning.AndroidVersions.U; /** * Shadow of {@link DisplayEventReceiver}. The {@link Choreographer} is a subclass of {@link @@ -95,7 +96,7 @@ public class ShadowDisplayEventReceiver { return nativeInit(receiver, msgQueue); } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected static long nativeInit( WeakReference<DisplayEventReceiver> receiver, WeakReference<Object> vsyncEventData, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java index bb9fc5538..f0211a180 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayHashManager.java @@ -3,7 +3,9 @@ package org.robolectric.shadows; import android.view.displayhash.DisplayHash; import android.view.displayhash.DisplayHashManager; import android.view.displayhash.VerifiedDisplayHash; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; +import java.util.Collection; import java.util.Set; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -13,6 +15,7 @@ import org.robolectric.annotation.Implements; public class ShadowDisplayHashManager { private static VerifiedDisplayHash verifyDisplayHashResult; + private static Set<String> supportedHashAlgorithms = ImmutableSet.of("PHASH"); /** * Sets the {@link VerifiedDisplayHash} that's going to be returned by following @@ -22,9 +25,23 @@ public class ShadowDisplayHashManager { ShadowDisplayHashManager.verifyDisplayHashResult = verifyDisplayHashResult; } + /** + * Sets the return value of #getSupportedHashAlgorithms. + * + * <p>If null is provided, getSupportedHashAlgorithms will throw a RuntimeException. + */ + public static void setSupportedHashAlgorithms(Collection<String> supportedHashAlgorithms) { + if (supportedHashAlgorithms == null) { + ShadowDisplayHashManager.supportedHashAlgorithms = null; + } else { + ShadowDisplayHashManager.supportedHashAlgorithms = + ImmutableSet.copyOf(supportedHashAlgorithms); + } + } + @Implementation(minSdk = 31) protected Set<String> getSupportedHashAlgorithms() { - return ImmutableSet.of("PHASH"); + return Preconditions.checkNotNull(supportedHashAlgorithms); } @Implementation(minSdk = 31) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java index c684b69fb..9d755d4ef 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEnvironment.java @@ -6,6 +6,7 @@ import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; import static org.robolectric.util.reflector.Reflector.reflector; import android.os.Environment; @@ -24,7 +25,9 @@ import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; +import org.robolectric.util.reflector.Static; @Implements(Environment.class) @SuppressWarnings("NewApi") @@ -37,6 +40,7 @@ public class ShadowEnvironment { private static Path tmpExternalFilesDirBase; private static final List<File> externalDirs = new ArrayList<>(); private static Map<Path, String> storageState = new HashMap<>(); + private static Path rootStorageDirectory; static Path EXTERNAL_CACHE_DIR; static Path EXTERNAL_FILES_DIR; @@ -74,11 +78,33 @@ public class ShadowEnvironment { } /** - * Sets the return value of {@link #getExternalStorageDirectory()}. Note that - * the default value provides a directory that is usable in the test environment. - * If the test app uses this method to override that default directory, please - * clean up any files written to that directory, as the Robolectric environment - * will not purge that directory when the test ends. + * Sets the return value of {@link #getStorageDirectory()}. This can be used for example, when + * testing code paths that need to perform regex matching on this directory. + * + * <p>Note that the default value provides a directory that is usable in the test environment. If + * the test app uses this method to override that default directory, please clean up any files + * written to that directory, as the Robolectric environment will not purge that directory when + * the test ends. + * + * @param directory Path to return from {@link #getStorageDirectory()}. + */ + public static void setStorageDirectory(Path directory) { + rootStorageDirectory = directory; + } + + @Implementation(minSdk = R) + protected static File getStorageDirectory() { + if (rootStorageDirectory == null) { + return reflector(EnvironmentReflector.class).getStorageDirectory(); + } + return rootStorageDirectory.toFile(); + } + + /** + * Sets the return value of {@link #getExternalStorageDirectory()}. Note that the default value + * provides a directory that is usable in the test environment. If the test app uses this method + * to override that default directory, please clean up any files written to that directory, as the + * Robolectric environment will not purge that directory when the test ends. * * @param directory Path to return from {@link #getExternalStorageDirectory()}. */ @@ -140,6 +166,7 @@ public class ShadowEnvironment { @Resetter public static void reset() { + rootStorageDirectory = null; EXTERNAL_CACHE_DIR = null; EXTERNAL_FILES_DIR = null; @@ -301,4 +328,11 @@ public class ShadowEnvironment { @Accessor("mExternalStorageAndroidData") void setExternalStorageAndroidData(File file); } + + @ForType(Environment.class) + interface EnvironmentReflector { + @Static + @Direct + File getStorageDirectory(); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java index 58cd55818..9f9722522 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java @@ -14,7 +14,6 @@ import android.graphics.ColorSpace.Named; import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.DecodeException; import android.graphics.ImageDecoder.Source; -import android.graphics.Point; import android.graphics.Rect; import android.util.Size; import java.io.ByteArrayInputStream; @@ -43,12 +42,14 @@ public class ShadowImageDecoder { private final int height; private final boolean animated = false; private final boolean ninePatch; + private final String mimeType; ImgStream() { InputStream inputStream = getInputStream(); - final Point size = ImageUtil.getImageSizeFromStream(inputStream); - this.width = size == null ? 10 : size.x; - this.height = size == null ? 10 : size.y; + final ImageUtil.ImageInfo info = ImageUtil.getImageInfoFromStream(inputStream); + this.width = info == null ? 10 : info.width; + this.height = info == null ? 10 : info.height; + this.mimeType = info == null ? "image/unknown" : info.mimeType; if (inputStream instanceof AssetManager.AssetInputStream) { ShadowAssetInputStream sis = Shadow.extract(inputStream); this.ninePatch = sis.isNinePatch(); @@ -74,6 +75,10 @@ public class ShadowImageDecoder { boolean isNinePatch() { return ninePatch; } + + String mimeType() { + return mimeType; + } } private static final class CppImageDecoder { @@ -84,6 +89,9 @@ public class ShadowImageDecoder { this.imgStream = imgStream; } + public String getMimeType() { + return imgStream.mimeType(); + } } private static final NativeObjRegistry<CppImageDecoder> NATIVE_IMAGE_DECODER_REGISTRY = @@ -247,9 +255,7 @@ public class ShadowImageDecoder { static String ImageDecoder_nGetMimeType(long nativePtr) { CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr); - // return encodedFormatToString(decoder.mCodec.getEncodedFormat()); - // TODO: fix this properly. Just hardcode to png for now or just remove GraphicsMode.LEGACY - return "image/png"; + return decoder.getMimeType(); } static ColorSpace ImageDecoder_nGetColorSpace(long nativePtr) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java index b8564ca40..140f664be 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java @@ -24,6 +24,7 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link android.media.ImageReader} */ @Implements(value = ImageReader.class, looseSignatures = true) @@ -69,7 +70,7 @@ public class ShadowImageReader { return nativeImageSetup(image); } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected int nativeImageSetup(Object /* Image */ image) { return nativeImageSetup((Image) image); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java index 1675a025a..8d447ed7c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java @@ -21,9 +21,13 @@ import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import java.util.Map; import java.util.concurrent.Executor; +import java.util.function.Consumer; +import org.robolectric.annotation.HiddenApi; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; +import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; @@ -42,7 +46,9 @@ import org.robolectric.util.reflector.Static; @SystemApi public class ShadowImsMmTelManager { - protected static final Map<Integer, ImsMmTelManager> existingInstances = new ArrayMap<>(); + private static final Map<Integer, ImsMmTelManager> existingInstances = new ArrayMap<>(); + private static final Map<Integer, Integer> subIdToRegistrationTransportTypeMap = new ArrayMap<>(); + private static final Map<Integer, Integer> subIdToRegistrationStateMap = new ArrayMap<>(); private final Map<ImsMmTelManager.RegistrationCallback, Executor> registrationCallbackExecutorMap = new ArrayMap<>(); @@ -53,12 +59,9 @@ public class ShadowImsMmTelManager { private MmTelCapabilities mmTelCapabilitiesAvailable = new MmTelCapabilities(); // start with empty private int imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_NONE; - private int subId; - - @Implementation(maxSdk = VERSION_CODES.R) - protected void __constructor__(int subId) { - this.subId = subId; - } + private Consumer<Integer> stateCallback; + private Consumer<Integer> transportTypeCallback; + @RealObject private ImsMmTelManager realImsMmTelManager; /** * Sets whether IMS is available on the device. Setting this to false will cause {@link @@ -204,6 +207,47 @@ public class ShadowImsMmTelManager { } } + public static void setRegistrationState(int subId, int registrationState) { + subIdToRegistrationStateMap.put(subId, registrationState); + } + + public Consumer<Integer> getRegistrationStateCallback() { + return stateCallback; + } + + @HiddenApi + @Implementation(minSdk = VERSION_CODES.R) + public void getRegistrationState(Executor executor, Consumer<Integer> stateCallback) { + this.stateCallback = stateCallback; + int subId = getSubscriptionId(); + if (subIdToRegistrationStateMap.containsKey(getSubscriptionId())) { + stateCallback.accept(subIdToRegistrationStateMap.get(subId)); + } + } + + public static void setRegistrationTransportType(int subId, int registrationTransportType) { + subIdToRegistrationTransportTypeMap.put(subId, registrationTransportType); + } + + public Consumer<Integer> getRegistrationTransportTypeCallback() { + return transportTypeCallback; + } + + @RequiresPermission( + anyOf = { + Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + Manifest.permission.READ_PRECISE_PHONE_STATE + }) + @Implementation(minSdk = VERSION_CODES.R) + public void getRegistrationTransportType( + Executor executor, Consumer<Integer> transportTypeCallback) { + this.transportTypeCallback = transportTypeCallback; + int subId = getSubscriptionId(); + if (subIdToRegistrationTransportTypeMap.containsKey(getSubscriptionId())) { + transportTypeCallback.accept(subIdToRegistrationTransportTypeMap.get(subId)); + } + } + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @Implementation protected void registerMmTelCapabilityCallback( @@ -248,7 +292,7 @@ public class ShadowImsMmTelManager { /** Get subscription id */ public int getSubscriptionId() { - return subId; + return reflector(ImsMmTelManagerReflector.class, realImsMmTelManager).getSubId(); } /** Returns only one instance per subscription id. */ @@ -268,13 +312,18 @@ public class ShadowImsMmTelManager { } @Resetter - public static void clearExistingInstances() { + public static void clearExistingInstancesAndStates() { existingInstances.clear(); + subIdToRegistrationTransportTypeMap.clear(); + subIdToRegistrationStateMap.clear(); } @ForType(ImsMmTelManager.class) interface ImsMmTelManagerReflector { + @Accessor("mSubId") + int getSubId(); + @Static @Direct ImsMmTelManager createForSubscriptionId(int subId); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java new file mode 100644 index 000000000..76fc9a172 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInformationElement.java @@ -0,0 +1,40 @@ +package org.robolectric.shadows; + +import android.net.wifi.ScanResult; +import android.os.Build.VERSION_CODES; +import org.robolectric.annotation.Implements; + +/** Shadow for {@link android.net.wifi.ScanResult.InformationElement}. */ +@Implements(value = ScanResult.InformationElement.class, minSdk = VERSION_CODES.R) +public class ShadowInformationElement { + /** + * A builder for creating ShadowInformationElement objects. Use build() to return the + * InformationElement object. + */ + public static class Builder { + private final ScanResult.InformationElement informationElement; + + public Builder() { + informationElement = new ScanResult.InformationElement(); + } + + public Builder setId(int id) { + informationElement.id = id; + return this; + } + + public Builder setIdExt(int idExt) { + informationElement.idExt = idExt; + return this; + } + + public Builder setBytes(byte[] bytes) { + informationElement.bytes = bytes; + return this; + } + + public ScanResult.InformationElement build() { + return informationElement; + } + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java index 298fabec6..e1e664759 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java @@ -22,6 +22,7 @@ import org.robolectric.annotation.Resetter; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link InputManager} */ @Implements(value = InputManager.class, looseSignatures = true) @@ -116,7 +117,7 @@ public class ShadowInputManager { @Resetter public static void reset() { - if (SDK_INT < ShadowBuild.UPSIDE_DOWN_CAKE) { + if (SDK_INT < U.SDK_INT) { ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java index 20a6d1364..dde69ced6 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java @@ -34,8 +34,10 @@ import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; +import org.robolectric.versioning.AndroidVersions.U; -@Implements(value = InputMethodManager.class) +/** Shadow for InputMethodManager. */ +@Implements(value = InputMethodManager.class, looseSignatures = true) public class ShadowInputMethodManager { /** @@ -81,6 +83,13 @@ public class ShadowInputMethodManager { return showSoftInput(view, flags, resultReceiver); } + @Implementation(minSdk = U.SDK_INT) + protected boolean showSoftInput( + Object view, Object statsToken, Object flags, Object resultReceiver, Object reason) { + return showSoftInput( + (View) view, (Integer) flags, (ResultReceiver) resultReceiver, (Integer) reason); + } + @Implementation(minSdk = S) protected boolean hideSoftInputFromWindow( IBinder windowToken, int flags, ResultReceiver resultReceiver, int ignoredReason) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java index da5070a7a..cc0722fbb 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java @@ -63,6 +63,7 @@ import javax.annotation.concurrent.GuardedBy; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.RealObject; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowActivity.IntentForResult; @@ -1181,4 +1182,22 @@ public class ShadowInstrumentation { } return null; } + + /** + * Executes a runnable depending on the LooperMode. + * + * <p>For INSTRUMENTATION_TEST mode, will post the runnable to the instrumentation thread and + * block the caller's thread until that runnable is executed. + * + * <p>For other modes, simply executes the runnable. + * + * @param runnable a runnable to be executed + */ + public static void runOnMainSyncNoIdle(Runnable runnable) { + if (ShadowLooper.looperMode() == LooperMode.Mode.INSTRUMENTATION_TEST) { + checkNotNull(getInstrumentation()).runOnMainSync(runnable); + } else { + runnable.run(); + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java index bf87b3a5b..c527b5f5b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java @@ -7,6 +7,7 @@ import android.app.job.JobParameters; import android.app.job.JobService; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.versioning.AndroidVersions.U; @Implements(value = JobService.class, minSdk = LOLLIPOP) public class ShadowJobService extends ShadowService { @@ -21,13 +22,23 @@ public class ShadowJobService extends ShadowService { } /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */ - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected void setNotification( JobParameters params, int notificationId, Notification notification, int jobEndNotificationPolicy) {} + /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */ + @Implementation(minSdk = U.SDK_INT) + protected void updateEstimatedNetworkBytes( + JobParameters params, long downloadBytes, long uploadBytes) {} + + /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */ + @Implementation(minSdk = U.SDK_INT) + protected void updateTransferredNetworkBytes( + JobParameters params, long downloadBytes, long uploadBytes) {} + /** * Returns whether the job has finished running. When using this shadow this returns true after * {@link #jobFinished(JobParameters, boolean)} is called. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java index 79b1cbc6b..d345491dc 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java @@ -10,6 +10,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.Integer.max; import static java.lang.Integer.min; +import static org.robolectric.util.reflector.Reflector.reflector; import android.graphics.Bitmap; import android.graphics.ColorSpace; @@ -19,6 +20,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.os.Parcel; +import android.os.Parcelable; import android.util.DisplayMetrics; import java.awt.Color; import java.awt.Graphics2D; @@ -40,6 +42,7 @@ import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; +import org.robolectric.versioning.AndroidVersions.U; @SuppressWarnings({"UnusedDeclaration"}) @Implements(value = Bitmap.class, isInAndroidSdk = false) @@ -681,6 +684,16 @@ public class ShadowLegacyBitmap extends ShadowBitmap { int[] pixels = new int[width * height]; getPixels(pixels, 0, width, 0, 0, width, height); p.writeIntArray(pixels); + + if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) { + Object gainmap = reflector(BitmapReflector.class, realBitmap).getGainmap(); + if (gainmap != null) { + p.writeBoolean(true); + p.writeTypedObject((Parcelable) gainmap, flags); + } else { + p.writeBoolean(false); + } + } } @Implementation diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java index a85af0c41..1401816df 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java @@ -426,7 +426,7 @@ public class ShadowLegacyMatrix extends ShadowMatrix { } private SimpleMatrix(float[] values) { - if (values.length != 9) { + if (values.length < 9) { throw new ArrayIndexOutOfBoundsException(); } mValues = Arrays.copyOf(values, 9); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java index 938e41aac..5f4c2ebbb 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMessage.java @@ -24,7 +24,8 @@ import org.robolectric.util.reflector.ForType; * <p>In {@link LooperMode.Mode.LEGACY}, each Message is associated with a Runnable posted to the * {@link Scheduler}. * - * @see ShadowLooper, ShadowLegacyMessageQueue + * @see ShadowLooper + * @see ShadowLegacyMessageQueue */ @Implements(value = Message.class, isInAndroidSdk = false) public class ShadowLegacyMessage extends ShadowMessage { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java index 8e38cc518..aa5082340 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocaleList.java @@ -18,15 +18,21 @@ public class ShadowLocaleList { @Resetter public static void reset() { LocaleListReflector localeListReflector = reflector(LocaleListReflector.class); - localeListReflector.setLastDefaultLocale(null); - localeListReflector.setDefaultLocaleList(null); - localeListReflector.setDefaultAdjustedLocaleList(null); - localeListReflector.setLastExplicitlySetLocaleList(null); + synchronized (localeListReflector.getLock()) { + localeListReflector.setLastDefaultLocale(null); + localeListReflector.setDefaultLocaleList(null); + localeListReflector.setDefaultAdjustedLocaleList(null); + localeListReflector.setLastExplicitlySetLocaleList(null); + } } @ForType(LocaleList.class) interface LocaleListReflector { @Static + @Accessor("sLock") + Object getLock(); + + @Static @Accessor("sLastDefaultLocale") void setLastDefaultLocale(Locale lastDefaultLocal); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java index 9bef2d193..ed7eb8c85 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java @@ -37,6 +37,7 @@ import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.versioning.AndroidVersions.U; /** * Implementation of {@link android.media.MediaCodec} which supports both asynchronous and @@ -408,7 +409,7 @@ public class ShadowMediaCodec { @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {} - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected void invalidateByteBufferLocked( @Nullable ByteBuffer[] buffers, int index, boolean input) {} @@ -416,14 +417,14 @@ public class ShadowMediaCodec { @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {} - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected void validateInputByteBufferLocked(@Nullable ByteBuffer[] buffers, int index) {} /** Prevents calling Android-only methods on basic ByteBuffer objects. */ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {} - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {} /** @@ -441,7 +442,7 @@ public class ShadowMediaCodec { } } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected void validateOutputByteBufferLocked( @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) { validateOutputByteBuffer(buffers, index, info); @@ -451,14 +452,14 @@ public class ShadowMediaCodec { @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {} - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected void invalidateByteBuffersLocked(@Nullable ByteBuffer[] buffers) {} /** Prevents attempting to free non-direct ByteBuffer objects. */ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU) protected void freeByteBuffer(@Nullable ByteBuffer buffer) {} - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected void freeByteBufferLocked(@Nullable ByteBuffer buffer) {} /** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */ diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java index 31fc79634..cf349ebdd 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static org.robolectric.util.reflector.Reflector.reflector; import android.content.ContentResolver; @@ -7,6 +8,11 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory.Options; import android.net.Uri; import android.provider.MediaStore; +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; @@ -19,10 +25,14 @@ import org.robolectric.util.reflector.ForType; public class ShadowMediaStore { private static Bitmap stubBitmap = null; + private static final List<CloudMediaChangedEvent> cloudMediaChangedEventList = new ArrayList<>(); + @Nullable private static String currentCloudMediaProviderAuthority = null; @Resetter public static void reset() { stubBitmap = null; + cloudMediaChangedEventList.clear(); + currentCloudMediaProviderAuthority = null; } /** Shadow for {@link MediaStore.Images}. */ @@ -97,4 +107,47 @@ public class ShadowMediaStore { @Direct Bitmap getThumbnail(ContentResolver cr, long imageId, int kind, Options options); } + + @Implementation(minSdk = TIRAMISU) + protected static void notifyCloudMediaChangedEvent( + ContentResolver resolver, String authority, String currentMediaCollectionId) { + cloudMediaChangedEventList.add( + CloudMediaChangedEvent.create(authority, currentMediaCollectionId)); + } + + /** + * Returns an {@link ImmutableList} of all {@link CloudMediaChangedEvent} objects that {@link + * MediaStore} has been notified of. + */ + public static ImmutableList<CloudMediaChangedEvent> getCloudMediaChangedEvents() { + return ImmutableList.copyOf(cloudMediaChangedEventList); + } + + public static void clearCloudMediaChangedEventList() { + cloudMediaChangedEventList.clear(); + } + + /** Event info for {@link MediaStore#notifyCloudMediaChangedEvent} notify events. */ + @AutoValue + public abstract static class CloudMediaChangedEvent { + public static CloudMediaChangedEvent create(String authority, String currentMediaCollectionId) { + return new AutoValue_ShadowMediaStore_CloudMediaChangedEvent( + authority, currentMediaCollectionId); + } + + public abstract String authority(); + + public abstract String currentMediaCollectionId(); + } + + @Implementation(minSdk = TIRAMISU) + protected static boolean isCurrentCloudMediaProviderAuthority( + ContentResolver resolver, String authority) { + return currentCloudMediaProviderAuthority.equals(authority); + } + + /** Mutator method to set the value of the current cloud media provider authority. */ + public static void setCurrentCloudMediaProviderAuthority(@Nullable String authority) { + currentCloudMediaProviderAuthority = authority; + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java index d1e9f3d6c..d0449af4a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMimeTypeMap.java @@ -52,7 +52,15 @@ public class ShadowMimeTypeMap { return null; } + /** + * @deprecated use addExtensionMimeTypeMapping + */ + @Deprecated public void addExtensionMimeTypMapping(String extension, String mimeType) { + addExtensionMimeTypeMapping(extension, mimeType); + } + + public void addExtensionMimeTypeMapping(String extension, String mimeType) { extensionToMimeTypeMap.put(extension, mimeType); mimeTypeToExtensionMap.put(mimeType, extension); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java index 79b1eafaf..5286b0794 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java @@ -3,19 +3,47 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.N_MR1; import static android.os.Build.VERSION_CODES.O; +import static org.robolectric.util.reflector.Reflector.reflector; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimatedVectorDrawable.VectorDrawableAnimatorRT; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; import org.robolectric.nativeruntime.AnimatedVectorDrawableNatives; import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.shadows.ShadowNativeAnimatedVectorDrawable.Picker; +import org.robolectric.util.reflector.Direct; +import org.robolectric.util.reflector.ForType; /** Shadow for {@link AnimatedVectorDrawable} that is backed by native code */ @Implements(value = AnimatedVectorDrawable.class, minSdk = O, shadowPicker = Picker.class) public class ShadowNativeAnimatedVectorDrawable extends ShadowDrawable { + @RealObject protected AnimatedVectorDrawable realAnimatedVectorDrawable; + + private boolean startInitiated; + + @Implementation + protected void start() { + reflector(AnimatedVectorDrawableReflector.class, realAnimatedVectorDrawable).start(); + startInitiated = true; + } + + @Implementation + protected void stop() { + reflector(AnimatedVectorDrawableReflector.class, realAnimatedVectorDrawable).stop(); + startInitiated = false; + } + + /** + * Returns true if {@link #start()} was called and false if {@link #start()} was not called or + * {@link #stop()} was called. + */ + public final boolean isStartInitiated() { + return startInitiated; + } + @Implementation(minSdk = N) protected static long nCreateAnimatorSet() { DefaultNativeRuntimeLoader.injectAndLoad(); @@ -117,4 +145,14 @@ public class ShadowNativeAnimatedVectorDrawable extends ShadowDrawable { super(null, ShadowNativeAnimatedVectorDrawable.class); } } + + /** Accessor interface for {@link AnimatedVectorDrawable} internals. */ + @ForType(AnimatedVectorDrawable.class) + private interface AnimatedVectorDrawableReflector { + @Direct + void start(); + + @Direct + void stop(); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java index 42080522f..945575eb1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java @@ -21,6 +21,7 @@ import org.robolectric.nativeruntime.BaseCanvasNatives; import org.robolectric.shadows.ShadowNativeBaseCanvas.Picker; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link BaseCanvas} that is backed by native code */ @Implements( @@ -685,7 +686,7 @@ public class ShadowNativeBaseCanvas extends ShadowCanvas { BaseCanvasNatives.nPunchHole(renderer, left, top, right, bottom, rx, ry); } - @Implementation(minSdk = 10000) + @Implementation(minSdk = U.SDK_INT) protected static void nPunchHole( long renderer, float left, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java index 1f061b53e..c0a8b0101 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java @@ -14,6 +14,7 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.nativeruntime.BaseRecordingCanvasNatives; import org.robolectric.shadows.ShadowNativeBaseRecordingCanvas.Picker; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link BaseRecordingCanvas} that is backed by native code */ @Implements( @@ -575,7 +576,7 @@ public class ShadowNativeBaseRecordingCanvas extends ShadowNativeCanvas { BaseRecordingCanvasNatives.nPunchHole(renderer, left, top, right, bottom, rx, ry); } - @Implementation(minSdk = 10000) + @Implementation(minSdk = U.SDK_INT) protected static void nPunchHole( long renderer, float left, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java index 0359bc37f..a1ff96f25 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java @@ -18,12 +18,14 @@ import android.graphics.ColorSpace.Rgb.TransferParameters; import android.graphics.Matrix; import android.hardware.HardwareBuffer; import android.os.Parcel; +import android.os.Parcelable; import java.io.InputStream; import java.io.OutputStream; import java.nio.Buffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; @@ -35,6 +37,7 @@ import org.robolectric.nativeruntime.NativeAllocationRegistryNatives; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link Bitmap} that is backed by native code */ @Implements(value = Bitmap.class, looseSignatures = true, minSdk = O, isInAndroidSdk = false) @@ -374,6 +377,16 @@ public class ShadowNativeBitmap extends ShadowBitmap { int[] pixels = new int[width * height]; realBitmap.getPixels(pixels, 0, width, 0, 0, width, height); p.writeIntArray(pixels); + + if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) { + Object gainmap = reflector(BitmapReflector.class, realBitmap).getGainmap(); + if (gainmap != null) { + p.writeBoolean(true); + p.writeTypedObject((Parcelable) gainmap, flags); + } else { + p.writeBoolean(false); + } + } } @Implementation @@ -413,11 +426,6 @@ public class ShadowNativeBitmap extends ShadowBitmap { return bitmap; } - @ForType(Bitmap.class) - interface BitmapReflector { - void checkRecycled(String errorMessage); - } - @Override public Bitmap getCreatedFromBitmap() { throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported"); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java index b2da82783..365b6a9a1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java @@ -11,6 +11,8 @@ import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.nativeruntime.FontFamilyBuilderNatives; import org.robolectric.nativeruntime.FontsFontFamilyNatives; import org.robolectric.shadows.ShadowNativeFontsFontFamily.Picker; +import org.robolectric.versioning.AndroidVersions.U; +import org.robolectric.versioning.AndroidVersions.V; /** Shadow for {@link FontFamily} that is backed by native code */ @Implements( @@ -63,7 +65,7 @@ public class ShadowNativeFontsFontFamily { return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback); } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT) protected static long nBuild( long builderPtr, String langTags, @@ -73,6 +75,17 @@ public class ShadowNativeFontsFontFamily { return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback); } + @Implementation(minSdk = V.SDK_INT) + protected static long nBuild( + long builderPtr, + String langTags, + int variant, + boolean isCustomFallback, + boolean isDefaultFallback, + int variableFamilyType) { + return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback); + } + @Implementation protected static long nGetReleaseNativeFamily() { return FontFamilyBuilderNatives.nGetReleaseNativeFamily(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java index 2a1b2827b..1bfaa90e7 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java @@ -19,6 +19,7 @@ import org.robolectric.annotation.Implements; import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.nativeruntime.HardwareRendererNatives; import org.robolectric.shadows.ShadowNativeHardwareRenderer.Picker; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link HardwareRenderer} that is backed by native code */ @Implements( @@ -370,7 +371,7 @@ public class ShadowNativeHardwareRenderer { presentationDeadlineNanos); } - @Implementation(minSdk = 10000) + @Implementation(minSdk = U.SDK_INT) protected static void nInitDisplayInfo( int width, int height, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java index f5d029cce..7133f83a7 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java @@ -10,16 +10,28 @@ import org.robolectric.annotation.Implements; import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.nativeruntime.LineBreakerNatives; import org.robolectric.shadows.ShadowNativeLineBreaker.Picker; +import org.robolectric.versioning.AndroidVersions.U; +import org.robolectric.versioning.AndroidVersions.V; /** Shadow for {@link LineBreaker} that is backed by native code */ @Implements(value = LineBreaker.class, minSdk = Q, shadowPicker = Picker.class) public class ShadowNativeLineBreaker { - @Implementation + @Implementation(maxSdk = U.SDK_INT) protected static long nInit( int breakStrategy, int hyphenationFrequency, boolean isJustified, int[] indents) { return LineBreakerNatives.nInit(breakStrategy, hyphenationFrequency, isJustified, indents); } + @Implementation(minSdk = V.SDK_INT) + protected static long nInit( + int breakStrategy, + int hyphenationFrequency, + boolean isJustified, + int[] indents, + boolean useBoundsForWidth) { + return nInit(breakStrategy, hyphenationFrequency, isJustified, indents); + } + @Implementation protected static long nGetReleaseFunc() { // Called first by the static initializer. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java index 5b82a6cf5..8cf433558 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java @@ -14,6 +14,8 @@ import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.nativeruntime.MeasuredTextBuilderNatives; import org.robolectric.nativeruntime.MeasuredTextNatives; import org.robolectric.shadows.ShadowNativeMeasuredText.Picker; +import org.robolectric.versioning.AndroidVersions.U; +import org.robolectric.versioning.AndroidVersions.V; /** Shadow for {@link MeasuredText} that is backed by native code */ @Implements(value = MeasuredText.class, minSdk = Q, shadowPicker = Picker.class) @@ -99,13 +101,27 @@ public class ShadowNativeMeasuredText { nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout); } - @Implementation(minSdk = TIRAMISU) + @Implementation(minSdk = TIRAMISU, maxSdk = U.SDK_INT) + protected static long nBuildMeasuredText( + /* Non Zero */ long nativeBuilderPtr, + long hintMtPtr, + char[] text, + boolean computeHyphenation, + boolean computeLayout, + boolean fastHyphenationMode) { + return MeasuredTextBuilderNatives.nBuildMeasuredText( + nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout); + } + + @Implementation(minSdk = V.SDK_INT) protected static long nBuildMeasuredText( /* Non Zero */ long nativeBuilderPtr, long hintMtPtr, char[] text, boolean computeHyphenation, boolean computeLayout, + boolean computeBounds, + /** ignored */ boolean fastHyphenationMode) { return MeasuredTextBuilderNatives.nBuildMeasuredText( nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java index 32d428088..4dbe0b03b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java @@ -10,6 +10,7 @@ import android.graphics.Paint; import android.graphics.Paint.FontMetrics; import android.graphics.Paint.FontMetricsInt; import android.graphics.Rect; +import android.graphics.RectF; import androidx.annotation.ColorInt; import androidx.annotation.ColorLong; import org.robolectric.annotation.Implementation; @@ -17,6 +18,8 @@ import org.robolectric.annotation.Implements; import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; import org.robolectric.nativeruntime.PaintNatives; import org.robolectric.shadows.ShadowNativePaint.Picker; +import org.robolectric.versioning.AndroidVersions.U; +import org.robolectric.versioning.AndroidVersions.V; /** Shadow for {@link Paint} that is backed by native code */ @Implements( @@ -813,7 +816,7 @@ public class ShadowNativePaint { paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics); } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT) protected static float nGetRunCharacterAdvance( long paintPtr, char[] text, @@ -838,6 +841,32 @@ public class ShadowNativePaint { advancesIndex); } + @Implementation(minSdk = V.SDK_INT) + protected static float nGetRunCharacterAdvance( + long paintPtr, + char[] text, + int start, + int end, + int contextStart, + int contextEnd, + boolean isRtl, + int offset, + float[] advances, + int advancesIndex, + RectF drawingBounds) { + return nGetRunCharacterAdvance( + paintPtr, + text, + start, + end, + contextStart, + contextEnd, + isRtl, + offset, + advances, + advancesIndex); + } + /** Shadow picker for {@link Paint}. */ public static final class Picker extends GraphicsShadowPicker<Object> { public Picker() { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java index 0c7fcf630..a0b5d2760 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java @@ -33,6 +33,7 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link Typeface} that is backed by native code */ @Implements(value = Typeface.class, looseSignatures = true, minSdk = O, isInAndroidSdk = false) @@ -202,7 +203,7 @@ public class ShadowNativeTypeface extends ShadowTypeface { return TypefaceNatives.nativeWriteTypefaces(buffer, nativePtrs); } - @Implementation(minSdk = 10000) + @Implementation(minSdk = U.SDK_INT) protected static int nativeWriteTypefaces(ByteBuffer buffer, int position, long[] nativePtrs) { return nativeWriteTypefaces(buffer, nativePtrs); } @@ -212,7 +213,7 @@ public class ShadowNativeTypeface extends ShadowTypeface { return TypefaceNatives.nativeReadTypefaces(buffer); } - @Implementation(minSdk = 10000) + @Implementation(minSdk = U.SDK_INT) protected static long[] nativeReadTypefaces(ByteBuffer buffer, int position) { return nativeReadTypefaces(buffer); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java index 4cdfb4532..38137474f 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static org.robolectric.util.reflector.Reflector.reflector; import android.app.Activity; @@ -11,7 +12,6 @@ import android.nfc.NdefMessage; import android.nfc.NfcAdapter; import android.nfc.Tag; import android.os.Build; -import android.os.Build.VERSION_CODES; import android.os.Bundle; import java.util.Map; import org.robolectric.RuntimeEnvironment; @@ -19,6 +19,8 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; @@ -49,6 +51,27 @@ public class ShadowNfcAdapter { return reflector(NfcAdapterReflector.class).getNfcAdapter(context); } + /** Factory method for creating a mock NfcAdapter.Tag */ + public static Tag createMockTag() { + if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) { + return ReflectionHelpers.callStaticMethod( + Tag.class, + "createMockTag", + ClassParameter.from(byte[].class, new byte[0]), + ClassParameter.from(int[].class, new int[0]), + ClassParameter.from(Bundle[].class, new Bundle[0])); + + } else { + return ReflectionHelpers.callStaticMethod( + Tag.class, + "createMockTag", + ClassParameter.from(byte[].class, new byte[0]), + ClassParameter.from(int[].class, new int[0]), + ClassParameter.from(Bundle[].class, new Bundle[0]), + ClassParameter.from(long.class, 0)); + } + } + @Implementation protected void enableForegroundDispatch( Activity activity, PendingIntent intent, IntentFilter[] filters, String[][] techLists) { @@ -221,7 +244,7 @@ public class ShadowNfcAdapter { } if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { nfcAdapterReflector.setHasNfcFeature(false); - if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.TIRAMISU) { + if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) { nfcAdapterReflector.setHasBeamFeature(false); } } @@ -249,4 +272,9 @@ public class ShadowNfcAdapter { @Static NfcAdapter getNfcAdapter(Context context); } + + @ForType(Tag.class) + interface TagReflector { + Tag createMockTag(byte[] id, int[] techList, Bundle[] techListExtras, long cookie); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java index 16a7398f7..b103034a2 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java @@ -342,6 +342,17 @@ public class ShadowPaint { } @Implementation + protected final boolean isFilterBitmap() { + return (flags & Paint.FILTER_BITMAP_FLAG) == Paint.FILTER_BITMAP_FLAG; + } + + @Implementation + protected final void setFilterBitmap(boolean filterBitmap) { + this.flags = + (flags & ~Paint.FILTER_BITMAP_FLAG) | (filterBitmap ? Paint.FILTER_BITMAP_FLAG : 0); + } + + @Implementation protected PathEffect getPathEffect() { return pathEffect; } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java index d898b4309..dd66cc764 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java @@ -2,23 +2,32 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; +import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE; import static org.robolectric.shadow.api.Shadow.invokeConstructor; import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; import static org.robolectric.util.reflector.Reflector.reflector; import android.annotation.SuppressLint; import android.os.Handler; +import android.os.Parcel; import android.os.ParcelFileDescriptor; +import android.os.Parcelable; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; @@ -32,7 +41,13 @@ public class ShadowParcelFileDescriptor { // level private static final String PIPE_TMP_DIR = "ShadowParcelFileDescriptor"; private static final String PIPE_FILE_NAME = "pipe"; + private static final Map<Integer, RandomAccessFile> filesInTransitById = + Collections.synchronizedMap(new HashMap<>()); + private static final AtomicInteger NEXT_FILE_ID = new AtomicInteger(); + private RandomAccessFile file; + private int fileIdPledgedOnClose; // != 0 if 'file' was written to a Parcel. + private int lazyFileId; // != 0 if we were created from a Parcel but don't own a 'file' yet. private boolean closed; private Handler handler; private ParcelFileDescriptor.OnCloseListener onCloseListener; @@ -41,6 +56,18 @@ public class ShadowParcelFileDescriptor { @RealObject private ParcelFileDescriptor realObject; @Implementation + protected static void __staticInitializer__() { + Shadow.directInitialize(ParcelFileDescriptor.class); + ReflectionHelpers.setStaticField( + ParcelFileDescriptor.class, "CREATOR", ShadowParcelFileDescriptor.CREATOR); + } + + @Resetter + public static void reset() { + filesInTransitById.clear(); + } + + @Implementation protected void __constructor__(ParcelFileDescriptor wrapped) { invokeConstructor( ParcelFileDescriptor.class, realObject, from(ParcelFileDescriptor.class, wrapped)); @@ -50,18 +77,53 @@ public class ShadowParcelFileDescriptor { } } + static final Parcelable.Creator<ParcelFileDescriptor> CREATOR = + new Parcelable.Creator<ParcelFileDescriptor>() { + @Override + public ParcelFileDescriptor createFromParcel(Parcel source) { + int fileId = source.readInt(); + ParcelFileDescriptor result = newParcelFileDescriptor(); + ShadowParcelFileDescriptor shadowResult = Shadow.extract(result); + shadowResult.lazyFileId = fileId; + return result; + } + + @Override + public ParcelFileDescriptor[] newArray(int size) { + return new ParcelFileDescriptor[size]; + } + }; + @Implementation - protected static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException { - ParcelFileDescriptor pfd = null; + protected void writeToParcel(Parcel out, int flags) { + if (fileIdPledgedOnClose == 0) { + fileIdPledgedOnClose = (lazyFileId != 0) ? lazyFileId : NEXT_FILE_ID.incrementAndGet(); + } + out.writeInt(fileIdPledgedOnClose); + + if ((flags & PARCELABLE_WRITE_RETURN_VALUE) != 0) { + try { + close(); + } catch (IOException e) { + // Close "quietly", just like Android does. + } + } + } + + private static ParcelFileDescriptor newParcelFileDescriptor() { if (RuntimeEnvironment.getApiLevel() > JELLY_BEAN) { - pfd = new ParcelFileDescriptor(new FileDescriptor()); + return new ParcelFileDescriptor(new FileDescriptor()); } else { // In Jelly Bean, the ParcelFileDescriptor(FileDescriptor) constructor was non-public. - pfd = - ReflectionHelpers.callConstructor( - ParcelFileDescriptor.class, - ClassParameter.from(FileDescriptor.class, new FileDescriptor())); + return ReflectionHelpers.callConstructor( + ParcelFileDescriptor.class, + ClassParameter.from(FileDescriptor.class, new FileDescriptor())); } + } + + @Implementation + protected static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException { + ParcelFileDescriptor pfd = newParcelFileDescriptor(); ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(pfd); shadowParcelFileDescriptor.file = new RandomAccessFile(file, getFileMode(mode)); if ((mode & ParcelFileDescriptor.MODE_TRUNCATE) != 0) { @@ -136,10 +198,21 @@ public class ShadowParcelFileDescriptor { return createPipe(); } + private RandomAccessFile getFile() { + if (file == null && lazyFileId != 0) { + file = filesInTransitById.remove(lazyFileId); + lazyFileId = 0; + if (file == null) { + throw new FileDescriptorFromParcelUnavailableException(); + } + } + return file; + } + @Implementation protected FileDescriptor getFileDescriptor() { try { - return file.getFD(); + return getFile().getFD(); } catch (IOException e) { throw new RuntimeException(e); } @@ -148,7 +221,7 @@ public class ShadowParcelFileDescriptor { @Implementation protected long getStatSize() { try { - return file.length(); + return getFile().length(); } catch (IOException e) { // This might occur when the file object has been closed. return -1; @@ -162,7 +235,7 @@ public class ShadowParcelFileDescriptor { } try { - return ReflectionHelpers.getField(file.getFD(), "fd"); + return ReflectionHelpers.getField(getFile().getFD(), "fd"); } catch (IOException e) { throw new RuntimeException(e); } @@ -175,7 +248,21 @@ public class ShadowParcelFileDescriptor { return; } - file.close(); + if (file != null) { + if (fileIdPledgedOnClose != 0) { + // Don't actually close 'file'! Instead stash it where our Parcel reader(s) can find it. + filesInTransitById.put(fileIdPledgedOnClose, file); + fileIdPledgedOnClose = 0; + + // Replace this.file with a dummy instance to be close()d below. This leaves instances that + // have been written to Parcels and never-parceled ones in exactly the same state. + File tempFile = Files.createTempFile(null, null).toFile(); + file = new RandomAccessFile(tempFile, "rw"); + tempFile.delete(); + } + file.close(); + } + reflector(ParcelFileDescriptorReflector.class, realParcelFd).close(); closed = true; if (handler != null && onCloseListener != null) { @@ -183,6 +270,17 @@ public class ShadowParcelFileDescriptor { } } + static class FileDescriptorFromParcelUnavailableException extends RuntimeException { + FileDescriptorFromParcelUnavailableException() { + super( + "ParcelFileDescriptors created from a Parcel refer to the same content as the" + + " ParcelFileDescriptor that originally wrote it. Robolectric has the unfortunate" + + " limitation that only one of these instances can be functional at a time. Try" + + " closing the original ParcelFileDescriptor before using any duplicates created via" + + " the Parcelable API."); + } + } + @ForType(ParcelFileDescriptor.class) interface ParcelFileDescriptorReflector { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java index ee3bef016..6e85f733a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java @@ -6,6 +6,7 @@ import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; import static org.robolectric.util.reflector.Reflector.reflector; import android.os.Build; +import android.os.ConditionVariable; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -24,25 +25,30 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; import org.robolectric.config.ConfigurationRegistry; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.Scheduler; +import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; /** - * The shadow Looper for {@link LooperMode.Mode.PAUSED}. + * The shadow Looper for {@link LooperMode.Mode.PAUSED and @link + * LooperMode.Mode.INSTRUMENTATION_TEST}. * * <p>This shadow differs from the legacy {@link ShadowLegacyLooper} in the following ways:\ - Has * no connection to {@link org.robolectric.util.Scheduler}. Its APIs are standalone - The main - * looper is always paused. Posted messages are not executed unless {@link #idle()} is called. - - * Just like in real Android, each looper has its own thread, and posted tasks get executed in that + * looper is always paused in PAUSED MODE but can be unpaused in INSTRUMENTATION_TEST mode. When a + * looper is paused, posted messages to it are not executed unless {@link #idle()} is called. - Just + * like in real Android, each looper has its own thread, and posted tasks get executed in that * thread. - - There is only a single {@link SystemClock} value that all loopers read from. Unlike * legacy behavior where each {@link org.robolectric.util.Scheduler} kept their own clock value. * @@ -122,7 +128,8 @@ public final class ShadowPausedLooper extends ShadowLooper { @Override public void unPause() { - if (realLooper == Looper.getMainLooper()) { + if (realLooper == Looper.getMainLooper() + && looperMode() != LooperMode.Mode.INSTRUMENTATION_TEST) { throw new UnsupportedOperationException("main looper cannot be unpaused"); } executeOnLooper(new UnPauseRunnable()); @@ -164,7 +171,9 @@ public final class ShadowPausedLooper extends ShadowLooper { @Override public void idleIfPaused() { - idle(); + if (isPaused()) { + idle(); + } } @Override @@ -202,11 +211,12 @@ public final class ShadowPausedLooper extends ShadowLooper { // compatibility for now @Override public void runPaused(Runnable runnable) { - if (isPaused && Thread.currentThread() == realLooper.getThread()) { + if (Thread.currentThread() == realLooper.getThread()) { // just run runnable.run(); } else { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException( + "this method can only be called on " + realLooper.getThread().getName()); } } @@ -256,17 +266,76 @@ public final class ShadowPausedLooper extends ShadowLooper { } @Resetter + @SuppressWarnings("deprecation") // This is Robolectric library code public static synchronized void resetLoopers() { - // do not use looperMode() here, because its cached value might already have been reset - if (ConfigurationRegistry.get(LooperMode.Mode.class) != LooperMode.Mode.PAUSED) { - // ignore if not realistic looper + // Do not use looperMode() here, because its cached value might already have been reset + LooperMode.Mode looperMode = ConfigurationRegistry.get(LooperMode.Mode.class); + + if (looperMode == LooperMode.Mode.LEGACY) { return; } - Collection<Looper> loopersCopy = new ArrayList(loopingLoopers); - for (Looper looper : loopersCopy) { - ShadowPausedMessageQueue shadowQueue = Shadow.extract(looper.getQueue()); - shadowQueue.reset(); + createMainThreadAndLooperIfNotAlive(); + for (Looper looper : getLoopers()) { + ShadowPausedLooper shadowPausedLooper = Shadow.extract(looper); + shadowPausedLooper.resetLooperToInitialState(); + } + } + + private static synchronized void createMainThreadAndLooperIfNotAlive() { + Looper mainLooper = Looper.getMainLooper(); + + switch (ConfigurationRegistry.get(LooperMode.Mode.class)) { + case INSTRUMENTATION_TEST: + if (mainLooper == null || !mainLooper.getThread().isAlive()) { + ConditionVariable mainThreadPrepared = new ConditionVariable(); + Thread mainThread = + new Thread(String.format("SDK %d Main Thread", RuntimeEnvironment.getApiLevel())) { + @Override + public void run() { + if (mainLooper == null) { + Looper.prepareMainLooper(); + } else { + ShadowPausedMessageQueue shadowQueue = Shadow.extract(mainLooper.getQueue()); + shadowQueue.reset(); + reflector(LooperReflector.class, mainLooper).setThread(Thread.currentThread()); + reflector(LooperReflector.class).getThreadLocal().set(mainLooper); + } + mainThreadPrepared.open(); + Looper.loop(); + } + }; + mainThread.start(); + mainThreadPrepared.block(); + Thread.currentThread() + .setName(String.format("SDK %d Test Thread", RuntimeEnvironment.getApiLevel())); + } + break; + case PAUSED: + if (Looper.myLooper() == null) { + Looper.prepareMainLooper(); + } + break; + default: + throw new UnsupportedOperationException( + "Only supports INSTRUMENTATION_TEST and PAUSED LooperMode."); + } + } + + private synchronized void resetLooperToInitialState() { + // Do not use looperMode() here, because its cached value might already have been reset + LooperMode.Mode looperMode = ConfigurationRegistry.get(LooperMode.Mode.class); + + ShadowPausedMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue()); + shadowQueue.reset(); + + boolean canBeUnpaused = + !(realLooper == Looper.getMainLooper() + && looperMode != LooperMode.Mode.INSTRUMENTATION_TEST); + if (canBeUnpaused && realLooper.getThread().isAlive()) { + if (isPaused()) { + unPause(); + } } } @@ -274,7 +343,7 @@ public final class ShadowPausedLooper extends ShadowLooper { protected static void prepareMainLooper() { reflector(LooperReflector.class).prepareMainLooper(); ShadowPausedLooper pausedLooper = Shadow.extract(Looper.getMainLooper()); - pausedLooper.isPaused = true; + pausedLooper.isPaused = looperMode() == Mode.PAUSED; } @Implementation @@ -463,7 +532,7 @@ public final class ShadowPausedLooper extends ShadowLooper { runnable.run(); } } else { - if (realLooper.equals(Looper.getMainLooper())) { + if (looperMode() == LooperMode.Mode.PAUSED && realLooper.equals(Looper.getMainLooper())) { throw new UnsupportedOperationException( "main looper can only be controlled from main thread"); } @@ -549,5 +618,11 @@ public final class ShadowPausedLooper extends ShadowLooper { @Direct void loop(); + + @Accessor("mThread") + void setThread(Thread thread); + + @Accessor("sThreadLocal") + ThreadLocal<Looper> getThreadLocal(); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java index 2174acf26..100cc4364 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessage.java @@ -3,9 +3,9 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static org.robolectric.util.reflector.Reflector.reflector; -import android.os.Build; import android.os.Handler; import android.os.Message; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.LooperMode; @@ -19,25 +19,25 @@ import org.robolectric.annotation.RealObject; @Implements(value = Message.class, isInAndroidSdk = false) public class ShadowPausedMessage extends ShadowMessage { - @RealObject private Message realObject; + @RealObject private Message realMessage; @Implementation protected long getWhen() { - return reflector(MessageReflector.class, realObject).getWhen(); + return reflector(MessageReflector.class, realMessage).getWhen(); } Message internalGetNext() { - return reflector(MessageReflector.class, realObject).getNext(); + return reflector(MessageReflector.class, realMessage).getNext(); } - // TODO: reconsider this being exposed as a public method + // TODO: Reconsider this being exposed as a public method @Override @Implementation(minSdk = LOLLIPOP) public void recycleUnchecked() { - if (Build.VERSION.SDK_INT >= LOLLIPOP) { - reflector(MessageReflector.class, realObject).recycleUnchecked(); + if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) { + reflector(MessageReflector.class, realMessage).recycleUnchecked(); } else { - reflector(MessageReflector.class, realObject).recycle(); + reflector(MessageReflector.class, realMessage).recycle(); } } @@ -46,7 +46,7 @@ public class ShadowPausedMessage extends ShadowMessage { throw new UnsupportedOperationException("Not supported in PAUSED LooperMode"); } - // we could support these methods, but intentionally do not for now as its unclear what the + // We could support these methods, but intentionally do not for now as its unclear what the // use case is. @Override @@ -61,6 +61,6 @@ public class ShadowPausedMessage extends ShadowMessage { @Implementation protected Handler getTarget() { - return reflector(MessageReflector.class, realObject).getTarget(); + return reflector(MessageReflector.class, realMessage).getTarget(); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java index 5caf01642..56af8a3bb 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java @@ -461,10 +461,11 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { Message msg = getMessages(); while (msg != null) { boolean unused = msgProcessor.apply(msg.getCallback()); - ShadowMessage shadowMsg = Shadow.extract(msg); - msg.recycle(); - msg = shadowMsg.getNext(); + Message next = shadowOfMsg(msg).internalGetNext(); + shadowOfMsg(msg).recycleUnchecked(); + msg = next; } + reflector(MessageQueueReflector.class, realQueue).setMessages(null); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java index 61f3c3c0d..c7a86f7e9 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPendingIntent.java @@ -230,7 +230,7 @@ public class ShadowPendingIntent { // Copy the last intent before filling it in to avoid modifying this PendingIntent. intentsToSend = Arrays.copyOf(savedIntents, savedIntents.length); Intent lastIntentCopy = new Intent(intentsToSend[intentsToSend.length - 1]); - lastIntentCopy.fillIn(intent, 0); + lastIntentCopy.fillIn(intent, flags); intentsToSend[intentsToSend.length - 1] = lastIntentCopy; } else { intentsToSend = savedIntents; @@ -691,7 +691,9 @@ public class ShadowPendingIntent { public static void reset() { synchronized (lock) { createdIntents.clear(); + parceledPendingIntents.clear(); } + } @ForType(PendingIntent.class) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java index cbac3339e..82f616549 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java @@ -1,10 +1,9 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.O; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.robolectric.util.reflector.Reflector.reflector; -import android.app.Activity; -import android.content.Context; -import android.content.ContextWrapper; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; @@ -13,14 +12,17 @@ import android.os.Handler; import android.os.Looper; import android.view.PixelCopy; import android.view.PixelCopy.OnPixelCopyFinishedListener; +import android.view.Surface; import android.view.SurfaceView; import android.view.View; -import android.view.ViewGroup; import android.view.Window; +import android.view.WindowManagerGlobal; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowWindowManagerGlobal.WindowManagerGlobalReflector; /** * Shadow for PixelCopy that uses View.draw to create screenshots. The real PixelCopy performs a @@ -29,8 +31,19 @@ import org.robolectric.annotation.Implements; * <p>If listenerThread is backed by a paused looper, make sure to call ShadowLooper.idle() to * ensure the screenshot finishes. */ -@Implements(value = PixelCopy.class, minSdk = P) +@Implements(value = PixelCopy.class, minSdk = O) public class ShadowPixelCopy { + + @Implementation + protected static void request( + SurfaceView source, + @NonNull Bitmap dest, + @NonNull OnPixelCopyFinishedListener listener, + @NonNull Handler listenerThread) { + takeScreenshot(source, dest, null); + alertFinished(listener, listenerThread, PixelCopy.SUCCESS); + } + @Implementation protected static void request( @NonNull SurfaceView source, @@ -38,14 +51,10 @@ public class ShadowPixelCopy { @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread) { - Activity activity = getActivity(source); if (srcRect != null && srcRect.isEmpty()) { throw new IllegalArgumentException("sourceRect is empty"); } - if (activity == null) { - throw new IllegalArgumentException("SourceView was not attached to an activity"); - } - takeScreenshot(activity.getWindow(), dest, srcRect); + takeScreenshot(source, dest, srcRect); alertFinished(listener, listenerThread, PixelCopy.SUCCESS); } @@ -55,8 +64,7 @@ public class ShadowPixelCopy { @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread) { - takeScreenshot(source, dest, null); - alertFinished(listener, listenerThread, PixelCopy.SUCCESS); + request(source, null, dest, listener, listenerThread); } @Implementation @@ -69,19 +77,61 @@ public class ShadowPixelCopy { if (srcRect != null && srcRect.isEmpty()) { throw new IllegalArgumentException("sourceRect is empty"); } - takeScreenshot(source, dest, srcRect); + View view = source.getDecorView(); + Rect adjustedSrcRect = null; + if (srcRect != null) { + adjustedSrcRect = new Rect(srcRect); + int[] locationInWindow = new int[2]; + view.getLocationInWindow(locationInWindow); + // offset the srcRect by the decor view's location in the window + adjustedSrcRect.offset(-locationInWindow[0], -locationInWindow[1]); + } + takeScreenshot(view, dest, adjustedSrcRect); alertFinished(listener, listenerThread, PixelCopy.SUCCESS); } - private static void takeScreenshot(Window window, Bitmap screenshot, @Nullable Rect srcRect) { - validateBitmap(screenshot); + @Implementation + protected static void request( + @NonNull Surface source, + @Nullable Rect srcRect, + @NonNull Bitmap dest, + @NonNull OnPixelCopyFinishedListener listener, + @NonNull Handler listenerThread) { + if (srcRect != null && srcRect.isEmpty()) { + throw new IllegalArgumentException("sourceRect is empty"); + } + + View view = findViewForSurface(checkNotNull(source)); + Rect adjustedSrcRect = null; + if (srcRect != null) { + adjustedSrcRect = new Rect(srcRect); + int[] locationInSurface = ShadowView.getLocationInSurfaceCompat(view); + // offset the srcRect by the decor view's location in the surface + adjustedSrcRect.offset(-locationInSurface[0], -locationInSurface[1]); + } + takeScreenshot(view, dest, adjustedSrcRect); + alertFinished(listener, listenerThread, PixelCopy.SUCCESS); + } + + private static View findViewForSurface(Surface source) { + for (View windowView : + reflector(WindowManagerGlobalReflector.class, WindowManagerGlobal.getInstance()) + .getWindowViews()) { + ShadowViewRootImpl shadowViewRoot = Shadow.extract(windowView.getViewRootImpl()); + if (source.equals(shadowViewRoot.getSurface())) { + return windowView; + } + } + + throw new IllegalArgumentException( + "Could not find view for surface. Is it attached to a window?"); + } - // Draw the view to a bitmap in the canvas that is the size of the view itself. - View decorView = window.getDecorView(); - Bitmap bitmap = - Bitmap.createBitmap(decorView.getWidth(), decorView.getHeight(), Bitmap.Config.ARGB_8888); + private static void takeScreenshot(View view, Bitmap screenshot, @Nullable Rect srcRect) { + validateBitmap(screenshot); + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); Canvas screenshotCanvas = new Canvas(bitmap); - decorView.draw(screenshotCanvas); + view.draw(screenshotCanvas); Rect dst = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()); @@ -99,7 +149,7 @@ public class ShadowPixelCopy { listenerThread.post(() -> listener.onPixelCopyFinished(result)); } - private static void validateBitmap(Bitmap bitmap) { + private static Bitmap validateBitmap(Bitmap bitmap) { if (bitmap == null) { throw new IllegalArgumentException("Bitmap cannot be null"); } @@ -109,33 +159,6 @@ public class ShadowPixelCopy { if (!bitmap.isMutable()) { throw new IllegalArgumentException("Bitmap is immutable"); } - } - - private static Activity getActivity(Context context) { - if (context instanceof Activity) { - return (Activity) context; - } else if (context instanceof ContextWrapper) { - return getActivity(((ContextWrapper) context).getBaseContext()); - } else { - return null; - } - } - - private static Activity getActivity(View view) { - Activity activity = getActivity(view.getContext()); - if (activity != null) { - return activity; - } - - if (view instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) view; - if (viewGroup.getChildCount() > 0) { - // getActivity is known to fail if View is a DecorView such as specified via espresso's - // isRoot(). - // Make another attempt to find the activity from its first child view - return getActivity(viewGroup.getChildAt(0).getContext()); - } - } - return null; + return bitmap; } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java index db47fc850..a5191fe7c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java @@ -30,7 +30,6 @@ import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.os.WorkSource; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.time.Duration; import java.util.ArrayList; @@ -67,8 +66,8 @@ public class ShadowPowerManager { @PowerManager.LocationPowerSaveMode private int locationMode = PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF; - private List<String> rebootReasons = new ArrayList<String>(); - private Map<String, Boolean> ignoringBatteryOptimizations = new HashMap<>(); + private final List<String> rebootReasons = new ArrayList<>(); + private final Map<String, Boolean> ignoringBatteryOptimizations = new HashMap<>(); private int thermalStatus = 0; // Intentionally use Object instead of PowerManager.OnThermalStatusChangedListener to avoid @@ -97,7 +96,7 @@ public class ShadowPowerManager { } /** - * @deprecated Use {@link #setIsInteractive(boolean)} instead. + * @deprecated Use {@link #turnScreenOn(boolean)} instead. */ @Deprecated public void setIsScreenOn(boolean screenOn) { @@ -333,7 +332,7 @@ public class ShadowPowerManager { } @Implementation - protected void reboot(String reason) { + protected void reboot(@Nullable String reason) { if (RuntimeEnvironment.getApiLevel() >= R && "userspace".equals(reason) && !isRebootingUserspaceSupported()) { @@ -348,9 +347,11 @@ public class ShadowPowerManager { return rebootReasons.size(); } - /** Returns the list of reasons for each reboot, in chronological order. */ - public ImmutableList<String> getRebootReasons() { - return ImmutableList.copyOf(rebootReasons); + /** + * Returns the list of reasons for each reboot, in chronological order. May contain {@code null}. + */ + public List<String> getRebootReasons() { + return new ArrayList<>(rebootReasons); } /** Sets the value returned by {@link #isAmbientDisplayAvailable()}. */ diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java index 0c42cefd0..2d0ccef53 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScanResult.java @@ -4,6 +4,7 @@ import static android.os.Build.VERSION_CODES.P; import android.net.wifi.ScanResult; import android.os.Build; +import java.util.List; import org.robolectric.shadow.api.Shadow; public class ShadowScanResult { @@ -45,7 +46,40 @@ public class ShadowScanResult { } else { scanResult.setFlag(0); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + scanResult.informationElements = new ScanResult.InformationElement[0]; + } } return scanResult; } + + public static ScanResult newInstance( + String ssid, + String bssid, + String caps, + int level, + int frequency, + boolean is80211McRttResponder, + List<ScanResult.InformationElement> informationElements) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + ScanResult scanResult = new ScanResult(); + scanResult.SSID = ssid; + scanResult.BSSID = bssid; + scanResult.capabilities = caps; + scanResult.level = level; + scanResult.frequency = frequency; + scanResult.informationElements = + informationElements.toArray(new ScanResult.InformationElement[0]); + if (is80211McRttResponder) { + scanResult.setFlag(ScanResult.FLAG_80211mc_RESPONDER); + } else { + scanResult.setFlag(0); + } + + return scanResult; + } else { + throw new UnsupportedOperationException( + "InformationElement not available on API " + Build.VERSION.SDK_INT); + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java index c40d24e96..89b9eb6e1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java @@ -22,6 +22,7 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.util.ReflectionHelpers; +import org.robolectric.versioning.AndroidVersions.U; @Implements(SoundPool.class) public class ShadowSoundPool { @@ -62,7 +63,7 @@ public class ShadowSoundPool { return 1; } - @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + @Implementation(minSdk = U.SDK_INT) protected int _play( int soundID, float leftVolume, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowStatsManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowStatsManager.java new file mode 100644 index 000000000..61a595d76 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowStatsManager.java @@ -0,0 +1,44 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.P; + +import android.app.StatsManager; +import java.util.HashMap; +import java.util.Map; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +/** Shadow for {@link ShadowStatsManager} */ +@Implements(value = StatsManager.class, isInAndroidSdk = false, minSdk = P) +public class ShadowStatsManager { + + private static final Map<Long, byte[]> dataMap = new HashMap<>(); + private static byte[] statsMetadata = new byte[] {}; + + @Resetter + public static void reset() { + dataMap.clear(); + statsMetadata = new byte[] {}; + } + + public static void addReportData(long configKey, byte[] data) { + dataMap.put(configKey, data); + } + + public static void setStatsMetadata(byte[] metadata) { + statsMetadata = metadata; + } + + @Implementation + protected byte[] getReports(long configKey) { + byte[] data = dataMap.getOrDefault(configKey, new byte[] {}); + dataMap.remove(configKey); + return data; + } + + @Implementation + protected byte[] getStatsMetadata() { + return statsMetadata; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java index 1b239d188..27b787d96 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java @@ -32,6 +32,7 @@ import org.robolectric.util.ReflectionHelpers; public class ShadowSubscriptionManager { private boolean readPhoneStatePermission = true; + private boolean readPhoneNumbersPermission = true; public static final int INVALID_PHONE_INDEX = ReflectionHelpers.getStaticField(SubscriptionManager.class, "INVALID_PHONE_INDEX"); @@ -178,9 +179,12 @@ public class ShadowSubscriptionManager { /** * Returns subscription that were set via {@link #setActiveSubscriptionInfoList} if it can find * one with the specified id or null if none found. + * + * <p>An exception will be thrown if the READ_PHONE_STATE permission has not been granted. */ @Implementation(minSdk = LOLLIPOP_MR1) protected SubscriptionInfo getActiveSubscriptionInfo(int subId) { + checkReadPhoneStatePermission(); if (subscriptionList == null) { return null; } @@ -417,13 +421,31 @@ public class ShadowSubscriptionManager { } /** + * When set to false methods requiring {@link android.Manifest.permission.READ_PHONE_NUMBERS} + * permission will throw a {@link SecurityException}. By default it's set to true for backwards + * compatibility. + */ + public void setReadPhoneNumbersPermission(boolean readPhoneNumbersPermission) { + this.readPhoneNumbersPermission = readPhoneNumbersPermission; + } + + private void checkReadPhoneNumbersPermission() { + if (!readPhoneNumbersPermission) { + throw new SecurityException(); + } + } + + /** * Returns the phone number for the given {@code subscriptionId}, or an empty string if not * available. * * <p>The phone number can be set by {@link #setPhoneNumber(int, String)} + * + * <p>An exception will be thrown if the READ_PHONE_NUMBERS permission has not been granted. */ @Implementation(minSdk = TIRAMISU) protected String getPhoneNumber(int subscriptionId) { + checkReadPhoneNumbersPermission(); return phoneNumberMap.getOrDefault(subscriptionId, ""); } @@ -521,6 +543,11 @@ public class ShadowSubscriptionManager { return this; } + public SubscriptionInfoBuilder setIsOpportunistic(boolean isOpportunistic) { + ReflectionHelpers.setField(subscriptionInfo, "mIsOpportunistic", isOpportunistic); + return this; + } + public SubscriptionInfoBuilder setMnc(String mnc) { if (VERSION.SDK_INT < Q) { ReflectionHelpers.setField(subscriptionInfo, "mMnc", Integer.valueOf(mnc)); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java index 49a93bcfe..b279892c9 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java @@ -19,6 +19,7 @@ import org.robolectric.annotation.Resetter; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for {@link android.view.SurfaceControl} */ @Implements(value = SurfaceControl.class, isInAndroidSdk = false, minSdk = JELLY_BEAN_MR2) @@ -83,7 +84,7 @@ public class ShadowSurfaceControl { void initializeNativeObject() { surfaceControlReflector.setNativeObject(nativeObject.incrementAndGet()); - if (RuntimeEnvironment.getApiLevel() >= ShadowBuild.UPSIDE_DOWN_CAKE) { + if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) { surfaceControlReflector.setFreeNativeResources(() -> {}); } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java index ef52021de..275266ad9 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelecomManager.java @@ -78,6 +78,7 @@ public class ShadowTelecomManager { private final LinkedHashMap<PhoneAccountHandle, PhoneAccount> accounts = new LinkedHashMap<>(); private final LinkedHashMap<PhoneAccountHandle, String> voicemailNumbers = new LinkedHashMap<>(); + private final LinkedHashMap<PhoneAccountHandle, String> line1Numbers = new LinkedHashMap<>(); private final List<IncomingCallRecord> incomingCalls = new ArrayList<>(); private final List<OutgoingCallRecord> outgoingCalls = new ArrayList<>(); @@ -91,6 +92,9 @@ public class ShadowTelecomManager { private boolean isInCall; private boolean ttySupported; private PhoneAccountHandle userSelectedOutgoingPhoneAccount; + private boolean readPhoneStatePermission = true; + private boolean callPhonePermission = true; + private boolean handleMmiValue = false; public CallRequestMode getCallRequestMode() { return callRequestMode; @@ -168,6 +172,7 @@ public class ShadowTelecomManager { @Implementation(minSdk = M) protected List<PhoneAccountHandle> getCallCapablePhoneAccounts() { + checkReadPhoneStatePermission(); return this.getCallCapablePhoneAccounts(false); } @@ -216,6 +221,7 @@ public class ShadowTelecomManager { @Implementation protected PhoneAccount getPhoneAccount(PhoneAccountHandle account) { + checkReadPhoneStatePermission(); return accounts.get(account); } @@ -325,7 +331,12 @@ public class ShadowTelecomManager { @Implementation(minSdk = LOLLIPOP_MR1) protected String getLine1Number(PhoneAccountHandle accountHandle) { - return null; + checkReadPhoneStatePermission(); + return line1Numbers.get(accountHandle); + } + + public void setLine1Number(PhoneAccountHandle accountHandle, String number) { + line1Numbers.put(accountHandle, number); } /** Sets the return value for {@link TelecomManager#isInCall}. */ @@ -387,6 +398,7 @@ public class ShadowTelecomManager { @Implementation protected boolean isTtySupported() { + checkReadPhoneStatePermission(); return ttySupported; } @@ -481,6 +493,7 @@ public class ShadowTelecomManager { @Implementation(minSdk = M) protected void placeCall(Uri address, Bundle extras) { + checkCallPhonePermission(); OutgoingCallRecord call = new OutgoingCallRecord(address, extras); outgoingCalls.add(call); @@ -592,14 +605,18 @@ public class ShadowTelecomManager { ServiceController.of(ReflectionHelpers.callConstructor(clazz), null).create().get()); } + public void setHandleMmiValue(boolean handleMmiValue) { + this.handleMmiValue = handleMmiValue; + } + @Implementation protected boolean handleMmi(String dialString) { - return false; + return handleMmiValue; } @Implementation(minSdk = M) protected boolean handleMmi(String dialString, PhoneAccountHandle accountHandle) { - return false; + return handleMmiValue; } @Implementation(minSdk = LOLLIPOP_MR1) @@ -700,6 +717,36 @@ public class ShadowTelecomManager { } } + /** + * When set to false methods requiring {@link android.Manifest.permission.READ_PHONE_STATE} + * permission will throw a {@link SecurityException}. By default it's set to true for backwards + * compatibility. + */ + public void setReadPhoneStatePermission(boolean readPhoneStatePermission) { + this.readPhoneStatePermission = readPhoneStatePermission; + } + + private void checkReadPhoneStatePermission() { + if (!readPhoneStatePermission) { + throw new SecurityException(); + } + } + + /** + * When set to false methods requiring {@link android.Manifest.permission.CALL_PHONE} permission + * will throw a {@link SecurityException}. By default it's set to true for backwards + * compatibility. + */ + public void setCallPhonePermission(boolean callPhonePermission) { + this.callPhonePermission = callPhonePermission; + } + + private void checkCallPhonePermission() { + if (!callPhonePermission) { + throw new SecurityException(); + } + } + /** Details about an incoming call request made via {@link TelecomManager#addNewIncomingCall}. */ public static class IncomingCallRecord extends CallRecord { private boolean isHandled = false; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java index 048abf03b..9ec00a58d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java @@ -55,6 +55,7 @@ import android.telephony.VisualVoicemailSmsFilterSettings; import android.telephony.emergency.EmergencyNumber; import android.text.TextUtils; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.google.common.base.Ascii; import com.google.common.base.Preconditions; @@ -95,20 +96,23 @@ public class ShadowTelephonyManager { private final Map<PhoneAccountHandle, Uri> voicemailRingtoneUriMap = new HashMap<>(); private final Map<PhoneAccountHandle, TelephonyManager> phoneAccountToTelephonyManagers = new HashMap<>(); + private final Map<PhoneAccountHandle, Integer> phoneAccountHandleSubscriptionId = new HashMap<>(); private PhoneStateListener lastListener; private /*TelephonyCallback*/ Object lastTelephonyCallback; private int lastEventFlags; private String deviceId; + private String deviceSoftwareVersion; private String imei; private String meid; private String groupIdLevel1; private String networkOperatorName = ""; private String networkCountryIso; private String networkOperator = ""; + private String networkSpecifier = ""; private Locale simLocale; - private String simOperator; + private String simOperator = ""; private String simOperatorName; private String simSerialNumber; private boolean readPhoneStatePermission = true; @@ -122,6 +126,7 @@ public class ShadowTelephonyManager { private CellLocation cellLocation = null; private int callState = CALL_STATE_IDLE; private int dataState = TelephonyManager.DATA_DISCONNECTED; + private int dataActivity = TelephonyManager.DATA_ACTIVITY_NONE; private String incomingPhoneNumber = null; private boolean isSmsCapable = true; private boolean voiceCapable = true; @@ -141,12 +146,14 @@ public class ShadowTelephonyManager { private int carrierIdFromSimMccMnc; private String subscriberId; private /*UiccSlotInfo[]*/ Object uiccSlotInfos; - private /*UiccCardInfo[]*/ Object uiccCardsInfo; + private /*UiccCardInfo[]*/ Object uiccCardsInfo = new ArrayList<>(); private String visualVoicemailPackageName = null; private SignalStrength signalStrength; private boolean dataEnabled = false; private final Set<Integer> dataDisabledReasons = new HashSet<>(); private boolean isRttSupported; + private boolean isTtyModeSupported; + private final SparseBooleanArray subIdToHasCarrierPrivileges = new SparseBooleanArray(); private final List<String> sentDialerSpecialCodes = new ArrayList<>(); private boolean hearingAidCompatibilitySupported = false; private int requestCellInfoUpdateErrorCode = 0; @@ -167,6 +174,8 @@ public class ShadowTelephonyManager { */ private Object callback; + private /*PhoneCapability*/ Object phoneCapability; + { resetSimStates(); resetSimCountryIsos(); @@ -205,6 +214,16 @@ public class ShadowTelephonyManager { this.callback = callback; } + public void setPhoneCapability(/*PhoneCapability*/ Object phoneCapability) { + this.phoneCapability = phoneCapability; + } + + @Implementation(minSdk = S) + @HiddenApi + public /*PhoneCapability*/ Object getPhoneCapability() { + return phoneCapability; + } + @Implementation protected void listen(PhoneStateListener listener, int flags) { lastListener = listener; @@ -324,6 +343,24 @@ public class ShadowTelephonyManager { this.dataState = dataState; } + /** + * Data activity may be specified via {@link #setDataActivity(int)}. If no override is set, this + * defaults to {@link TelephonyManager#DATA_ACTIVITY_NONE}. + */ + @Implementation + protected int getDataActivity() { + return dataActivity; + } + + /** + * Sets the value to be returned by calls to {@link #getDataActivity()}. This <b>should</b> + * correspond to one of the {@code DATA_ACTIVITY_*} constants defined on {@link TelephonyManager}, + * but this is not enforced. + */ + public void setDataActivity(int dataActivity) { + this.dataActivity = dataActivity; + } + @Implementation protected String getDeviceId() { checkReadPhoneStatePermission(); @@ -334,6 +371,16 @@ public class ShadowTelephonyManager { deviceId = newDeviceId; } + @Implementation + protected String getDeviceSoftwareVersion() { + checkReadPhoneStatePermission(); + return deviceSoftwareVersion; + } + + public void setDeviceSoftwareVersion(String newDeviceSoftwareVersion) { + deviceSoftwareVersion = newDeviceSoftwareVersion; + } + @Implementation(minSdk = LOLLIPOP_MR1) public void setNetworkOperatorName(String networkOperatorName) { this.networkOperatorName = networkOperatorName; @@ -422,6 +469,15 @@ public class ShadowTelephonyManager { return networkOperator; } + public void setNetworkSpecifier(String networkSpecifier) { + this.networkSpecifier = networkSpecifier; + } + + @Implementation(minSdk = O) + protected String getNetworkSpecifier() { + return networkSpecifier; + } + @Implementation protected String getSimOperator() { return simOperator; @@ -892,6 +948,7 @@ public class ShadowTelephonyManager { */ @Implementation(minSdk = M) protected String getDeviceId(int slot) { + checkReadPhoneStatePermission(); return slotIndexToDeviceId.get(slot); } @@ -1095,6 +1152,16 @@ public class ShadowTelephonyManager { this.subscriberId = subscriberId; } + @Implementation(minSdk = R) + protected int getSubscriptionId(PhoneAccountHandle handle) { + checkReadPhoneStatePermission(); + return phoneAccountHandleSubscriptionId.get(handle); + } + + public void setPhoneAccountHandleSubscriptionId(PhoneAccountHandle handle, int subscriptionId) { + phoneAccountHandleSubscriptionId.put(handle, subscriptionId); + } + /** Returns the value set by {@link #setVisualVoicemailPackageName(String)}. */ @Implementation(minSdk = O) protected String getVisualVoicemailPackageName() { @@ -1280,6 +1347,41 @@ public class ShadowTelephonyManager { } /** + * Implementation for {@link TelephonyManager#isTtyModeSupported}. + * + * @return False by default, unless set with {@link #setTtyModeSupported(boolean)}. + */ + @Implementation(minSdk = Build.VERSION_CODES.M) + protected boolean isTtyModeSupported() { + checkReadPhoneStatePermission(); + return isTtyModeSupported; + } + + /** Sets the value to be returned by {@link #isTtyModeSupported()} */ + public void setTtyModeSupported(boolean isTtyModeSupported) { + this.isTtyModeSupported = isTtyModeSupported; + } + + /** + * @return False by default, unless set with {@link #setHasCarrierPrivileges(int, boolean)}. + */ + @Implementation(minSdk = Build.VERSION_CODES.N) + @HiddenApi + protected boolean hasCarrierPrivileges(int subId) { + return subIdToHasCarrierPrivileges.get(subId); + } + + public void setHasCarrierPrivileges(boolean hasCarrierPrivileges) { + int subId = ReflectionHelpers.callInstanceMethod(realTelephonyManager, "getSubId"); + setHasCarrierPrivileges(subId, hasCarrierPrivileges); + } + + /** Sets the {@code hasCarrierPrivileges} for the given {@code subId}. */ + public void setHasCarrierPrivileges(int subId, boolean hasCarrierPrivileges) { + subIdToHasCarrierPrivileges.put(subId, hasCarrierPrivileges); + } + + /** * Implementation for {@link TelephonyManager#sendDialerSpecialCode(String)}. * * @param inputCode special code to be sent. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java index e442a2396..f2c06efd2 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTextUtils.java @@ -6,9 +6,7 @@ import android.text.TextUtils.TruncateAt; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -/** - * Implement {@lint TextUtils#ellipsize} by truncating the text. - */ +/** Implement {@link TextUtils#ellipsize} by truncating the text. */ @SuppressWarnings({"UnusedDeclaration"}) @Implements(TextUtils.class) public class ShadowTextUtils { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java index 56b7eb08f..e1a5edc64 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTimeManager.java @@ -1,25 +1,23 @@ package org.robolectric.shadows; -import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; -import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; - import android.annotation.SystemApi; import android.app.time.Capabilities; import android.app.time.Capabilities.CapabilityState; import android.app.time.ExternalTimeSuggestion; -import android.app.time.LocationTimeZoneAlgorithmStatus; -import android.app.time.TelephonyTimeZoneAlgorithmStatus; import android.app.time.TimeManager; import android.app.time.TimeZoneCapabilities; import android.app.time.TimeZoneCapabilitiesAndConfig; import android.app.time.TimeZoneConfiguration; -import android.app.time.TimeZoneDetectorStatus; import android.os.Build.VERSION_CODES; import android.os.UserHandle; import java.util.Objects; import java.util.concurrent.Executor; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.versioning.AndroidVersions.U; /** Shadow for internal Android {@code TimeManager} class introduced in S. */ @Implements(value = TimeManager.class, minSdk = VERSION_CODES.S, isInAndroidSdk = false) @@ -28,21 +26,7 @@ public class ShadowTimeManager { public static final String CONFIGURE_GEO_DETECTION_CAPABILITY = "configure_geo_detection_capability"; - private TimeZoneCapabilities timeZoneCapabilities = - new TimeZoneCapabilities.Builder(UserHandle.CURRENT) - .setConfigureAutoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED) - .setUseLocationEnabled(true) - .setConfigureGeoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(Capabilities.CAPABILITY_POSSESSED) - .build(); - - private TimeZoneDetectorStatus detectorStatus = - new TimeZoneDetectorStatus( - DETECTOR_STATUS_RUNNING, - new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING), - new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, - LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null, - LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null)); + private TimeZoneCapabilities timeZoneCapabilities = getTimeZoneCapabilities(); private TimeZoneConfiguration timeZoneConfiguration; @@ -66,11 +50,52 @@ public class ShadowTimeManager { @Implementation @SystemApi - protected TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig() { + protected TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig() + throws ClassNotFoundException { Objects.requireNonNull(timeZoneConfiguration, "timeZoneConfiguration was not set"); - return new TimeZoneCapabilitiesAndConfig( - detectorStatus, timeZoneCapabilities, timeZoneConfiguration); + if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) { + Object telephonyAlgoStatus = + ReflectionHelpers.callConstructor( + Class.forName("android.app.time.TelephonyTimeZoneAlgorithmStatus"), + ClassParameter.from(int.class, 3)); + Object locationAlgoStatus = + ReflectionHelpers.callConstructor( + Class.forName("android.app.time.LocationTimeZoneAlgorithmStatus"), + ClassParameter.from(int.class, 3), + ClassParameter.from(int.class, 3), + ClassParameter.from( + Class.forName("android.service.timezone.TimeZoneProviderStatus"), null), + ClassParameter.from(int.class, 3), + ClassParameter.from( + Class.forName("android.service.timezone.TimeZoneProviderStatus"), null)); + + Object timeZoneDetectorStatus = + ReflectionHelpers.callConstructor( + Class.forName("android.app.time.TimeZoneDetectorStatus"), + ClassParameter.from(int.class, 0), + ClassParameter.from( + Class.forName("android.app.time.TelephonyTimeZoneAlgorithmStatus"), + telephonyAlgoStatus), + ClassParameter.from( + Class.forName("android.app.time.LocationTimeZoneAlgorithmStatus"), + locationAlgoStatus)); + return ReflectionHelpers.callConstructor( + TimeZoneCapabilitiesAndConfig.class, + ClassParameter.from( + Class.forName("android.app.time.TimeZoneDetectorStatus"), timeZoneDetectorStatus), + ClassParameter.from( + Class.forName("android.app.time.TimeZoneCapabilities"), timeZoneCapabilities), + ClassParameter.from( + Class.forName("android.app.time.TimeZoneConfiguration"), timeZoneConfiguration)); + } else { + return ReflectionHelpers.callConstructor( + TimeZoneCapabilitiesAndConfig.class, + ClassParameter.from( + Class.forName("android.app.time.TimeZoneCapabilities"), timeZoneCapabilities), + ClassParameter.from( + Class.forName("android.app.time.TimeZoneConfiguration"), timeZoneConfiguration)); + } } @Implementation @@ -89,4 +114,29 @@ public class ShadowTimeManager { @Implementation protected void suggestExternalTime(ExternalTimeSuggestion timeSuggestion) {} + + private TimeZoneCapabilities getTimeZoneCapabilities() { + TimeZoneCapabilities.Builder timeZoneCapabilitiesBuilder = + new TimeZoneCapabilities.Builder(UserHandle.CURRENT) + .setConfigureAutoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED) + .setConfigureGeoDetectionEnabledCapability(Capabilities.CAPABILITY_POSSESSED); + + if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) { + ReflectionHelpers.callInstanceMethod( + timeZoneCapabilitiesBuilder, + "setUseLocationEnabled", + ClassParameter.from(boolean.class, true)); + ReflectionHelpers.callInstanceMethod( + timeZoneCapabilitiesBuilder, + "setSetManualTimeZoneCapability", + ClassParameter.from(int.class, Capabilities.CAPABILITY_POSSESSED)); + return timeZoneCapabilitiesBuilder.build(); + } else { + ReflectionHelpers.callInstanceMethod( + timeZoneCapabilitiesBuilder, + "setSuggestManualTimeZoneCapability", + ClassParameter.from(int.class, Capabilities.CAPABILITY_POSSESSED)); + return timeZoneCapabilitiesBuilder.build(); + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java index 73575c813..dd43e8165 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java @@ -10,6 +10,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Sets.newConcurrentHashSet; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -26,7 +27,6 @@ import android.graphics.Paint; import android.graphics.Point; import android.os.Build; import android.os.IBinder; -import android.os.Looper; import android.provider.Settings; import android.view.Display; import android.view.InputEvent; @@ -41,11 +41,12 @@ import androidx.test.runner.lifecycle.ActivityLifecycleMonitor; import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; import androidx.test.runner.lifecycle.Stage; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; @@ -88,51 +89,68 @@ public class ShadowUiAutomation { @Implementation protected boolean setRotation(int rotation) { - if (rotation == UiAutomation.ROTATION_FREEZE_CURRENT - || rotation == UiAutomation.ROTATION_UNFREEZE) { - return true; - } - Display display = ShadowDisplay.getDefaultDisplay(); - int currentRotation = display.getRotation(); - boolean isRotated = - (rotation == ROTATION_FREEZE_0 || rotation == ROTATION_FREEZE_180) - != (currentRotation == ROTATION_FREEZE_0 || currentRotation == ROTATION_FREEZE_180); - shadowOf(display).setRotation(rotation); - if (isRotated) { - int currentOrientation = Resources.getSystem().getConfiguration().orientation; - String rotationQualifier = - "+" + (currentOrientation == Configuration.ORIENTATION_PORTRAIT ? "land" : "port"); - ShadowDisplayManager.changeDisplay(display.getDisplayId(), rotationQualifier); - RuntimeEnvironment.setQualifiers(rotationQualifier); - } - return true; + AtomicBoolean result = new AtomicBoolean(false); + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + if (rotation == UiAutomation.ROTATION_FREEZE_CURRENT + || rotation == UiAutomation.ROTATION_UNFREEZE) { + result.set(true); + return; + } + Display display = ShadowDisplay.getDefaultDisplay(); + int currentRotation = display.getRotation(); + boolean isRotated = + (rotation == ROTATION_FREEZE_0 || rotation == ROTATION_FREEZE_180) + != (currentRotation == ROTATION_FREEZE_0 + || currentRotation == ROTATION_FREEZE_180); + shadowOf(display).setRotation(rotation); + if (isRotated) { + int currentOrientation = Resources.getSystem().getConfiguration().orientation; + String rotationQualifier = + "+" + (currentOrientation == Configuration.ORIENTATION_PORTRAIT ? "land" : "port"); + ShadowDisplayManager.changeDisplay(display.getDisplayId(), rotationQualifier); + RuntimeEnvironment.setQualifiers(rotationQualifier); + } + result.set(true); + }); + return result.get(); } @Implementation protected void throwIfNotConnectedLocked() {} @Implementation - protected Bitmap takeScreenshot() { + protected Bitmap takeScreenshot() throws Exception { if (!ShadowView.useRealGraphics()) { return null; } - Point displaySize = new Point(); - ShadowDisplay.getDefaultDisplay().getRealSize(displaySize); - Bitmap screenshot = Bitmap.createBitmap(displaySize.x, displaySize.y, Bitmap.Config.ARGB_8888); - Canvas screenshotCanvas = new Canvas(screenshot); - Paint paint = new Paint(); - for (Root root : getViewRoots().reverse()) { - View rootView = root.getRootView(); - if (rootView.getWidth() <= 0 || rootView.getHeight() <= 0) { - continue; - } - Bitmap window = - Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888); - Canvas windowCanvas = new Canvas(window); - rootView.draw(windowCanvas); - screenshotCanvas.drawBitmap(window, root.params.x, root.params.y, paint); - } - return screenshot; + + FutureTask<Bitmap> screenshotTask = + new FutureTask<>( + () -> { + Point displaySize = new Point(); + ShadowDisplay.getDefaultDisplay().getRealSize(displaySize); + Bitmap screenshot = + Bitmap.createBitmap(displaySize.x, displaySize.y, Bitmap.Config.ARGB_8888); + Canvas screenshotCanvas = new Canvas(screenshot); + Paint paint = new Paint(); + for (Root root : getViewRoots().reverse()) { + View rootView = root.getRootView(); + if (rootView.getWidth() <= 0 || rootView.getHeight() <= 0) { + continue; + } + Bitmap window = + Bitmap.createBitmap( + rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888); + Canvas windowCanvas = new Canvas(window); + rootView.draw(windowCanvas); + screenshotCanvas.drawBitmap(window, root.params.x, root.params.y, paint); + } + return screenshot; + }); + + ShadowInstrumentation.runOnMainSyncNoIdle(screenshotTask); + return screenshotTask.get(); } /** @@ -141,14 +159,18 @@ public class ShadowUiAutomation { * UiAutomation} API, this method is provided for backwards compatibility with SDK < 18. */ public static boolean injectInputEvent(InputEvent event) { - checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!"); - if (event instanceof MotionEvent) { - return injectMotionEvent((MotionEvent) event); - } else if (event instanceof KeyEvent) { - return injectKeyEvent((KeyEvent) event); - } else { - throw new IllegalArgumentException("Unrecognized event type: " + event); - } + AtomicBoolean result = new AtomicBoolean(false); + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + if (event instanceof MotionEvent) { + result.set(injectMotionEvent((MotionEvent) event)); + } else if (event instanceof KeyEvent) { + result.set(injectKeyEvent((KeyEvent) event)); + } else { + throw new IllegalArgumentException("Unrecognized event type: " + event); + } + }); + return result.get(); } @Implementation @@ -251,12 +273,15 @@ public class ShadowUiAutomation { } private static Set<IBinder> getStartedActivityTokens() { - ActivityLifecycleMonitor monitor = ActivityLifecycleMonitorRegistry.getInstance(); - return ImmutableSet.<Activity>builder() - .addAll(monitor.getActivitiesInStage(Stage.STARTED)) - .addAll(monitor.getActivitiesInStage(Stage.RESUMED)) - .build() - .stream() + Set<Activity> startedActivities = newConcurrentHashSet(); + ShadowInstrumentation.runOnMainSyncNoIdle( + () -> { + ActivityLifecycleMonitor monitor = ActivityLifecycleMonitorRegistry.getInstance(); + startedActivities.addAll(monitor.getActivitiesInStage(Stage.STARTED)); + startedActivities.addAll(monitor.getActivitiesInStage(Stage.RESUMED)); + }); + + return startedActivities.stream() .map(activity -> activity.getWindow().getDecorView().getApplicationWindowToken()) .collect(toSet()); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java index 4eb7f180c..a1bb4c1c9 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java @@ -80,6 +80,8 @@ public class ShadowUserManager { private static boolean isMultiUserSupported = false; private static boolean isHeadlessSystemUserMode = false; + private final Object lock = new Object(); + @RealObject private UserManager realObject; private UserManagerState userManagerState; private Boolean managedProfile; @@ -484,8 +486,10 @@ public class ShadowUserManager { @Implementation(minSdk = LOLLIPOP) protected boolean hasUserRestriction(String restrictionKey, UserHandle userHandle) { - Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier()); - return bundle != null && bundle.getBoolean(restrictionKey); + synchronized (lock) { + Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier()); + return bundle != null && bundle.getBoolean(restrictionKey); + } } /** @@ -496,7 +500,9 @@ public class ShadowUserManager { @Implementation(minSdk = JELLY_BEAN_MR2) protected void setUserRestriction(String key, boolean value, UserHandle userHandle) { Bundle bundle = getUserRestrictionsForUser(userHandle); - bundle.putBoolean(key, value); + synchronized (lock) { + bundle.putBoolean(key, value); + } } @Implementation(minSdk = JELLY_BEAN_MR2) @@ -524,12 +530,14 @@ public class ShadowUserManager { } private Bundle getUserRestrictionsForUser(UserHandle userHandle) { - Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier()); - if (bundle == null) { - bundle = new Bundle(); - userManagerState.userRestrictions.put(userHandle.getIdentifier(), bundle); + synchronized (lock) { + Bundle bundle = userManagerState.userRestrictions.get(userHandle.getIdentifier()); + if (bundle == null) { + bundle = new Bundle(); + userManagerState.userRestrictions.put(userHandle.getIdentifier(), bundle); + } + return bundle; } - return bundle; } /** diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java index 4a9848dd5..30da1ba7d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVMRuntime.java @@ -5,6 +5,7 @@ import static android.os.Build.VERSION_CODES.Q; import android.annotation.TargetApi; import dalvik.system.VMRuntime; +import java.lang.ref.WeakReference; import java.lang.reflect.Array; import javax.annotation.Nullable; import org.robolectric.annotation.Implementation; @@ -15,7 +16,7 @@ import org.robolectric.res.android.NativeObjRegistry; @Implements(value = VMRuntime.class, isInAndroidSdk = false) public class ShadowVMRuntime { - private final NativeObjRegistry<Object> nativeObjRegistry = + private final NativeObjRegistry<WeakReference<Object>> nativeObjRegistry = new NativeObjRegistry<>("VRRuntime.nativeObjectRegistry"); // There actually isn't any android JNI code to call through to in Robolectric due to // cross-platform compatibility issues. We default to a reasonable value that reflects the devices @@ -42,7 +43,7 @@ public class ShadowVMRuntime { */ @Implementation public long addressOf(Object obj) { - return nativeObjRegistry.register(obj); + return nativeObjRegistry.register(new WeakReference<>(obj)); } /** @@ -50,7 +51,7 @@ public class ShadowVMRuntime { */ public @Nullable Object getObjectForAddress(long address) { - return nativeObjRegistry.getNativeObject(address); + return nativeObjRegistry.getNativeObject(address).get(); } /** diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java index 5d4f329d1..f19492496 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java @@ -20,6 +20,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; @@ -55,6 +56,7 @@ import org.robolectric.annotation.ReflectorObject; import org.robolectric.annotation.Resetter; import org.robolectric.config.ConfigurationRegistry; import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowViewRootImpl.ViewRootImplReflector; import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.TimeUtils; import org.robolectric.util.reflector.Accessor; @@ -149,6 +151,22 @@ public class ShadowView { return shadowView.innerText(); } + static int[] getLocationInSurfaceCompat(View view) { + int[] locationInSurface = new int[2]; + if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { + view.getLocationInSurface(locationInSurface); + } else { + view.getLocationInWindow(locationInSurface); + Rect surfaceInsets = + reflector(ViewRootImplReflector.class, view.getViewRootImpl()) + .getWindowAttributes() + .surfaceInsets; + locationInSurface[0] += surfaceInsets.left; + locationInSurface[1] += surfaceInsets.top; + } + return locationInSurface; + } + // Only override up to kitkat, while this version exists after kitkat it just calls through to the // __constructor__(Context, AttributeSet, int, int) variant below. @Implementation(maxSdk = KITKAT) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java index 2dd2e9a9e..9dd19321b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java @@ -18,6 +18,7 @@ import android.view.Display; import android.view.HandlerActionQueue; import android.view.IWindowSession; import android.view.InsetsState; +import android.view.Surface; import android.view.SurfaceControl; import android.view.View; import android.view.ViewRootImpl; @@ -306,6 +307,10 @@ public class ShadowViewRootImpl { } } + Surface getSurface() { + return reflector(ViewRootImplReflector.class, realObject).getSurface(); + } + /** Reflector interface for {@link ViewRootImpl}'s internals. */ @ForType(ViewRootImpl.class) protected interface ViewRootImplReflector { @@ -345,6 +350,12 @@ public class ShadowViewRootImpl { @Accessor("mSurfaceControl") SurfaceControl getSurfaceControl(); + @Accessor("mSurface") + Surface getSurface(); + + @Accessor("mWindowAttributes") + WindowManager.LayoutParams getWindowAttributes(); + // <= JELLY_BEAN void dispatchResized( int w, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java index 2d4ac3b90..d612539a8 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVoiceInteractionSession.java @@ -1,17 +1,26 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static org.robolectric.util.ReflectionHelpers.callConstructor; import android.app.Dialog; +import android.app.VoiceInteractor; +import android.app.VoiceInteractor.PickOptionRequest.Option; import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; import android.service.voice.VoiceInteractionSession; +import android.service.voice.VoiceInteractionSession.CommandRequest; +import android.service.voice.VoiceInteractionSession.Request; +import com.android.internal.app.IVoiceInteractorCallback; +import com.android.internal.app.IVoiceInteractorRequest; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; @@ -127,11 +136,79 @@ public class ShadowVoiceInteractionSession { startVoiceActivityException = exception; } + /** + * Simulates the creation of the {@link VoiceInteractionSession.CommandRequest} related to the + * provided {@link VoiceInteractor.CommandRequest}, as if it was being created by the framework. + * The method calls {@link VoiceInteractionSession#onRequestCommand(CommandRequest)} with newly + * created {@link VoiceInteractionSession.CommandRequest}. + * + * @param commandRequest: Command request sent by a third-party application. + * @param packageName: Package name of the application that initiated the request. + * @param uid: User ID of the application that initiated the request. + * @return newly created {@link VoiceInteractionSession.CommandRequest} + */ + public CommandRequest sendCommandRequest( + @Nonnull VoiceInteractor.CommandRequest commandRequest, + @Nonnull String packageName, + int uid) { + String command = ReflectionHelpers.getField(commandRequest, "mCommand"); + Bundle extras = ReflectionHelpers.getField(commandRequest, "mArgs"); + + IVoiceInteractorCallback callback = new ShadowVoiceInteractorCallback(commandRequest); + + CommandRequest internalCommandRequest = + createCommandRequest(packageName, uid, callback, command, extras); + realSession.onRequestCommand(internalCommandRequest); + return internalCommandRequest; + } + + /** + * Creates the {@link VoiceInteractionSession.CommandRequest}. + * + * @param packageName: Package name of the application that initiated the request. + * @param uid: User ID of the application that initiated the request. + * @param callback: IVoiceInteractorCallback. + * @param command: RequestCommand command. + * @param extras: Additional extra information that was supplied as part of the request. + * @return created {@link VoiceInteractionSession.CommandRequest}. + */ + private CommandRequest createCommandRequest( + @Nonnull String packageName, + int uid, + @Nonnull IVoiceInteractorCallback callback, + @Nonnull String command, + @Nonnull Bundle extras) { + CommandRequest commandRequest = + callConstructor( + CommandRequest.class, + ClassParameter.from(String.class, packageName), + ClassParameter.from(int.class, uid), + ClassParameter.from(IVoiceInteractorCallback.class, callback), + ClassParameter.from(VoiceInteractionSession.class, realSession), + ClassParameter.from(String.class, command), + ClassParameter.from(Bundle.class, extras)); + ReflectionHelpers.callInstanceMethod( + realSession, "addRequest", ClassParameter.from(Request.class, commandRequest)); + return commandRequest; + } + // Extends com.android.internal.app.IVoiceInteractionManagerService.Stub private class FakeVoiceInteractionManagerService { + // Removed in Android U // @Override public boolean showSessionFromSession(IBinder token, Bundle args, int flags) { + return showSessionFromSessionImpl(args, flags); + } + + // Added in Android U + // @Override + public boolean showSessionFromSession( + IBinder token, Bundle args, int flags, String attributionTag) { + return showSessionFromSessionImpl(args, flags); + } + + private boolean showSessionFromSessionImpl(Bundle args, int flags) { try { Class<?> callbackClass = Class.forName("com.android.internal.app.IVoiceInteractionSessionShowCallback"); @@ -177,4 +254,48 @@ public class ShadowVoiceInteractionSession { isFinishing = true; } } + + private static class ShadowVoiceInteractorCallback implements IVoiceInteractorCallback { + private final VoiceInteractor.CommandRequest commandRequest; + + ShadowVoiceInteractorCallback(VoiceInteractor.CommandRequest commandRequest) { + this.commandRequest = commandRequest; + } + + @Override + public void deliverConfirmationResult( + IVoiceInteractorRequest request, boolean confirmed, Bundle result) throws RemoteException {} + + @Override + public void deliverPickOptionResult( + IVoiceInteractorRequest request, boolean finished, Option[] selections, Bundle result) + throws RemoteException {} + + @Override + public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result) + throws RemoteException {} + + @Override + public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) + throws RemoteException {} + + @Override + public void deliverCommandResult( + IVoiceInteractorRequest request, boolean finished, Bundle result) throws RemoteException { + commandRequest.onCommandResult(finished, result); + } + + @Override + public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException { + commandRequest.onCancel(); + } + + @Override + public void destroy() throws RemoteException {} + + @Override + public IBinder asBinder() { + return null; + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java index cc6cfccf9..a5c700067 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java @@ -73,6 +73,10 @@ public class ShadowWifiManager { private boolean startScanSucceeds = true; private boolean is5GHzBandSupported = false; private boolean isStaApConcurrencySupported = false; + private boolean isWpa3SaeSupported = false; + private boolean isWpa3SaeH2eSupported = false; + private boolean isWpa3SaePublicKeySupported = false; + private boolean isWpa3SuiteBSupported = false; private AtomicInteger activeLockCount = new AtomicInteger(0); private final BitSet readOnlyNetworkIds = new BitSet(); private final ConcurrentHashMap<WifiManager.OnWifiUsabilityStatsListener, Executor> @@ -128,7 +132,7 @@ public class ShadowWifiManager { this.is5GHzBandSupported = is5GHzBandSupported; } - /** Returns last value provided to #setStaApConcurrencySupported. */ + /** Returns last value provided to {@link #setStaApConcurrencySupported}. */ @Implementation(minSdk = R) protected boolean isStaApConcurrencySupported() { return isStaApConcurrencySupported; @@ -139,6 +143,50 @@ public class ShadowWifiManager { this.isStaApConcurrencySupported = isStaApConcurrencySupported; } + /** Returns last value provided to {@link #setWpa3SaeSupported}. */ + @Implementation(minSdk = Q) + protected boolean isWpa3SaeSupported() { + return isWpa3SaeSupported; + } + + /** Sets whether WPA3-Personal SAE is supported. */ + public void setWpa3SaeSupported(boolean isWpa3SaeSupported) { + this.isWpa3SaeSupported = isWpa3SaeSupported; + } + + /** Returns last value provided to {@link #setWpa3SaePublicKeySupported}. */ + @Implementation(minSdk = S) + protected boolean isWpa3SaePublicKeySupported() { + return isWpa3SaePublicKeySupported; + } + + /** Sets whether WPA3 SAE Public Key is supported. */ + public void setWpa3SaePublicKeySupported(boolean isWpa3SaePublicKeySupported) { + this.isWpa3SaePublicKeySupported = isWpa3SaePublicKeySupported; + } + + /** Returns last value provided to {@link #setWpa3SaeH2eSupported}. */ + @Implementation(minSdk = S) + protected boolean isWpa3SaeH2eSupported() { + return isWpa3SaeH2eSupported; + } + + /** Sets whether WPA3 SAE Hash-to-Element is supported. */ + public void setWpa3SaeH2eSupported(boolean isWpa3SaeH2eSupported) { + this.isWpa3SaeH2eSupported = isWpa3SaeH2eSupported; + } + + /** Returns last value provided to {@link #setWpa3SuiteBSupported}. */ + @Implementation(minSdk = Q) + protected boolean isWpa3SuiteBSupported() { + return isWpa3SuiteBSupported; + } + + /** Sets whether WPA3-Enterprise Suite-B-192 is supported. */ + public void setWpa3SuiteBSupported(boolean isWpa3SuiteBSupported) { + this.isWpa3SuiteBSupported = isWpa3SuiteBSupported; + } + /** Sets the connection info as the provided {@link WifiInfo}. */ public void setConnectionInfo(WifiInfo wifiInfo) { this.wifiInfo = wifiInfo; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java index 75b0371eb..4317d26c0 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java @@ -21,6 +21,7 @@ import android.view.View; import android.view.WindowManagerGlobal; import androidx.annotation.Nullable; import java.lang.reflect.Proxy; +import java.util.List; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -151,6 +152,9 @@ public class ShadowWindowManagerGlobal { @Static @Accessor("sUseBLASTAdapter") void setUseBlastAdapter(boolean useBlastAdapter); + + @Accessor("mViews") + List<View> getWindowViews(); } private static class WindowSessionDelegate { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java index 4c38710f2..37b3f8d65 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java @@ -2,7 +2,6 @@ package org.robolectric.shadows; import android.net.wifi.WifiUsabilityStatsEntry; import android.net.wifi.WifiUsabilityStatsEntry.ContentionTimeStats; -import android.net.wifi.WifiUsabilityStatsEntry.LinkStats; import android.net.wifi.WifiUsabilityStatsEntry.RadioStats; import android.net.wifi.WifiUsabilityStatsEntry.RateStats; import android.os.Build.VERSION_CODES; @@ -118,44 +117,44 @@ public class WifiUsabilityStatsEntryBuilder { ClassParameter.from(boolean.class, isSameRegisteredCell), ClassParameter.from(SparseArray.class, new SparseArray<>())); // new in >T } else { - return new WifiUsabilityStatsEntry( - timeStampMillis, - rssi, - linkSpeedMbps, - totalTxSuccess, - totalTxRetries, - totalTxBad, - totalRxSuccess, - totalRadioOnTimeMillis, - totalRadioTxTimeMillis, - totalRadioRxTimeMillis, - totalScanTimeMillis, - totalNanScanTimeMillis, - totalBackgroundScanTimeMillis, - totalRoamScanTimeMillis, - totalPnoScanTimeMillis, - totalHotspot2ScanTimeMillis, - totalCcaBusyFreqTimeMillis, - totalRadioOnFreqTimeMillis, - totalBeaconRx, - probeStatusSinceLastUpdate, - probeElapsedTimeSinceLastUpdateMillis, - probeMcsRateSinceLastUpdate, - rxLinkSpeedMbps, - timeSliceDutyCycleInPercent, - new ContentionTimeStats[] {}, - new RateStats[] {}, - new RadioStats[] {}, - CHANNEL_UTILIZATION_RATIO, - isThroughputSufficient, - isWifiScoringEnabled, - isCellularDataAvailable, - cellularDataNetworkType, - cellularSignalStrengthDbm, - cellularSignalStrengthDb, - isSameRegisteredCell, - new SparseArray<LinkStats>()); - + return ReflectionHelpers.callConstructor( + WifiUsabilityStatsEntry.class, + ClassParameter.from(long.class, timeStampMillis), + ClassParameter.from(int.class, rssi), + ClassParameter.from(int.class, linkSpeedMbps), + ClassParameter.from(long.class, totalTxSuccess), + ClassParameter.from(long.class, totalTxRetries), + ClassParameter.from(long.class, totalTxBad), + ClassParameter.from(long.class, totalRxSuccess), + ClassParameter.from(long.class, totalRadioOnTimeMillis), + ClassParameter.from(long.class, totalRadioTxTimeMillis), + ClassParameter.from(long.class, totalRadioRxTimeMillis), + ClassParameter.from(long.class, totalScanTimeMillis), + ClassParameter.from(long.class, totalNanScanTimeMillis), + ClassParameter.from(long.class, totalBackgroundScanTimeMillis), + ClassParameter.from(long.class, totalRoamScanTimeMillis), + ClassParameter.from(long.class, totalPnoScanTimeMillis), + ClassParameter.from(long.class, totalHotspot2ScanTimeMillis), + ClassParameter.from(long.class, totalCcaBusyFreqTimeMillis), + ClassParameter.from(long.class, totalRadioOnFreqTimeMillis), + ClassParameter.from(long.class, totalBeaconRx), + ClassParameter.from(int.class, probeStatusSinceLastUpdate), + ClassParameter.from(int.class, probeElapsedTimeSinceLastUpdateMillis), + ClassParameter.from(int.class, probeMcsRateSinceLastUpdate), + ClassParameter.from(int.class, rxLinkSpeedMbps), + ClassParameter.from(int.class, timeSliceDutyCycleInPercent), // new in T + ClassParameter.from( + ContentionTimeStats[].class, new ContentionTimeStats[] {}), // new in T + ClassParameter.from(RateStats[].class, new RateStats[] {}), // new in T + ClassParameter.from(RadioStats[].class, new RadioStats[] {}), // new in T + ClassParameter.from(int.class, CHANNEL_UTILIZATION_RATIO), // new in T + ClassParameter.from(boolean.class, isThroughputSufficient), // new in T + ClassParameter.from(boolean.class, isWifiScoringEnabled), // new in T + ClassParameter.from(boolean.class, isCellularDataAvailable), // new in T + ClassParameter.from(int.class, cellularDataNetworkType), + ClassParameter.from(int.class, cellularSignalStrengthDbm), + ClassParameter.from(int.class, cellularSignalStrengthDb), + ClassParameter.from(boolean.class, isSameRegisteredCell)); } } diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java index 631451883..e5431eb97 100644 --- a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java +++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java @@ -446,7 +446,7 @@ public final class AndroidVersions { public static final String SHORT_CODE = "V"; - public static final String VERSION = "15.0"; + public static final String VERSION = "15"; } /** The current release this process is running on. */ @@ -738,6 +738,9 @@ public final class AndroidVersions { try { Field activeCodeFields = targetClass.getDeclaredField("ACTIVE_CODENAMES"); String[] activeCodeNames = (String[]) activeCodeFields.get(null); + if (activeCodeNames == null) { + return new ArrayList<>(); + } return asList(activeCodeNames); } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException ex) { return new ArrayList<>(); diff --git a/utils/build.gradle b/utils/build.gradle index c31c9a0e0..54a2531e2 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -21,29 +21,11 @@ tasks.withType(GenerateModuleMetadata).configureEach { } compileKotlin { - // Use java/main classes directory to replace default kotlin/main to - // avoid d8 error when dexing & desugaring kotlin classes with non-exist - // kotlin/main directory because utils module doesn't have kotlin code - // in production. If utils module starts to add Kotlin code in main source - // set, we can remove this destinationDirectory modification. - destinationDirectory = file("${projectDir}/build/classes/java/main") compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } -afterEvaluate { - configurations { - runtimeElements { - attributes { - // We should add artifactType with jar to ensure standard runtimeElements variant - // has a max priority selection sequence than other variants that brought by - // kotlin plugin. - attribute( - Attribute.of("artifactType", String.class), - ArtifactTypeDefinition.JAR_TYPE - ) - } - } - } +compileTestKotlin { + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 } dependencies { @@ -52,8 +34,6 @@ dependencies { api libs.javax.inject api libs.javax.annotation.api - // For @VisibleForTesting and ByteStreams - implementation libs.guava compileOnly libs.findbugs.jsr305 testCompileOnly libs.auto.service.annotations diff --git a/utils/src/main/java/org/robolectric/util/Util.java b/utils/src/main/java/org/robolectric/util/Util.java index b7292ad93..8f74dc28a 100644 --- a/utils/src/main/java/org/robolectric/util/Util.java +++ b/utils/src/main/java/org/robolectric/util/Util.java @@ -1,6 +1,5 @@ package org.robolectric.util; -import com.google.common.io.ByteStreams; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -37,9 +36,10 @@ public class Util { version.substring(0, dotPos > -1 ? dotPos : dashPos > -1 ? dashPos : version.length())); } + @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"}) // not relevant, always runs on JVM public static void copy(InputStream in, OutputStream out) throws IOException { try { - ByteStreams.copy(in, out); + in.transferTo(out); } finally { in.close(); } @@ -52,9 +52,10 @@ public class Util { * @return The bytes read from the stream. * @throws IOException Error reading from stream. */ + @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"}) // not relevant, always runs on JVM public static byte[] readBytes(InputStream is) throws IOException { try { - return ByteStreams.toByteArray(is); + return is.readAllBytes(); } finally { is.close(); } |