diff --git a/.bazelproject b/.bazelproject index 84486b6f3..0607dd038 100644 --- a/.bazelproject +++ b/.bazelproject @@ -19,4 +19,4 @@ additional_languages: # Please uncomment an android-SDK platform. Available SDKs are: -android_sdk_platform: android-28 +android_sdk_platform: android-34 diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 000000000..c642de9eb --- /dev/null +++ b/.bazelrc @@ -0,0 +1,9 @@ +# Include debug info in the compiled jars +build --javacopt=-g +build --host_javacopt=-g + +build --experimental_google_legacy_api +build --enable_platform_specific_config +build:linux --sandbox_tmpfs_path=/tmp +test --enable_platform_specific_config +test:linux --sandbox_tmpfs_path=/tmp diff --git a/.bazelversion b/.bazelversion index 1545d9665..18bb4182d 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -3.5.0 +7.5.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..57c46906b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: Build +on: + workflow_dispatch: + push: + branches: + - main + - 'axt_**_release_branch' + + pull_request: + branches: + - main + - 'axt_**_release_branch' + +env: + cache-version: v2 + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Install Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '21' + - name: 'Cache Bazel files' + uses: actions/cache@v4 + with: + path: ~/.cache/bazel + key: ${{ runner.os }}-${{ env.cache-version }}-bazel-build-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-version }}-bazel-build- + - name: Build maven artifacts + run: bazelisk build //:axt_m2repository + shell: bash + - name: cp to upload dir + run: | + mkdir -p ~/download + cp bazel-bin/axt_m2repository.zip ~/download + shell: bash + - name: 'Upload local snapshot for tests' + uses: actions/upload-artifact@v4 + with: + name: local-snapshot + path: ~/download + - name: 'Clean bazel cache' + run: | + rm -rf $(bazel info repository_cache) + shell: bash + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Install Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '21' + - name: 'Cache Bazel files' + uses: actions/cache@v4 + with: + path: ~/.cache/bazel + key: ${{ runner.os }}-${{ env.cache-version }}-bazel-test-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-version }}-bazel-test- + - name: Run Robolectric tests and fast tagged tests + run: bazelisk test --test_tag_filters=robolectric,fast --build_tag_filters=robolectric,fast --test_output=all ... + shell: bash + - name: 'Clean bazel cache' + run: | + rm -rf $(bazel info repository_cache) + shell: bash + gradle-emulator-test: + runs-on: ubuntu-latest + needs: [build] + timeout-minutes: 20 + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Install Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '21' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: 'Cache Gradle files' + uses: gradle/gradle-build-action@v3 + - name: 'Download local snapshot for tests' + uses: actions/download-artifact@v4 + with: + name: local-snapshot + path: ~/download + - name: 'Install to local maven repo' + run: | + mkdir -p ~/.m2 + unzip ~/download/axt_m2repository.zip -d ~/.m2/ + shell: bash + - name: 'Setup Android SDK' + uses: android-actions/setup-android@v3 + - name: 'Run gradle tests' + run: | + cd ${{ github.workspace }}/gradle-tests + ./gradlew nexusOneDebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" --no-watch-fs --stacktrace + shell: bash + - name: 'Upload test reports' + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: test-reports + path: gradle-tests/**/build/reports/androidTests/ diff --git a/.gitignore b/.gitignore index c5e4416c2..ab0ffd830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ # Bazel bazel-* +MODULE.bazel.lock # Android Studio .aswb + +# Intellij IDEA +.idea +.gradle +.ijwb diff --git a/BUILD b/BUILD new file mode 100644 index 000000000..2b7a74ea8 --- /dev/null +++ b/BUILD @@ -0,0 +1,62 @@ +load("@io_bazel_rules_kotlin//kotlin:core.bzl", "define_kt_toolchain") +load("@rules_jvm_external//:defs.bzl", "artifact") +load("@rules_license//rules:license.bzl", "license") +load("//build_extensions:axt_deps_versions.bzl", "KOTLIN_LANG_VERSION") +load("//build_extensions/maven:maven_repo.bzl", "maven_repository") + +package(default_visibility = ["//:__subpackages__"]) + +exports_files([ + "proguard_binary.cfg", + "LICENSE", + "repo.bzl", +]) + +# Setup kotlin toolchain +define_kt_toolchain( + name = "kotlin_toolchain", + api_version = KOTLIN_LANG_VERSION, + language_version = KOTLIN_LANG_VERSION, +) + +# Creates maven release repository +maven_repository( + name = "axt_m2repository", + testonly = 1, + srcs = [ + "//core/java/androidx/test/core:core_maven_artifact", + "//espresso/accessibility/java/androidx/test/espresso/accessibility:accessibility_checks_maven_artifact", + "//espresso/contrib/java/androidx/test/espresso/contrib:espresso_contrib_maven_artifact", + "//espresso/core/java/androidx/test/espresso:espresso_core_maven_artifact", + "//espresso/device/java/androidx/test/espresso/device:device_maven_artifact", + "//espresso/idling_resource/concurrent/java/androidx/test/espresso/idling/concurrent:idling_concurrent_maven_artifact", + "//espresso/idling_resource/java/androidx/test/espresso:espresso_idling_resource_maven_artifact", + "//espresso/idling_resource/net/java/androidx/test/espresso/idling/net:idling_net_maven_artifact", + "//espresso/intents/java/androidx/test/espresso/intent:espresso_intents_maven_artifact", + "//espresso/remote/java/androidx/test/espresso/remote:espresso_remote_maven_artifact", + "//espresso/web/java/androidx/test/espresso/web:espresso_web_maven_artifact", + "//ext/junit/java/androidx/test/ext/junit:junit_maven_artifact", + "//ext/truth/java/androidx/test/ext/truth:truth_maven_artifact", + "//ktx/core/java/androidx/test/core:core_maven_artifact", + "//ktx/ext/junit/java/androidx/test/ext/junit:junit_maven_artifact", + "//runner/android_junit_runner/java/androidx/test:runner_maven_artifact", + "//runner/android_test_orchestrator/stubapp:orchestrator_release_maven_artifact", + "//runner/monitor/java/androidx/test:monitor_maven_artifact", + "//runner/rules/java/androidx/test:rules_maven_artifact", + "//services:test_services_maven_artifact", + "//services/storage/java/androidx/test/services/storage:test_storage_maven_artifact", + ], +) + +java_test( + name = "instrumentation_test_runner", + testonly = 1, + tags = ["manual"], + test_class = "com.google.android.apps.common.testing.suite.AndroidDeviceTestSuite", + visibility = ["//visibility:public"], + runtime_deps = [ + "//opensource:entry_point_import", + ], +) + +license(name = "license") diff --git a/BUILD.bazel b/BUILD.bazel deleted file mode 100644 index 4cabcbf83..000000000 --- a/BUILD.bazel +++ /dev/null @@ -1,130 +0,0 @@ -package(default_visibility = ["//:__subpackages__"]) - -load("//build_extensions:maven_repo.bzl", "maven_repository") -load("@rules_jvm_external//:defs.bzl", "artifact") - -exports_files([ - "proguard_binary.cfg", - "LICENSE", - "repo.bzl", -]) - -# Creates maven release repository -maven_repository( - name = "axt_m2repository", - srcs = [ - "//core/java/androidx/test/core:core_maven_artifact", - "//espresso/accessibility/java/androidx/test/espresso/accessibility:accessibility_checks_maven_artifact", - "//espresso/contrib/java/androidx/test/espresso/contrib:espresso_contrib_maven_artifact", - "//espresso/core/java/androidx/test/espresso:espresso_core_maven_artifact", - "//espresso/core/java/androidx/test/espresso/remote:espresso_remote_maven_artifact", - "//espresso/idling_resource/concurrent/java/androidx/test/espresso/idling/concurrent:idling_concurrent_maven_artifact", - "//espresso/idling_resource/java/androidx/test/espresso:espresso_idling_resource_maven_artifact", - "//espresso/idling_resource/net/java/androidx/test/espresso/idling/net:idling_net_maven_artifact", - "//espresso/intents/java/androidx/test/espresso/intent:espresso_intents_maven_artifact", - "//espresso/web/java/androidx/test/espresso/web:espresso_web_maven_artifact", - "//ext/junit/java/androidx/test/ext/junit:junit_maven_artifact", - "//ext/truth/java/androidx/test/ext/truth:truth_maven_artifact", - "//ktx/core/java/androidx/test/core:core_maven_artifact", - "//ktx/ext/junit/java/androidx/test/ext/junit:junit_maven_artifact", - "//runner/android_junit_runner/java/androidx/test:runner_maven_artifact", - "//runner/android_test_orchestrator/stubapp:orchestrator_release_maven_artifact", - "//runner/monitor/java/androidx/test:monitor_maven_artifact", - "//runner/rules/java/androidx/test:rules_maven_artifact", - "//services:test_services_maven_artifact", - "//services/storage/java/androidx/test/services/storage:test_storage_maven_artifact", - ], -) - -java_test( - name = "instrumentation_test_runner", - testonly = 1, - tags = ["manual"], - test_class = "com.google.android.apps.common.testing.suite.AndroidDeviceTestSuite", - visibility = ["//visibility:public"], - runtime_deps = [ - "//opensource:entry_point_import", - ], -) - -alias( - name = "third_party/java/jdk/jar", - actual = "@local_jdk//:jar", -) - -# Support library aliases - -alias( - name = "androidx_appcompat", - actual = artifact("androidx.appcompat:appcompat"), -) - -alias( - name = "google_android_material", - actual = artifact("com.google.android.material:material"), -) - -alias( - name = "androidx_multidex", - actual = artifact("androidx.multidex:multidex"), -) - -alias( - name = "androidx_annotation", - actual = artifact("androidx.annotation:annotation"), -) - -alias( - name = "androidx_lifecycle_common", - actual = artifact("androidx.lifecycle:lifecycle-common"), -) - -alias( - name = "androidx_core", - actual = artifact("androidx.core:core"), -) - -alias( - name = "androidx_legacy_support_core_ui", - actual = artifact("androidx.legacy:legacy-support-core-ui"), -) - -alias( - name = "androidx_legacy_support_core_utils", - actual = artifact("androidx.legacy:legacy-support-core-utils"), -) - -alias( - name = "androidx_fragment", - actual = artifact("androidx.fragment:fragment"), -) - -alias( - name = "androidx_legacy_support_v4", - actual = artifact("androidx.legacy:legacy-support-v4"), -) - -alias( - name = "androidx_recyclerview", - actual = artifact("androidx.recyclerview:recyclerview"), -) - -alias( - name = "androidx_viewpager", - actual = artifact("androidx.viewpager:viewpager"), -) - -alias( - name = "androidx_drawerlayout", - actual = artifact("androidx.drawerlayout:drawerlayout"), -) - -alias( - name = "androidx_cursoradapter", - actual = artifact("androidx.cursoradapter:cursoradapter"), -) - -alias( - name = "uiautomator", - actual = artifact("androidx.test.uiautomator:uiautomator"), -) diff --git a/CHANGELOG_TEMPLATE.md b/CHANGELOG_TEMPLATE.md new file mode 100644 index 000000000..152e0ff90 --- /dev/null +++ b/CHANGELOG_TEMPLATE.md @@ -0,0 +1,11 @@ +**Bug Fixes** + +**New Features** + +**Breaking Changes** + +**API Changes** + +**Breaking API Changes** + +**Known Issues** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 935b7d93b..b50156990 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,45 +17,39 @@ again. ## Building and Testing -AndroidX Test uses the [bazel](https://bazel.build) build system. +AndroidX Test uses the [Bazel](https://bazel.build) build system. -Currently only Linux is fully supported. For Mac and windows users, you may be able to build -and run the Robolectric tests. +Currently only Linux is fully supported. Mac may work but is not regularly tested ### One time setup * [Fork](https://help.github.com/articles/fork-a-repo/) and [clone](https://help.github.com/articles/cloning-a-repository/) the [AndroidX Test repo](https://github.com/android/android-test) -* Install [bazel](https://docs.bazel.build/versions/master/install.html). - Version 3.5.0 is recommended. For instrumentation testing on Linux make sure your environment - meets the following - [prerequisites](https://docs.bazel.build/versions/master/android-instrumentation-test.html#prerequisites) +* Install [Bazelisk](https://github.com/bazelbuild/bazelisk/blob/master/README.md) + Note that instrumentation test execution support is currently not setup + for androidx test libraries. * Install [maven](http://maven.apache.org/install.html) and make it available on PATH. * Install the [Android SDK](https://developer.android.com/studio/install) and run the following command to ensure you have the necessary components: - `./tools/bin/sdkmanager --install 'build-tools;30.0.2' - 'platforms;android-30' 'emulator' 'platform-tools' - 'system-images;android-19;default;x86' - 'system-images;android-21;default;x86' - 'system-images;android-22;default;x86' - 'system-images;android-23;default;x86'` + `cmdline-tools/latest/bin/sdkmanager "build-tools;36.0.0" "platforms;android-36"` * Set the `ANDROID_HOME` environment variable to point to the SDK install location. For example - * On Linux: export ANDROID_HOME=/home/$USER/Android/Sdk - * On Mac: export ANDROID_HOME=/Users/$USER/Library/Android/sdk - You can also add this command to your ~/.bashrc, ~/.zshrc, or ~/.profile file to make it + * On Linux: `export ANDROID_HOME=/home/$USER/Android/Sdk` + * On Mac: `export ANDROID_HOME=/Users/$USER/Library/Android/sdk` + You can also add this command to your ~/.bashrc, ~/.zshrc, or ~/.profile file to make it permanent. +*. Install Zulu Java 21 and add to PATH ### IDE setup Android Studio is recommended. -* Install the [Bazel Android Studio plugin](https://docs.bazel.build/versions/master/ide.html). +* Install the [Bazel Android Studio plugin](https://plugins.jetbrains.com/plugin/9185-bazel-for-android-studio) * Setup Bazel Android Studio plugin: * Navigate to `Settings > Other Settings > Bazel Settings` - * Update `Bazel binary location` to `/path/to/bazel/binary` (on Mac it's usually + * Update `Bazel binary location` to `/path/to/bazel/binary` (on Mac it's usually `/usr/local/bin/bazel`) * Select 'Import Bazel project' and set workspace location to android-test github repo @@ -66,28 +60,35 @@ Check [Troubleshooting](#troubleshooting) for tips on resolving common build iss ### Building ``` -bazel build +bazelisk build ``` For example, to build the AndroidX Test maven repository: ``` -bazel build :axt_m2repository +bazelisk build :axt_m2repository ``` ### Testing ``` -bazel test --spawn_strategy=local +bazelisk test ``` -eg to run the androidx-test-core tests +e.g. to run the androidx-test-core tests: ``` -bazel test //core/javatests/… --spawn_strategy=local --host_force_python=PY2 +bazelisk test //core/javatests/... ``` -To run all the robolectric local tests (and thus replicate the Google Cloud -Build CI) `bazel test ... --test_tag_filters=robolectric ---build_tag_filters=robolectric` +To run all the robolectric local tests (and thus replicate the GitHub CI): +`bazelisk test ... --test_tag_filters=robolectric --build_tag_filters=robolectric` + +To run the gradle integration tests: +``` +bazelisk build :axt_m2repository +unzip bazel-bin/axt_m2repository.zip -d ~/.m2/ +cd gradle-tests +./gradlew nexusOneDebugAndroidTest +``` ### Troubleshooting @@ -102,13 +103,13 @@ If your project fails to build because of unresolved imports two things might be ```bazel android_sdk_repository( ... - api_level = 30, - build_tools_version = "30.0.2", + api_level = 33, + build_tools_version = "33.0.2", ... ) ``` -2. Something might be wrong with `ANDROID_HOME` environment variable setup. Try adding the path of +2. Something might be wrong with `ANDROID_HOME` environment variable setup. Try adding the path of the android-sdk to the `WORKSPACE` file: ```bazel diff --git a/Dockerfile b/Dockerfile index 2a3f82acd..ae94c81f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,4 @@ RUN \ # download and extract sdk while suppressing the progress bar output wget -nv https://dl.google.com/android/repository/commandlinetools-linux-6609375_latest.zip && \ unzip -q commandlinetools-linux-6609375_latest.zip -d $ANDROID_HOME && \ - yes | sdkmanager --install 'build-tools;30.0.2' 'platforms;android-30' --sdk_root=$ANDROID_HOME | grep -v = || true + yes | sdkmanager --install 'build-tools;30.0.2' 'platforms;android-31' --sdk_root=$ANDROID_HOME | grep -v = || true diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000..c6667d8e9 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,97 @@ +# These need needs to be consistent with their counterparts in build_extensions/axt_deps_versions.bzl. +KOTLIN_VERSION = "1.9.21" +KOTLINX_COROUTINES_VERSION = "1.8.1" +GRPC_VERSION = "1.71.0" + +bazel_dep(name = "rules_java", version = "8.6.3") +bazel_dep(name = "rules_jvm_external", version = "6.7") +bazel_dep(name = "rules_android", version = "0.6.3", repo_name = "build_bazel_rules_android") +bazel_dep(name = "rules_kotlin", version = "2.1.3", repo_name = "io_bazel_rules_kotlin") +bazel_dep(name = "protobuf", version = "29.3", repo_name = "com_google_protobuf") +bazel_dep(name = "grpc-java", version = GRPC_VERSION) +bazel_dep(name = "rules_robolectric", version = "4.14.1.2", repo_name = "robolectric") +bazel_dep(name = "rules_python", version = "1.2.0") + +# Pin the version of rules_robolectric so that it matches the robolectric version we get from maven. +single_version_override( + module_name = "rules_robolectric", + version = "4.14.1.2", +) + +# python setup +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = True, + python_version = "3.12.8", + # See https://github.com/bazel-contrib/rules_python/issues/1169#issuecomment-1513804247 + ignore_root_user_error = True, +) + +# maven dependencies +maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") + +maven.artifact( + artifact = "accessibility-test-framework", + # exclude the org.checkerframework dependency since that require + # java8 compatibility. See b/176926990 + # accessibility-test-framework depends on hamcrest 2.2 which causes 'Using type org.hamcrest.Matcher from an indirect dependency' compile errors + exclusions = [ + "org.checkerframework:checker", + "org.hamcrest:hamcrest-core", + "org.hamcrest:hamcrest-library", + ], + group = "com.google.android.apps.common.testing.accessibility.framework", + version = "3.1.2", +) + +maven.install( + name = "maven", + artifacts = [ + "androidx.annotation:annotation:1.7.0", + "androidx.concurrent:concurrent-futures:1.2.0", + "androidx.concurrent:concurrent-futures-ktx:1.2.0", + "androidx.core:core:1.6.0", + "androidx.lifecycle:lifecycle-common:2.3.1", + "androidx.tracing:tracing:1.1.0", + "androidx.window:window-java:1.1.0", + "androidx.window:window-core:1.1.0", + "com.google.dagger:dagger-compiler:2.46", + "com.google.dagger:dagger-producers:2.46", + "com.google.dagger:dagger:2.46", + "com.google.googlejavaformat:google-java-format:1.4", + "com.squareup:javapoet:1.9.0", + "junit:junit:4.13.2", + "org.ccil.cowan.tagsoup:tagsoup:1.2.1", + "org.hamcrest:hamcrest-library:1.3", + "org.pantsbuild:jarjar:1.7.2", + "org.jetbrains.kotlin:kotlin-stdlib:%s" % KOTLIN_VERSION, + "org.jetbrains.kotlinx:kotlinx-coroutines-core:%s" % KOTLINX_COROUTINES_VERSION, + "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:%s" % KOTLINX_COROUTINES_VERSION, + "org.jetbrains.kotlinx:kotlinx-coroutines-android:%s" % KOTLINX_COROUTINES_VERSION, + "org.robolectric:robolectric:4.14.1", + ], + fetch_sources = True, + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + "https://dl.bintray.com/linkedin/maven", + ], +) + +use_repo(maven, "maven") + +# need to have a isolated version tree for listenablefuture, because otherwise +# listenablefuture will get resolved to 9999.0-empty-to-avoid-conflict-with-guava +maven.install( + name = "maven_listenablefuture", + artifacts = [ + "com.google.guava:listenablefuture:1.0", + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + "https://dl.bintray.com/linkedin/maven", + ], +) +use_repo(maven, "maven_listenablefuture") + diff --git a/README.md b/README.md index 1589ceead..81afc5dad 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ -# AndroidX Test Library +This GitHub project hosts two somewhat distinct projects: +1. AndroidX Test libraries +2. Bazel support for android_instrumentation_test + +# AndroidX Test Libraries The AndroidX Test Library provides an extensive framework for testing Android apps. This library provides a set of APIs that allow you to quickly build and run test code for your apps, including JUnit 4 and functional user interface (UI) tests. You can run tests created using these APIs from the Android Studio IDE or from the command line. For more details see [developers.android.com/testing](https://developers.android.com/testing) +The following maven libraries are hosted in this repo: + +androidx.test:annotation +androidx.test:core +androidx.test.espresso* +androidx.test.ext:junit +androidx.test:orchestrator +androidx.test:runner +androidx.test:rules +androidx.test:services + +androidx.test.uiautomator and androidx.test:ext:junit-gtest are hosted on [AOSP](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/README.md) + ## Contributing See [CONTRIBUTING.md](https://github.com/android/android-test/blob/master/CONTRIBUTING.md) @@ -19,7 +36,12 @@ Please see the for general questions and discussion, and please direct specific questions to [Stack Overflow](https://stackoverflow.com/questions/tagged/androidx-test). -## Bazel integration +## Releases + +https://developer.android.com/jetpack/androidx/releases/test is the canonical source for release notes, and +https://maven.google.com for release artifacts and source snapshots. + +# Bazel android_instrumentation_test support To depend on this repository in Bazel, add the following snippet to your WORKSPACE file: @@ -35,3 +57,4 @@ http_archive( load("@android_test_support//:repo.bzl", "android_test_repositories") android_test_repositories() ``` + diff --git a/WORKSPACE b/WORKSPACE index 95230da31..1e2a63b2e 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,162 +1,12 @@ -# TODO(b/114418172): rename to androidx_test. Requires a bazel change -workspace(name = "android_test_support") +# Define project setup not yet supported in MODULE.bzlmod -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -RULES_JVM_EXTERNAL_TAG = "2.1" - -RULES_JVM_EXTERNAL_SHA = "515ee5265387b88e4547b34a57393d2bcb1101314bcc5360ec7a482792556f42" - -http_archive( - name = "rules_jvm_external", - sha256 = RULES_JVM_EXTERNAL_SHA, - strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG, - url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG, -) - -# rules_proto defines proto_library. -http_archive( - name = "rules_proto", - sha256 = "2490dca4f249b8a9a3ab07bd1ba6eca085aaf8e45a734af92aad0c42d9dc7aaf", - strip_prefix = "rules_proto-218ffa7dfa5408492dc86c01ee637614f8695c45", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_proto/archive/218ffa7dfa5408492dc86c01ee637614f8695c45.tar.gz", - "https://github.com/bazelbuild/rules_proto/archive/218ffa7dfa5408492dc86c01ee637614f8695c45.tar.gz", - ], -) - -load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains") -rules_proto_dependencies() -rules_proto_toolchains() - -# The 'com_google_protobuf_javalite' package is required for Bazel 2.x and below. -http_archive( - name = "com_google_protobuf_javalite", - sha256 = "832c476bb442ca98a59c2291b8a504648d1c139b74acc15ef667a0e8f5e984e7", - strip_prefix = "protobuf-3.11.3", - urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.11.3.zip"], -) - -load("@rules_jvm_external//:defs.bzl", "maven_install") -load("@rules_jvm_external//:specs.bzl", "maven") -load( - "//build_extensions:axt_versions.bzl", - "ANDROIDX_JUNIT_VERSION", - "ANDROIDX_LIFECYCLE_VERSION", - "ANDROIDX_MULTIDEX_VERSION", - "ANDROIDX_VERSION", - "CORE_VERSION", - "GOOGLE_MATERIAL_VERSION", - "RUNNER_VERSION", - "UIAUTOMATOR_VERSION", -) - -maven_install( - name = "maven", - artifacts = [ - "androidx.annotation:annotation:" + ANDROIDX_VERSION, - "androidx.annotation:annotation-experimental:jar:" + ANDROIDX_VERSION, - "androidx.appcompat:appcompat:" + ANDROIDX_VERSION, - "androidx.core:core:" + ANDROIDX_VERSION, - "androidx.cursoradapter:cursoradapter:" + ANDROIDX_VERSION, - "androidx.drawerlayout:drawerlayout:" + ANDROIDX_VERSION, - "androidx.fragment:fragment:" + ANDROIDX_VERSION, - "androidx.legacy:legacy-support-core-ui:" + ANDROIDX_VERSION, - "androidx.legacy:legacy-support-core-utils:" + ANDROIDX_VERSION, - "androidx.legacy:legacy-support-v4:" + ANDROIDX_VERSION, - "androidx.lifecycle:lifecycle-common:" + ANDROIDX_LIFECYCLE_VERSION, - "androidx.multidex:multidex:" + ANDROIDX_MULTIDEX_VERSION, - "androidx.recyclerview:recyclerview:" + ANDROIDX_VERSION, - "androidx.test.uiautomator:uiautomator:" + UIAUTOMATOR_VERSION, - "androidx.viewpager:viewpager:" + ANDROIDX_VERSION, - "aopalliance:aopalliance:1.0", - "com.beust:jcommander:1.72", - maven.artifact( - group = "com.google.android.apps.common.testing.accessibility.framework", - artifact = "accessibility-test-framework", - version = "3.1", - exclusions = [ - # exclude the org.checkerframework dependency since that require - # java8 compatibility. See b/176926990 - maven.exclusion( - group = "org.checkerframework", - artifact = "checker" - ), - ] - ), - - "com.google.android.material:material:" + GOOGLE_MATERIAL_VERSION, - "com.google.auto.value:auto-value:1.5.1", - "com.google.code.findbugs:jsr305:3.0.2", - "com.google.code.gson:gson:2.8.5", - "com.google.dagger:dagger-compiler:2.11", - "com.google.dagger:dagger-producers:2.11", - "com.google.dagger:dagger:2.10", - "com.google.errorprone:javac-shaded:9-dev-r4023-3", - "com.google.flogger:flogger-system-backend:0.4", - "com.google.flogger:flogger:0.4", - "com.google.flogger:google-extensions:0.4", - "com.google.googlejavaformat:google-java-format:1.4", - "com.google.guava:guava:27.1-android", - "com.google.guava:guava-testlib:27.1-android", - "com.google.inject.extensions:guice-multibindings:4.1.0", - "com.google.inject:guice:4.1.0", - "com.google.truth:truth:1.0", - "com.googlecode.jarjar:jarjar:1.3", - "com.linkedin.dexmaker:dexmaker-mockito:jar:2.28.1", - "com.linkedin.dexmaker:dexmaker:2.28.1", - "com.squareup:javapoet:1.9.0", - "javax.annotation:javax.annotation-api:1.3.1", - "javax.inject:javax.inject:1", - "joda-time:joda-time:2.10.1", - "junit:junit:4.12", - "net.bytebuddy:byte-buddy-agent:1.9.11", - "net.bytebuddy:byte-buddy:1.9.11", - "net.sf.kxml:kxml2:jar:2.3.0", - "org.ccil/cowan.tagsoup:tagsoup:1.2", - "org.checkerframework:checker-compat-qual:2.5.5", - "org.hamcrest:hamcrest-all:1.3", - "org.mockito:mockito-core:2.25.0", - "org.objenesis:objenesis:2.1", - "org.pantsbuild:jarjar:1.7.2", - "org.robolectric:robolectric:4.4", - ], - repositories = [ - "https://maven.google.com", - "https://repo1.maven.org/maven2", - "https://dl.bintray.com/linkedin/maven", - ], -) +# Load Android Sdk android_sdk_repository( name = "androidsdk", - api_level = 30, - build_tools_version = "30.0.2", + api_level = 36, + build_tools_version = "36.0.0", ) -load("//:repo.bzl", "android_test_repositories") -android_test_repositories(with_dev_repositories = True) - -load("@robolectric//bazel:robolectric.bzl", "robolectric_repositories") -robolectric_repositories() - -# Kotlin toolchains -rules_kotlin_version = "686518ffd8e58609a21f258616d154ba2934a8e8" -http_archive( - name = "io_bazel_rules_kotlin", - sha256 = "0237910a921ad492aa8520bf88923c42d745d74522d3507a34c9bfd39b4e295c", - strip_prefix = "rules_kotlin-%s" % rules_kotlin_version, - type = "zip", - urls = ["https://github.com/bazelbuild/rules_kotlin/archive/%s.zip" % rules_kotlin_version], -) -load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kotlin_repositories", "kt_register_toolchains") -kotlin_repositories() -kt_register_toolchains() - -# Android bazel rules -http_archive( - name = "build_bazel_rules_android", - urls = ["https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"], - sha256 = "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", - strip_prefix = "rules_android-0.1.1", -) +register_toolchains("//:kotlin_toolchain") diff --git a/api/README.md b/api/README.md index 38e36910c..5165dfef3 100644 --- a/api/README.md +++ b/api/README.md @@ -1,3 +1,5 @@ -Text output for the androidx.test API. +Deprecated! + +Previous repository for api definitions for androidx.test. +API definitions have been moved to per-library directories. -Generated via blaze build //third_party/android/androidx_test:axt_doc diff --git a/build_extensions/AndroidManifest_instrumentation_test_template.xml b/build_extensions/AndroidManifest_instrumentation_test_template.xml index 3910b95b2..3379a5a0b 100644 --- a/build_extensions/AndroidManifest_instrumentation_test_template.xml +++ b/build_extensions/AndroidManifest_instrumentation_test_template.xml @@ -18,8 +18,8 @@ package="${applicationId}" > + android:minSdkVersion="${minSdkVersion}" + android:targetSdkVersion="34" /> diff --git a/build_extensions/AndroidManifest_robolectric.xml b/build_extensions/AndroidManifest_robolectric.xml new file mode 100644 index 000000000..4685d5733 --- /dev/null +++ b/build_extensions/AndroidManifest_robolectric.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/build_extensions/AndroidManifest_target_stub.xml b/build_extensions/AndroidManifest_target_stub.xml index 08bc6fc11..1a520ad8d 100644 --- a/build_extensions/AndroidManifest_target_stub.xml +++ b/build_extensions/AndroidManifest_target_stub.xml @@ -18,7 +18,7 @@ package="${applicationId}" > + android:minSdkVersion="23" + android:targetSdkVersion="34" /> diff --git a/build_extensions/BUILD b/build_extensions/BUILD new file mode 100644 index 000000000..9be9f6ce5 --- /dev/null +++ b/build_extensions/BUILD @@ -0,0 +1,17 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +licenses(["notice"]) + +package(default_visibility = [ + "//visibility:public", +]) + +exports_files([ + "AndroidManifest_target_stub.xml", + "AndroidManifest_instrumentation_test_template.xml", + "AndroidManifest_robolectric.xml", + "robolectric.properties", + "mainDexClasses.rules", + "axt_released_versions.bzl", + "axt_versions.bzl", +]) diff --git a/build_extensions/BUILD.bazel b/build_extensions/BUILD.bazel deleted file mode 100644 index 22eaa94ea..000000000 --- a/build_extensions/BUILD.bazel +++ /dev/null @@ -1,47 +0,0 @@ -load("@bazel_skylib//:bzl_library.bzl", "bzl_library") - -licenses(["notice"]) # Apache License 2.0 - -package(default_visibility = [ - "//visibility:public", -]) - -filegroup( - name = "bzl", - srcs = [ - "add_or_update_file_in_zip.bzl", - "combine_jars.bzl", - "maven_repo.bzl", - "release.bzl", - "remove_from_jar.bzl", - ], -) - -# Used to generate a maven repository. -py_binary( - name = "maven_repository", - srcs = ["maven_repository.py"], - deps = [ - "@absl_py//absl:app", - "@absl_py//absl/flags", - ], -) - -# Used to generate a maven artifact. -sh_binary( - name = "maven_artifact", - srcs = ["maven_artifact.sh"], -) - -exports_files([ - "noJarJarRules.txt", - "atslToAxtJarJarRules.txt", - "AndroidManifest_target_stub.xml", - "AndroidManifest_instrumentation_test_template.xml", - "robolectric.properties", -]) - -bzl_library( - name = "remove_from_jar_lib", - srcs = ["remove_from_jar.bzl"], -) diff --git a/build_extensions/add_or_update_file_in_zip.bzl b/build_extensions/add_or_update_file_in_zip.bzl deleted file mode 100644 index a0b1e10e6..000000000 --- a/build_extensions/add_or_update_file_in_zip.bzl +++ /dev/null @@ -1,43 +0,0 @@ -"""Update a zip file to update or add a particular file within.""" - -def add_or_update_file_in_zip( - name, - src, - out, - update_src, - update_path, - **kwargs): - """Update a zip file to update or add a particular file within. - - Args: - name: Rule name - src: The zip file to update - out: Name of the output zip file - update_src: The source for the file to update in the zip - update_path: Path for the file to update within the zip. - **kwargs: Extra arguments that will be passed to the underlying - genrule rule. - """ - - native.genrule( - name = name, - srcs = [ - update_src, - src, - ], - outs = [ - out, - ], - cmd = ";".join([ - "tmp={}_tmp".format(name), - "rm -rf $$tmp", - "mkdir -p $$tmp", - "cp $(location {update_src}) $$tmp/{update_path}".format( - update_src = update_src, - update_path = update_path), - "zip -j -X -q -l " - + "$(location {src}) $$tmp/{update_path} -O $@".format( - src = src, - update_path = update_path) - ]), - **kwargs) \ No newline at end of file diff --git a/build_extensions/android_app_instrumentation_tests.bzl b/build_extensions/android_app_instrumentation_tests.bzl index 6a544501f..970f5fa84 100644 --- a/build_extensions/android_app_instrumentation_tests.bzl +++ b/build_extensions/android_app_instrumentation_tests.bzl @@ -9,12 +9,23 @@ load( "infer_java_package_name", "infer_java_package_name_from_label", ) +load("//build_extensions:kt_android_library.bzl", "kt_android_library") +load("//build_extensions:register_extension_info.bzl", "register_extension_info") +def android_app_instrumentation_tests( + name, + binary_target, + srcs, + deps, + device_list = [], + test_java_package = None, + binary_target_package = None, + library_args = {}, + binary_args = {}, + **kwargs): + """DEPRECATED: use axt_android_library_test instead. -def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_devices, - test_java_package = None, binary_target_package = None, - library_args = {}, binary_args = {}, **kwargs): - """A macro for an instrumentation test whose target under test is an android_binary. + A macro for an instrumentation test whose target under test is an android_binary. The intent of this wrapper is to simplify the build API for creating instrumentation test rules for simple cases, while still supporting build_cleaner for automatic dependency management. @@ -23,7 +34,7 @@ def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_de - a test_lib android_library, containing all sources and dependencies - a test_binary android_binary (soon to be android_application) - the manifest to use for the test library. - - for each device: + - for each src + device combination: - a android_instrumentation_test rule Args: @@ -32,8 +43,9 @@ def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_de binary_target: the android_binary under test srcs: the test sources to generate rules for deps: the build dependencies to use for the generated test library - target_devices: array of device targets to execute on - test_java_package_name: Optional. A custom root package name to use for the tests. If unset + device_list: list of device structs to execute on, generated from phone_devices.bzl:devices(). + By default this method returns a device for each available API level + test_java_package: Optional. A custom root package name to use for the tests. If unset will be derived based on current path to a java source root binary_target_package: Optional: the android package name of binary_target. If unset, will be derived from binary target's path to a java source root @@ -45,7 +57,7 @@ def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_de test_java_package_name = test_java_package if test_java_package else infer_java_package_name() instrumentation_target_package = binary_target_package if binary_target_package else infer_java_package_name_from_label(binary_target) - native.android_library( + kt_android_library( name = library_name, srcs = srcs, testonly = 1, @@ -57,7 +69,7 @@ def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_de name = name, srcs = srcs, deps = [library_name], - target_devices = target_devices, + device_list = device_list, test_java_package_name = test_java_package_name, # always append .tests at the end to avoid potential conflict with instrumentation_target_package test_android_package_name = instrumentation_target_package + ".tests", @@ -66,3 +78,9 @@ def android_app_instrumentation_tests(name, binary_target, srcs, deps, target_de binary_args = binary_args, **kwargs ) + +# registers the wrapper with build_cleaner so it can manage dependencies automatically +register_extension_info( + extension = android_app_instrumentation_tests, + label_regex_for_dep = "{extension_name}_library", +) diff --git a/build_extensions/android_library_instrumentation_tests.bzl b/build_extensions/android_library_instrumentation_tests.bzl index 36b2ab829..8617ea21f 100644 --- a/build_extensions/android_library_instrumentation_tests.bzl +++ b/build_extensions/android_library_instrumentation_tests.bzl @@ -8,11 +8,23 @@ load( "//build_extensions:infer_java_package_name.bzl", "infer_java_package_name", ) +load("//build_extensions:kt_android_library.bzl", "kt_android_library") +load("//build_extensions:register_extension_info.bzl", "register_extension_info") -def android_library_instrumentation_tests(name, srcs, deps, target_devices, - test_java_package = None, library_args = {}, - binary_args = {}, **kwargs): - """A macro for an instrumentation test whose target under test is an android_library. +def android_library_instrumentation_tests( + name, + srcs, + deps, + target_devices = [], + device_list = [], + test_java_package = None, + library_args = {}, + binary_args = {}, + **kwargs): + """DEPRECATED: use axt_android_library_test instead + + + A macro for an instrumentation test whose target under test is an android_library. Will generate a 'self-instrumentating' test binary and other associated rules @@ -20,11 +32,10 @@ def android_library_instrumentation_tests(name, srcs, deps, target_devices, for simple cases, while still supporting build_cleaner for automatic dependency management. This will generate: - - an unused stub android_binary under test, to placate bazel - a test_lib android_library, containing all sources and dependencies - a test_binary android_binary (soon to be android_application) - the manifest to use for the test library. - - for each device combination: + - for each src + device combination: - an android_instrumentation_test rule) Args: @@ -32,7 +43,8 @@ def android_library_instrumentation_tests(name, srcs, deps, target_devices, manage dependencies srcs: the test sources to generate rules for deps: the build dependencies to use for the generated test binary - target_devices: array of device targets to execute on + device_list: list of device structs to execute on, generated from phone_devices.bzl:devices() + By default this method returns a device for each available API level test_java_package: Optional. A custom root package name to use for the tests. If unset will be derived based on current path to a java source root library_args: additional arguments to pass to generated android_library @@ -42,15 +54,7 @@ def android_library_instrumentation_tests(name, srcs, deps, target_devices, library_name = "%s_library" % name test_java_package_name = test_java_package if test_java_package else infer_java_package_name() - native.android_binary( - name = "target_stub_binary", - manifest = "//build_extensions:AndroidManifest_target_stub.xml", - # use the same package name as the test package, so it gets overridden - manifest_values = {"applicationId": test_java_package_name}, - testonly = 1, - ) - - native.android_library( + kt_android_library( name = library_name, srcs = srcs, testonly = 1, @@ -62,11 +66,17 @@ def android_library_instrumentation_tests(name, srcs, deps, target_devices, name = name, srcs = srcs, deps = [library_name], - target_devices = target_devices, + device_list = device_list, test_java_package_name = test_java_package_name, test_android_package_name = test_java_package_name, instrumentation_target_package = test_java_package_name, - instruments = ":target_stub_binary", + instruments = None, binary_args = binary_args, **kwargs ) + +# registers the wrapper with build_cleaner so it can manage dependencies automatically +register_extension_info( + extension = android_library_instrumentation_tests, + label_regex_for_dep = "{extension_name}_library", +) diff --git a/build_extensions/android_library_local_tests.bzl b/build_extensions/android_library_local_tests.bzl deleted file mode 100644 index 3e7ad3c08..000000000 --- a/build_extensions/android_library_local_tests.bzl +++ /dev/null @@ -1,91 +0,0 @@ -"""A rule wrapper for generating android_local_tests for an android library.""" - -load( - "//build_extensions:infer_java_package_name.bzl", - "infer_java_package_name", -) - -_CONFIG_JAR_COMMAND = """ -set -e -JAR="$(location @local_jdk//:jar)" -SRC="$<" -[[ "$$(basename "$${SRC}")" = 'robolectric.properties' ]] || { - echo 'Must be named: robolectric.properties'; - exit 1; -} -$${JAR} -cf "$@" -C "$$(dirname "$${SRC}")" "$$(basename "$${SRC}")" -""" - -def android_library_local_tests(name, srcs, deps, test_java_package = None, **kwargs): - """A rule for generating android_local_tests whose target under test is an android_library. - - Intended to have similar semantics as android_library_instrumentation_tests - - This will generate: - - a test_lib android_library, containing all sources and dependencies - - the manifest to use for the test library. - - an android_local_test rule for each src - - Args: - name: the name to use for the generated android_library rule. This is needed for build_cleaner to - manage dependencies - srcs: the test sources to generate rules for - deps: the build dependencies to use for the generated local test - test_java_package_name: Optional. The root java package name of the tests. Inferred based on - the current directory if unset - **kwargs: arguments to pass to generated android_local_test rules - """ - - test_java_package_name = test_java_package if test_java_package else infer_java_package_name() - library_name = name - _robolectric_config( - name = "%s_config" % library_name, - src = "//build_extensions:robolectric.properties", - ) - native.android_library( - name = library_name, - srcs = srcs, - testonly = 1, - deps = deps + [ - ":%s_config" % library_name, - "@maven//:org_robolectric_robolectric", - "@robolectric//bazel:android-all", - ], - ) - for src in srcs: - # assume src has .java suffix - name = src.rstrip(".java") - native.android_local_test( - name = name, - tags = ["robolectric"], - manifest = "//build_extensions:AndroidManifest_target_stub.xml", - manifest_values = {"applicationId": test_java_package_name}, - deps = [ - library_name, - ], - **kwargs - ) - -def _robolectric_config(name, src): - """Creates a JAR file containing the given Robolectric properties file at the top level. - - Args: - name: a string, the name of the rule - src: a label, the properties file to package - """ - native.genrule( - name = name + "_gen", - srcs = [src], - outs = ["%s.jar" % name], - message = "Generating Robolectric config...", - cmd = _CONFIG_JAR_COMMAND, - tools = [ - "@local_jdk//:jar", - ], - visibility = ["//visibility:private"], - ) - native.java_import( - name = name, - constraints = ["android"], - jars = [name + "_gen"], - ) diff --git a/build_extensions/android_library_test.bzl b/build_extensions/android_library_test.bzl index e5c471b61..b280be928 100644 --- a/build_extensions/android_library_test.bzl +++ b/build_extensions/android_library_test.bzl @@ -1,36 +1,33 @@ -"""Wrappers for android_library_test.""" +"""Wrapper around for android_library_test that adds additionsl features.""" -load("//tools/build_defs/android:rules.bzl", "android_library_test") -load( - "//third_party/android/androidx_test/build_extensions:infer_java_package_name.bzl", - "infer_java_package_name", -) +load("//build_extensions:kt_android_library.bzl", "kt_android_library") def axt_android_library_test( - manifest = None, - manifest_values = {}, + name, + args = [], + srcs = [], custom_package = None, + data = [], + device_list = None, + manifest = None, + deps = [], **kwargs): - """A wrapper around android_library_test that auto-generates a manifest if not provided. + """Placeholder for future instrumentation test support. - TODO(b/172615902): look into replacing with a macro that returns a manifest instead. Or remove entirely if - this functionality is ever added to android_library_test + Currently only generates an android_library - Args: - manifest: the AndroidManifest label to provide to android_library_test. If not specified, a manifest will be auto generated. - manifest_values: the dictionary of manifest substitutions to provide to android_library_test - custom_package: the custom application id to use. If unspecified, the application id will be derived based on current package name - **kwargs: args to pass to android_library_test """ + + # always define a manifest to work around 'manifest is required when resource_files or assets are defined.' inherent + # kt_android_library if not manifest: - test_application_id = custom_package if custom_package else infer_java_package_name() - manifest_values = { - "applicationId": test_application_id, - "instrumentationTargetPackage": test_application_id, - } - manifest = "//third_party/android/androidx_test/build_extensions:AndroidManifest_instrumentation_test_template.xml" - android_library_test( + manifest = "//build_extensions:AndroidManifest_instrumentation_test_template.xml" + + kt_android_library( + name = "%s_lib" % name, + srcs = srcs, + exports_manifest = True, manifest = manifest, - manifest_values = manifest_values, - **kwargs + deps = deps, + testonly = 1, ) diff --git a/build_extensions/android_multidevice_instrumentation_test.bzl b/build_extensions/android_multidevice_instrumentation_test.bzl index 894beaae8..c252b03bb 100644 --- a/build_extensions/android_multidevice_instrumentation_test.bzl +++ b/build_extensions/android_multidevice_instrumentation_test.bzl @@ -1,18 +1,17 @@ """Utility for running single test on multiple emulator targets.""" def android_multidevice_instrumentation_test(name, target_devices, **kwargs): - """Generates a android_instrumentation_test rule for each given device. - - Args: - name: Name prefix to use for the rules. The name of the generated rules will follow: - name + target_device[-6:] eg name-15_x86 - target_devices: array of device targets - **kwargs: arguments to pass to generated android_test rules - """ - for device in target_devices: - native.android_instrumentation_test( - name = name + "-" + device[-6:], - target_device = device, - **kwargs - ) + """Generates a android_instrumentation_test rule for each given device. + Args: + name: Name prefix to use for the rules. The name of the generated rules will follow: + name + target_device[-6:] eg name-15_x86 + target_devices: array of device targets + **kwargs: arguments to pass to generated android_test rules + """ + for device in target_devices: + native.android_instrumentation_test( + name = name + "-" + device[-6:], + target_device = device, + **kwargs + ) diff --git a/build_extensions/api_checks.bzl b/build_extensions/api_checks.bzl new file mode 100644 index 000000000..aa4f1e927 --- /dev/null +++ b/build_extensions/api_checks.bzl @@ -0,0 +1,20 @@ +"""A macro for generating androidx.test api definitions and checks.""" + +# TODO: implement me + +def api_checks( + name, + runtime_dep, + src_jar, + testonly = 1): + """Generates api definitions and checks for a macro. + + This macro will generate two api definition files, one for public api, one for internal RestrictTo(Scope.LIBRARY_GROUP) apis, + and generate diff checks to compare them against api/current-public.txt and api/current-internal.txt respectively. + + Args: + name: name of rule + runtime_dep: runtime java dependency of the srcs + src_jar: contains the source to generate api for + testonly: the testonly restriction. Default 1 + """ diff --git a/build_extensions/atslToAxtJarJarRules.txt b/build_extensions/atslToAxtJarJarRules.txt deleted file mode 100644 index eca16a381..000000000 --- a/build_extensions/atslToAxtJarJarRules.txt +++ /dev/null @@ -1,10 +0,0 @@ -rule android.support.test.InstrumentationRegistry androidx.test.InstrumentationRegistry -rule android.support.test.annotation.** androidx.test.annotation.@1 -rule android.support.test.espresso.** "androidx.test.espresso.@1 -rule android.support.test.filters.** androidx.test.filters.@1 -rule android.support.test.orchestrator.** androidx.test.orchestrator.@1 -rule android.support.test.rule.** androidx.test.rule.@1 -rule android.support.test.runner.** androidx.test.runner.@1 -rule android.support.test.services.** androidx.test.services.@1 -rule android.support.test.internal.** androidx.test.internal.@1 -rule android.support.test.ui.app.** androidx.test.ui.app.@1 diff --git a/build_extensions/atsl_to_axt_jarjar.bzl b/build_extensions/atsl_to_axt_jarjar.bzl deleted file mode 100644 index e1c864c0a..000000000 --- a/build_extensions/atsl_to_axt_jarjar.bzl +++ /dev/null @@ -1,28 +0,0 @@ -"""Conditionally rewrites android.support.test references to androidx.test. - -It is intended for modifying references to android.support.test -in prebuilt java libraries, to point to their renamed androidx.test equivalent. - -This functionality is guarded by a flag, so initially use of this function -will have no effect. - -See go/jetpack-test-lsc -""" - -def atsl_to_axt_jarjar(name, src_jar, out_jar, **kwargs): - # this will be swapped to "atslToAxtJarJarRules.txt" in the ATSL -> AXT LSC - JARJAR_RULES = "//third_party/android/androidx_test/build_extensions:atslToAxtJarJarRules.txt" - - # TODO(b/78906684): use jetifier instead of jarjar - native.genrule( - name = name, - srcs = [src_jar], - outs = [out_jar], - cmd = ("$(location //third_party/java/jarjar:jarjar_bin) process " + - "$(location %s) '$<' '$@'" % JARJAR_RULES), - tools = [ - JARJAR_RULES, - "//third_party/java/jarjar:jarjar_bin", - ], - **kwargs - ) diff --git a/build_extensions/axt_android_application_test.bzl b/build_extensions/axt_android_application_test.bzl new file mode 100644 index 000000000..451a081f9 --- /dev/null +++ b/build_extensions/axt_android_application_test.bzl @@ -0,0 +1,2 @@ +def axt_android_application_test(**kwargs): + """Placeholder for future instrumentation test support.""" diff --git a/build_extensions/axt_android_local_test.bzl b/build_extensions/axt_android_local_test.bzl new file mode 100644 index 000000000..0b8cffdb1 --- /dev/null +++ b/build_extensions/axt_android_local_test.bzl @@ -0,0 +1,82 @@ +"""A rule wrapper for generating android_local_test .""" + +load("@build_bazel_rules_android//android:rules.bzl", "android_library", "android_local_test") +load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") +load("//build_extensions:create_jar.bzl", "create_jar") + +def axt_android_local_test(name, srcs = [], deps = [], manifest = "//build_extensions:AndroidManifest_robolectric.xml", tags = ["robolectric"], jvm_flags = [], **kwargs): + """A wrapper around android_local_test that provides sensible defaults for androidx.test. + + + Args: + name: the name to use for the generated android_local_test rule. + srcs: the test sources to generate rules for + deps: the build dependencies to use for the generated local test + manifest: the android manifest. Default: AndroidManifest_robolectric.xml + tags: the tags to pass to android_local_test. Default ["robolectric"]. + If overridden, it is recommended to pass the "robolectric" tag so + the test gets executed on github CI + **kwargs: arguments to pass to generated android_local_test rules + """ + + _robolectric_config( + name = "%s_config" % name, + src = "//build_extensions:robolectric.properties", + ) + deps = depset(deps + [ + "%s_config" % name, + # the blaze-robolectric target exports these by default, so export them here too for consistency + "@robolectric//bazel:android-all", + "@maven//:org_robolectric_robolectric", + "@maven//:org_robolectric_shadows_framework", + "@maven//:org_robolectric_shadowapi", + "@maven//:org_robolectric_annotations", + "//ext/junit", + "//core", + ]).to_list() + + if _is_kotlin(srcs): + kt_android_library( + name = "%s_kt_lib" % name, + srcs = srcs, + exports_manifest = True, + manifest = manifest, + deps = deps, + testonly = True, + ) + deps = [":%s_kt_lib" % name] + srcs = [] + + android_local_test( + name = name, + srcs = srcs, + tags = tags, + manifest = manifest, + # Allow running tests on JDK 21. See https://github.com/bazelbuild/bazel/issues/14502 + jvm_flags = jvm_flags + ["-Djava.security.manager=allow"], + deps = deps, + **kwargs + ) + +def _robolectric_config(name, src): + """Creates a JAR file containing the given Robolectric properties file at the top level. + + Args: + name: a string, the name of the rule + src: a label, the properties file to package + """ + create_jar( + name = name + "_gen", + srcs = [src], + ) + native.java_import( + name = name, + constraints = ["android"], + jars = [name + "_gen.jar"], + ) + +def _is_kotlin(srcs): + for s in srcs: + if s.endswith(".kt"): + return True + return False diff --git a/build_extensions/axt_deps_versions.bzl b/build_extensions/axt_deps_versions.bzl new file mode 100644 index 000000000..81e8ee03b --- /dev/null +++ b/build_extensions/axt_deps_versions.bzl @@ -0,0 +1,11 @@ +"""Defines versions of androidx.test dependencies.""" + +# These must match versions specified in MODULE.bazel +# Unfortunately MODULE.bazel files do not support load +# so there is no known way to share these constants + +# Maven dependency versions +ANDROIDX_ANNOTATION_VERSION = "1.7.0" +KOTLIN_VERSION = "1.9.21" +KOTLIN_LANG_VERSION = "1.9" +GRPC_VERSION = "1.71.0" diff --git a/build_extensions/axt_released_versions.bzl b/build_extensions/axt_released_versions.bzl new file mode 100644 index 000000000..d6d2de156 --- /dev/null +++ b/build_extensions/axt_released_versions.bzl @@ -0,0 +1,13 @@ +"""Defines current released AXT versions. +""" + +RUNNER_VERSION = "1.7.0" +RULES_VERSION = "1.7.0" +MONITOR_VERSION = "1.8.0" +ESPRESSO_VERSION = "3.7.0" +CORE_VERSION = "1.7.0" +ESPRESSO_DEVICE_VERSION = "1.1.0" +ANDROIDX_JUNIT_VERSION = "1.3.0" +ANDROIDX_TRUTH_VERSION = "1.7.0" +ORCHESTRATOR_VERSION = "1.6.1" +SERVICES_VERSION = "1.6.0" diff --git a/build_extensions/axt_stable_versions.bzl b/build_extensions/axt_stable_versions.bzl new file mode 100644 index 000000000..08f26edb7 --- /dev/null +++ b/build_extensions/axt_stable_versions.bzl @@ -0,0 +1,14 @@ +"""Defines currently released stable AXT versions.""" + +# currently only used for documentation purposes + +RUNNER_VERSION = "1.7.0" +RULES_VERSION = "1.7.0" +MONITOR_VERSION = "1.8.0" +ESPRESSO_VERSION = "3.7.0" +CORE_VERSION = "1.7.0" +ESPRESSO_DEVICE_VERSION = "1.1.0" +ANDROIDX_JUNIT_VERSION = "1.3.0" +ANDROIDX_TRUTH_VERSION = "1.7.0" +ORCHESTRATOR_VERSION = "1.6.1" +SERVICES_VERSION = "1.6.0" diff --git a/build_extensions/axt_versions.bzl b/build_extensions/axt_versions.bzl index ca1f820fc..eeb87e7ee 100644 --- a/build_extensions/axt_versions.bzl +++ b/build_extensions/axt_versions.bzl @@ -1,33 +1,20 @@ -"""Defines current AXT versions and dependencies. +"""Defines next to be released AXT versions. -Ensure UsageTrackerRegistry is updated accordingly when incrementing version numbers. +Use tools/release/validate_and_propagate_versions.sh to propagate these versions to +//:axt_m2_repository and gradle-tests/settings.gradle """ -# AXT versions -RUNNER_VERSION = "1.4.1-alpha01" # stable 1.4.0 -RULES_VERSION = "1.4.1-alpha01" # stable 1.4.0 -MONITOR_VERSION = "1.5.0-alpha01" # stable 1.4.0 -ESPRESSO_VERSION = "3.5.0-alpha01" # stable 3.4.0 -CORE_VERSION = "1.4.1-alpha01" # stable 1.4.0 -ANDROIDX_JUNIT_VERSION = "1.1.4-alpha01" # stable 1.1.3 -ANDROIDX_TRUTH_VERSION = "1.5.0-alpha01" # stable 1.4.0 -UIAUTOMATOR_VERSION = "2.2.0" -JANK_VERSION = "1.0.1" -SERVICES_VERSION = "1.4.1-alpha01" # stable 1.4.0 -ORCHESTRATOR_VERSION = "1.4.1-alpha01" # stable 1.4.0 +RUNNER_VERSION = "1.8.0-alpha01" +RULES_VERSION = "1.8.0-alpha01" +MONITOR_VERSION = "1.9.0-alpha02" +ESPRESSO_VERSION = "3.8.0-alpha01" +CORE_VERSION = "1.8.0-alpha01" +ESPRESSO_DEVICE_VERSION = "1.2.0-alpha01" +ANDROIDX_JUNIT_VERSION = "1.4.0-alpha01" +ANDROIDX_TRUTH_VERSION = "1.8.0-alpha01" +ORCHESTRATOR_VERSION = "1.7.0-alpha01" +SERVICES_VERSION = "1.7.0-alpha01" -# Maven dependency versions -ANDROIDX_VERSION = "1.0.0" -ANDROIDX_VERSION_PATH = "1.0.0" -GOOGLE_MATERIAL_VERSION = "1.0.0" -ANDROIDX_LIFECYCLE_VERSION = "2.0.0" -ANDROIDX_MULTIDEX_VERSION = "2.0.0" -KOTLIN_VERSION = "1.4.30" - -# accessibilitytestframework -ATF_VERSION = "3.1.2" - -JUNIT_VERSION = "4.12" -HAMCREST_VERSION = "1.3" -TRUTH_VERSION = "1.0" -GUAVA_VERSION = "27.0.1-android" +# Full maven artifact strings for apks. +SERVICES_APK_ARTIFACT = "androidx.test.services:test-services:%s" % SERVICES_VERSION +ORCHESTRATOR_ARTIFACT = "androidx.test:orchestrator:%s" % ORCHESTRATOR_VERSION diff --git a/build_extensions/combine_jars.bzl b/build_extensions/combine_jars.bzl deleted file mode 100644 index 98d065305..000000000 --- a/build_extensions/combine_jars.bzl +++ /dev/null @@ -1,36 +0,0 @@ -"""Combines multiple jars into one jar.""" - -def combine_jars(name, srcs, **genrule_kwargs): - '''Combines multiple jars into one jar. - - Args: - name: Name to be used for this rule. It produces name.jar - srcs: List of jars to be combined. - genrule_kwargs: Keyword arguments to pass through to the genrule. - ''' - - native.genrule( - name=name, - srcs=srcs, - outs=["%s.jar" % name], - tools=["@local_jdk//:jar"], - message="Combining following jars: %s" % ",".join(srcs), - cmd=( - # Absolutify $JAR for jdk-in-perforce - '{ [[ "$${JAR=$(location @local_jdk//:jar)}" =~ ^/ ]] || ' + - 'JAR="$$PWD/$$JAR"; } && ' + - "cwd=$$PWD && tmp=$$(mktemp -d) && cd $${tmp} && " + - # Extract each jar to its own subdirectory so there's no race between - # parallel extraction processes trying to write/overwrite same files. - "src_jar_num=0 && " + - "for src in $(SRCS);" + - " do mkdir $${src_jar_num} && " + - " (cd ./$${src_jar_num} && $$JAR xf $${cwd}/$${src} > /dev/null) & " + - " src_jar_num=$$((src_jar_num + 1));" + - "done && wait && " + - "i=1 && while ((i < src_jar_num));" + - " do cp -R ./$$i/* ./0/; (rm -rf ./$$i) & i=$$((i+1)); done && " + - "if ((src_jar_num == 0)); then mkdir 0; fi && " + - "($$JAR cf $${cwd}/$@ -C ./0 . > /dev/null) && wait &&" + - "rm -fr $${tmp} > /dev/null"), - **genrule_kwargs) diff --git a/build_extensions/create_jar.bzl b/build_extensions/create_jar.bzl new file mode 100644 index 000000000..bcb8fe519 --- /dev/null +++ b/build_extensions/create_jar.bzl @@ -0,0 +1,38 @@ +"""Build rule to create a single jar from given files of any type.""" + +def _create_jar_impl(ctx): + """ + Construct a single jar from given files of any type. + """ + + input_paths = [] + for target in ctx.attr.srcs: + input_paths.extend(target.files.to_list()) + + args = ctx.actions.args() + args.add(ctx.outputs.output) + args.add_all(input_paths) + + ctx.actions.run( + inputs = input_paths, + outputs = [ctx.outputs.output], + executable = ctx.executable._create_jar_java, + arguments = [args], + mnemonic = "CreateJAR", + ) + +create_jar = rule( + attrs = { + "srcs": attr.label_list(allow_files = True), + "_create_jar_java": attr.label( + executable = True, + cfg = "exec", + allow_files = True, + default = Label("//build_extensions/jar_creator/java/androidx/test/tools/jarcreator:jarcreator"), + ), + }, + outputs = { + "output": "%{name}.jar", + }, + implementation = _create_jar_impl, +) diff --git a/build_extensions/dackka_test.bzl b/build_extensions/dackka_test.bzl new file mode 100644 index 000000000..ac61f2a93 --- /dev/null +++ b/build_extensions/dackka_test.bzl @@ -0,0 +1,8 @@ +# Simple wrapper macro that generates rules to create a dackka zip and a build_test to verify it + +def dackka_test(name, **kwargs): + """Placeholder for ref doc using dackka and a test to verify success + + Currently unsupported in bazel + """ + return diff --git a/build_extensions/generate_instrumentation_tests.bzl b/build_extensions/generate_instrumentation_tests.bzl index af60370c4..ff7f907ca 100644 --- a/build_extensions/generate_instrumentation_tests.bzl +++ b/build_extensions/generate_instrumentation_tests.bzl @@ -1,15 +1,10 @@ """Internal helper function for generating instrumentation tests .""" -load( - "//build_extensions:android_multidevice_instrumentation_test.bzl", - "android_multidevice_instrumentation_test", -) - def generate_instrumentation_tests( name, srcs, deps, - target_devices, + device_list, test_java_package_name, test_android_package_name, instrumentation_target_package, @@ -18,6 +13,8 @@ def generate_instrumentation_tests( **kwargs): """A helper rule to generate instrumentation tests. + Currently unsupported in bazel + This will generate: - a test_binary android_binary (soon to be android_application) @@ -29,7 +26,7 @@ def generate_instrumentation_tests( name: unique prefix to use for generated rules srcs: the test sources to generate rules for deps: the build dependencies to use for the generated test binary - target_devices: array of device targets to execute on + device_list: list of device structs to execute on, generated from phone_devices.bzl:devices() test_java_package_name: the root java package name for the tests. test_android_package_name: the android package name to use for the android_binary test app. Typically this is the same as test_java_package_name instrumentation_target_package: the android package name to specify as instrumentationTargetPackage in the test_app manifest @@ -37,26 +34,3 @@ def generate_instrumentation_tests( binary_args: Optional additional arguments to pass to generated android_binary **kwargs: arguments to pass to generated android_instrumentation_test rules """ - - _manifest_values = { - "applicationId": test_android_package_name, - "instrumentationTargetPackage": instrumentation_target_package, - } - _manifest_values.update(binary_args.pop("manifest_values", {})) - native.android_binary( - name = "%s_binary" % name, - instruments = instruments, - manifest = "//build_extensions:AndroidManifest_instrumentation_test_template.xml", - manifest_values = _manifest_values, - testonly = 1, - deps = deps + [ - "//runner/android_junit_runner", - ], - **binary_args - ) - android_multidevice_instrumentation_test( - name = "%s_tests" % name, - target_devices = target_devices, - test_app = "%s_binary" % name, - **kwargs - ) diff --git a/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/BUILD b/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/BUILD new file mode 100644 index 000000000..20a5bb87e --- /dev/null +++ b/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/BUILD @@ -0,0 +1,23 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +package( + default_visibility = [ + "//:__subpackages__", + ], +) + +kt_jvm_library( + name = "jarcombiner_lib", + srcs = glob([ + "*.kt", + ]), +) + +java_binary( + name = "jarcombiner", + srcs = ["Main.java"], + main_class = "androidx.test.tools.jarcombiner.Main", + deps = [ + ":jarcombiner_lib", + ], +) diff --git a/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/JarCombiner.kt b/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/JarCombiner.kt new file mode 100644 index 000000000..496e9d075 --- /dev/null +++ b/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/JarCombiner.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarcombiner + +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.jar.JarInputStream +import java.util.jar.JarOutputStream +import java.util.zip.ZipEntry + +fun combineJars(args: Array) { + require(args.size >= 2) { "Must provide a least two files: " } + + val outputFile = args[0] + val jarOutputStream = JarOutputStream(FileOutputStream(outputFile, false)) + jarOutputStream.use { + // keep track of what zip each entry belongs to, in order to have a better error + // message in case of duplicates + val entryToJar: MutableMap = HashMap() + + for (i in 1 until args.size) { + val inputFile = args[i] + val jarInputStream = JarInputStream(FileInputStream(inputFile)) + jarInputStream.use { addToJar(entryToJar, jarOutputStream, jarInputStream, args[i]) } + } + } +} + +private fun addToJar( + entryToJar: MutableMap, + jarOutputStream: JarOutputStream, + inputJarStream: JarInputStream, + inputJarName: String, +) { + + var entry = inputJarStream.nextEntry + while (entry != null) { + // JarOutputStream will throw an error if any duplicate entry is added, which is undesirable for + // directories and certain classes. + // Keep track of the list of directories already added to prevent this + if (entryToJar.containsKey(entry.name)) { + if (!isAllowedDuplicate(entry)) { + throw RuntimeException( + "Duplicate entry: ${entry.name} is present in both ${entryToJar.get(entry.name)} and $inputJarName" + ) + } + } else if (shouldAddEntry(entry)) { + jarOutputStream.putNextEntry(entry) + inputJarStream.transferTo(jarOutputStream) + entryToJar.put(entry.name, inputJarName) + } + entry = inputJarStream.nextEntry + } +} + +private fun isAllowedDuplicate(entry: ZipEntry): Boolean { + if (entry.isDirectory) { + // always allow duplicate directories + return true + } else if (entry.name.startsWith("com/google/protobuf")) { + // bazel's new version of rules_proto creates java wrapper libraries around any proto_library + // dependencies, which contain classes already present in the main protobuf-javalite-3.21.7.jar + return true + } + return false +} + +private fun shouldAddEntry(entry: ZipEntry): Boolean { + if (entry.name.matches(Regex(".*/R[\\.|\\$].*class$"))) { + // strip generated R.class from resulting jar + return false + } else if (entry.name.equals("protobuf.meta")) { + return false + } else if (entry.name.startsWith("META-INF/maven")) { + // strip out files added to META-INF/maven since this can lead to duplicate file errors + return false + } else if (entry.name.startsWith("META-INF/MANIFEST.MF")) { + // strip out files added to META-INF/MANIFEST.MF since this can lead to duplicate file errors + return false + } else if (entry.name.startsWith("google/protobuf")) { + // strip out all the google/protobuf/*.proto files since this can lead to duplicate file errors + return false + } else if (entry.name.startsWith("META-INF/com.google.dagger_dagger.version")) { + // strip out META-INF/com.google.dagger_dagger.version since this can lead to duplicate file + // errors + return false + } + return true +} diff --git a/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/Main.java b/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/Main.java new file mode 100644 index 000000000..548b630a2 --- /dev/null +++ b/build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner/Main.java @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarcombiner; + +public class Main { + private Main() {} + + public static void main(String[] args) { + JarCombinerKt.combineJars(args); + } +} diff --git a/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/BUILD b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/BUILD new file mode 100644 index 000000000..ebe2f2346 --- /dev/null +++ b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/BUILD @@ -0,0 +1,17 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + +kt_jvm_test( + name = "JarCombinerTest", + srcs = ["JarCombinerTest.kt"], + data = [ + "//build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures:libjar1.jar", + "//build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures:libjar2.jar", + "//build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures:libjar_with_r.jar", + ], + deps = [ + "//build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner:jarcombiner_lib", + "//build_extensions/jar_validator/java/androidx/test/tools/jarvalidator:jarvalidator_lib", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +) diff --git a/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/JarCombinerTest.kt b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/JarCombinerTest.kt new file mode 100644 index 000000000..4ae3ebe2d --- /dev/null +++ b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/JarCombinerTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.test.tools.jarcombiner + +import androidx.test.tools.jarvalidator.getClassesInJar +import com.google.common.truth.Truth.assertThat +import java.io.File +import java.nio.file.Paths +import java.util.zip.ZipException +import kotlin.io.path.exists +import kotlin.streams.toList +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class JarCombinerTest { + + @Test + fun invalidInput() { + assertThrows(java.lang.IllegalArgumentException::class.java) { combineJars(emptyArray()) } + } + + @Test + fun combineJars() { + val outFile = File.createTempFile("combineJarsOut", ".jar") + + combineJars( + arrayOf(outFile.absolutePath, getDataJarPath("libjar1.jar"), getDataJarPath("libjar2.jar")) + ) + val classes = getClassesInJar(outFile.absolutePath) + assertThat(classes.toList()) + .containsExactly( + "androidx.test.tools.jarcombiner.fixtures.Jar1Class", + "androidx.test.tools.jarcombiner.fixtures.Jar2Class" + ) + } + + @Test + fun rClassesRemoved() { + val outFile = File.createTempFile("rClassesRemovedOut", ".jar") + + combineJars(arrayOf(outFile.absolutePath, getDataJarPath("libjar_with_r.jar"))) + val classes = getClassesInJar(outFile.absolutePath) + assertThat(classes.toList()) + .containsExactly("androidx.test.tools.jarcombiner.fixtures.Jar1Class") + } + + @Test + fun duplicateClasses() { + val outFile = File.createTempFile("duplicateClasses", ".jar") + + assertThrows( + ZipException::class.java, + { + combineJars( + arrayOf( + outFile.absolutePath, + getDataJarPath("libjar1.jar"), + getDataJarPath("libjar_with_r.jar") + ) + ) + } + ) + } + + private fun getDataJarPath(name: String): String { + val path = + Paths.get( + System.getenv("TEST_SRCDIR"), + "build_extensions", + "jar_combiner", + "javatests", + "androidx", + "test", + "tools", + "jarcombiner", + "fixtures", + name + ) + assertThat(path.exists()).isTrue() + return path.toAbsolutePath().toString() + } +} diff --git a/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/BUILD b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/BUILD new file mode 100644 index 000000000..dd7ffcd93 --- /dev/null +++ b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/BUILD @@ -0,0 +1,32 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package(default_applicable_licenses = ["//:license"]) + +java_library( + name = "jar1", + srcs = ["Jar1Class.java"], + visibility = [ + "//build_extensions/jar_combiner/javatests:__subpackages__", + ], +) + +java_library( + name = "jar2", + srcs = [ + "Jar2Class.java", + ], + visibility = [ + "//build_extensions/jar_combiner/javatests:__subpackages__", + ], +) + +java_library( + name = "jar_with_r", + srcs = [ + "Jar1Class.java", + "R.java", + ], + visibility = [ + "//build_extensions/jar_combiner/javatests:__subpackages__", + ], +) diff --git a/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/Jar1Class.java b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/Jar1Class.java new file mode 100644 index 000000000..564618743 --- /dev/null +++ b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/Jar1Class.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarcombiner.fixtures; + +public class Jar1Class { + private Jar1Class() {} +} diff --git a/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/Jar2Class.java b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/Jar2Class.java new file mode 100644 index 000000000..d31256f15 --- /dev/null +++ b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/Jar2Class.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarcombiner.fixtures; + +public class Jar2Class { + private Jar2Class() {} +} diff --git a/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/R.java b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/R.java new file mode 100644 index 000000000..6bc60df87 --- /dev/null +++ b/build_extensions/jar_combiner/javatests/androidx/test/tools/jarcombiner/fixtures/R.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarcombiner.fixtures; + +public class R { + + private R() {} + + public static class Inner {} +} diff --git a/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/BUILD b/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/BUILD new file mode 100644 index 000000000..b685ccf9e --- /dev/null +++ b/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/BUILD @@ -0,0 +1,23 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +package( + default_visibility = [ + "//:__subpackages__", + ], +) + +kt_jvm_library( + name = "jarcreator_lib", + srcs = glob([ + "*.kt", + ]), +) + +java_binary( + name = "jarcreator", + srcs = ["Main.java"], + main_class = "androidx.test.tools.jarcreator.Main", + deps = [ + ":jarcreator_lib", + ], +) diff --git a/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/JarCreator.kt b/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/JarCreator.kt new file mode 100644 index 000000000..246e35a80 --- /dev/null +++ b/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/JarCreator.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025 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. + */ +package androidx.test.tools.jarcreator + +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream + +/** + * This is a simple utility that creates a jar file of input files. + * + * Unlike other solutions like invoking jar command directly or using bazel's + * java_common.pack_sources, this will create a jar without timestamp and with files in the root + * directory of the jar. + */ +fun createJar(args: Array) { + require(args.size >= 2) { "Must provide at least two files: " } + + val outputFile = args[0] + val jarOutputStream = JarOutputStream(FileOutputStream(outputFile, false)) + jarOutputStream.use { + for (i in 1 until args.size) { + val inputFile = File(args[i]) + val inputStream = BufferedInputStream(FileInputStream(inputFile)) + inputStream.use { addToJar(jarOutputStream, inputStream, inputFile) } + } + } +} + +private fun addToJar(jarOutputStream: JarOutputStream, inputStream: InputStream, inputFile: File) { + val entry = JarEntry(inputFile.name) + jarOutputStream.putNextEntry(entry) + inputStream.transferTo(jarOutputStream) +} diff --git a/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/Main.java b/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/Main.java new file mode 100644 index 000000000..0cb0bf699 --- /dev/null +++ b/build_extensions/jar_creator/java/androidx/test/tools/jarcreator/Main.java @@ -0,0 +1,24 @@ +/* + * Copyright 2025 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. + */ +package androidx.test.tools.jarcreator; + +public class Main { + private Main() {} + + public static void main(String[] args) { + JarCreatorKt.createJar(args); + } +} diff --git a/build_extensions/jar_creator/javatests/androidx/test/tools/jarcreator/BUILD b/build_extensions/jar_creator/javatests/androidx/test/tools/jarcreator/BUILD new file mode 100644 index 000000000..2225df551 --- /dev/null +++ b/build_extensions/jar_creator/javatests/androidx/test/tools/jarcreator/BUILD @@ -0,0 +1,11 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + +kt_jvm_test( + name = "JarCreatorTest", + srcs = ["JarCreatorTest.kt"], + deps = [ + "//build_extensions/jar_creator/java/androidx/test/tools/jarcreator:jarcreator_lib", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +) diff --git a/build_extensions/jar_creator/javatests/androidx/test/tools/jarcreator/JarCreatorTest.kt b/build_extensions/jar_creator/javatests/androidx/test/tools/jarcreator/JarCreatorTest.kt new file mode 100644 index 000000000..4cce604d0 --- /dev/null +++ b/build_extensions/jar_creator/javatests/androidx/test/tools/jarcreator/JarCreatorTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.test.tools.jarcreator + +import com.google.common.truth.Truth.assertThat +import java.io.File +import java.util.jar.JarFile +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class JarCreatorTest { + + @Test + fun invalidInput() { + assertThrows(java.lang.IllegalArgumentException::class.java) { createJar(emptyArray()) } + } + + @Test + fun combineJars() { + val outFile = File.createTempFile("createJarOut", ".jar") + val fileToInclude = File.createTempFile("include", ".txt") + + createJar(arrayOf(outFile.absolutePath, fileToInclude.absolutePath)) + + val contents = JarFile(outFile.absolutePath).stream().map { it.name } + assertThat(contents).containsExactly(fileToInclude.name) + } +} diff --git a/build_extensions/jar_validator/BUILD b/build_extensions/jar_validator/BUILD new file mode 100644 index 000000000..327b24cea --- /dev/null +++ b/build_extensions/jar_validator/BUILD @@ -0,0 +1,5 @@ +alias( + name = "jar_validator", + actual = "//build_extensions/jar_validator/java/androidx/test/tools/jarvalidator", + visibility = ["//build_extensions:__subpackages__"], +) diff --git a/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/BUILD b/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/BUILD new file mode 100644 index 000000000..f5517603c --- /dev/null +++ b/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/BUILD @@ -0,0 +1,23 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "jarvalidator_lib", + srcs = glob([ + "*.kt", + ]), + visibility = [ + "//:__subpackages__", + ], +) + +java_binary( + name = "jarvalidator", + srcs = ["Main.java"], + main_class = "androidx.test.tools.jarvalidator.Main", + visibility = [ + "//:__subpackages__", + ], + deps = [ + ":jarvalidator_lib", + ], +) diff --git a/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/JarValidator.kt b/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/JarValidator.kt new file mode 100644 index 000000000..97f5b5901 --- /dev/null +++ b/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/JarValidator.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarvalidator + +import java.io.File +import java.lang.System +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.stream.Stream + +/** Validates the classes contained in a given jar file. */ +fun validateJar(args: Array): Boolean { + require(args.size >= 3) { + "Usage: ..." + } + + val outputFile = File(args[0]) + val expectedPrefixes = args.asList().drop(2) + val nonMatchingEntries = mutableListOf() + val jarFilePath = args[1] + val classes = getClassesInJar(jarFilePath) + for (className in classes) { + if (!matchesExpectedPrefixes(expectedPrefixes, className)) { + nonMatchingEntries.add(className) + } + } + return if (nonMatchingEntries.size > 0) { + nonMatchingEntries.sort() + val error = + "Error: The following classes in $jarFilePath did not match one of the expected prefixes $expectedPrefixes \n" + + nonMatchingEntries.joinToString("\n") + System.err.println(error) + outputFile.writeText(error) + false + } else { + outputFile.writeText("Success!") + true + } +} + +fun getClassesInJar(filePath: String): Stream { + return JarFile(filePath) + .stream() + .filter { it.name.endsWith(".class") } + .map { classNameFromPath(it) } +} + +private fun matchesExpectedPrefixes(expectedPrefixes: List, className: String): Boolean { + for (pkg in expectedPrefixes) { + if (className.startsWith(pkg)) { + return true + } + } + return false +} + +private fun classNameFromPath(it: JarEntry) = it.name.replace('/', '.').replace(".class", "") diff --git a/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/Main.java b/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/Main.java new file mode 100644 index 000000000..c883d01cd --- /dev/null +++ b/build_extensions/jar_validator/java/androidx/test/tools/jarvalidator/Main.java @@ -0,0 +1,11 @@ +package androidx.test.tools.jarvalidator; + +public class Main { + private Main() {} + + public static void main(String[] args) { + if (!JarValidatorKt.validateJar(args)) { + System.exit(1); + } + } +} diff --git a/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/BUILD b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/BUILD new file mode 100644 index 000000000..53a4895cc --- /dev/null +++ b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/BUILD @@ -0,0 +1,15 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + +kt_jvm_test( + name = "JarValidatorTest", + srcs = ["JarValidatorTest.kt"], + data = [ + "//build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures:libmatching.jar", + "//build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures:libnotmatching.jar", + ], + deps = [ + "//build_extensions/jar_validator/java/androidx/test/tools/jarvalidator:jarvalidator_lib", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +) diff --git a/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/JarValidatorTest.kt b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/JarValidatorTest.kt new file mode 100644 index 000000000..ade4f3db2 --- /dev/null +++ b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/JarValidatorTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.test.tools.jarvalidator + +import com.google.common.truth.Truth.assertThat +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.util.Arrays +import kotlin.io.path.Path +import kotlin.io.path.exists +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.function.ThrowingRunnable +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class JarValidatorTest { + + @Test + fun invalidInput() { + assertThrows(java.lang.IllegalArgumentException::class.java) { validateJar(emptyArray()) } + } + + @Test + fun matchingClasses() { + val matchingJarPath = getDataJarPath("libmatching.jar") + assertThat(matchingJarPath.exists()).isTrue() + + val outFile = File.createTempFile("matchingClasses", ".txt") + + assertThat(validateJar(arrayOf(outFile.absolutePath, matchingJarPath.toAbsolutePath().toString(), "androidx.test.tools.jarvalidator.fixtures.matching" ))).isTrue() + } + + @Test + fun notMatchingClasses() { + val matchingJarPath = getDataJarPath("libnotmatching.jar") + assertThat(matchingJarPath.exists()).isTrue() + + val outFile = File.createTempFile("notMatchingClasses", ".txt") + + assertThat(validateJar(arrayOf(outFile.absolutePath, matchingJarPath.toAbsolutePath().toString(), "androidx.test.tools.jarvalidator.fixtures.matching" ))).isFalse() + val outFileContents = outFile.readText() + // assert not matching class is listed + assertThat(outFileContents).contains("androidx.test.tools.jarvalidator.fixtures.notmatching.NotMatching") + assertThat(outFileContents).doesNotContain("androidx.test.tools.jarvalidator.fixtures.matching.Matching") + } + + private fun getDataJarPath(name: String) = + Paths.get(System.getenv("TEST_SRCDIR"), "build_extensions", "jar_validator", "javatests", "androidx", "test", "tools", "jarvalidator", "fixtures" , name) +} \ No newline at end of file diff --git a/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/BUILD b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/BUILD new file mode 100644 index 000000000..1258b9fc9 --- /dev/null +++ b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/BUILD @@ -0,0 +1,22 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package(default_applicable_licenses = ["//:license"]) + +java_library( + name = "matching", + srcs = ["matching/Matching.java"], + visibility = [ + "//build_extensions/jar_validator/javatests:__subpackages__", + ], +) + +java_library( + name = "notmatching", + srcs = [ + "matching/Matching.java", + "notmatching/NotMatching.java", + ], + visibility = [ + "//build_extensions/jar_validator/javatests:__subpackages__", + ], +) diff --git a/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/matching/Matching.java b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/matching/Matching.java new file mode 100644 index 000000000..994df2c8a --- /dev/null +++ b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/matching/Matching.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarvalidator.fixtures.matching; + +public class Matching {} diff --git a/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/notmatching/NotMatching.java b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/notmatching/NotMatching.java new file mode 100644 index 000000000..326003f8a --- /dev/null +++ b/build_extensions/jar_validator/javatests/androidx/test/tools/jarvalidator/fixtures/notmatching/NotMatching.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.jarvalidator.fixtures.notmatching; + +public class NotMatching {} diff --git a/build_extensions/java_service_map.bzl b/build_extensions/java_service_map.bzl index 7a42732cc..103642fc7 100644 --- a/build_extensions/java_service_map.bzl +++ b/build_extensions/java_service_map.bzl @@ -1,94 +1,99 @@ - def java_service_map( - name, providers, compatible_with=None, restricted_to=None, visibility=None): - """ - A rule type that generates service provider mappings for use in Java JAR files + name, + providers, + compatible_with = None, + restricted_to = None, + visibility = None): + """ + A rule type that generates service provider mappings for use in Java JAR files - Args: - name: A unique name. (Name) - providers: Mapping of services to service providers. - (Dictionary of strings to string lists) - A dictionary whose key is the fully-qualified binary name of the service's - type. (i.e. The interface or abstract class.) The value should be a list - of strings that are the fully-qualified names of the concrete - implementations. + Args: + name: A unique name. (Name) + providers: Mapping of services to service providers. + (Dictionary of strings to string lists) + A dictionary whose key is the fully-qualified binary name of the service's + type. (i.e. The interface or abstract class.) The value should be a list + of strings that are the fully-qualified names of the concrete + implementations. - To use, specify a map of services (interfaces, abstract classes, etc.) to - concrete implementations using a java_service_map rule in your BUILD file. - The output of this rule can then be added to the 'deps' of other Java - library or binary targets. For example: + To use, specify a map of services (interfaces, abstract classes, etc.) to + concrete implementations using a java_service_map rule in your BUILD file. + The output of this rule can then be added to the 'deps' of other Java + library or binary targets. For example: - load("//build_extensions/java_service_map.bzl", "java_service_map") + load("//build_extensions/java_service_map.bzl", "java_service_map") - java_service_map( - name = "my_service_map", - providers = { - "com.google.myservice.MyInterface": [ - "com.google.myservice.impl.MyClass", - "com.google.myservice.impl.MyOtherClass", - ], - # ... - }, - ) + java_service_map( + name = "my_service_map", + providers = { + "com.google.myservice.MyInterface": [ + "com.google.myservice.impl.MyClass", + "com.google.myservice.impl.MyOtherClass", + ], + # ... + }, + ) - java_library( - name = "my_service_lib", - srcs = [ - "impl/MyClass.java", - "impl/MyOtherClass.java", - # ... - ], - deps = [ - ":my_service_map", - # ... - ], - ) + java_library( + name = "my_service_lib", + srcs = [ + "impl/MyClass.java", + "impl/MyOtherClass.java", + # ... + ], + deps = [ + ":my_service_map", + # ... + ], + ) - The 'providers' attribute of the build rule is a dictionary that maps service - names to a list of providers. The key of the dictionary is a string that is - the fully-qualified binary name of the service's type. (i.e. The interface or - abstract class.) The value is a list of strings that are the fully-qualified - names of the concrete implementations. The output of this rule is a JAR file - that contains the appropriate files in META-INF/services. + The 'providers' attribute of the build rule is a dictionary that maps service + names to a list of providers. The key of the dictionary is a string that is + the fully-qualified binary name of the service's type. (i.e. The interface or + abstract class.) The value is a list of strings that are the fully-qualified + names of the concrete implementations. The output of this rule is a JAR file + that contains the appropriate files in META-INF/services. - This rule generalizes the strategy outlined by kylemarvin: - http://wiki/Nonconf/JavaLibraryMetaInf - """ + This rule generalizes the strategy outlined by kylemarvin: + http://wiki/Nonconf/JavaLibraryMetaInf + """ - # Make sure the directories exist - files_dir = "$(@D)/" + name + "_files" - services_dir = files_dir + "/META-INF/services" - cmd = "rm -rf " + files_dir + ";" - cmd += "mkdir -p " + services_dir + ";" + # Make sure the directories exist + files_dir = "$(@D)/" + name + "_files" + services_dir = files_dir + "/META-INF/services" + cmd = "rm -rf " + files_dir + ";" + cmd += "mkdir -p " + services_dir + ";" - # Write individual META-INF files - for service, provider_list in sorted(providers.items()): - file_path = services_dir + "/" + service - cmd += ("echo '# Generated by Blaze' > " + file_path + ";") - for provider in provider_list: - cmd += ("echo '" + provider + "' >> " + file_path + ";") + # Write individual META-INF files + for service, provider_list in sorted(providers.items()): + file_path = services_dir + "/" + service + cmd += ("echo '# Generated by Blaze' > " + file_path + ";") + for provider in provider_list: + cmd += ("echo '" + provider + "' >> " + file_path + ";") - # Make a JAR file including all the manifest files. - # Use the 'zip' command instead of 'jar' to help with bazel's output caching. - #cmd += zip_cmd + " -q -jt -X -wd " + files_dir + " -r $@ META-INF" - cmd += "cwd=$$(pwd); " - cmd += "cd " + files_dir + "; " - cmd += "zip -X -r $$cwd/$@ .; " - cmd += "cd $$cwd" + # Make a JAR file including all the manifest files. + # Use the 'zip' command instead of 'jar' to help with bazel's output caching. + #cmd += zip_cmd + " -q -jt -X -wd " + files_dir + " -r $@ META-INF" + cmd += "cwd=$$(pwd); " + cmd += "cd " + files_dir + "; " + cmd += "zip -X -r $$cwd/$@ .; " + cmd += "cd $$cwd" - # Go! - native.genrule( - name = name + "_gen", - srcs = [], - outs = [name + ".jar"], - compatible_with = compatible_with, - restricted_to = restricted_to, - cmd = cmd, - visibility = visibility) + # Go! + native.genrule( + name = name + "_gen", + srcs = [], + outs = [name + ".jar"], + compatible_with = compatible_with, + restricted_to = restricted_to, + cmd = cmd, + visibility = visibility, + ) - native.java_import( - name = name, - jars = [name + ".jar"], - compatible_with = compatible_with, - restricted_to = restricted_to, - visibility = visibility) + native.java_import( + name = name, + jars = [name + ".jar"], + compatible_with = compatible_with, + restricted_to = restricted_to, + visibility = visibility, + ) diff --git a/build_extensions/jetify.bzl b/build_extensions/jetify.bzl new file mode 100644 index 000000000..e1c405669 --- /dev/null +++ b/build_extensions/jetify.bzl @@ -0,0 +1,9 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_binary", "android_library") + +def jetify_android_library(jetify_sources = False, **kwargs): + # ignore, not supported in bazel + android_library(**kwargs) + +def jetify_android_binary(jetify_sources = False, **kwargs): + # ignore, not supported in bazel + android_binary(**kwargs) diff --git a/build_extensions/kt_android_app_instrumentation_tests.bzl b/build_extensions/kt_android_app_instrumentation_tests.bzl deleted file mode 100644 index 90aa4b852..000000000 --- a/build_extensions/kt_android_app_instrumentation_tests.bzl +++ /dev/null @@ -1,75 +0,0 @@ -"""A rule wrapper for a Kotlin instrumentation test for an android binary.""" - -load( - "//build_extensions:generate_instrumentation_tests.bzl", - "generate_instrumentation_tests", -) -load( - "//build_extensions:infer_java_package_name.bzl", - "infer_java_package_name", - "infer_java_package_name_from_label", -) - -def kt_android_app_instrumentation_tests( - name, - binary_target, - srcs, - deps, - target_devices, - test_java_package = None, - binary_target_package = None, - library_args = {}, - binary_args = {}, - **kwargs): - """A macro for a Kotlin instrumentation test whose target under test is an android_binary. - - The intent of this wrapper is to simplify the build API for creating instrumentation test rules - for simple cases, while still supporting build_cleaner for automatic dependency management. - - This will generate: - - a test_lib android_library, containing all sources and dependencies - - a test_binary android_binary (soon to be android_application) - - the manifest to use for the test library. - - for each src + device combination: - - a android_instrumentation_test rule - - Args: - name: the name to use for the generated kt_android_library rule. This is needed for build_cleaner to - manage dependencies - binary_target: the android_binary under test - srcs: the test sources to generate rules for - deps: the build dependencies to use for the generated test library - target_devices: array of device targets to execute on - test_java_package_name: Optional. A custom root package name to use for the tests. If unset - will be derived based on current path to a java source root - binary_target_package: Optional: the android package name of binary_target. If unset, will be - derived from binary target's path to a java source root - library_args: additional arguments to pass to generated android_library - binary_args: additional arguments to pass to generated android_binary - **kwargs: arguments to pass to generated android_instrumentation_test rules - """ - library_name = "%s_library" % name - test_java_package_name = test_java_package if test_java_package else infer_java_package_name() - instrumentation_target_package = binary_target_package if binary_target_package else infer_java_package_name_from_label(binary_target) - - native.kt_android_library( - name = library_name, - srcs = srcs, - testonly = 1, - deps = deps, - **library_args - ) - - generate_instrumentation_tests( - name = name, - srcs = srcs, - deps = [library_name], - target_devices = target_devices, - test_java_package_name = test_java_package_name, - # always append .tests at the end to avoid potential conflict with instrumentation_target_package - test_android_package_name = instrumentation_target_package + ".tests", - instrumentation_target_package = instrumentation_target_package, - instruments = binary_target, - binary_args = binary_args, - **kwargs - ) diff --git a/build_extensions/kt_android_library.bzl b/build_extensions/kt_android_library.bzl new file mode 100644 index 000000000..44bbbc44b --- /dev/null +++ b/build_extensions/kt_android_library.bzl @@ -0,0 +1,8 @@ +"""Wrapper for android_library for bazel. +""" + +load("@io_bazel_rules_kotlin//kotlin:android.bzl", io_kt_android_library = "kt_android_library") + +def kt_android_library(testonly = 1, **kwargs): + # explicitly set testonly to 1 because io_kt_android_library doesn't seem to respect package(default_testonly = 1) + io_kt_android_library(testonly = testonly, **kwargs) diff --git a/build_extensions/kt_android_library_instrumentation_tests.bzl b/build_extensions/kt_android_library_instrumentation_tests.bzl deleted file mode 100644 index 99333af07..000000000 --- a/build_extensions/kt_android_library_instrumentation_tests.bzl +++ /dev/null @@ -1,72 +0,0 @@ -"""A rule wrapper for an kotlin instrumentation test for an android library.""" - -load( - "//build_extensions:generate_instrumentation_tests.bzl", - "generate_instrumentation_tests", -) -load( - "//build_extensions:infer_java_package_name.bzl", - "infer_java_package_name", -) - -def kt_android_library_instrumentation_tests(name, srcs, deps, target_devices, - test_java_package = None, library_args = {}, - binary_args = {}, **kwargs): - """A macro for an kotlin instrumentation test whose target under test is an (kt_)android_library. - - Will generate a 'self-instrumentating' test binary and other associated rules - - The intent of this wrapper is to simplify the build API for creating instrumentation test rules - for simple cases, while still supporting build_cleaner for automatic dependency management. - - This will generate: - - an unused stub android_binary under test, to placate bazel - - a test_lib android_library, containing all sources and dependencies - - a test_binary android_binary (soon to be android_application) - - the manifest to use for the test library. - - for each device combination: - - an android_instrumentation_test rule) - - Args: - name: the name to use for the generated android_library rule. This is needed for build_cleaner to - manage dependencies - srcs: the test sources to generate rules for - deps: the build dependencies to use for the generated test binary - target_devices: array of device targets to execute on - test_java_package: Optional. A custom root package name to use for the tests. If unset - will be derived based on current path to a java source root - library_args: additional arguments to pass to generated android_library - binary_args: additional arguments to pass to generated android_binary - **kwargs: arguments to pass to generated android_instrumentation_test rules - """ - library_name = "%s_library" % name - test_java_package_name = test_java_package if test_java_package else infer_java_package_name() - - native.android_binary( - name = "target_stub_binary", - manifest = "//build_extensions:AndroidManifest_target_stub.xml", - # use the same package name as the test package, so it gets overridden - manifest_values = {"applicationId": test_java_package_name}, - testonly = 1, - ) - - native.kt_android_library( - name = library_name, - srcs = srcs, - testonly = 1, - deps = deps, - **library_args - ) - - generate_instrumentation_tests( - name = name, - srcs = srcs, - deps = [library_name], - target_devices = target_devices, - test_java_package_name = test_java_package_name, - test_android_package_name = test_java_package_name, - instrumentation_target_package = test_java_package_name, - instruments = ":target_stub_binary", - binary_args = binary_args, - **kwargs - ) diff --git a/build_extensions/maven/BUILD b/build_extensions/maven/BUILD new file mode 100644 index 000000000..fe16132c5 --- /dev/null +++ b/build_extensions/maven/BUILD @@ -0,0 +1,17 @@ +licenses(["notice"]) + +package(default_visibility = [ + "//visibility:public", +]) + +# Used to generate a maven artifact. +sh_binary( + name = "maven_artifact_sh", + srcs = ["maven_artifact.sh"], +) + +java_binary( + name = "jarjar_bin", + main_class = "org.pantsbuild.jarjar.Main", + runtime_deps = ["@maven//:org_pantsbuild_jarjar"], +) diff --git a/build_extensions/maven/add_or_update_file_in_zip.bzl b/build_extensions/maven/add_or_update_file_in_zip.bzl new file mode 100644 index 000000000..00d923ce7 --- /dev/null +++ b/build_extensions/maven/add_or_update_file_in_zip.bzl @@ -0,0 +1,26 @@ +"""Update a zip file to update or add a particular file within.""" + +def add_or_update_file_in_zip(ctx, name, src, out, update_src, update_path): + """Update a zip file to update or add a particular file within. + + """ + ctx.actions.run_shell( + inputs = [src, update_src], + outputs = [out], + mnemonic = "AndroidxTestZipUpdate", + command = ";".join([ + "tmp={}_tmp".format(name), + "rm -rf $$tmp", + "mkdir -p $$tmp", + "cp {update_src} $$tmp/{update_path}".format( + update_src = update_src.path, + update_path = update_path, + ), + "zip -j -X -q -l " + + "{src} $$tmp/{update_path} -O {out}".format( + src = src.path, + update_path = update_path, + out = out.path, + ), + ]), + ) diff --git a/build_extensions/maven/axt_android_aar.bzl b/build_extensions/maven/axt_android_aar.bzl new file mode 100644 index 000000000..a8a4b3603 --- /dev/null +++ b/build_extensions/maven/axt_android_aar.bzl @@ -0,0 +1,129 @@ +"""Generate AXT android archive (aar).""" + +load("@build_bazel_rules_android//providers:providers.bzl", "AndroidLibraryAarInfo") +load("@rules_java//java:defs.bzl", "JavaInfo", "java_common") +load("//build_extensions/maven:add_or_update_file_in_zip.bzl", "add_or_update_file_in_zip") +load("//build_extensions/maven:combine_jars.bzl", "combine_jars") +load("//build_extensions/maven:jarjar.bzl", "jarjar_rule") +load("//build_extensions/maven:maven_info.bzl", "MavenFilesInfo", "MavenInfo", "collect_maven_info") + +def _android_aar_impl(ctx): + # current_aar will include almost everything needed: an AndroidManifest.xml, compiled resources, + # and a proguard.txt. However, its classes.jar will only include direct compiled srcs. Missing + # will be any sources from transitive dependencies that also need to be bundled in the aar + current_aar = ctx.attr.included_dep[AndroidLibraryAarInfo].aar + if not current_aar: + fail("included_dep %s does not produce an aar. Is it an android_library rule with an AndroidManifest.xml?" % ctx.attr.included_dep.label) + + if not ctx.attr.included_dep[MavenInfo].artifact: + fail("Could not find maven artifact for included_dep %s. Is it listed in maven_registry?" % ctx.attr.included_dep.label) + + # build a combined classes jar from all dependencies that are part of this maven artifact + classes_jar = ctx.actions.declare_file(ctx.attr.name + "_combined_classes.jar") + combine_jars( + ctx = ctx, + input_jars_deps = ctx.attr.included_dep[MavenInfo].transitive_included_runtime_jars, + output = classes_jar, + ) + + # optionally use jarjar to rename shaded dependencies + if (ctx.attr.jarjar_rule): + jarjar_classes_jar = ctx.actions.declare_file(ctx.attr.name + "_jarjar_classes.jar") + jarjar_rule(ctx, rule = ctx.file.jarjar_rule, src = classes_jar, out = jarjar_classes_jar) + classes_jar = jarjar_classes_jar + + # TODO: tying validation to an output was the only way to get this to run, but + # according to docs it shouldn't be necessary + #validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation") + validation_output = ctx.outputs.validation + ctx.actions.run( + inputs = [classes_jar], + outputs = [validation_output], + mnemonic = "AxtAndroidAarValidateJar", + executable = ctx.executable._validate_jar_java, + arguments = [validation_output.path, classes_jar.path] + ctx.attr.expected_class_prefixes, + ) + _validate_maven_deps(sorted(ctx.attr.included_dep[MavenInfo].transitive_maven_direct_deps.to_list()), ctx.attr.banned_maven_deps) + + # update the aar with the new classes.jar + add_or_update_file_in_zip( + ctx, + ctx.attr.name + "_classes", + src = current_aar, + out = ctx.outputs.aar, + update_src = classes_jar, + update_path = "classes.jar", + ) + + # produce src jar + combine_jars( + ctx = ctx, + input_jars_deps = ctx.attr.included_dep[MavenInfo].transitive_included_src_jars, + output = ctx.outputs.src_jar, + ) + + return [ + ctx.attr.included_dep[MavenInfo], + MavenFilesInfo(runtime = ctx.outputs.aar, src_jar = ctx.outputs.src_jar, validation = validation_output), + OutputGroupInfo(_validation = depset([validation_output])), + ] + +def _validate_maven_deps(maven_deps, banned_dep_patterns): + for banned_dep_pattern in banned_dep_patterns: + for dep in maven_deps: + if banned_dep_pattern in dep: + fail("%s is not an allowed dependency" % dep) + +axt_android_aar = rule( + implementation = _android_aar_impl, + attrs = { + "included_dep": attr.label( + doc = "The android_library target to use as a basis for the android archive. " + + "This must include an AndroidManifest.xml, and optionally resources, proguard_specs", + mandatory = True, + providers = [JavaInfo, AndroidLibraryAarInfo], + aspects = [collect_maven_info], + ), + "expected_class_prefixes": attr.string_list( + doc = "The list of class prefixes expected to be containing in resulting .aar. All classes in aar must match at least one of the given prefixes.", + mandatory = True, + ), + "jarjar_rule": attr.label( + doc = "Optional file containing jarjar rules to be applied to the classes.", + mandatory = False, + allow_single_file = [".txt"], + ), + "banned_maven_deps": attr.string_list( + doc = ("List of strings that specify the set of disallowed maven dependencies. The " + + "rule will fail if any maven dependency contains one or more of these strings."), + default = ["com.google.guava:guava", "com.google.dagger"], + ), + "_jdk": attr.label( + default = Label("@bazel_tools//tools/jdk"), + providers = [java_common.JavaRuntimeInfo], + ), + "_combine_jars_java": attr.label( + executable = True, + cfg = "exec", + allow_files = True, + default = Label("//build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner"), + ), + "_validate_jar_java": attr.label( + executable = True, + cfg = "exec", + allow_files = True, + default = Label("//build_extensions/jar_validator/java/androidx/test/tools/jarvalidator"), + ), + "_jarjar": attr.label( + default = Label("//build_extensions/maven:jarjar_bin"), + executable = True, + cfg = "exec", + ), + }, + outputs = { + "aar": "%{name}.aar", + "src_jar": "%{name}-src.jar", + # TODO: remove, this shouldn't be necessary + "validation": "%{name}.validation", + }, +) diff --git a/build_extensions/maven/axt_maven_apk.bzl b/build_extensions/maven/axt_maven_apk.bzl new file mode 100644 index 000000000..4232539d5 --- /dev/null +++ b/build_extensions/maven/axt_maven_apk.bzl @@ -0,0 +1,40 @@ +"""Generate AXT android archive (aar).""" + +load("@build_bazel_rules_android//providers:providers.bzl", "ApkInfo") +load("@rules_java//java:defs.bzl", "JavaInfo") +load("//build_extensions/maven:combine_jars.bzl", "combine_jars") +load("//build_extensions/maven:maven_info.bzl", "MavenFilesInfo", "MavenInfo", "collect_maven_apk_info") + +def _axt_maven_apk_impl(ctx): + # produce src jar + combine_jars( + ctx = ctx, + input_jars_deps = ctx.attr.included_dep[MavenInfo].transitive_included_src_jars, + output = ctx.outputs.src_jar, + ) + + return [ + ctx.attr.included_dep[MavenInfo], + MavenFilesInfo(runtime = ctx.attr.included_dep[ApkInfo].signed_apk, src_jar = ctx.outputs.src_jar, validation = None), + ] + +axt_maven_apk = rule( + implementation = _axt_maven_apk_impl, + attrs = { + "included_dep": attr.label( + doc = "The android_binary to publish", + mandatory = True, + providers = [JavaInfo, ApkInfo], + aspects = [collect_maven_apk_info], + ), + "_combine_jars_java": attr.label( + executable = True, + cfg = "exec", + allow_files = True, + default = Label("//build_extensions/jar_combiner/java/androidx/test/tools/jarcombiner"), + ), + }, + outputs = { + "src_jar": "%{name}-src.jar", + }, +) diff --git a/build_extensions/maven/combine_jars.bzl b/build_extensions/maven/combine_jars.bzl new file mode 100644 index 000000000..00b934f94 --- /dev/null +++ b/build_extensions/maven/combine_jars.bzl @@ -0,0 +1,28 @@ +"""Combines multiple jars into one jar.""" + +def combine_jars(ctx, input_jars_deps, output): + """Combine several jars into a single jar. + + Bazel wrapper for build_extensions/jar_combiner. + + Args: + ctx: the rule context + input_jars_deps: depset of input jars + output: the output file path to use + """ + if not input_jars_deps: + fail("must provide at least one input_jar") + if not output: + fail("must provide output file") + + args = ctx.actions.args() + args.add(output) + args.add_all(input_jars_deps) + + ctx.actions.run( + executable = ctx.executable._combine_jars_java, + mnemonic = "AndroidxTestCombineJars", + inputs = input_jars_deps, + arguments = [args], + outputs = [output], + ) diff --git a/build_extensions/maven/jarjar.bzl b/build_extensions/maven/jarjar.bzl new file mode 100644 index 000000000..3adeb565a --- /dev/null +++ b/build_extensions/maven/jarjar.bzl @@ -0,0 +1,58 @@ +"""Runs jarjar over a jar file.""" + +load("@rules_java//java:defs.bzl", "java_common") + +def jarjar_rule(ctx, rule, src, out): + """API to run jarjar from a rule. + + See https://github.com/pantsbuild/jarjar. + + Args: + ctx: the context + rule: a text file containing the list of jarjar transforms to apply + src: The input jar + out: The output jar. + + """ + args = ctx.actions.args() + args.add("process") + args.add(rule) + args.add(src) + args.add(out) + + ctx.actions.run( + executable = ctx.executable._jarjar, + inputs = [rule, src], + outputs = [out], + arguments = [args], + mnemonic = "JarJar", + ) + +def _jarjar_impl(ctx): + jarjar_rule(ctx, ctx.file.rule, ctx.file.src, ctx.outputs.jar) + +jarjar = rule( + implementation = _jarjar_impl, + attrs = { + "src": attr.label( + doc = "Jar file to transform", + allow_single_file = [".jar"], + ), + "rule": attr.label( + doc = "File containing jarjar rules to be applied to the classes.", + allow_single_file = [".txt"], + ), + "_jdk": attr.label( + default = Label("@bazel_tools//tools/jdk"), + providers = [java_common.JavaRuntimeInfo], + ), + "_jarjar": attr.label( + default = Label("//build_extensions/maven:jarjar_bin"), + executable = True, + cfg = "exec", + ), + }, + outputs = { + "jar": "%{name}.jar", + }, +) diff --git a/build_extensions/maven/kotlin_info.bzl b/build_extensions/maven/kotlin_info.bzl new file mode 100644 index 000000000..f7e0a4141 --- /dev/null +++ b/build_extensions/maven/kotlin_info.bzl @@ -0,0 +1,6 @@ +"""Utility for determing if given target is a kotlin target""" + +load("@io_bazel_rules_kotlin//kotlin/internal:defs.bzl", "KtJvmInfo") + +def is_kotlin(target): + return KtJvmInfo in target diff --git a/build_extensions/maven/maven_artifact.bzl b/build_extensions/maven/maven_artifact.bzl new file mode 100644 index 000000000..8a55f9f96 --- /dev/null +++ b/build_extensions/maven/maven_artifact.bzl @@ -0,0 +1,272 @@ +"""Skylark rule to create a maven repository from a single artifact.""" + +load("//build_extensions/maven:maven_info.bzl", "MavenFilesInfo", "MavenInfo") + +_pom_tmpl = "\n".join([ + '', + '', + " 4.0.0", + " {group_id}", + " {artifact_id}", + " {version}", + " {packaging}", + " AndroidX Test Library", + " The AndroidX Test Library provides an extensive framework for testing Android apps", + " https://developer.android.com/testing", + " 2015", + " ", + "{licenses}", + " ", + " ", + " ", + " The Android Open Source Project", + " ", + " ", + " ", + "{dependencies}", + " ", + "", + "", +]) + +_dependency_tmpl = "\n".join([ + " ", + " {group_id}", + " {artifact_id}", + " {version}", + " compile", + "{exclusions}", + " ", +]) + +_exclusions_tmpl = "\n".join([ + " ", + "{exclusion}", + " ", +]) + +_exclusion_tmpl = "\n".join([ + " ", + " {groupId}", + " {artifactId}", + " ", + "", +]) + +_metadata_tmpl = "\n".join([ + '', + "", + " {group_id}", + " {artifact_id}", + " {version}", + " ", + " {version}", + " ", + " {version}", + " ", + " {last_updated}", + " ", + "", + "", +]) + +_license_impl = "\n".join([ + " ", + " {name}", + " {url}", + " repo", + " ", +]) + +def _packaging_type(f): + """Returns the packaging type used by the file f.""" + if f.basename.endswith(".aar"): + return "aar" + elif f.basename.endswith(".apk"): + return "apk" + elif f.basename.endswith(".jar"): + return "jar" + fail("Artifact has unknown packaging type: %s" % f.short_path) + +def _create_pom_string( + ctx, + group_id, + artifact_id, + version, + packaging_type, + maven_dependencies, + maven_dependencies_exclusions = {}): + """Returns the contents of the pom file as a string.""" + dependencies = [] + for dep in maven_dependencies: + dep_group_id, dep_artifact_id, dep_version = _parse_artifact_versioning(dep) + exclusions_string = _create_exclusions_string(dep_group_id, dep_artifact_id, maven_dependencies_exclusions) + dependencies.append(_dependency_tmpl.format( + group_id = dep_group_id, + artifact_id = dep_artifact_id, + version = dep_version, + exclusions = exclusions_string, + )) + + licenses = [] + if ctx.attr.license_name and ctx.attr.license_url: + licenses.append(_license_impl.format( + name = ctx.attr.license_name, + url = ctx.attr.license_url, + )) + else: + licenses.append(_license_impl.format( + name = "The Apache Software License, Version 2.0", + url = "http://www.apache.org/licenses/LICENSE-2.0.txt", + )) + + return _pom_tmpl.format( + group_id = group_id, + artifact_id = artifact_id, + version = version, + packaging = packaging_type, + dependencies = "\n".join(dependencies), + licenses = "\n".join(licenses), + ) + +def _create_metadata_string(ctx, group_id, artifact_id, version): + """Returns the string contents of maven-metadata.xml for the group.""" + return _metadata_tmpl.format( + group_id = group_id, + artifact_id = artifact_id, + version = version, + last_updated = ctx.attr.last_updated, + ) + +def _create_exclusions_string(group_id, artifact_id, excluded_dependencies_map): + """Returns the string contents of excluded dependencies for the dependency.""" + excluded_dependencies_csv = excluded_dependencies_map.get("%s:%s" % (group_id, artifact_id)) + if not excluded_dependencies_csv: + return "" + excluded_dependencies = excluded_dependencies_csv.split(",") + excluded_dependencies_string = "" + for excluded_dependency in excluded_dependencies: + excluded_group, excluded_artifact = _parse_group_artifact(excluded_dependency) + excluded_dependencies_string += _exclusion_tmpl.format( + groupId = excluded_group, + artifactId = excluded_artifact, + ) + return _exclusions_tmpl.format(exclusion = excluded_dependencies_string) + +def _parse_artifact_versioning(artifact_coordinates): + """Parse out artifact_id, version and group info from a full coordinate strings. + + Expected format groupId:artifactId:[type:]version + """ + segments = artifact_coordinates.split(":") + if len(segments) == 3: + return segments + elif len(segments) == 4: + return segments[0], segments[1], segments[3] + + fail("artifact_deps values must be of form: groupId:artifactId:[type:]version. Found %s" % artifact_coordinates) + +def _parse_group_artifact(artifact_coordinates): + """Parse out artifact_id, and group info from a coordinate string""" + if artifact_coordinates.count(":") != 1: + fail("artifact_deps values must be of form: groupId:artifactId. Found %s" % artifact_coordinates) + + return artifact_coordinates.split(":") + +def _rename_artifact(ctx, tpl_string, src_file, packaging_type, artifact_id, version): + """Rename the artifact to match maven naming conventions.""" + artifact = ctx.actions.declare_file(tpl_string % (artifact_id, version, packaging_type)) + ctx.actions.run_shell( + inputs = [src_file], + outputs = [artifact], + command = "cp %s %s" % (src_file.path, artifact.path), + mnemonic = "AndroidxMavenArtifactRename", + ) + return artifact + +def _maven_artifact_impl(ctx): + """Generates maven repository for a single artifact.""" + + group_id, artifact_id, version = _parse_artifact_versioning(ctx.attr.target[MavenInfo].artifact) + pom = ctx.actions.declare_file( + "%s-%s.pom" % (artifact_id, version), + ) + + maven_deps = sorted(ctx.attr.target[MavenInfo].transitive_maven_direct_deps.to_list()) + + packaging_type = _packaging_type(ctx.attr.target[MavenFilesInfo].runtime) + pom_content = _create_pom_string(ctx, group_id, artifact_id, version, packaging_type, maven_deps, ctx.attr.excluded_dependencies) + ctx.actions.write(output = pom, content = pom_content) + + metadata = ctx.actions.declare_file("%s_maven-metadata.xml" % (ctx.label.name)) + ctx.actions.write(output = metadata, content = _create_metadata_string(ctx, group_id, artifact_id, version)) + + # Rename binary artifact to artifact_id-version.packaging_type + artifact = _rename_artifact(ctx, "%s-%s.%s", ctx.attr.target[MavenFilesInfo].runtime, packaging_type, artifact_id, version) + + arguments = [ + "--group_path=%s" % group_id.replace(".", "/"), + "--artifact_id=%s" % artifact_id, + "--version=%s" % version, + "--artifact=%s" % artifact.path, + "--pom=%s" % pom.path, + "--metadata=%s" % metadata.path, + "--output=%s" % ctx.outputs.m2repository.path, + ] + inputs = [pom, metadata, artifact] + + if ctx.attr.target[MavenFilesInfo].src_jar: + # Rename sources jar artifact to artifact_id-version-sources.jar + source = _rename_artifact(ctx, "%s-%s-sources.%s", ctx.attr.target[MavenFilesInfo].src_jar, "jar", artifact_id, version) + arguments.append("--source=%s" % source.path) + inputs.append(source) + + # TODO: remove, shouldn't be necessary + # add validation to inputs so it runs on rebuild + if ctx.attr.target[MavenFilesInfo].validation: + inputs.append(ctx.attr.target[MavenFilesInfo].validation) + + ctx.actions.run( + inputs = inputs, + outputs = [ctx.outputs.m2repository], + arguments = arguments, + executable = ctx.executable._maven_artifact_sh, + progress_message = ( + "Packaging repository: %s" % ctx.outputs.m2repository.short_path + ), + mnemonic = "AndroidxMavenRepositoryGen", + ) + +maven_artifact = rule( + implementation = _maven_artifact_impl, + attrs = { + "target": attr.label( + doc = "The target to be published to maven. Must be a axt_android_aar or axt_android_apk", + mandatory = True, + providers = [MavenInfo, MavenFilesInfo], + ), + # TODO: derive this? + "last_updated": attr.string(mandatory = True), + "license_file": attr.label( + mandatory = False, + allow_single_file = ["LICENSE"], + ), + "license_name": attr.string(mandatory = False), + "license_url": attr.string(mandatory = False), + "excluded_dependencies": attr.string_dict( + mandatory = False, + doc = "Map of maven dependency to a csv list of excluded dependencies. eg {\"com.google.foo:foo\":\"com.google.bar:bar,com.google.bar:bar-none\"}", + ), + "_maven_artifact_sh": attr.label( + default = Label("//build_extensions/maven:maven_artifact_sh"), + executable = True, + allow_files = True, + cfg = "exec", + ), + }, + outputs = { + "m2repository": "%{name}.zip", + }, +) diff --git a/build_extensions/maven_artifact.sh b/build_extensions/maven/maven_artifact.sh similarity index 92% rename from build_extensions/maven_artifact.sh rename to build_extensions/maven/maven_artifact.sh index 33342ce20..a024120d0 100755 --- a/build_extensions/maven_artifact.sh +++ b/build_extensions/maven/maven_artifact.sh @@ -63,8 +63,8 @@ do done set -- "${POSITIONAL[@]}" # restore positional parameters in case we need that -out_tmp="$(mktemp -d --suffix=_repo)" -dirname="m2repository" +out_tmp="$(mktemp -d )" +dirname="repository" repo="$out_tmp/$dirname" @@ -74,8 +74,10 @@ cp "$FLAGS_metadata" "$repo/$FLAGS_group_path/$FLAGS_artifact_id/" cp "$FLAGS_pom" "$repo/$FLAGS_group_path/$FLAGS_artifact_id/$FLAGS_version/" cp "$FLAGS_artifact" \ "$repo/$FLAGS_group_path/$FLAGS_artifact_id/$FLAGS_version/" -cp "$FLAGS_source" \ - "$repo/$FLAGS_group_path/$FLAGS_artifact_id/$FLAGS_version/" +if [ ! -z "$FLAGS_source" ]; then + cp "$FLAGS_source" \ + "$repo/$FLAGS_group_path/$FLAGS_artifact_id/$FLAGS_version/" +fi for file in $(find "$repo" -type f); do echo -n "$(sha1sum "$file" | cut -f 1 -d ' ')" > "$file.sha1" diff --git a/build_extensions/maven/maven_info.bzl b/build_extensions/maven/maven_info.bzl new file mode 100644 index 000000000..f26849306 --- /dev/null +++ b/build_extensions/maven/maven_info.bzl @@ -0,0 +1,210 @@ +# Copyright 2023 The Android Open Source Project. All rights reserved. +# +# 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. +# +"""Starlark rules to collect Maven artifacts information. +""" + +load("@rules_java//java:defs.bzl", "JavaInfo") +load("//build_extensions:axt_deps_versions.bzl", "KOTLIN_VERSION") +load("//build_extensions/maven:kotlin_info.bzl", "is_kotlin") +load("//build_extensions/maven:maven_registry.bzl", "get_artifact_from_label", "get_maven_apk_deps", "is_axt_label", "is_shaded_from_label") + +# logic here largely inspired from https://github.com/google/dagger/blob/master/tools/maven_info.bzl + +# provider for the built maven files to be published +MavenFilesInfo = provider( + fields = { + "runtime": """ + The runtime artifact. Either a .jar, .aar or .apk + """, + "src_jar": """ + The jar of source files + """, + "validation": """ + The validation output file + """, + }, +) + +# provider for info on building the maven artifacts and its related dependency info +MavenInfo = provider( + fields = { + "artifact": """ + The Maven coordinate for the artifact that is exported by this target. Can only be null if is_compileonly or is_shaded are True. + """, + "is_compileOnly": """ + True if this target is a compile only dependency, and should not be included in final combined artifact. + """, + "is_shaded": """ + True if this is a shaded dependency and it should be directly bundled inside the final artifact. + """, + "transitive_included_runtime_jars": """ + depset of jars to bundle into same artifact. + """, + "transitive_included_src_jars": """ + depset of src jars to bundle into same artifact. + """, + "transitive_maven_direct_deps": """ + depset of external maven artifacts that are a direct dependency of this artifact. + """, + }, +) + +MAVEN_COORDINATES_PREFIX = "maven_coordinates=" + +def _collect_maven_info_impl(target, ctx): + tags = getattr(ctx.rule.attr, "tags", []) + neverlink = getattr(ctx.rule.attr, "neverlink", False) + deps = getattr(ctx.rule.attr, "deps", []) + exports = getattr(ctx.rule.attr, "exports", []) + + # ignore non-runtime or non-java dependencies eg proto_library + if neverlink or JavaInfo not in target: + return MavenInfo( + artifact = None, + is_compileOnly = True, + is_shaded = False, + transitive_included_runtime_jars = depset(), + transitive_included_src_jars = depset(), + transitive_maven_direct_deps = depset(), + ) + + artifact = get_artifact_from_label(target.label) + is_shaded = is_shaded_from_label(target.label) + + if not artifact and not is_shaded: + for tag in tags: + if tag.startswith(MAVEN_COORDINATES_PREFIX): + if artifact: + fail("%s: Each target must belong to one and only one maven artifact: Does target have multiple '%s' tags?" % (target.label, MAVEN_COORDINATES_PREFIX)) + artifact = tag[len(MAVEN_COORDINATES_PREFIX):] + + included_runtime_jars = [] + included_runtime_jars += target[JavaInfo].runtime_output_jars + + # shaded source won't be correct, so just exclude it + included_src_jars = [] if is_shaded else target[JavaInfo].source_jars + transitive_included_runtime_jars = [] + transitive_included_src_jars = [] + maven_direct_deps = [] + transitive_maven_direct_deps = [] + + # add implicit runtime dependencies needed by certain rules + if is_kotlin(target): + maven_direct_deps.append("org.jetbrains.kotlin:kotlin-stdlib:%s" % KOTLIN_VERSION) + + grpc_protobuf_jar = _findGrpcJavaProtoJar(target) + if grpc_protobuf_jar: + # java_grpc_library rule's generated code implicitly depends on this artifact + # which needs to be shaded + included_runtime_jars.append(grpc_protobuf_jar) + + # this is a bit of a hack, but java_lite_proto_library have empty runtime_output_jars! + # So add in all transitive_runtime_jars instead, because we know for protos all dependencies must + # be embedded in resulting aar + if _isJavaProtoTarget((deps + exports)): + included_runtime_jars = target[JavaInfo].transitive_runtime_jars.to_list() + + for dep in (deps + exports): + if is_shaded: + transitive_included_runtime_jars.append(dep[MavenInfo].transitive_included_runtime_jars) + if dep[MavenInfo].artifact == artifact: + transitive_included_runtime_jars.append(dep[MavenInfo].transitive_included_runtime_jars) + transitive_included_src_jars.append(dep[MavenInfo].transitive_included_src_jars) + + # also need to pick up any external maven deps from this included dep + transitive_maven_direct_deps.append(dep[MavenInfo].transitive_maven_direct_deps) + elif dep[MavenInfo].is_shaded: + transitive_included_runtime_jars.append(dep[MavenInfo].transitive_included_runtime_jars) + elif dep[MavenInfo].artifact: + # this dependency is an external maven artifact, just need to add it to direct deps + maven_direct_deps.append(dep[MavenInfo].artifact) + elif not dep[MavenInfo].is_compileOnly and not dep[MavenInfo].is_shaded: + fail("%s: Each target must belong to one and only one maven artifact: Did not find maven info for dep %s" % (target.label, dep.label)) + + return [MavenInfo( + artifact = artifact, + is_compileOnly = False, + is_shaded = is_shaded, + transitive_included_runtime_jars = depset(included_runtime_jars, transitive = transitive_included_runtime_jars), + transitive_included_src_jars = depset(included_src_jars, transitive = transitive_included_src_jars), + transitive_maven_direct_deps = depset(maven_direct_deps, transitive = transitive_maven_direct_deps), + )] + +def _isJavaProtoTarget(all_deps): + for dep in all_deps: + if ProtoInfo in dep: + return True + return False + +def _findGrpcJavaProtoJar(target): + for jar in target[JavaInfo].transitive_runtime_jars.to_list(): + if "grpc-java/protobuf-lite" in jar.path: + return jar + return None + +collect_maven_info = aspect( + attr_aspects = [ + "deps", + "exports", + ], + doc = """ + Collects the Maven information for targets, their dependencies, and their transitive exports. + """, + implementation = _collect_maven_info_impl, +) + +def _collect_maven_apk_info_impl(target, ctx): + neverlink = getattr(ctx.rule.attr, "neverlink", False) + deps = getattr(ctx.rule.attr, "deps", []) + exports = getattr(ctx.rule.attr, "exports", []) + + # ignore non-runtime or non-java dependencies eg proto_library or non-axt-labels + if neverlink or JavaInfo not in target or not is_axt_label(target.label): + return MavenInfo( + artifact = None, + is_compileOnly = True, + is_shaded = False, + transitive_included_runtime_jars = depset(), + transitive_included_src_jars = depset(), + transitive_maven_direct_deps = depset(), + ) + + artifact = get_artifact_from_label(target.label) + included_src_jars = target[JavaInfo].source_jars + transitive_included_src_jars = [] + + for dep in (deps + exports): + transitive_included_src_jars.append(dep[MavenInfo].transitive_included_src_jars) + + maven_deps = get_maven_apk_deps(artifact) + return [MavenInfo( + artifact = artifact, + is_compileOnly = False, + is_shaded = False, + transitive_included_runtime_jars = depset(), + transitive_included_src_jars = depset(included_src_jars, transitive = transitive_included_src_jars), + transitive_maven_direct_deps = depset(maven_deps), + )] + +collect_maven_apk_info = aspect( + attr_aspects = [ + "deps", + "exports", + ], + doc = """ + Collects the Maven apk information for targets, their dependencies, and their transitive exports. + """, + implementation = _collect_maven_apk_info_impl, +) diff --git a/build_extensions/maven/maven_registry.bzl b/build_extensions/maven/maven_registry.bzl new file mode 100644 index 000000000..01c7d5f27 --- /dev/null +++ b/build_extensions/maven/maven_registry.bzl @@ -0,0 +1,127 @@ +"""Defines maven artifact definitions""" + +load( + "//build_extensions:axt_deps_versions.bzl", + "GRPC_VERSION", +) +load( + "//build_extensions:axt_versions.bzl", + "ANDROIDX_JUNIT_VERSION", + "ANDROIDX_TRUTH_VERSION", + "CORE_VERSION", + "ESPRESSO_DEVICE_VERSION", + "ESPRESSO_VERSION", + "MONITOR_VERSION", + "ORCHESTRATOR_VERSION", + "RULES_VERSION", + "RUNNER_VERSION", + "SERVICES_VERSION", +) + +# map of target path prefixes to maven artifact. +# This map is based on the androidx.test architecture principle that there is one and only one maven artifact for all targets +# under a given directory. Or in other words, that code in this repo is organized according to which +# maven artifact it belongs to. +_TARGET_TO_MAVEN_ARTIFACT = { + "//runner/android_junit_runner/java/": "androidx.test:runner:%s" % RUNNER_VERSION, + "//runner/rules/java/": "androidx.test:rules:%s" % RULES_VERSION, + "//runner/rules:rules": "androidx.test:rules:%s" % RULES_VERSION, + "//runner/monitor/java/": "androidx.test:monitor:%s" % MONITOR_VERSION, + "//runner/monitor:monitor": "androidx.test:monitor:%s" % MONITOR_VERSION, + "//core/java/": "androidx.test:core:%s" % CORE_VERSION, + "//ktx/core/java/": "androidx.test:core-ktx:%s" % CORE_VERSION, + "//espresso/accessibility/java/": "androidx.test.espresso:espresso-accessibility:%s" % ESPRESSO_VERSION, + "//espresso/contrib/java/": "androidx.test.espresso:espresso-contrib:%s" % ESPRESSO_VERSION, + "//espresso/core/java/": "androidx.test.espresso:espresso-core:%s" % ESPRESSO_VERSION, + "//espresso/device/java/": "androidx.test.espresso:espresso-device:%s" % ESPRESSO_DEVICE_VERSION, + "//espresso/idling_resource/java/": "androidx.test.espresso:espresso-idling-resource:%s" % ESPRESSO_VERSION, + "//espresso/idling_resource/concurrent/java/": "androidx.test.espresso.idling:idling-concurrent:%s" % ESPRESSO_VERSION, + "//espresso/idling_resource/net/java/": "androidx.test.espresso.idling:idling-net:%s" % ESPRESSO_VERSION, + "//espresso/intents/java/": "androidx.test.espresso:espresso-intents:%s" % ESPRESSO_VERSION, + "//espresso/remote/java/": "androidx.test.espresso:espresso-remote:%s" % ESPRESSO_VERSION, + "//espresso/web/java/": "androidx.test.espresso:espresso-web:%s" % ESPRESSO_VERSION, + "//ext/junit/java/": "androidx.test.ext:junit:%s" % ANDROIDX_JUNIT_VERSION, + "//ktx/ext/junit/java/": "androidx.test.ext:junit-ktx:%s" % ANDROIDX_JUNIT_VERSION, + "//ext/truth/java/": "androidx.test.ext:truth:%s" % ANDROIDX_TRUTH_VERSION, + "//services/storage/java/": "androidx.test.services:storage:%s" % SERVICES_VERSION, + + # services/events/java gets built into both androidx.test.runner as well as orchestrator v2 + "//services/events/java/": "androidx.test:runner:%s" % RUNNER_VERSION, + "//services:test_services": "androidx.test.services:test-services:%s" % SERVICES_VERSION, + "//runner/android_test_orchestrator/stubapp:stubapp": "androidx.test:orchestrator:%s" % ORCHESTRATOR_VERSION, + + # map gRPC deps introduced by bazel grpc rules + "@@grpc-java~//okhttp:okhttp": "io.grpc:grpc-okhttp:%s" % GRPC_VERSION, + "@@grpc-java~//api": "io.grpc:grpc-api:%s" % GRPC_VERSION, + "@@grpc-java~//core": "io.grpc:grpc-core:%s" % GRPC_VERSION, + "@@grpc-java~//context": "io.grpc:grpc-context:%s" % GRPC_VERSION, + "@@grpc-java~//util": "io.grpc:grpc-util:%s" % GRPC_VERSION, + "@@grpc-java~//stub": "io.grpc:grpc-stub:%s" % GRPC_VERSION, +} + +_SHADED_TARGETS = [ + "@com_google_protobuf//:protobuf_javalite", + "@@protobuf~//java/core:lite", + "//opensource/proto:any_java_proto_lite", + "@com_google_protobuf//:any_proto", + "//opensource/dagger:dagger", + "@com_google_protobuf_protobuf_javalite//:com_google_protobuf_protobuf_javalite", + # emulator controller proto for bazel gets embedded inside espresso-device + "//opensource/emulator/proto:emulator_controller_java_grpc", + "//opensource/emulator/proto:emulator_controller_java_proto_lite", +] + +# maven apk definitions +SERVICES_APK_ARTIFACT = "androidx.test.services:test-services:%s" % SERVICES_VERSION +ORCHESTRATOR_ARTIFACT = "androidx.test:orchestrator:%s" % ORCHESTRATOR_VERSION + +def get_artifact_from_label(label): + """Retrieve the maven artifact (if known) from the build label.""" + label_string = str(label) + result = None + for path, artifact in _TARGET_TO_MAVEN_ARTIFACT.items(): + if path in label_string: + if result: + fail("Found multiple maven artifacts for %s path." % label_string) + result = artifact + + return result + +def is_axt_label(label): + """Determine if given target label is from androidx_test. + + Args: + label: the target label + + Returns: + True if the label is in a recognized androidx_test maven artifact + """ + label_string = str(label) + for path in _TARGET_TO_MAVEN_ARTIFACT.keys(): + if path in label_string: + return True + + # special case the apk source dirs + if "//services" in label_string: + return True + if "//runner/android_test_orchestrator" in label_string: + return True + return False + +def get_maven_apk_deps(artifact): + # TODO: don't hardcode this, instead try to obtain from build rule + if artifact == ORCHESTRATOR_ARTIFACT: + return [SERVICES_APK_ARTIFACT] + else: + return [] + +def is_shaded_from_label(label): + """Returns true if given target should be shaded. + + A shaded target is one whose classes should be embedded in resulting aar and + renamed via jarjar. + """ + + # bazel mysteriously prefixes a '@' onto //opensource/dagger, sometimes two, so just remove it + string_label = str(label).replace("@//", "//").replace("@//", "//") + return string_label in _SHADED_TARGETS diff --git a/build_extensions/maven/maven_repo.bzl b/build_extensions/maven/maven_repo.bzl new file mode 100644 index 000000000..4c39be9cb --- /dev/null +++ b/build_extensions/maven/maven_repo.bzl @@ -0,0 +1,39 @@ +"""Starlark rule to create a single maven repository zip from a set of zips of each artifact.""" + +def _maven_repository_impl(ctx): + """Merges several maven artifacts into combined zip.""" + + source_files = [] + for src in ctx.attr.srcs: + source_files.extend(src.files.to_list()) + + args = ctx.actions.args() + args.add(ctx.outputs.m2repository.path) + args.add_all([f.path for f in source_files]) + + ctx.actions.run( + inputs = source_files, + outputs = [ctx.outputs.m2repository], + mnemonic = "MavenRepository", + arguments = [args], + executable = ctx.executable._zip_combiner, + progress_message = ( + "Packaging repository: %s" % ctx.outputs.m2repository.short_path + ), + ) + +maven_repository = rule( + implementation = _maven_repository_impl, + attrs = { + "srcs": attr.label_list(allow_rules = ["maven_artifact"]), + "_zip_combiner": attr.label( + default = Label("//build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner"), + executable = True, + allow_files = True, + cfg = "exec", + ), + }, + outputs = { + "m2repository": "%{name}.zip", + }, +) diff --git a/build_extensions/maven/reduce_jar.bzl b/build_extensions/maven/reduce_jar.bzl new file mode 100644 index 000000000..13b92118d --- /dev/null +++ b/build_extensions/maven/reduce_jar.bzl @@ -0,0 +1,32 @@ +"""Remove entries from a jar that exist in another jar.""" + +def reduce_jar(ctx, input_jar, overlapping_jar, output_jar): + """Remove entries from jar that exist in another jar. + + Bazel wrapper for build_extensions/jar_reducer. + + Args: + ctx: the rule context + input_jar: the input jar to reduce + overlapping_jar: the baseline jar that contains entries to remove + output_jar: the produced output + """ + if not input_jar: + fail("must provide input_jar") + if not output_jar: + fail("must provide output file") + if not reduce_jar: + fail("must provide reduce jar file") + + args = ctx.actions.args() + args.add(input_jar) + args.add(overlapping_jar) + args.add(output_jar) + + ctx.actions.run( + executable = ctx.executable._reduce_jar_java, + inputs = [input_jar, overlapping_jar], + arguments = [args], + outputs = [output_jar], + mnemonic = "ReduceJar", + ) diff --git a/build_extensions/maven_repo.bzl b/build_extensions/maven_repo.bzl deleted file mode 100644 index 4f79782bb..000000000 --- a/build_extensions/maven_repo.bzl +++ /dev/null @@ -1,222 +0,0 @@ -"""Skylark rule to create a maven repository from a single artifact.""" - -_pom_tmpl = "\n".join([ - '', - '', - " 4.0.0", - " {group_id}", - " {artifact_id}", - " {version}", - " {packaging}", - " AndroidX Test Library", - " The AndroidX Test Library provides an extensive framework for testing Android apps", - " https://developer.android.com/testing", - " 2015", - " ", - " ", - " The Apache Software License, Version 2.0", - " http://www.apache.org/licenses/LICENSE-2.0.txt", - " repo", - " ", - " ", - " ", - " ", - " The Android Open Source Project", - " ", - " ", - " ", - "{dependencies}", - " ", - "", - "", -]) - -_dependency_tmpl = "\n".join([ - " ", - " {group_id}", - " {artifact_id}", - " {version}", - " compile", - " ", -]) - -_metadata_tmpl = "\n".join([ - '', - "", - " {group_id}", - " {artifact_id}", - " {version}", - " ", - " {version}", - " ", - " {version}", - " ", - " {last_updated}", - " ", - "", - "", -]) - -def _packaging_type(f): - """Returns the packaging type used by the file f.""" - if f.basename.endswith(".aar"): - return "aar" - elif f.basename.endswith(".apk"): - return "apk" - elif f.basename.endswith(".jar"): - return "jar" - fail("Artifact has unknown packaging type: %s" % f.short_path) - -def _create_pom_string(ctx): - """Returns the contents of the pom file as a string.""" - dependencies = [] - for dep in ctx.attr.artifact_deps: - if dep.count(":") != 2: - fail("artifact_deps values must be of form: groupId:artifactId:version") - - group_id, artifact_id, version = dep.split(":") - dependencies.append(_dependency_tmpl.format( - group_id = group_id, - artifact_id = artifact_id, - version = version, - )) - - return _pom_tmpl.format( - group_id = ctx.attr.group_id, - artifact_id = ctx.attr.artifact_id, - version = ctx.attr.version, - packaging = _packaging_type(ctx.file.src), - dependencies = "\n".join(dependencies), - ) - -def _create_metadata_string(ctx): - """Returns the string contents of maven-metadata.xml for the group.""" - return _metadata_tmpl.format( - group_id = ctx.attr.group_id, - artifact_id = ctx.attr.artifact_id, - version = ctx.attr.version, - last_updated = ctx.attr.last_updated, - ) - -def _rename_artifact(ctx, tpl_string, src_file, packaging_type): - """Rename the artifact to match maven naming conventions.""" - artifact = ctx.actions.declare_file(tpl_string % (ctx.attr.artifact_id, ctx.attr.version, packaging_type)) - ctx.actions.run_shell( - inputs = [src_file], - outputs = [artifact], - command = "cp %s %s" % (src_file.path, artifact.path), - ) - return artifact - -def _maven_artifact_impl(ctx): - """Generates maven repository for a single artifact.""" - pom = ctx.actions.declare_file( - "%s-%s.pom" % (ctx.attr.artifact_id, ctx.attr.version) - ) - ctx.actions.write(output = pom, content = _create_pom_string(ctx)) - - metadata = ctx.actions.declare_file("maven-metadata.xml") - ctx.actions.write(output = metadata, content = _create_metadata_string(ctx)) - - # Rename binary artifact to artifact_id-version.packaging_type - artifact = _rename_artifact(ctx, "%s-%s.%s", ctx.file.src, _packaging_type(ctx.file.src)) - - # Rename sources jar artifact to artifact_id-version-sources.jar - source = _rename_artifact(ctx, "%s-%s-sources.%s", ctx.file.src_jar, "jar") - - arguments = [ - "--group_path=%s" % ctx.attr.group_id.replace(".", "/"), - "--artifact_id=%s" % ctx.attr.artifact_id, - "--version=%s" % ctx.attr.version, - "--artifact=%s" % artifact.path, - "--source=%s" % source.path, - "--pom=%s" % pom.path, - "--metadata=%s" % metadata.path, - "--output=%s" % ctx.outputs.m2repository.path, - ] - - inputs = [pom, metadata, artifact, source] - - if ctx.file.javadoc_jar != None: - # Rename javadoc jar artifact to artifact_id-version-javadoc.jar - javadoc = _rename_artifact(ctx, "%s-%s-javadoc.%s", ctx.file.javadoc_jar, "jar") - arguments.append("--javadoc=%s" % javadoc.path) - inputs.append(javadoc) - - ctx.actions.run( - inputs = inputs, - outputs = [ctx.outputs.m2repository], - arguments = arguments, - executable = ctx.executable._maven_artifact, - progress_message = ( - "Packaging repository: %s" % ctx.outputs.m2repository.short_path - ), - ) - -def _maven_repository_impl(ctx): - """Generates maven repository for multiple artifacts.""" - source_files = [] - for src in ctx.attr.srcs: - source_files.extend(src.files.to_list()) - ctx.actions.run( - inputs = source_files, - outputs = [ctx.outputs.m2repository], - arguments = [ - "--sources=%s" % ",".join([f.path for f in source_files]), - "--output=%s" % ctx.outputs.m2repository.path, - ], - executable = ctx.executable._maven_repository, - progress_message = ( - "Packaging repository: %s" % ctx.outputs.m2repository.short_path - ), - ) - -maven_artifact = rule( - implementation = _maven_artifact_impl, - attrs = { - "src": attr.label( - mandatory = True, - allow_single_file = [".aar", ".jar", ".apk"], - ), - "src_jar": attr.label( - mandatory = True, - allow_single_file = [".jar"], - ), - "javadoc_jar": attr.label( - mandatory = False, - allow_single_file = [".jar", ".zip"], - ), - "group_id": attr.string(mandatory = True), - "artifact_id": attr.string(mandatory = True), - "version": attr.string(mandatory = True), - "last_updated": attr.string(mandatory = True), - "artifact_deps": attr.string_list(), - "_maven_artifact": attr.label( - default = Label("//build_extensions:maven_artifact"), - executable = True, - allow_files = True, - cfg = "host", - ), - }, - outputs = { - "m2repository": "%{name}.zip", - }, -) - -maven_repository = rule( - implementation = _maven_repository_impl, - attrs = { - "srcs": attr.label_list(allow_rules = ["maven_artifact"]), - "_maven_repository": attr.label( - default = Label("//build_extensions:maven_repository"), - executable = True, - allow_files = True, - cfg = "host", - ), - }, - outputs = { - "m2repository": "%{name}.zip", - }, -) \ No newline at end of file diff --git a/build_extensions/maven_repository.py b/build_extensions/maven_repository.py deleted file mode 100644 index ad16c9c27..000000000 --- a/build_extensions/maven_repository.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2018 The Android Open Source Project. All rights reserved. -# -# 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. - -"""Script to package up multiple maven artifacts into a repository.""" - -import zipfile - -from absl import app -from absl import flags - -FLAGS = flags.FLAGS - -flags.DEFINE_list('sources', None, 'List of source m2repository files') -flags.mark_flag_as_required('sources') - -flags.DEFINE_string('output', None, 'Output zip file') -flags.mark_flag_as_required('output') - - -def main(_): - output_zip = zipfile.ZipFile(FLAGS.output, 'w', zipfile.ZIP_STORED) - for source in FLAGS.sources: - source_zip = zipfile.ZipFile(source, 'r') - for zip_info in source_zip.infolist(): - output_zip.writestr(zip_info, source_zip.read(zip_info)) - source_zip.close() - output_zip.close() - - -if __name__ == '__main__': - app.run(main)() diff --git a/build_extensions/phone_devices.bzl b/build_extensions/phone_devices.bzl new file mode 100644 index 000000000..713a6a415 --- /dev/null +++ b/build_extensions/phone_devices.bzl @@ -0,0 +1,28 @@ +"""Defines common device targets for unit tests.""" + +gmscore_channel = struct( + NONE = struct(id = 0, suffix = ""), + PREBUILT = struct(id = 1, suffix = ""), +) + +def devices(api_list = None, device_type = "generic_phone", gms_channel = gmscore_channel.NONE, use_slim = False): + """Returns target_devices for android_instrumentation_tests. + + Currently unsupported in bazel + """ + return [] + +def apis(min_api = 15, max_api = 10000, exclude_apis = []): + """Returns a list of api level ints filtered by input parameters. + + Currently unsupported in bazel + + Args: + min_api: int: the minimum android api level to return. Default 15 + max_api: int: the maximum android api level to return. Default 10000 + exclude_apis: int list: list of api levels to exclude. Default: empty + + Returns: + the list of api ints + """ + return [] diff --git a/build_extensions/register_extension_info.bzl b/build_extensions/register_extension_info.bzl new file mode 100644 index 000000000..6171ebf8c --- /dev/null +++ b/build_extensions/register_extension_info.bzl @@ -0,0 +1,2 @@ +def register_extension_info(extension, label_regex_for_dep = None): + """Stub definition for unsupported in bazel register_extension_info feature.""" diff --git a/build_extensions/release.bzl b/build_extensions/release.bzl deleted file mode 100644 index 622d75132..000000000 --- a/build_extensions/release.bzl +++ /dev/null @@ -1,134 +0,0 @@ -"""Generate AXT release artifacts.""" - -load("//build_extensions:remove_from_jar.bzl", "remove_from_jar") -load("//build_extensions:add_or_update_file_in_zip.bzl", "add_or_update_file_in_zip") - -def axt_release_lib( - name, - deps, - custom_package = None, - proguard_specs = None, - proguard_library = None, - multidex = "off", - jarjar_rules = "//build_extensions:noJarJarRules.txt", - keep_spec = None, - remove_spec = None, - overlapping_jars = [], - resource_files = None, - visibility = None): - """Generates release artifacts for a AXT library. - - Resulting output will be two files: - name_no_deps.jar and name.aar - - Args: - name: The target name - deps: The dependencies that make up the library - custom_package: Option custom android package to use - proguard_specs: Proguard to apply when building the jar - proguard_library: Proguard to bundle with the jar - jarjar_rules: Optional file containing jarjar rules to be applied - keep_spec: A regex to match items to retain in the jar. This is typically the - root java namespace of the library. - remove_spec: A regex to match items to remove from the jar. - overlapping_jars: jars containing entries to be removed from the main jar. - This is useful when the library has dependencies whose java package namespaces - overlap with this jar. See remove_from_jar docs for more details. - resource_files: res files to include in library - visibility: optional visibility to use for generated rules - """ - - # The rules here produce a final .aar artifact and jar for external release. - - # It is a 5 stage pipeline: - # 1. Produce a placeholder .aar - # 2. Produce a .jar including all classes and all its dependencies, and optionally proguard it via - # proguard_specs - # 3. Rename classes if necessary via jarjar - # 4. Strip out external dependencies from .jar - # 5. Optionally, add in the proguard_library files to be bundled in the .aar - # 6. Update the classes.jar inside the .aar from step 1 with the .jar from step 3 - - # Step 1. Generate initial shell aar. The generated classes.jar will be empty. - # See - # https://bazel.build/versions/master/docs/be/android.html#android_library, - # name.aar - native.android_library( - name = "%s_initial" % name, - manifest = "AndroidManifest.xml", - resource_files = resource_files, - visibility = ["//visibility:private"], - custom_package = custom_package, - testonly = 1, - exports = deps, - ) - - # Step 2. Generate jar containing all classes including dependencies. - native.android_binary( - name = "%s_all" % name, - testonly = 1, - manifest = "AndroidManifest.xml", - multidex = multidex, - custom_package = custom_package, - proguard_specs = proguard_specs, - deps = [ - ":%s_initial" % name, - ], - visibility = visibility, - ) - - expected_output = ":%s_all_deploy.jar" % name - if proguard_specs: - expected_output = ":%s_all_proguard.jar" % name - - # Step 3. Rename classes via jarjar - native.java_binary( - name = "jarjar_bin", - main_class = "org.pantsbuild.jarjar.Main", - runtime_deps = ["@maven//:org_pantsbuild_jarjar"], - visibility = visibility, - ) - native.genrule( - name = "%s_jarjared" % name, - srcs = [expected_output], - outs = ["%s_jarjared.jar" % name], - cmd = ("$(location :jarjar_bin) process " + - "$(location %s) '$<' '$@'") % jarjar_rules, - tools = [ - jarjar_rules, - ":jarjar_bin", - ], - visibility = visibility, - ) - - # Step 4. Strip out external dependencies. This produces the final name_no_deps.jar. - remove_from_jar( - name = "%s_no_deps" % name, - jar = ":%s_jarjared.jar" % name, - keep_spec = keep_spec, - remove_spec = remove_spec, - overlapping_jars = overlapping_jars, - visibility = visibility, - ) - - expected_output = ":%s_initial.aar" % name - if proguard_library: - expected_output = "%s_with_proguard.aar" % name - - # Step 5. Add the proguard library file to the aar from the first step - add_or_update_file_in_zip( - name = "%s_add_proguard" % name, - src = ":%s_initial.aar" % name, - out = expected_output, - update_path = "proguard.txt", - update_src = proguard_library, - ) - - # Step 6. Update the .aar produced in the first step with the final .jar - add_or_update_file_in_zip( - name = name, - src = expected_output, - out = "%s.aar" % name, - update_path = "classes.jar", - update_src = ":%s_no_deps.jar" % name, - ) diff --git a/build_extensions/remove_from_jar.bzl b/build_extensions/remove_from_jar.bzl deleted file mode 100644 index 794f7c3e9..000000000 --- a/build_extensions/remove_from_jar.bzl +++ /dev/null @@ -1,145 +0,0 @@ -"""Removes files / directories from jar (or any zip) file. - -If overlapping_jars is present, then it also removes entries in the -primary jar that exists in any of the overlapping_jars. -""" - -def remove_from_jar( - name, - jar, - keep_spec, - remove_spec = None, - overlapping_jars = [], - visibility = None, - constraints = None, - **kwargs): - """Removes specified entries from a jar file. - - The entries to remove can be specified with with any combination of 'removes' - and 'overlapping_jars'. - It generates two relevant targets: and lib, - where target is pure genrule output, that has 'concrete' jar, while - lib is a java_library rule that contains in its srcs. - - Args: - name: Name of the remove jars target, String. - jar: jar from which to remove files, label. - keep_spec: Regex to match items to be retained in jar file. - remove_spec: Regex of items to be removed from a jar file. - overlapping_jars: jars containing entries to be removed from the main jar. - visibility: (Optional) visibility of the rules generated by this macro. - constraints: (Optional) constraints imposed on this rule as a Java library. - Currently defaults to ["android"] for compatibility reasons, but this will - be removed in the future. - **kwargs: Args to be passed to genrule and java_library, so valid args are - common set between those two: - deprecation, distribs, licenses, obsolete, tags, testonly, visibility - - Usage: - remove_from_jar( - name = "jar_cleaned", - keep_spec = "foo/bar/,*|bar/foo/,*", - overlapping_jars = [":fooapp_deploy.jar"]) - Explanation: - Removes all items not matching foo/bar/.* or bar/foo/.* from the jar file. - Uses fooapp_deploy.jar overlapping jar, to remove entries in main - jar that are also present in the overlapping jar. - - remove_from_jar( - name = "jar_cleaned", - keep_spec = "foo/bar.*" - removes = [ "foo/bar/RemoveMe" ]) - Explanation: - This will retain everything from foo/bar/.* except foo/bar/RemoveMe. - """ - if not jar or jar == "": - fail('"jar" attribute cannot be null or empty') - - if constraints == None: - constraints = ["android"] - - srcs = [jar] - message = ('Keeping %s from "%s." ' % (keep_spec, jar)) - message += ('Removing %s from "%s." ' % (remove_spec, jar)) if remove_spec else "" - - # Add overlapping_jars to sources if specified. - for overlapping_jar in overlapping_jars: - srcs += [overlapping_jar] - message += ( - 'Removing elements in "%s" that are present in overlapping jar "%s."' % - (jar, overlapping_jar) - ) - - cmd = [ - "set +o pipefail;", - "tmpdir=$$(mktemp -d);", - "cp $(location %s) $@;" % jar, - "chmod +w $@;", - "$(location @local_jdk//:jar) tf $@ > $$tmpdir/file_list.txt;", - "cat $$tmpdir/file_list.txt | ", - 'egrep -v "%s" | ' % keep_spec, - "xargs --no-run-if-empty zip -d $@ >", - "$$tmpdir/keep_from_jar_result.txt 2>&1 || {", - " RESULT=$$?;", - " cat $$tmpdir/keep_from_jar_result.txt;", - " exit $${RESULT};", - " };", - ] - - if remove_spec: - cmd += [ - "$(location @local_jdk//:jar) tf $@ >", - "$$tmpdir/remove_file_list.txt;", - "cat $$tmpdir/remove_file_list.txt | ", - 'egrep "%s" | ' % remove_spec, - "xargs --no-run-if-empty zip -d $@ >", - "$$tmpdir/remove_from_jar_result.txt 2>&1 || {", - " RESULT=$$?;", - " cat $$tmpdir/remove_from_jar_result.txt;", - " exit $${RESULT};", - " };", - ] - - if overlapping_jars: - for overlapping_jar in overlapping_jars: - cmd += [ - ("$(location @local_jdk//:jar) tf $(location %s) >> " + - "$$tmpdir/overlapping_jar.txt;") % - overlapping_jar, - ] - cmd += [ - "$(location @local_jdk//:jar) tf $@ >", - "$$tmpdir/original_jar.txt;", - "grep -F -x -f $$tmpdir/overlapping_jar.txt", - "$$tmpdir/original_jar.txt", - "| xargs --no-run-if-empty zip -d $@ >", - "$$tmpdir/remove_from_overlapping_jar_result.txt 2>&1 || {", - " RESULT=$$?;", - " cat $$tmpdir/remove_from_overlapping_jar_result.txt;", - " exit $${RESULT};", - " };", - ] - - cmd += ["rm -rf $$tmpdir;"] - - native.genrule( - name = name, - srcs = srcs, - outs = ["%s.jar" % name], - tools = [ - "@local_jdk//:jar", - - ], - message = message, - visibility = visibility, - cmd = " ".join(cmd), - **kwargs - ) - - native.java_import( - name = "lib%s" % name, - jars = [name], - constraints = constraints, - visibility = visibility, - **kwargs - ) diff --git a/build_extensions/robolectric.properties b/build_extensions/robolectric.properties index b67e91d12..b3a33a53f 100644 --- a/build_extensions/robolectric.properties +++ b/build_extensions/robolectric.properties @@ -1,4 +1,4 @@ -# Use SDK 28 by default -sdk=28 +# Use SDK 34 by default +sdk=34 # make Robolectric match the emulator used -qualifiers=w480dp-h800dp \ No newline at end of file +qualifiers=w480dp-h800dp diff --git a/build_extensions/test_devices.bzl b/build_extensions/test_devices.bzl deleted file mode 100644 index b6b9ad40c..000000000 --- a/build_extensions/test_devices.bzl +++ /dev/null @@ -1,24 +0,0 @@ -"""Defines common device targets for unit tests.""" - -# TODO: consider expanding set of devices. for now just use API 19 only -# since it seems most stable -_ALL_DEVICES = [ - struct(sdk = 19, target = "//tools/android/emulated_devices/generic_phone:android_19_x86"), -] - -def devices(min_sdk = 15, max_sdk = 10000): - """Returns target_devices for android_instrumentation_tests. - - Args: - min_sdk: the minimum android api level to return. Default 15 - max_sdk: the maximum android api level to return. Default 10000 - - Returns: - list of device targets - """ - devices = [] - for device in _ALL_DEVICES: - if (device.sdk >= min_sdk and device.sdk <= max_sdk): - devices.append(device.target) - - return devices \ No newline at end of file diff --git a/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/BUILD b/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/BUILD new file mode 100644 index 000000000..ba3024598 --- /dev/null +++ b/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/BUILD @@ -0,0 +1,23 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +package( + default_visibility = [ + "//:__subpackages__", + ], +) + +kt_jvm_library( + name = "zipcombiner_lib", + srcs = glob([ + "*.kt", + ]), +) + +java_binary( + name = "zipcombiner", + srcs = ["Main.java"], + main_class = "androidx.test.tools.zipcombiner.Main", + deps = [ + ":zipcombiner_lib", + ], +) diff --git a/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/Main.java b/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/Main.java new file mode 100644 index 000000000..43620b1db --- /dev/null +++ b/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/Main.java @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.zipcombiner; + +public class Main { + private Main() {} + + public static void main(String[] args) { + ZipCombinerKt.combineZips(args); + } +} diff --git a/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/ZipCombiner.kt b/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/ZipCombiner.kt new file mode 100644 index 000000000..8f0267821 --- /dev/null +++ b/build_extensions/zip_combiner/java/androidx/test/tools/zipcombiner/ZipCombiner.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.test.tools.zipcombiner + +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +fun combineZips(args: Array) { + require(args.size >= 2) { "Must provide a least two files: " } + + val outputFile = args[0] + val zipOutputStream = ZipOutputStream(FileOutputStream(outputFile, false)) + zipOutputStream.use { + // keep track of what zip each entry belongs to, in order to have a better error + // message in case of duplicates + val entryToZip: MutableMap = HashMap() + + for (i in 1 until args.size) { + val inputFile = args[i] + val zipInputStream = ZipInputStream(FileInputStream(inputFile)) + zipInputStream.use { addToZip(entryToZip, zipOutputStream, zipInputStream, args[i]) } + } + } +} + +private fun addToZip( + entryToZip: MutableMap, + zipOutputStream: ZipOutputStream, + inputZipStream: ZipInputStream, + inputZipName: String, +) { + var entry = inputZipStream.nextEntry + while (entry != null) { + // ZipOutputStream will throw an obscure error if any duplicate entry is added, which is + // undesirable for + // directories and certain classes. + // Add our own handling to allow certain duplicates, and throw a more descriptive error in + // case there are unexpected duplicates + if (entryToZip.containsKey(entry.name)) { + if (!isAllowedDuplicate(entry)) { + throw RuntimeException( + "Duplicate entry: ${entry.name} is present in both ${entryToZip.get(entry.name)} and $inputZipName" + ) + } + } else { + zipOutputStream.putNextEntry(entry) + inputZipStream.transferTo(zipOutputStream) + entryToZip.put(entry.name, inputZipName) + } + entry = inputZipStream.nextEntry + } +} + +private fun isAllowedDuplicate(entry: ZipEntry): Boolean { + // always allow duplicate directories + return entry.isDirectory +} diff --git a/cloudbuild.yaml b/cloudbuild.yaml deleted file mode 100644 index b9e32bd18..000000000 --- a/cloudbuild.yaml +++ /dev/null @@ -1,27 +0,0 @@ -steps: -# pull the latest docker image contains bazel + android SDK -- name: gcr.io/cloud-builders/docker - args: ['pull', 'gcr.io/$PROJECT_ID/bazel-android:latest'] -# build the docker image, caching from the latest image -- name: gcr.io/cloud-builders/docker - args: [ - 'build', - '--tag', 'gcr.io/$PROJECT_ID/bazel-android:latest', - '--cache-from', 'gcr.io/$PROJECT_ID/bazel-android:latest', - '.' - ] -# build the maven repository using bazel -- name: gcr.io/$PROJECT_ID/bazel-android - args: ['build', '//:axt_m2repository'] -# run the robolectric tests -- name: gcr.io/$PROJECT_ID/bazel-android - args: ['test', '...', '--test_tag_filters=robolectric,-gcb_ignore', '--build_tag_filters=robolectric', '--test_output=all'] - -timeout: '1200s' - -# use a 32 vCPU 28.8GB memory machine. -options: - machineType: "N1_HIGHCPU_32" - -# push the built docker image to google cloud storage -images: ['gcr.io/$PROJECT_ID/bazel-android:latest'] diff --git a/core/BUILD b/core/BUILD new file mode 100644 index 000000000..aed3d6546 --- /dev/null +++ b/core/BUILD @@ -0,0 +1,23 @@ +# Publicly visible androidx.test.core API library + +package( + default_applicable_licenses = ["//:license"], + default_testonly = 1, +) + +licenses(["notice"]) + +alias( + name = "core", + actual = "//core/java/androidx/test/core", + visibility = ["//visibility:public"], +) + +# The manifest entries for ActivityScenario's bootstrap activities. +# Add this to to your application under test to avoid a separate test process getting launched +# for the bootstrap activities +alias( + name = "manifest", + actual = "//core/java/androidx/test/core:manifest", + visibility = ["//visibility:public"], +) diff --git a/core/BUILD.bazel b/core/BUILD.bazel deleted file mode 100644 index e30368815..000000000 --- a/core/BUILD.bazel +++ /dev/null @@ -1,9 +0,0 @@ -# Publicly visible truth extensions for Android - -licenses(["notice"]) # Apache License 2.0 - -android_library( - name = "core", - visibility = ["//visibility:public"], - exports = ["//core/java/androidx/test/core"], -) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md new file mode 100644 index 000000000..e5c230db6 --- /dev/null +++ b/core/CHANGELOG.md @@ -0,0 +1,19 @@ +### Core Core-ktx {version} {:#core-{version}} + +{{date}} + +`androidx.test:core:{version}` and `androidx.test:core-ktx:{version}` are released. + +**Bug Fixes** + +**New Features** + +**Breaking Changes** + +**API Changes** + +* Update to minSdkVersion 23 and remove all related logic for SDKs < 23 + +**Breaking API Changes** + +**Known Issues** diff --git a/core/java/androidx/test/core/AndroidManifest.xml b/core/java/androidx/test/core/AndroidManifest.xml index 5882076fc..692342834 100644 --- a/core/java/androidx/test/core/AndroidManifest.xml +++ b/core/java/androidx/test/core/AndroidManifest.xml @@ -18,7 +18,7 @@ package="androidx.test.core" > @@ -28,24 +28,24 @@ android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity" android:exported="true" android:theme="@style/WhiteBackgroundTheme"> - - + + - - + + - - + + diff --git a/core/java/androidx/test/core/BUILD b/core/java/androidx/test/core/BUILD new file mode 100644 index 000000000..5f0a5a67f --- /dev/null +++ b/core/java/androidx/test/core/BUILD @@ -0,0 +1,101 @@ +# Description: Build rules for building androidx.test.truth from source + +load("@build_bazel_rules_android//android:rules.bzl", "android_library") +load("//build_extensions:api_checks.bzl", "api_checks") +load("//build_extensions:dackka_test.bzl", "dackka_test") +load("//build_extensions:kt_android_library.bzl", "kt_android_library") +load("//build_extensions/maven:axt_android_aar.bzl", "axt_android_aar") +load("//build_extensions/maven:maven_artifact.bzl", "maven_artifact") + +# all users should reference the equivalent targets in //core +package( + default_applicable_licenses = ["//:license"], + default_testonly = 1, + default_visibility = [ + ":allowlist", + ], +) + +package_group( + name = "allowlist", + packages = [ + "//...", + ], +) + +licenses(["notice"]) + +# target containing bootstrap activity manifest entries +# Add this to your application under test to avoid a separate test process getting launched +# for the bootstrap activities +android_library( + name = "manifest", + exports_manifest = 1, + manifest = "AndroidManifest.xml", + resource_files = glob(["res/**"]), +) + +kt_android_library( + name = "core", + srcs = glob( + [ + "**/*.java", + "**/*.kt", + ], + ), + tags = ["alt_dep=//core"], + deps = [ + ":manifest", + "//opensource/androidx:annotation", + "//runner/monitor", + "@maven//:androidx_concurrent_concurrent_futures_ktx", + "@maven//:androidx_lifecycle_lifecycle_common", + "@maven//:androidx_tracing_tracing", + "@maven//:org_jetbrains_kotlinx_kotlinx_coroutines_core_jvm", + "@maven_listenablefuture//:com_google_guava_listenablefuture", + ], +) + +# kt_android_library does not produce an aar, so wrap in a android_library for release +android_library( + name = "core_aar_lib", + manifest = "AndroidManifest.xml", + resource_files = glob(["res/**"]), + visibility = ["//visibility:private"], + exports = [ + ":core", + ], +) + +alias( + name = "core-src", + actual = ":core_aar-src.jar", + visibility = ["//visibility:private"], +) + +# Generate rules for the release artifacts. +axt_android_aar( + name = "core_aar", + expected_class_prefixes = [ + "androidx.test.core", + ], + included_dep = ":core_aar_lib", +) + +maven_artifact( + name = "core_maven_artifact", + last_updated = "20180403000000", + target = ":core_aar", +) + +dackka_test( + name = "core_doc", + runtime_dep = ":core_aar_lib", + src_jar = ":core_aar-src.jar", +) + +api_checks( + name = "core_api", + runtime_dep = ":core_aar_lib", + src_jar = ":core_aar-src.jar", +) diff --git a/core/java/androidx/test/core/BUILD.bazel b/core/java/androidx/test/core/BUILD.bazel deleted file mode 100644 index c1a715d20..000000000 --- a/core/java/androidx/test/core/BUILD.bazel +++ /dev/null @@ -1,67 +0,0 @@ -load("//build_extensions:release.bzl", "axt_release_lib") -load("//build_extensions:maven_repo.bzl", "maven_artifact") -load("//build_extensions:axt_versions.bzl", "ANDROIDX_LIFECYCLE_VERSION", "ANDROIDX_VERSION", "CORE_VERSION", "KOTLIN_VERSION", "MONITOR_VERSION") -load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") - -# Description: Build rules for building androidx.test.core from source -licenses(["notice"]) # Apache License 2.0 - -package( - default_visibility = [ - "//visibility:public", - ], -) - -kt_android_library( - name = "core", - srcs = glob( - ["**/*.java"], - ), - exports_manifest = 1, - manifest = ":AndroidManifest.xml", - resource_files = glob(["res/**"]), - deps = [ - "//:androidx_annotation", - "//:androidx_lifecycle_common", - "//runner/monitor", - ], -) - -# group of targets to use to produce release binary + docs -android_library( - name = "core_release_lib", - exports = [ - ":core", - ], -) - -# Generate rules for the release artifacts. This generates three targets -# genrule output: core_release_no_deps.jar and core_release.aar -# a java_library target libcore_release_no_deps -axt_release_lib( - name = "core_release", - # keep all androidx.test.core classes except androidx.test.core.R, since that will be - # auto-generated by consuming build system - keep_spec = "androidx/test/core/.*", - remove_spec = "androidx/test/core/R[$$\\.]", - resource_files = glob(["res/**"]), - deps = [ - ":core_release_lib", - ], -) - -maven_artifact( - name = "core_maven_artifact", - src = ":core_release.aar", - artifact_deps = [ - "androidx.annotation:annotation:%s" % ANDROIDX_VERSION, - "androidx.test:monitor:%s" % MONITOR_VERSION, - "androidx.lifecycle:lifecycle-common:%s" % ANDROIDX_LIFECYCLE_VERSION, - "org.jetbrains.kotlin:kotlin-stdlib:%s" % KOTLIN_VERSION, - ], - artifact_id = "core", - group_id = "androidx.test", - last_updated = "20180403000000", - src_jar = ":libcore-src.jar", - version = "%s" % CORE_VERSION, -) diff --git a/core/java/androidx/test/core/api/1.4.0_public.txt b/core/java/androidx/test/core/api/1.4.0_public.txt new file mode 100644 index 000000000..b9da37a06 --- /dev/null +++ b/core/java/androidx/test/core/api/1.4.0_public.txt @@ -0,0 +1,93 @@ +// Signature format: 3.0 +package androidx.test.core.app { + + public final class ActivityScenario implements java.lang.AutoCloseable java.io.Closeable { + method public void close(); + method public android.app.Instrumentation.ActivityResult! getResult(); + method public android.arch.lifecycle.Lifecycle.State! getState(); + method public static androidx.test.core.app.ActivityScenario! launch(Class!); + method public static androidx.test.core.app.ActivityScenario! launch(Class!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!, android.os.Bundle?); + method public androidx.test.core.app.ActivityScenario! moveToState(android.arch.lifecycle.Lifecycle.State!); + method public androidx.test.core.app.ActivityScenario! onActivity(androidx.test.core.app.ActivityScenario.ActivityAction!); + method public androidx.test.core.app.ActivityScenario! recreate(); + } + + public static interface ActivityScenario.ActivityAction { + method public void perform(A!); + } + + public final class ApplicationProvider { + method public static T! getApplicationContext(); + } + +} + +package androidx.test.core.content.pm { + + public final class ApplicationInfoBuilder { + method public android.content.pm.ApplicationInfo! build(); + method public static androidx.test.core.content.pm.ApplicationInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setName(String?); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setPackageName(String!); + } + + public final class PackageInfoBuilder { + method public android.content.pm.PackageInfo! build(); + method public static androidx.test.core.content.pm.PackageInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.PackageInfoBuilder! setApplicationInfo(android.content.pm.ApplicationInfo!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setPackageName(String!); + } + +} + +package androidx.test.core.os { + + public final class Parcelables { + method public static T! forceParcel(T!, android.os.Parcelable.Creator!); + } + +} + +package androidx.test.core.view { + + public class MotionEventBuilder { + method public android.view.MotionEvent! build(); + method public static androidx.test.core.view.MotionEventBuilder! newBuilder(); + method public androidx.test.core.view.MotionEventBuilder! setAction(int); + method public androidx.test.core.view.MotionEventBuilder! setActionIndex(int); + method public androidx.test.core.view.MotionEventBuilder! setButtonState(int); + method public androidx.test.core.view.MotionEventBuilder! setDeviceId(int); + method public androidx.test.core.view.MotionEventBuilder! setDownTime(long); + method public androidx.test.core.view.MotionEventBuilder! setEdgeFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setEventTime(long); + method public androidx.test.core.view.MotionEventBuilder! setFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setMetaState(int); + method public androidx.test.core.view.MotionEventBuilder! setPointer(float, float); + method public androidx.test.core.view.MotionEventBuilder! setPointer(android.view.MotionEvent.PointerProperties!, android.view.MotionEvent.PointerCoords!); + method public androidx.test.core.view.MotionEventBuilder! setSource(int); + method public androidx.test.core.view.MotionEventBuilder! setXPrecision(float); + method public androidx.test.core.view.MotionEventBuilder! setYPrecision(float); + } + + public class PointerCoordsBuilder { + method public android.view.MotionEvent.PointerCoords! build(); + method public static androidx.test.core.view.PointerCoordsBuilder! newBuilder(); + method public androidx.test.core.view.PointerCoordsBuilder! setCoords(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setOrientation(float); + method public androidx.test.core.view.PointerCoordsBuilder! setPressure(float); + method public androidx.test.core.view.PointerCoordsBuilder! setSize(float); + method public androidx.test.core.view.PointerCoordsBuilder! setTool(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setTouch(float, float); + } + + public class PointerPropertiesBuilder { + method public android.view.MotionEvent.PointerProperties! build(); + method public static androidx.test.core.view.PointerPropertiesBuilder! newBuilder(); + method public androidx.test.core.view.PointerPropertiesBuilder! setId(int); + method public androidx.test.core.view.PointerPropertiesBuilder! setToolType(int); + } + +} + diff --git a/core/java/androidx/test/core/api/1.5.0_internal.txt b/core/java/androidx/test/core/api/1.5.0_internal.txt new file mode 100644 index 000000000..364571b06 --- /dev/null +++ b/core/java/androidx/test/core/api/1.5.0_internal.txt @@ -0,0 +1,9 @@ +// Signature format: 3.0 +package androidx.test.core.app { + + public final class DeviceCapture { + method @RequiresApi(android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @androidx.test.annotation.ExperimentalTestApi public static android.graphics.Bitmap takeScreenshotNoSync() throws java.lang.RuntimeException; + } + +} + diff --git a/core/java/androidx/test/core/api/1.5.0_public.txt b/core/java/androidx/test/core/api/1.5.0_public.txt new file mode 100644 index 000000000..d80883783 --- /dev/null +++ b/core/java/androidx/test/core/api/1.5.0_public.txt @@ -0,0 +1,113 @@ +// Signature format: 3.0 +package androidx.test.core.app { + + public final class ActivityScenario implements java.lang.AutoCloseable java.io.Closeable { + method public void close(); + method public android.app.Instrumentation.ActivityResult! getResult(); + method public androidx.lifecycle.Lifecycle.State! getState(); + method public static androidx.test.core.app.ActivityScenario! launch(Class!); + method public static androidx.test.core.app.ActivityScenario! launch(Class!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent, android.os.Bundle?); + method public androidx.test.core.app.ActivityScenario! moveToState(androidx.lifecycle.Lifecycle.State!); + method public androidx.test.core.app.ActivityScenario! onActivity(androidx.test.core.app.ActivityScenario.ActivityAction!); + method public androidx.test.core.app.ActivityScenario! recreate(); + } + + public static interface ActivityScenario.ActivityAction { + method public void perform(A!); + } + + public final class ApplicationProvider { + method public static T! getApplicationContext(); + } + + public final class DeviceCapture { + } + +} + +package androidx.test.core.content.pm { + + public final class ApplicationInfoBuilder { + method public android.content.pm.ApplicationInfo! build(); + method public static androidx.test.core.content.pm.ApplicationInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setName(String?); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setPackageName(String!); + } + + public final class PackageInfoBuilder { + method public android.content.pm.PackageInfo! build(); + method public static androidx.test.core.content.pm.PackageInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.PackageInfoBuilder! setApplicationInfo(android.content.pm.ApplicationInfo!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setPackageName(String!); + } + +} + +package androidx.test.core.graphics { + + public final class BitmapStorage { + } + +} + +package androidx.test.core.os { + + public final class Parcelables { + method public static T! forceParcel(T!, android.os.Parcelable.Creator!); + } + +} + +package androidx.test.core.view { + + public class MotionEventBuilder { + method public android.view.MotionEvent! build(); + method public static androidx.test.core.view.MotionEventBuilder! newBuilder(); + method public androidx.test.core.view.MotionEventBuilder! setAction(int); + method public androidx.test.core.view.MotionEventBuilder! setActionIndex(int); + method public androidx.test.core.view.MotionEventBuilder! setButtonState(int); + method public androidx.test.core.view.MotionEventBuilder! setDeviceId(int); + method public androidx.test.core.view.MotionEventBuilder! setDownTime(long); + method public androidx.test.core.view.MotionEventBuilder! setEdgeFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setEventTime(long); + method public androidx.test.core.view.MotionEventBuilder! setFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setMetaState(int); + method public androidx.test.core.view.MotionEventBuilder! setPointer(float, float); + method public androidx.test.core.view.MotionEventBuilder! setPointer(android.view.MotionEvent.PointerProperties!, android.view.MotionEvent.PointerCoords!); + method public androidx.test.core.view.MotionEventBuilder! setSource(int); + method public androidx.test.core.view.MotionEventBuilder! setXPrecision(float); + method public androidx.test.core.view.MotionEventBuilder! setYPrecision(float); + } + + public class PointerCoordsBuilder { + method public android.view.MotionEvent.PointerCoords! build(); + method public static androidx.test.core.view.PointerCoordsBuilder! newBuilder(); + method public androidx.test.core.view.PointerCoordsBuilder! setCoords(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setOrientation(float); + method public androidx.test.core.view.PointerCoordsBuilder! setPressure(float); + method public androidx.test.core.view.PointerCoordsBuilder! setSize(float); + method public androidx.test.core.view.PointerCoordsBuilder! setTool(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setTouch(float, float); + } + + public class PointerPropertiesBuilder { + method public android.view.MotionEvent.PointerProperties! build(); + method public static androidx.test.core.view.PointerPropertiesBuilder! newBuilder(); + method public androidx.test.core.view.PointerPropertiesBuilder! setId(int); + method public androidx.test.core.view.PointerPropertiesBuilder! setToolType(int); + } + + public final class ViewCapture { + } + + public final class WindowCapture { + } + +} + diff --git a/core/java/androidx/test/core/api/1.6.0_internal.txt b/core/java/androidx/test/core/api/1.6.0_internal.txt new file mode 100644 index 000000000..c6d1ac143 --- /dev/null +++ b/core/java/androidx/test/core/api/1.6.0_internal.txt @@ -0,0 +1,26 @@ +// Signature format: 3.0 +package androidx.test.core.app { + + public final class DeviceCapture { + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static boolean canTakeScreenshot(); + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static android.graphics.Bitmap takeScreenshotNoSync() throws java.lang.RuntimeException; + } + +} + +package androidx.test.core.graphics { + + public final class BitmapStorage { + method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static void writeToTestStorage(android.graphics.Bitmap, androidx.test.platform.io.PlatformTestStorage testStorage, String name) throws java.io.IOException; + } + +} + +package androidx.test.core.view { + + public final class ViewCapture { + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static suspend Object? forceRedraw(android.view.View, kotlin.coroutines.Continuation); + } + +} + diff --git a/core/java/androidx/test/core/api/1.6.0_public.txt b/core/java/androidx/test/core/api/1.6.0_public.txt new file mode 100644 index 000000000..62dfd9a6e --- /dev/null +++ b/core/java/androidx/test/core/api/1.6.0_public.txt @@ -0,0 +1,123 @@ +// Signature format: 3.0 +package androidx.test.core.app { + + public final class ActivityScenario implements java.lang.AutoCloseable java.io.Closeable { + method public void close(); + method public android.app.Instrumentation.ActivityResult! getResult(); + method public androidx.lifecycle.Lifecycle.State! getState(); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario! launch(Class!); + method public static androidx.test.core.app.ActivityScenario! launch(Class!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class, android.os.Bundle?); + method public androidx.test.core.app.ActivityScenario! moveToState(androidx.lifecycle.Lifecycle.State!); + method public androidx.test.core.app.ActivityScenario! onActivity(androidx.test.core.app.ActivityScenario.ActivityAction!); + method public androidx.test.core.app.ActivityScenario! recreate(); + } + + public static interface ActivityScenario.ActivityAction { + method public void perform(A!); + } + + public final class ApplicationProvider { + method public static T! getApplicationContext(); + } + + public final class DeviceCapture { + method @kotlin.jvm.Throws(exceptionClasses=RuntimeException::class) public static android.graphics.Bitmap takeScreenshot() throws java.lang.RuntimeException; + } + +} + +package androidx.test.core.content.pm { + + public final class ApplicationInfoBuilder { + method public android.content.pm.ApplicationInfo! build(); + method public static androidx.test.core.content.pm.ApplicationInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setFlags(int); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setName(String?); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setPackageName(String!); + } + + public final class PackageInfoBuilder { + method public androidx.test.core.content.pm.PackageInfoBuilder! addRequestedPermission(String!, int); + method public android.content.pm.PackageInfo! build(); + method public static androidx.test.core.content.pm.PackageInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.PackageInfoBuilder! setApplicationInfo(android.content.pm.ApplicationInfo!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setPackageName(String!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setVersionCode(long); + method public androidx.test.core.content.pm.PackageInfoBuilder! setVersionName(String!); + } + +} + +package androidx.test.core.graphics { + + public final class BitmapStorage { + method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static void writeToTestStorage(android.graphics.Bitmap, String name) throws java.io.IOException; + } + +} + +package androidx.test.core.os { + + public final class Parcelables { + method public static T! forceParcel(T!, android.os.Parcelable.Creator!); + } + +} + +package androidx.test.core.view { + + public class MotionEventBuilder { + method public android.view.MotionEvent! build(); + method public static androidx.test.core.view.MotionEventBuilder! newBuilder(); + method public androidx.test.core.view.MotionEventBuilder! setAction(int); + method public androidx.test.core.view.MotionEventBuilder! setActionIndex(int); + method public androidx.test.core.view.MotionEventBuilder! setButtonState(int); + method public androidx.test.core.view.MotionEventBuilder! setDeviceId(int); + method public androidx.test.core.view.MotionEventBuilder! setDownTime(long); + method public androidx.test.core.view.MotionEventBuilder! setEdgeFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setEventTime(long); + method public androidx.test.core.view.MotionEventBuilder! setFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setMetaState(int); + method public androidx.test.core.view.MotionEventBuilder! setPointer(android.view.MotionEvent.PointerProperties!, android.view.MotionEvent.PointerCoords!); + method public androidx.test.core.view.MotionEventBuilder! setPointer(float, float); + method public androidx.test.core.view.MotionEventBuilder! setSource(int); + method public androidx.test.core.view.MotionEventBuilder! setXPrecision(float); + method public androidx.test.core.view.MotionEventBuilder! setYPrecision(float); + } + + public class PointerCoordsBuilder { + method public android.view.MotionEvent.PointerCoords! build(); + method public static androidx.test.core.view.PointerCoordsBuilder! newBuilder(); + method public androidx.test.core.view.PointerCoordsBuilder! setCoords(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setOrientation(float); + method public androidx.test.core.view.PointerCoordsBuilder! setPressure(float); + method public androidx.test.core.view.PointerCoordsBuilder! setSize(float); + method public androidx.test.core.view.PointerCoordsBuilder! setTool(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setTouch(float, float); + } + + public class PointerPropertiesBuilder { + method public android.view.MotionEvent.PointerProperties! build(); + method public static androidx.test.core.view.PointerPropertiesBuilder! newBuilder(); + method public androidx.test.core.view.PointerPropertiesBuilder! setId(int); + method public androidx.test.core.view.PointerPropertiesBuilder! setToolType(int); + } + + public final class ViewCapture { + method public static suspend Object? captureToBitmap(android.view.View, android.graphics.Rect? rect = null, kotlin.coroutines.Continuation); + method public static com.google.common.util.concurrent.ListenableFuture captureToBitmapAsync(android.view.View, android.graphics.Rect? rect = null); + } + + public final class WindowCapture { + method public static suspend Object? captureRegionToBitmap(android.view.Window, android.graphics.Rect? boundsInWindow = null, kotlin.coroutines.Continuation); + method public static com.google.common.util.concurrent.ListenableFuture captureRegionToBitmapAsync(android.view.Window, android.graphics.Rect? boundsInWindow = null); + } + +} + diff --git a/core/java/androidx/test/core/api/1.7.0_internal.txt b/core/java/androidx/test/core/api/1.7.0_internal.txt new file mode 100644 index 000000000..c6d1ac143 --- /dev/null +++ b/core/java/androidx/test/core/api/1.7.0_internal.txt @@ -0,0 +1,26 @@ +// Signature format: 3.0 +package androidx.test.core.app { + + public final class DeviceCapture { + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static boolean canTakeScreenshot(); + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static android.graphics.Bitmap takeScreenshotNoSync() throws java.lang.RuntimeException; + } + +} + +package androidx.test.core.graphics { + + public final class BitmapStorage { + method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static void writeToTestStorage(android.graphics.Bitmap, androidx.test.platform.io.PlatformTestStorage testStorage, String name) throws java.io.IOException; + } + +} + +package androidx.test.core.view { + + public final class ViewCapture { + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static suspend Object? forceRedraw(android.view.View, kotlin.coroutines.Continuation); + } + +} + diff --git a/core/java/androidx/test/core/api/1.7.0_public.txt b/core/java/androidx/test/core/api/1.7.0_public.txt new file mode 100644 index 000000000..62dfd9a6e --- /dev/null +++ b/core/java/androidx/test/core/api/1.7.0_public.txt @@ -0,0 +1,123 @@ +// Signature format: 3.0 +package androidx.test.core.app { + + public final class ActivityScenario implements java.lang.AutoCloseable java.io.Closeable { + method public void close(); + method public android.app.Instrumentation.ActivityResult! getResult(); + method public androidx.lifecycle.Lifecycle.State! getState(); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario! launch(Class!); + method public static androidx.test.core.app.ActivityScenario! launch(Class!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class, android.os.Bundle?); + method public androidx.test.core.app.ActivityScenario! moveToState(androidx.lifecycle.Lifecycle.State!); + method public androidx.test.core.app.ActivityScenario! onActivity(androidx.test.core.app.ActivityScenario.ActivityAction!); + method public androidx.test.core.app.ActivityScenario! recreate(); + } + + public static interface ActivityScenario.ActivityAction { + method public void perform(A!); + } + + public final class ApplicationProvider { + method public static T! getApplicationContext(); + } + + public final class DeviceCapture { + method @kotlin.jvm.Throws(exceptionClasses=RuntimeException::class) public static android.graphics.Bitmap takeScreenshot() throws java.lang.RuntimeException; + } + +} + +package androidx.test.core.content.pm { + + public final class ApplicationInfoBuilder { + method public android.content.pm.ApplicationInfo! build(); + method public static androidx.test.core.content.pm.ApplicationInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setFlags(int); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setName(String?); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setPackageName(String!); + } + + public final class PackageInfoBuilder { + method public androidx.test.core.content.pm.PackageInfoBuilder! addRequestedPermission(String!, int); + method public android.content.pm.PackageInfo! build(); + method public static androidx.test.core.content.pm.PackageInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.PackageInfoBuilder! setApplicationInfo(android.content.pm.ApplicationInfo!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setPackageName(String!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setVersionCode(long); + method public androidx.test.core.content.pm.PackageInfoBuilder! setVersionName(String!); + } + +} + +package androidx.test.core.graphics { + + public final class BitmapStorage { + method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static void writeToTestStorage(android.graphics.Bitmap, String name) throws java.io.IOException; + } + +} + +package androidx.test.core.os { + + public final class Parcelables { + method public static T! forceParcel(T!, android.os.Parcelable.Creator!); + } + +} + +package androidx.test.core.view { + + public class MotionEventBuilder { + method public android.view.MotionEvent! build(); + method public static androidx.test.core.view.MotionEventBuilder! newBuilder(); + method public androidx.test.core.view.MotionEventBuilder! setAction(int); + method public androidx.test.core.view.MotionEventBuilder! setActionIndex(int); + method public androidx.test.core.view.MotionEventBuilder! setButtonState(int); + method public androidx.test.core.view.MotionEventBuilder! setDeviceId(int); + method public androidx.test.core.view.MotionEventBuilder! setDownTime(long); + method public androidx.test.core.view.MotionEventBuilder! setEdgeFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setEventTime(long); + method public androidx.test.core.view.MotionEventBuilder! setFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setMetaState(int); + method public androidx.test.core.view.MotionEventBuilder! setPointer(android.view.MotionEvent.PointerProperties!, android.view.MotionEvent.PointerCoords!); + method public androidx.test.core.view.MotionEventBuilder! setPointer(float, float); + method public androidx.test.core.view.MotionEventBuilder! setSource(int); + method public androidx.test.core.view.MotionEventBuilder! setXPrecision(float); + method public androidx.test.core.view.MotionEventBuilder! setYPrecision(float); + } + + public class PointerCoordsBuilder { + method public android.view.MotionEvent.PointerCoords! build(); + method public static androidx.test.core.view.PointerCoordsBuilder! newBuilder(); + method public androidx.test.core.view.PointerCoordsBuilder! setCoords(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setOrientation(float); + method public androidx.test.core.view.PointerCoordsBuilder! setPressure(float); + method public androidx.test.core.view.PointerCoordsBuilder! setSize(float); + method public androidx.test.core.view.PointerCoordsBuilder! setTool(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setTouch(float, float); + } + + public class PointerPropertiesBuilder { + method public android.view.MotionEvent.PointerProperties! build(); + method public static androidx.test.core.view.PointerPropertiesBuilder! newBuilder(); + method public androidx.test.core.view.PointerPropertiesBuilder! setId(int); + method public androidx.test.core.view.PointerPropertiesBuilder! setToolType(int); + } + + public final class ViewCapture { + method public static suspend Object? captureToBitmap(android.view.View, android.graphics.Rect? rect = null, kotlin.coroutines.Continuation); + method public static com.google.common.util.concurrent.ListenableFuture captureToBitmapAsync(android.view.View, android.graphics.Rect? rect = null); + } + + public final class WindowCapture { + method public static suspend Object? captureRegionToBitmap(android.view.Window, android.graphics.Rect? boundsInWindow = null, kotlin.coroutines.Continuation); + method public static com.google.common.util.concurrent.ListenableFuture captureRegionToBitmapAsync(android.view.Window, android.graphics.Rect? boundsInWindow = null); + } + +} + diff --git a/core/java/androidx/test/core/api/current.txt b/core/java/androidx/test/core/api/current.txt deleted file mode 100644 index 1b60de3da..000000000 --- a/core/java/androidx/test/core/api/current.txt +++ /dev/null @@ -1,93 +0,0 @@ -// Signature format: 3.0 -package androidx.test.core.app { - - public final class ActivityScenario implements java.lang.AutoCloseable java.io.Closeable { - method public void close(); - method public android.app.Instrumentation.ActivityResult! getResult(); - method public androidx.lifecycle.Lifecycle.State! getState(); - method public static androidx.test.core.app.ActivityScenario! launch(Class!); - method public static androidx.test.core.app.ActivityScenario! launch(Class!, android.os.Bundle?); - method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!); - method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!, android.os.Bundle?); - method public androidx.test.core.app.ActivityScenario! moveToState(androidx.lifecycle.Lifecycle.State!); - method public androidx.test.core.app.ActivityScenario! onActivity(androidx.test.core.app.ActivityScenario.ActivityAction!); - method public androidx.test.core.app.ActivityScenario! recreate(); - } - - public static interface ActivityScenario.ActivityAction { - method public void perform(A!); - } - - public final class ApplicationProvider { - method public static T! getApplicationContext(); - } - -} - -package androidx.test.core.content.pm { - - public final class ApplicationInfoBuilder { - method public android.content.pm.ApplicationInfo! build(); - method public static androidx.test.core.content.pm.ApplicationInfoBuilder! newBuilder(); - method public androidx.test.core.content.pm.ApplicationInfoBuilder! setName(String?); - method public androidx.test.core.content.pm.ApplicationInfoBuilder! setPackageName(String!); - } - - public final class PackageInfoBuilder { - method public android.content.pm.PackageInfo! build(); - method public static androidx.test.core.content.pm.PackageInfoBuilder! newBuilder(); - method public androidx.test.core.content.pm.PackageInfoBuilder! setApplicationInfo(android.content.pm.ApplicationInfo!); - method public androidx.test.core.content.pm.PackageInfoBuilder! setPackageName(String!); - } - -} - -package androidx.test.core.os { - - public final class Parcelables { - method public static T! forceParcel(T!, android.os.Parcelable.Creator!); - } - -} - -package androidx.test.core.view { - - public class MotionEventBuilder { - method public android.view.MotionEvent! build(); - method public static androidx.test.core.view.MotionEventBuilder! newBuilder(); - method public androidx.test.core.view.MotionEventBuilder! setAction(int); - method public androidx.test.core.view.MotionEventBuilder! setActionIndex(int); - method public androidx.test.core.view.MotionEventBuilder! setButtonState(int); - method public androidx.test.core.view.MotionEventBuilder! setDeviceId(int); - method public androidx.test.core.view.MotionEventBuilder! setDownTime(long); - method public androidx.test.core.view.MotionEventBuilder! setEdgeFlags(int); - method public androidx.test.core.view.MotionEventBuilder! setEventTime(long); - method public androidx.test.core.view.MotionEventBuilder! setFlags(int); - method public androidx.test.core.view.MotionEventBuilder! setMetaState(int); - method public androidx.test.core.view.MotionEventBuilder! setPointer(float, float); - method public androidx.test.core.view.MotionEventBuilder! setPointer(android.view.MotionEvent.PointerProperties!, android.view.MotionEvent.PointerCoords!); - method public androidx.test.core.view.MotionEventBuilder! setSource(int); - method public androidx.test.core.view.MotionEventBuilder! setXPrecision(float); - method public androidx.test.core.view.MotionEventBuilder! setYPrecision(float); - } - - public class PointerCoordsBuilder { - method public android.view.MotionEvent.PointerCoords! build(); - method public static androidx.test.core.view.PointerCoordsBuilder! newBuilder(); - method public androidx.test.core.view.PointerCoordsBuilder! setCoords(float, float); - method public androidx.test.core.view.PointerCoordsBuilder! setOrientation(float); - method public androidx.test.core.view.PointerCoordsBuilder! setPressure(float); - method public androidx.test.core.view.PointerCoordsBuilder! setSize(float); - method public androidx.test.core.view.PointerCoordsBuilder! setTool(float, float); - method public androidx.test.core.view.PointerCoordsBuilder! setTouch(float, float); - } - - public class PointerPropertiesBuilder { - method public android.view.MotionEvent.PointerProperties! build(); - method public static androidx.test.core.view.PointerPropertiesBuilder! newBuilder(); - method public androidx.test.core.view.PointerPropertiesBuilder! setId(int); - method public androidx.test.core.view.PointerPropertiesBuilder! setToolType(int); - } - -} - diff --git a/core/java/androidx/test/core/api/current_internal.txt b/core/java/androidx/test/core/api/current_internal.txt new file mode 100644 index 000000000..ad1479406 --- /dev/null +++ b/core/java/androidx/test/core/api/current_internal.txt @@ -0,0 +1,26 @@ +// Signature format: 5.0 +package androidx.test.core.app { + + public final class DeviceCapture { + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static boolean canTakeScreenshot(); + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static android.graphics.Bitmap takeScreenshotNoSync() throws java.lang.RuntimeException; + } + +} + +package androidx.test.core.graphics { + + public final class BitmapStorage { + method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static void writeToTestStorage(android.graphics.Bitmap, androidx.test.platform.io.PlatformTestStorage testStorage, String name) throws java.io.IOException; + } + +} + +package androidx.test.core.view { + + public final class ViewCapture { + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static suspend Object? forceRedraw(android.view.View, kotlin.coroutines.Continuation); + } + +} + diff --git a/core/java/androidx/test/core/api/current_public.txt b/core/java/androidx/test/core/api/current_public.txt new file mode 100644 index 000000000..8eee5532a --- /dev/null +++ b/core/java/androidx/test/core/api/current_public.txt @@ -0,0 +1,123 @@ +// Signature format: 5.0 +package androidx.test.core.app { + + public final class ActivityScenario implements java.lang.AutoCloseable java.io.Closeable { + method public void close(); + method public android.app.Instrumentation.ActivityResult! getResult(); + method public androidx.lifecycle.Lifecycle.State! getState(); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!); + method public static androidx.test.core.app.ActivityScenario! launch(android.content.Intent!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario! launch(Class!); + method public static androidx.test.core.app.ActivityScenario! launch(Class!, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(android.content.Intent, android.os.Bundle?); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class); + method public static androidx.test.core.app.ActivityScenario launchActivityForResult(Class, android.os.Bundle?); + method public androidx.test.core.app.ActivityScenario! moveToState(androidx.lifecycle.Lifecycle.State!); + method public androidx.test.core.app.ActivityScenario! onActivity(androidx.test.core.app.ActivityScenario.ActivityAction!); + method public androidx.test.core.app.ActivityScenario! recreate(); + } + + public static interface ActivityScenario.ActivityAction { + method public void perform(A); + } + + public final class ApplicationProvider { + method public static T getApplicationContext(); + } + + public final class DeviceCapture { + method @kotlin.jvm.Throws(exceptionClasses=RuntimeException::class) public static android.graphics.Bitmap takeScreenshot() throws java.lang.RuntimeException; + } + +} + +package androidx.test.core.content.pm { + + public final class ApplicationInfoBuilder { + method public android.content.pm.ApplicationInfo! build(); + method public static androidx.test.core.content.pm.ApplicationInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setFlags(int); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setName(String?); + method public androidx.test.core.content.pm.ApplicationInfoBuilder! setPackageName(String!); + } + + public final class PackageInfoBuilder { + method public androidx.test.core.content.pm.PackageInfoBuilder! addRequestedPermission(String!, int); + method public android.content.pm.PackageInfo! build(); + method public static androidx.test.core.content.pm.PackageInfoBuilder! newBuilder(); + method public androidx.test.core.content.pm.PackageInfoBuilder! setApplicationInfo(android.content.pm.ApplicationInfo!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setPackageName(String!); + method public androidx.test.core.content.pm.PackageInfoBuilder! setVersionCode(long); + method public androidx.test.core.content.pm.PackageInfoBuilder! setVersionName(String!); + } + +} + +package androidx.test.core.graphics { + + public final class BitmapStorage { + method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static void writeToTestStorage(android.graphics.Bitmap, String name) throws java.io.IOException; + } + +} + +package androidx.test.core.os { + + public final class Parcelables { + method public static T forceParcel(T, android.os.Parcelable.Creator!); + } + +} + +package androidx.test.core.view { + + public class MotionEventBuilder { + method public android.view.MotionEvent! build(); + method public static androidx.test.core.view.MotionEventBuilder! newBuilder(); + method public androidx.test.core.view.MotionEventBuilder! setAction(int); + method public androidx.test.core.view.MotionEventBuilder! setActionIndex(int); + method public androidx.test.core.view.MotionEventBuilder! setButtonState(int); + method public androidx.test.core.view.MotionEventBuilder! setDeviceId(int); + method public androidx.test.core.view.MotionEventBuilder! setDownTime(long); + method public androidx.test.core.view.MotionEventBuilder! setEdgeFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setEventTime(long); + method public androidx.test.core.view.MotionEventBuilder! setFlags(int); + method public androidx.test.core.view.MotionEventBuilder! setMetaState(int); + method public androidx.test.core.view.MotionEventBuilder! setPointer(android.view.MotionEvent.PointerProperties!, android.view.MotionEvent.PointerCoords!); + method public androidx.test.core.view.MotionEventBuilder! setPointer(float, float); + method public androidx.test.core.view.MotionEventBuilder! setSource(int); + method public androidx.test.core.view.MotionEventBuilder! setXPrecision(float); + method public androidx.test.core.view.MotionEventBuilder! setYPrecision(float); + } + + public class PointerCoordsBuilder { + method public android.view.MotionEvent.PointerCoords! build(); + method public static androidx.test.core.view.PointerCoordsBuilder! newBuilder(); + method public androidx.test.core.view.PointerCoordsBuilder! setCoords(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setOrientation(float); + method public androidx.test.core.view.PointerCoordsBuilder! setPressure(float); + method public androidx.test.core.view.PointerCoordsBuilder! setSize(float); + method public androidx.test.core.view.PointerCoordsBuilder! setTool(float, float); + method public androidx.test.core.view.PointerCoordsBuilder! setTouch(float, float); + } + + public class PointerPropertiesBuilder { + method public android.view.MotionEvent.PointerProperties! build(); + method public static androidx.test.core.view.PointerPropertiesBuilder! newBuilder(); + method public androidx.test.core.view.PointerPropertiesBuilder! setId(int); + method public androidx.test.core.view.PointerPropertiesBuilder! setToolType(int); + } + + public final class ViewCapture { + method public static suspend Object? captureToBitmap(android.view.View, optional android.graphics.Rect? rect, kotlin.coroutines.Continuation); + method public static com.google.common.util.concurrent.ListenableFuture captureToBitmapAsync(android.view.View, optional android.graphics.Rect? rect); + } + + public final class WindowCapture { + method public static suspend Object? captureRegionToBitmap(android.view.Window, optional android.graphics.Rect? boundsInWindow, kotlin.coroutines.Continuation); + method public static com.google.common.util.concurrent.ListenableFuture captureRegionToBitmapAsync(android.view.Window, optional android.graphics.Rect? boundsInWindow); + } + +} + diff --git a/core/java/androidx/test/core/app/ActivityScenario.java b/core/java/androidx/test/core/app/ActivityScenario.java index 31d59bf36..e82c603b1 100644 --- a/core/java/androidx/test/core/app/ActivityScenario.java +++ b/core/java/androidx/test/core/app/ActivityScenario.java @@ -23,14 +23,17 @@ import android.app.Activity; import android.app.Instrumentation.ActivityResult; +import android.content.ComponentName; import android.content.Intent; import android.os.Build.VERSION; import android.os.Bundle; import android.os.Looper; +import android.os.SystemClock; import android.provider.Settings; -import androidx.annotation.Nullable; import android.util.Log; import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle.Event; import androidx.lifecycle.Lifecycle.State; import androidx.test.internal.platform.ServiceLoaderWrapper; @@ -40,6 +43,7 @@ import androidx.test.runner.lifecycle.ActivityLifecycleMonitor; import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; import androidx.test.runner.lifecycle.Stage; +import androidx.tracing.Trace; import java.io.Closeable; import java.util.Arrays; import java.util.EnumMap; @@ -58,8 +62,9 @@ *

The ActivityScenario API uses {@link State} extensively. If you are unfamiliar with {@link * androidx.lifecycle} components, please read lifecycle - * before starting. It is crucial to understand the difference between {@link State} and {@link - * Event}. + * before starting. + * + *

It is crucial to understand the difference between {@link State} and {@link Event}. * *

{@link ActivityScenario#moveToState(State)} allows you to transition your Activity's state to * {@link State#CREATED}, {@link State#STARTED}, {@link State#RESUMED}, or {@link State#DESTROYED}. @@ -109,6 +114,7 @@ * } * } */ +@SuppressWarnings("NewApi") // suppress AutoCloseable usage error public final class ActivityScenario implements AutoCloseable, Closeable { private static final String TAG = ActivityScenario.class.getSimpleName(); @@ -184,6 +190,8 @@ private ActivityScenario(Class activityClass) { * *

If you need to supply parameters to the start activity intent, use {@link #launch(Intent)}. * + *

If you need to get the activity result, use {@link #launchActivityForResult(Class)}. + * *

This method cannot be called from the main thread except in Robolectric tests. * * @param activityClass an activity class to launch @@ -192,19 +200,20 @@ private ActivityScenario(Class activityClass) { */ public static ActivityScenario launch(Class activityClass) { ActivityScenario scenario = new ActivityScenario<>(checkNotNull(activityClass)); - scenario.launchInternal(/*activityOptions=*/ null); + scenario.launchInternal(/*activityOptions=*/ null, /*launchActivityForResult=*/ false); return scenario; } /** * @see #launch(Class) + * @param activityClass an activity class to launch * @param activityOptions an activity options bundle to be passed along with the intent to start * activity. */ public static ActivityScenario launch( Class activityClass, @Nullable Bundle activityOptions) { ActivityScenario scenario = new ActivityScenario<>(checkNotNull(activityClass)); - scenario.launchInternal(activityOptions); + scenario.launchInternal(activityOptions, /*launchActivityForResult=*/ false); return scenario; } @@ -215,6 +224,8 @@ public static ActivityScenario launch( * {@link Activity#finish} from your {@link Activity#onCreate}, the state is {@link * State#DESTROYED} when this method returns. * + *

If you need to get the activity result, use {@link #launchActivityForResult(Intent)}. + * *

This method cannot be called from the main thread except in Robolectric tests. * * @param startActivityIntent an intent to start the activity @@ -223,19 +234,102 @@ public static ActivityScenario launch( */ public static ActivityScenario launch(Intent startActivityIntent) { ActivityScenario scenario = new ActivityScenario<>(checkNotNull(startActivityIntent)); - scenario.launchInternal(/*activityOptions=*/ null); + scenario.launchInternal(/*activityOptions=*/ null, /*launchActivityForResult=*/ false); return scenario; } /** - * @see #launch(Intent) + * Launches an activity by a given intent and activity options and constructs ActivityScenario + * with the activity. @see #launch(Intent) + * + * @param startActivityIntent an intent to start the activity * @param activityOptions an activity options bundle to be passed along with the intent to start * activity. */ public static ActivityScenario launch( Intent startActivityIntent, @Nullable Bundle activityOptions) { ActivityScenario scenario = new ActivityScenario<>(checkNotNull(startActivityIntent)); - scenario.launchInternal(activityOptions); + scenario.launchInternal(activityOptions, /*launchActivityForResult=*/ false); + return scenario; + } + + /** + * Launches an activity of a given class and constructs ActivityScenario with the activity. Waits + * for the lifecycle state transitions to be complete. Typically the initial state of the activity + * is {@link State#RESUMED} but can be in another state. For instance, if your activity calls + * {@link Activity#finish} from your {@link Activity#onCreate}, the state is {@link + * State#DESTROYED} when this method returns. Broadcasts activity result. + * + *

If you need to supply parameters to the start activity intent, use {@link + * #launchActivityForResult(Intent)}. + * + *

This method cannot be called from the main thread except in Robolectric tests. + * + * @param activityClass an activity class to launch + * @throws AssertionError if the lifecycle state transition never completes within the timeout + * @return ActivityScenario which you can use to make further state transitions + */ + @NonNull + public static ActivityScenario launchActivityForResult( + @NonNull Class activityClass) { + ActivityScenario scenario = new ActivityScenario<>(checkNotNull(activityClass)); + scenario.launchInternal(/*activityOptions=*/ null, /*launchActivityForResult=*/ true); + return scenario; + } + + /** + * Launches an activity of a given class and activity options and constructs ActivityScenario with + * the activity. @see #launchActivityForResult(Class) + * + * @param activityClass an activity class to launch + * @param activityOptions an activity options bundle to be passed along with the intent to start + * activity. + */ + @NonNull + public static ActivityScenario launchActivityForResult( + @NonNull Class activityClass, @Nullable Bundle activityOptions) { + ActivityScenario scenario = new ActivityScenario<>(checkNotNull(activityClass)); + scenario.launchInternal(activityOptions, /*launchActivityForResult=*/ true); + return scenario; + } + + /** + * Launches an activity by a given intent and constructs ActivityScenario with the activity. Waits + * for the lifecycle state transitions to be complete. Typically the initial state of the activity + * is {@link State#RESUMED} but can be in another state. For instance, if your activity calls + * {@link Activity#finish} from your {@link Activity#onCreate}, the state is {@link + * State#DESTROYED} when this method returns. Broadcasts activity result. + * + *

This method cannot be called from the main thread except in Robolectric tests. + * + *

If you are using AndroidX based activities, use androidx.activity.result.ActivityResult + * instead of this method. See https://developer.android.com/training/basics/intents/result#test + * + * @param startActivityIntent an intent to start the activity + * @throws AssertionError if the lifecycle state transition never completes within the timeout + * @return ActivityScenario which you can use to make further state transitions + */ + @NonNull + public static ActivityScenario launchActivityForResult( + @NonNull Intent startActivityIntent) { + ActivityScenario scenario = new ActivityScenario<>(checkNotNull(startActivityIntent)); + scenario.launchInternal(/*activityOptions=*/ null, /*launchActivityForResult=*/ true); + return scenario; + } + + /** + * Launches an activity by a given intent and constructs ActivityScenario with the activity. @see + * #launchActivityForResult(Intent) + * + * @param startActivityIntent an intent to start the activity + * @param activityOptions an activity options bundle to be passed along with the intent to start + * activity. + */ + @NonNull + public static ActivityScenario launchActivityForResult( + @NonNull Intent startActivityIntent, @Nullable Bundle activityOptions) { + ActivityScenario scenario = new ActivityScenario<>(checkNotNull(startActivityIntent)); + scenario.launchInternal(activityOptions, /*launchActivityForResult=*/ true); return scenario; } @@ -244,8 +338,9 @@ public static ActivityScenario launch( * along with preconditions checks around device's configuration. * * @param activityOptions activity options bundle to be passed when launching this activity + * @param launchActivityForResult whether or not activity result code and data is needed */ - private void launchInternal(@Nullable Bundle activityOptions) { + private void launchInternal(@Nullable Bundle activityOptions, boolean launchActivityForResult) { checkState( Settings.System.getInt( getInstrumentation().getTargetContext().getContentResolver(), @@ -255,21 +350,37 @@ private void launchInternal(@Nullable Bundle activityOptions) { "\"Don't keep activities\" developer options must be disabled for ActivityScenario"); checkNotMainThread(); - getInstrumentation().waitForIdleSync(); - ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(activityLifecycleObserver); + Trace.beginSection("ActivityScenario launch"); + try { + getInstrumentation().waitForIdleSync(); - // prefer the single argument variant for startActivity for backwards compatibility with older - // Robolectric versions - if (activityOptions == null) { - activityInvoker.startActivity(startActivityIntent); - } else { - activityInvoker.startActivity(startActivityIntent, activityOptions); - } + ActivityLifecycleMonitorRegistry.getInstance() + .addLifecycleCallback(activityLifecycleObserver); - // Accept any steady states. An activity may start another activity in its onCreate method. Such - // an activity goes back to created or started state immediately after it is resumed. - waitForActivityToBecomeAnyOf(STEADY_STATES.values().toArray(new State[0])); + // prefer the single argument variant for startActivity for backwards compatibility with older + // Robolectric versions + if (activityOptions == null) { + if (launchActivityForResult) { + activityInvoker.startActivityForResult(startActivityIntent); + } else { + activityInvoker.startActivity(startActivityIntent); + } + } else { + if (launchActivityForResult) { + activityInvoker.startActivityForResult(startActivityIntent, activityOptions); + } else { + activityInvoker.startActivity(startActivityIntent, activityOptions); + } + } + + // Accept any steady states. An activity may start another activity in its onCreate method. + // Such + // an activity goes back to created or started state immediately after it is resumed. + waitForActivityToBecomeAnyOf(STEADY_STATES.values().toArray(new State[0])); + } finally { + Trace.endSection(); + } } /** @@ -305,9 +416,14 @@ private void launchInternal(@Nullable Bundle activityOptions) { */ @Override public void close() { - moveToState(State.DESTROYED); - ActivityLifecycleMonitorRegistry.getInstance() - .removeLifecycleCallback(activityLifecycleObserver); + Trace.beginSection("ActivityScenario close"); + try { + moveToState(State.DESTROYED); + ActivityLifecycleMonitorRegistry.getInstance() + .removeLifecycleCallback(activityLifecycleObserver); + } finally { + Trace.endSection(); + } } /** @@ -325,12 +441,12 @@ private void waitForActivityToBecomeAnyOf(State... expectedStates) { return; } - long now = System.currentTimeMillis(); + long now = SystemClock.elapsedRealtime(); long deadline = now + TIMEOUT_MILLISECONDS; while (now < deadline && !expectedStateSet.contains(STEADY_STATES.get(currentActivityStage))) { stateChangedCondition.await(deadline - now, TimeUnit.MILLISECONDS); - now = System.currentTimeMillis(); + now = SystemClock.elapsedRealtime(); } if (!expectedStateSet.contains(STEADY_STATES.get(currentActivityStage))) { @@ -443,7 +559,11 @@ private static boolean activityMatchesIntent( if (!equals(startActivityIntent.getType(), activityIntent.getType())) { return false; } - if (!equals(startActivityIntent.getPackage(), activityIntent.getPackage())) { + boolean isActivityInSamePackage = + hasPackageEquivalentComponent(startActivityIntent) + && hasPackageEquivalentComponent(activityIntent); + if (!isActivityInSamePackage + && !equals(startActivityIntent.getPackage(), activityIntent.getPackage())) { return false; } if (startActivityIntent.getComponent() != null) { @@ -463,6 +583,20 @@ private static boolean activityMatchesIntent( return true; } + /** + * Return {@code true} if the component name is not null and is in the same package that this + * intent limited to. otherwise return {@code false}. Note: this code is copied from {@code + * Intent#hasPackageEquivalentComponent}. + */ + private static boolean hasPackageEquivalentComponent(Intent intent) { + ComponentName componentName = intent.getComponent(); + String packageName = intent.getPackage(); + // packageName may be null when the resolved Activity is in the same package to this + // running process. + return componentName != null + && (packageName == null || packageName.equals(componentName.getPackageName())); + } + // reimplementation of Objects.equals since it is only available on APIs >= 19 private static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); @@ -585,11 +719,11 @@ public ActivityScenario recreate() { activityInvoker.recreateActivity(prevActivityState.activity); ActivityState activityState; - long now = System.currentTimeMillis(); + long now = SystemClock.elapsedRealtime(); long deadline = now + TIMEOUT_MILLISECONDS; do { waitForActivityToBecomeAnyOf(State.RESUMED); - now = System.currentTimeMillis(); + now = SystemClock.elapsedRealtime(); activityState = getCurrentActivityState(); } while (now < deadline && activityState.activity == prevActivityState.activity); if (activityState.activity == prevActivityState.activity) { @@ -679,18 +813,24 @@ public ActivityScenario onActivity(final ActivityAction action) { /** * Waits for the activity to be finished and returns the activity result. * + *

ActivityScenario.launchActivityForResult() must be used to launch an Activity before this + * method is called. + * *

Note: This method doesn't call {@link Activity#finish()}. The activity must be finishing or * finished otherwise this method will throws runtime exception after the timeout. * *

{@code
    * Example:
-   *   ActivityScenario scenario = ActivityScenario.launch(MyActivity.class);
+   *   ActivityScenario scenario =
+   *      ActivityScenario.launchActivityForResult(MyActivity.class);
    *   // Let's say MyActivity has a button that finishes itself.
    *   onView(withId(R.id.finish_button)).perform(click());
    *   assertThat(scenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_OK);
    * }
* * @return activity result of the activity that managed by this scenario class. + * @throws IllegalStateException when you call this method with an Activity that was not started + * by {@link #launchActivityForResult} */ public ActivityResult getResult() { return activityInvoker.getActivityResult(); diff --git a/core/java/androidx/test/core/app/ApplicationProvider.java b/core/java/androidx/test/core/app/ApplicationProvider.java index eaa8f311e..28587a08a 100644 --- a/core/java/androidx/test/core/app/ApplicationProvider.java +++ b/core/java/androidx/test/core/app/ApplicationProvider.java @@ -31,9 +31,9 @@ public final class ApplicationProvider { private ApplicationProvider() {} /** - * Returns the application {@link Context} for the application under test. + * Returns the application {@link android.content.Context} for the application under test. * - * @see {@link Context#getApplicationContext()} + * @see Context#getApplicationContext() */ @SuppressWarnings("unchecked") public static T getApplicationContext() { diff --git a/core/java/androidx/test/core/app/DeviceCapture.kt b/core/java/androidx/test/core/app/DeviceCapture.kt new file mode 100644 index 000000000..033efcdad --- /dev/null +++ b/core/java/androidx/test/core/app/DeviceCapture.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2021 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. + */ +@file:JvmName("DeviceCapture") + +package androidx.test.core.app + +import android.app.UiAutomation +import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.Choreographer +import androidx.annotation.RestrictTo +import androidx.test.core.internal.os.HandlerExecutor +import androidx.test.core.view.forceRedraw +import androidx.test.internal.util.Checks +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.platform.graphics.HardwareRendererCompat +import androidx.test.platform.view.inspector.WindowInspectorCompat +import java.lang.RuntimeException +import kotlin.coroutines.resumeWithException +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout + +/** + * Returns false if calling [takeScreenshot] will fail. + * + * Taking a screenshot requires [UiAutomation] and can only be called off of the main thread. If + * this method returns false then attempting to take a screenshot will fail. Note that taking a + * screenshot may still fail if this method returns true, for example if the call to [UiAutomation] + * fails. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun canTakeScreenshot(): Boolean = + getInstrumentation().uiAutomation != null && Looper.myLooper() != Looper.getMainLooper() + +/** + * Captures an image of the device's screen into a [Bitmap]. + * + * This is essentially a wrapper for [UIAutomation#takeScreenshot()] that attempts to get a stable + * screenshot by forcing all the current application's root window views to redraw, and also handles + * cases where hardware renderer drawing is disabled. + * + * This API is intended for use cases like debugging where an image of the entire screen is needed. + * For use cases where the image will be used for validation, its recommended to take a more + * isolated, targeted screenshot of a specific view or compose node. See + * [androidx.test.core.view.captureToBitmap], [androidx.test.espresso.screenshot.captureToBitmap] + * and [androidx.compose.ui.test.captureToImage]. + * + * This API does not support concurrent usage. + * + * This API is currently experimental and subject to change or removal. + * + * @return a [Bitmap] that contains the image + * @throws [IllegalStateException] if called on the main thread. This is a limitation of connecting + * to UiAutomation, [RuntimeException] if UiAutomation fails to take the screenshot + */ +@Suppress("FutureReturnValueIgnored") +@Throws(RuntimeException::class) +fun takeScreenshot(): Bitmap { + getInstrumentation().waitForIdleSync() + return takeScreenshotNoSync() +} + +/** + * An internal variant of [takeScreenshot] that skips an idle sync call. + * + * This intended for failure handling cases where caller does not want to wait for main thread to be + * idle. + * + * @return a [Bitmap] + * @throws [IllegalStateException] if called on the main thread. This is a limitation of connecting + * to UiAutomation, [RuntimeException] if UiAutomation fails to take the screenshot + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@Suppress("FutureReturnValueIgnored") +@Throws(RuntimeException::class) +fun takeScreenshotNoSync(): Bitmap { + Checks.checkState(canTakeScreenshot()) + + var bitmap: Bitmap? = null + var exception: Exception? = null + val mainHandlerDispatcher = + HandlerExecutor(Handler(Looper.getMainLooper())).asCoroutineDispatcher() + val uiAutomation = getInstrumentation().uiAutomation + if (uiAutomation == null) { + throw RuntimeException("uiautomation is null") + } + + val hardwareDrawingEnabled = HardwareRendererCompat.isDrawingEnabled() + HardwareRendererCompat.setDrawingEnabled(true) + + return runBlocking(mainHandlerDispatcher) { + withTimeout(5.seconds) { + forceRedrawGlobalWindowViews() + bitmap = takeScreenshotOnNextFrame(uiAutomation, hardwareDrawingEnabled) + exception?.let { throw it } + bitmap!! + } + } +} + +private suspend fun forceRedrawGlobalWindowViews() { + val views = WindowInspectorCompat.getGlobalWindowViews() + Log.d("DeviceCapture", "Found ${views.size} global views to redraw") + for (view in views) { + view.forceRedraw() + } +} + +private suspend fun takeScreenshotOnNextFrame( + uiAutomation: UiAutomation, + hardwareDrawingEnabled: Boolean, +): Bitmap { + // wait on the next frame to increase probability the draw from previous step is + // committed + // TODO(b/289244795): use a transaction callback instead + + return suspendCancellableCoroutine { cont -> + Choreographer.getInstance().postFrameCallback { + // do multiple retries of uiAutomation.takeScreenshot because it is known to return null + // on API 31+ b/257274080 + var bitmap: Bitmap? = null + for (i in 1..3) { + bitmap = uiAutomation.takeScreenshot() + if (bitmap != null) { + Log.i("DeviceCapture", "got bitmap, returning") + break + } + } + HardwareRendererCompat.setDrawingEnabled(hardwareDrawingEnabled) + if (bitmap == null) { + Log.w("DeviceCapture", "failed to get bitmap, returning exception") + cont.resumeWithException(RuntimeException("uiAutomation.takeScreenshot returned null")) + } else { + cont.resume(bitmap, {}) + } + } + } +} diff --git a/core/java/androidx/test/core/app/InstrumentationActivityInvoker.java b/core/java/androidx/test/core/app/InstrumentationActivityInvoker.java index 896666ef9..dbaa81c6c 100644 --- a/core/java/androidx/test/core/app/InstrumentationActivityInvoker.java +++ b/core/java/androidx/test/core/app/InstrumentationActivityInvoker.java @@ -16,12 +16,14 @@ package androidx.test.core.app; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.internal.util.Checks.checkNotNull; import static androidx.test.internal.util.Checks.checkState; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import android.app.Activity; +import android.app.ActivityOptions; import android.app.Instrumentation.ActivityResult; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -31,9 +33,11 @@ import android.content.IntentSender; import android.content.pm.ActivityInfo; import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; -import androidx.annotation.Nullable; import android.util.Log; +import androidx.annotation.Nullable; import androidx.test.internal.platform.app.ActivityInvoker; import androidx.test.internal.platform.app.ActivityLifecycleTimeout; import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; @@ -114,9 +118,6 @@ class InstrumentationActivityInvoker implements ActivityInvoker { private static final String FINISH_EMPTY_ACTIVITIES = "androidx.test.core.app.InstrumentationActivityInvoker.FINISH_EMPTY_ACTIVITIES"; - // TODO(b/176898246): Update to PendingIntent.FLAG_MUTABLE once available - private static final int FLAG_MUTABLE = 1 << 25; - /** * BootstrapActivity starts a test target activity specified by the extras bundle with key {@link * #TARGET_ACTIVITY_INTENT_KEY} in the intent that starts this bootstrap activity. The target @@ -142,7 +143,7 @@ public void onReceive(Context context, Intent intent) { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - registerReceiver(receiver, new IntentFilter(FINISH_BOOTSTRAP_ACTIVITY)); + registerBroadcastReceiver(this, receiver, new IntentFilter(FINISH_BOOTSTRAP_ACTIVITY)); isTargetActivityStarted = (savedInstanceState != null @@ -167,9 +168,11 @@ protected void onResume() { isTargetActivityStarted = true; PendingIntent startTargetActivityIntent = checkNotNull(getIntent().getParcelableExtra(TARGET_ACTIVITY_INTENT_KEY)); - Bundle options = getIntent().getBundleExtra(TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY); + Bundle options = + optInToGrantBalPrivileges( + getIntent().getBundleExtra(TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY)); try { - if (options == null || Build.VERSION.SDK_INT < 16) { + if (options == null) { // Override and disable FLAG_ACTIVITY_NEW_TASK flag by flagsMask and flagsValue. // PendingIntentRecord#sendInner() will mask the original intent flag with the flagsMask // then override those bits with the new flagsValue specified here. This override is @@ -269,7 +272,7 @@ public void onReceive(Context context, Intent intent) { }; IntentFilter intentFilter = new IntentFilter(BOOTSTRAP_ACTIVITY_RESULT_RECEIVED); intentFilter.addAction(CANCEL_ACTIVITY_RESULT_WAITER); - context.registerReceiver(receiver, intentFilter); + registerBroadcastReceiver(context, receiver, intentFilter); } /** @@ -286,7 +289,7 @@ public ActivityResult getActivityResult() { } checkNotNull( activityResult, - "onActivityResult never be called after %d milliseconds", + "onActivityResult was not called within %d milliseconds", ActivityLifecycleTimeout.getMillis()); return activityResult; } @@ -315,7 +318,7 @@ public void onReceive(Context context, Intent intent) { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - registerReceiver(receiver, new IntentFilter(FINISH_EMPTY_ACTIVITIES)); + registerBroadcastReceiver(this, receiver, new IntentFilter(FINISH_EMPTY_ACTIVITIES)); // disable starting animations overridePendingTransition(0, 0); @@ -364,7 +367,7 @@ public void onReceive(Context context, Intent intent) { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - registerReceiver(receiver, new IntentFilter(FINISH_EMPTY_ACTIVITIES)); + registerBroadcastReceiver(this, receiver, new IntentFilter(FINISH_EMPTY_ACTIVITIES)); // disable starting animations overridePendingTransition(0, 0); @@ -406,8 +409,48 @@ public void startActivity(Intent intent, @Nullable Bundle activityOptions) { getApplicationContext().sendBroadcast(new Intent(FINISH_BOOTSTRAP_ACTIVITY)); getApplicationContext().sendBroadcast(new Intent(FINISH_EMPTY_ACTIVITIES)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + if (Build.VERSION.SDK_INT < 28) { + if (activityOptions != null) { + throw new IllegalStateException( + "Starting an activity with activityOptions is not supported on APIs below 28."); + } else { + getInstrumentation().startActivitySync(intent); + } + } else { + getInstrumentation().startActivitySync(intent, activityOptions); + } + } + + @Override + public void startActivity(Intent intent) { + startActivity(intent, null); + } + + /** Starts an Activity using the given intent. */ + @Override + public void startActivityForResult(Intent intent, @Nullable Bundle activityOptionsBundle) { + // make sure the intent can resolve an activity + ActivityInfo ai = intent.resolveActivityInfo(getApplicationContext().getPackageManager(), 0); + if (ai == null) { + throw new IllegalStateException("Unable to resolve activity for: " + intent); + } + // Close empty activities and bootstrap activity if it's running. This might happen if the + // previous test crashes before it cleans up the state. + getApplicationContext().sendBroadcast(new Intent(FINISH_BOOTSTRAP_ACTIVITY)); + getApplicationContext().sendBroadcast(new Intent(FINISH_EMPTY_ACTIVITIES)); + activityResultWaiter = new ActivityResultWaiter(getApplicationContext()); + activityOptionsBundle = optInToGrantBalPrivileges(activityOptionsBundle); + + // make an immutable intent if its implicit + int intentMutability = + intent.getPackage() == null && intent.getComponent() == null + ? PendingIntent.FLAG_IMMUTABLE + : PendingIntent.FLAG_MUTABLE; + // Note: Instrumentation.startActivitySync(Intent) cannot be used here because BootstrapActivity // may start in different process. Also, we use PendingIntent because the target activity may // set "exported" attribute to false so that it prohibits starting the activity outside of their @@ -419,27 +462,43 @@ public void startActivity(Intent intent, @Nullable Bundle activityOptions) { TARGET_ACTIVITY_INTENT_KEY, PendingIntent.getActivity( getApplicationContext(), - /*requestCode=*/ 0, + /* requestCode= */ 0, intent, - /*flags=*/ PendingIntent.FLAG_UPDATE_CURRENT | FLAG_MUTABLE)) - .putExtra(TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY, activityOptions); + /* flags= */ PendingIntent.FLAG_UPDATE_CURRENT | intentMutability)) + .putExtra(TARGET_ACTIVITY_OPTIONS_BUNDLE_KEY, activityOptionsBundle); - if (Build.VERSION.SDK_INT < 16) { - // activityOptions not supported - getApplicationContext().startActivity(bootstrapIntent); - } else { - getApplicationContext().startActivity(bootstrapIntent, activityOptions); + getApplicationContext().startActivity(bootstrapIntent, activityOptionsBundle); + } + + private static Bundle optInToGrantBalPrivileges(Bundle activityOptionsBundle) { + if (VERSION.SDK_INT < VERSION_CODES.UPSIDE_DOWN_CAKE) { + return activityOptionsBundle; + } + // Initialize a bundle to grant this activities start privilege. + Bundle updatedActivityOptions = + ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .toBundle(); + // Merge the bundle with the one passed in. This allows overriding the start mode if desired. + if (activityOptionsBundle != null) { + updatedActivityOptions.putAll(activityOptionsBundle); } + return updatedActivityOptions; } @Override - public void startActivity(Intent intent) { - startActivity(intent, null); + public void startActivityForResult(Intent intent) { + startActivityForResult(intent, null); } @Override public ActivityResult getActivityResult() { - return checkNotNull(activityResultWaiter, "You must start Activity first").getActivityResult(); + if (activityResultWaiter == null) { + throw new IllegalStateException( + "You must start Activity first. Make sure you are using launchActivityForResult() to" + + " launch an Activity."); + } + return activityResultWaiter.getActivityResult(); } /** Resumes the tested activity by finishing empty activities. */ @@ -468,8 +527,8 @@ public void onReceive(Context context, Intent intent) { latch.countDown(); } }; - getApplicationContext() - .registerReceiver(receiver, new IntentFilter(EMPTY_FLOATING_ACTIVITY_RESUMED)); + registerBroadcastReceiver( + getApplicationContext(), receiver, new IntentFilter(EMPTY_FLOATING_ACTIVITY_RESUMED)); // Starting an arbitrary Activity (android:windowIsFloating = true) forces the tested Activity // to the paused state. @@ -481,7 +540,7 @@ public void onReceive(Context context, Intent intent) { try { latch.await(ActivityLifecycleTimeout.getMillis(), TimeUnit.MILLISECONDS); } catch (InterruptedException e) { - throw new AssertionError("Failed to pause activity", e); + throw new RuntimeException("Failed to pause activity", e); } finally { getApplicationContext().unregisterReceiver(receiver); } @@ -503,7 +562,8 @@ public void onReceive(Context context, Intent intent) { latch.countDown(); } }; - getApplicationContext().registerReceiver(receiver, new IntentFilter(EMPTY_ACTIVITY_RESUMED)); + registerBroadcastReceiver( + getApplicationContext(), receiver, new IntentFilter(EMPTY_ACTIVITY_RESUMED)); // Starting an arbitrary Activity (android:windowIsFloating = false) forces the tested Activity // to the stopped state. @@ -514,7 +574,7 @@ public void onReceive(Context context, Intent intent) { try { latch.await(ActivityLifecycleTimeout.getMillis(), TimeUnit.MILLISECONDS); } catch (InterruptedException e) { - throw new AssertionError("Failed to stop activity", e); + throw new RuntimeException("Failed to stop activity", e); } finally { getApplicationContext().unregisterReceiver(receiver); } @@ -549,11 +609,15 @@ public void finishActivity(Activity activity) { // for the API level above 19. startEmptyActivitySync(); getInstrumentation().runOnMainSync(activity::finish); - getApplicationContext().sendBroadcast(new Intent(FINISH_BOOTSTRAP_ACTIVITY)); - startEmptyActivitySync(); - getInstrumentation().runOnMainSync(activity::finish); + if (activityResultWaiter != null) { + getApplicationContext().sendBroadcast(new Intent(FINISH_BOOTSTRAP_ACTIVITY)); + startEmptyActivitySync(); + getInstrumentation().runOnMainSync(activity::finish); + } getApplicationContext().sendBroadcast(new Intent(FINISH_EMPTY_ACTIVITIES)); - getApplicationContext().sendBroadcast(new Intent(CANCEL_ACTIVITY_RESULT_WAITER)); + if (activityResultWaiter != null) { + getApplicationContext().sendBroadcast(new Intent(CANCEL_ACTIVITY_RESULT_WAITER)); + } } private static void checkActivityStageIsIn(Activity activity, Stage... expected) { @@ -573,4 +637,13 @@ private static void checkActivityStageIsIn(Activity activity, Set expecte stage); }); } + + private static void registerBroadcastReceiver( + Context context, BroadcastReceiver broadcastReceiver, IntentFilter intentFilter) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(broadcastReceiver, intentFilter); + } else { + context.registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED); + } + } } diff --git a/core/java/androidx/test/core/app/ListFuture.java b/core/java/androidx/test/core/app/ListFuture.java new file mode 100644 index 000000000..b154ac7be --- /dev/null +++ b/core/java/androidx/test/core/app/ListFuture.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2021 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. + */ + +package androidx.test.core.app; + +import static androidx.test.internal.util.Checks.checkNotNull; +import static androidx.test.internal.util.Checks.checkState; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.concurrent.futures.CallbackToFutureAdapter; +import androidx.test.platform.concurrent.DirectExecutor; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * The Class is based on the ListFuture in Guava and to use the CallbackToFutureAdapter instead of + * the AbstractFuture. + * + *

Class that implements {@link Futures#allAsList(Collection)} and {@link + * Futures#successfulAsList(Collection)}. The idea is to create a (null-filled) List and register a + * listener with each component future to fill out the value in the List when that future completes. + * + *

This is a temporary fork of androidx.camera.core.impl.utils.futures.ListFuture. It will be + * removed in a future change in favor of using coroutines. + */ +class ListFuture implements ListenableFuture> { + @Nullable List> mFutures; + @Nullable List mValues; + private final boolean mAllMustSucceed; + @NonNull private final AtomicInteger mRemaining; + @NonNull private final ListenableFuture> mResult; + CallbackToFutureAdapter.Completer> mResultNotifier; + + /** + * Constructor. + * + * @param futures all the futures to build the list from + * @param allMustSucceed whether a single failure or cancellation should propagate to this future + * @param listenerExecutor used to run listeners on all the passed in futures. + */ + ListFuture( + @NonNull List> futures, + boolean allMustSucceed, + @NonNull Executor listenerExecutor) { + mFutures = checkNotNull(futures); + mValues = new ArrayList<>(futures.size()); + mAllMustSucceed = allMustSucceed; + mRemaining = new AtomicInteger(futures.size()); + mResult = + CallbackToFutureAdapter.getFuture( + new CallbackToFutureAdapter.Resolver>() { + @Override + public Object attachCompleter( + @NonNull CallbackToFutureAdapter.Completer> completer) { + checkState(mResultNotifier == null, "The result can only set once!"); + mResultNotifier = completer; + return "ListFuture[" + this + "]"; + } + }); + + init(listenerExecutor); + } + + private void init(@NonNull Executor listenerExecutor) { + // First, schedule cleanup to execute when the Future is done. + addListener( + new Runnable() { + @Override + public void run() { + // By now the mValues array has either been set as the Future's value, + // or (in case of failure) is no longer useful. + ListFuture.this.mValues = null; + + // Let go of the memory held by other mFutures + ListFuture.this.mFutures = null; + } + }, + directExecutor()); + + // Now begin the "real" initialization. + + // Corner case: List is empty. + if (mFutures.isEmpty()) { + mResultNotifier.set(new ArrayList<>(mValues)); + return; + } + + // Populate the results list with null initially. + for (int i = 0; i < mFutures.size(); ++i) { + mValues.add(null); + } + + // Register a listener on each Future in the list to update + // the state of this future. + // Note that if all the mFutures on the list are done prior to completing + // this loop, the last call to addListener() will callback to + // setOneValue(), transitively call our cleanup listener, and set + // mFutures to null. + // We store a reference to mFutures to avoid the NPE. + List> localFutures = mFutures; + for (int i = 0; i < localFutures.size(); i++) { + final ListenableFuture listenable = localFutures.get(i); + final int index = i; + listenable.addListener( + new Runnable() { + @Override + public void run() { + setOneValue(index, listenable); + } + }, + listenerExecutor); + } + } + + private static Executor directExecutor() { + return DirectExecutor.INSTANCE; + } + + /** Sets the value at the given index to that of the given future. */ + void setOneValue(int index, @NonNull Future future) { + List localValues = mValues; + if (isDone() || localValues == null) { + // Some other future failed or has been cancelled, causing this one to + // also be cancelled or have an exception set. This should only happen + // if mAllMustSucceed is true. + checkState(mAllMustSucceed, "Future was done before all dependencies completed"); + return; + } + + try { + checkState(future.isDone(), "Tried to set value from future which is not done"); + localValues.set(index, getUninterruptibly(future)); + } catch (CancellationException e) { + if (mAllMustSucceed) { + // Set ourselves as cancelled. Let the input futures keep running + // as some of them may be used elsewhere. + // (Currently we don't override interruptTask, so + // mayInterruptIfRunning==false isn't technically necessary.) + cancel(false); + } + } catch (ExecutionException e) { + if (mAllMustSucceed) { + // As soon as the first one fails, throw the exception up. + // The mResult of all other inputs is then ignored. + mResultNotifier.setException(e.getCause()); + } + } catch (RuntimeException e) { + if (mAllMustSucceed) { + mResultNotifier.setException(e); + } + } catch (Error e) { + // Propagate errors up ASAP - our superclass will rethrow the error + mResultNotifier.setException(e); + } finally { + int newRemaining = mRemaining.decrementAndGet(); + checkState(newRemaining >= 0, "Less than 0 remaining futures"); + if (newRemaining == 0) { + localValues = mValues; + if (localValues != null) { + mResultNotifier.set(new ArrayList<>(localValues)); + } else { + checkState(isDone()); + } + } + } + } + + @Override + public void addListener(@NonNull Runnable listener, @NonNull Executor executor) { + mResult.addListener(listener, executor); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (mFutures != null) { + for (ListenableFuture f : mFutures) { + f.cancel(mayInterruptIfRunning); + } + } + + return mResult.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return mResult.isCancelled(); + } + + @Override + public boolean isDone() { + return mResult.isDone(); + } + + @Override + @Nullable + public List get() throws InterruptedException, ExecutionException { + callAllGets(); + + // This may still block in spite of the calls above, as the listeners may + // be scheduled for execution in other threads. + return mResult.get(); + } + + @Override + public List get(long timeout, @NonNull TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return mResult.get(timeout, unit); + } + + /** + * Calls the get method of all dependency futures to work around a bug in some ListenableFutures + * where the listeners aren't called until get() is called. + */ + private void callAllGets() throws InterruptedException { + List> oldFutures = mFutures; + if (oldFutures != null && !isDone()) { + for (ListenableFuture future : oldFutures) { + // We wait for a little while for the future, but if it's not done, + // we check that no other futures caused a cancellation or failure. + // This can introduce a delay of up to 10ms in reporting an exception. + while (!future.isDone()) { + try { + future.get(); + } catch (Error e) { + throw e; + } catch (InterruptedException e) { + throw e; + } catch (Throwable e) { + // ExecutionException / CancellationException / RuntimeException + if (mAllMustSucceed) { + return; + } else { + continue; + } + } + } + } + } + } + + /** + * Invokes {@code Future.}{@link Future#get() get()} uninterruptibly. + * + * @throws ExecutionException if the computation threw an exception + * @throws CancellationException if the computation was cancelled + */ + @Nullable + private static V getUninterruptibly(@NonNull Future future) throws ExecutionException { + boolean interrupted = false; + try { + while (true) { + try { + return future.get(); + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/core/java/androidx/test/core/content/pm/ApplicationInfoBuilder.java b/core/java/androidx/test/core/content/pm/ApplicationInfoBuilder.java index 835591a7b..d5953cd32 100644 --- a/core/java/androidx/test/core/content/pm/ApplicationInfoBuilder.java +++ b/core/java/androidx/test/core/content/pm/ApplicationInfoBuilder.java @@ -24,6 +24,7 @@ public final class ApplicationInfoBuilder { @Nullable private String name; @Nullable private String packageName; + private int flags = 0; private ApplicationInfoBuilder() {} @@ -58,11 +59,22 @@ public ApplicationInfoBuilder setName(@Nullable String name) { return this; } + /** + * Sets the flags. + * + * @see ApplicationInfo#flags + */ + public ApplicationInfoBuilder setFlags(int flags) { + this.flags = flags; + return this; + } + /** Returns a {@link ApplicationInfo} with the provided data. */ public ApplicationInfo build() { checkNotNull(packageName, "Mandatory field 'packageName' missing."); ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.flags = flags; applicationInfo.name = name; applicationInfo.packageName = packageName; diff --git a/core/java/androidx/test/core/content/pm/PackageInfoBuilder.java b/core/java/androidx/test/core/content/pm/PackageInfoBuilder.java index f01e75a3b..bc8bd2e7d 100644 --- a/core/java/androidx/test/core/content/pm/PackageInfoBuilder.java +++ b/core/java/androidx/test/core/content/pm/PackageInfoBuilder.java @@ -18,14 +18,22 @@ import static androidx.test.internal.util.Checks.checkNotNull; import static androidx.test.internal.util.Checks.checkState; +import android.annotation.TargetApi; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; +import android.os.Build; import androidx.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; /** Builder for {@link PackageInfo}. */ public final class PackageInfoBuilder { @Nullable private String packageName; @Nullable private ApplicationInfo applicationInfo; + private long longVersionCode = 0L; + @Nullable private String versionName; + /** Map of a requested permission to its requested permission flag. */ + private final Map requestedPermissionsMap = new HashMap<>(); private PackageInfoBuilder() {} @@ -50,6 +58,50 @@ public PackageInfoBuilder setPackageName(String packageName) { return this; } + /** + * Sets the version code. + * + *

On SDK P+, this value will be returned for both {@link PackageInfo#getLongVersionCode()} and + * {@link PackageInfo#versionCode}. Note that the value of {@link PackageInfo#versionCode} will be + * truncated if a value larger than Integer.MAX_VALUE is provided. + * + *

Default is 0L. + * + * @see PackageInfo#setLongVersionCode(long) + * @see PackageInfo#versionCode + */ + @TargetApi(Build.VERSION_CODES.P) + public PackageInfoBuilder setVersionCode(long longVersionCode) { + this.longVersionCode = longVersionCode; + return this; + } + + /** + * Sets the version name. + * + *

Default is {@code null}. + * + * @see PackageInfo#versionName + */ + public PackageInfoBuilder setVersionName(String versionName) { + this.versionName = versionName; + return this; + } + + /** + * Adds a requested permission and its flag for the app. + * + *

This can be called several times to add multiple permissions. + * + * @see PackageInfo#requestedPermissions + * @see PackageInfo#requestedPermissionsFlags + */ + public PackageInfoBuilder addRequestedPermission( + String requestedPermission, int requestedPermissionFlag) { + requestedPermissionsMap.put(requestedPermission, requestedPermissionFlag); + return this; + } + /** * Sets the application info. * @@ -69,11 +121,28 @@ public PackageInfo build() { PackageInfo packageInfo = new PackageInfo(); packageInfo.packageName = packageName; + packageInfo.versionName = versionName; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // setLongVersionCode will automatically set the version code. + packageInfo.setLongVersionCode(longVersionCode); + } else { + packageInfo.versionCode = (int) longVersionCode; + } if (applicationInfo == null) { applicationInfo = ApplicationInfoBuilder.newBuilder().setPackageName(packageName).build(); } packageInfo.applicationInfo = applicationInfo; + packageInfo.requestedPermissions = requestedPermissionsMap.keySet().toArray(new String[0]); + + Integer[] requestedPermissionsFlags = requestedPermissionsMap.values().toArray(new Integer[0]); + // Stream APIs such as `mapToInt` are not supported below API 24. + int[] requestedPermissionsFlagsIntArray = new int[requestedPermissionsFlags.length]; + for (int i = 0; i < requestedPermissionsFlags.length; i++) { + requestedPermissionsFlagsIntArray[i] = requestedPermissionsFlags[i]; + } + packageInfo.requestedPermissionsFlags = requestedPermissionsFlagsIntArray; checkState( packageInfo.packageName.equals(packageInfo.applicationInfo.packageName), diff --git a/core/java/androidx/test/core/graphics/BitmapStorageExt.kt b/core/java/androidx/test/core/graphics/BitmapStorageExt.kt new file mode 100644 index 000000000..683c1af87 --- /dev/null +++ b/core/java/androidx/test/core/graphics/BitmapStorageExt.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 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. + */ + +@file:JvmName("BitmapStorage") + +package androidx.test.core.graphics + +import android.graphics.Bitmap +import androidx.annotation.RestrictTo +import androidx.test.platform.io.PlatformTestStorage +import androidx.test.platform.io.PlatformTestStorageRegistry +import java.io.IOException + +/** + * Writes the contents of the [Bitmap] to a compressed png file on [PlatformTestStorage] + * + * @param name a descriptive base name for the resulting file. '.png' will be appended to this name. + * @throws IOException if bitmap could not be compressed or written to ds + */ +@Throws(IOException::class) +fun Bitmap.writeToTestStorage(name: String) { + writeToTestStorage(PlatformTestStorageRegistry.getInstance(), name) +} + +/** + * @deprecated + * @hide + */ +@Deprecated( + "use PlatformTestStorageRegistry.setInstance in the rare cases where you want to override the PlatformTestStorage to use", + replaceWith = ReplaceWith("writeToTestStorage()"), +) +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // legacy - used by espresso 3.5.0 DefaultFailureHandler +@Throws(IOException::class) +fun Bitmap.writeToTestStorage(testStorage: PlatformTestStorage, name: String) { + testStorage.openOutputFile("$name.png").use { + if ( + !this.compress( + Bitmap.CompressFormat.PNG, + /** PNG is lossless, so quality is ignored. */ + 0, + it, + ) + ) { + throw IOException("Failed to compress bitmap") + } + } +} diff --git a/core/java/androidx/test/core/internal/os/HandlerExecutor.kt b/core/java/androidx/test/core/internal/os/HandlerExecutor.kt new file mode 100644 index 000000000..e8fc62fd2 --- /dev/null +++ b/core/java/androidx/test/core/internal/os/HandlerExecutor.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 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. + */ +package androidx.test.core.internal.os + +import android.os.Handler +import androidx.annotation.RestrictTo +import java.util.concurrent.Executor + +/** + * A likely temporary utility class that redirects Executor calls to a Handler. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY) +class HandlerExecutor(val handler: Handler) : Executor { + + override fun execute(command: Runnable) { + if (Thread.currentThread().equals(handler.looper.thread)) { + command.run() + } else { + handler.post(command) + } + } +} diff --git a/core/java/androidx/test/core/view/ViewCapture.kt b/core/java/androidx/test/core/view/ViewCapture.kt new file mode 100644 index 000000000..7dba04134 --- /dev/null +++ b/core/java/androidx/test/core/view/ViewCapture.kt @@ -0,0 +1,324 @@ +/* + * Copyright 2021 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. + */ +@file:JvmName("ViewCapture") + +package androidx.test.core.view + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.PixelCopy +import android.view.Surface +import android.view.SurfaceView +import android.view.View +import android.view.ViewTreeObserver.OnDrawListener +import android.view.WindowManager +import androidx.annotation.RequiresApi +import androidx.annotation.RestrictTo +import androidx.concurrent.futures.SuspendToFutureAdapter +import androidx.test.core.internal.os.HandlerExecutor +import androidx.test.internal.platform.ServiceLoaderWrapper +import androidx.test.internal.platform.os.ControlledLooper +import androidx.test.internal.platform.reflect.ReflectiveField +import androidx.test.internal.platform.reflect.ReflectiveMethod +import androidx.test.internal.util.Checks.checkState +import androidx.test.platform.graphics.HardwareRendererCompat +import com.google.common.util.concurrent.ListenableFuture +import java.util.function.Consumer +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Suspend function for capturing an image of the underlying view into a [Bitmap]. + * + * For devices below [Build.VERSION_CODES#O], the image is obtained using [View#draw]. Otherwise, + * [PixelCopy] is used. Note when PixelCopy is used, the resulting image will be taken from the + * View's window, then cropped to the approximate location of the View in the window. So depending + * on window content you may see content from other View's within the resulting image. + * + * This method will also enable [HardwareRendererCompat#setDrawingEnabled(boolean)] if required. + * + * This API is primarily intended for use in lower layer libraries or frameworks. For test authors, + * it's recommended to use Espresso's captureToBitmap action or Compose's captureToImage. + * + * If a rect is supplied, this will further crop locally from the bounds of the given view. For + * example, if the given view is at (10, 10 - 30, 30) and the rect is (5, 5 - 10, 10), the final + * bitmap will be a 5x5 bitmap that spans (15, 15 - 20, 20). This is particularly useful for + * Compose, which only has a singular view that contains a hierarchy of nodes. + * + * This API must be called on the View's handler thread. If you're calling this from another + * context, eg directly from the test thread, you can use something like + *

{@code
+ *   runBlocking(view.handler.asCoroutineDispatcher()) {
+ *     withTimeout(10.seconds) {
+ *       view.captureToBitmap(rect)
+ *     }
+ *   }
+ * }
+ * + * The resulting image is captured after forcing the View to redraw, and waiting for the draw to + * operation complete. This is done as a means to improve the stability of the resulting image - + * especially in cases where hardware rendering drawing is off initially. + */ +suspend fun View.captureToBitmap(rect: Rect? = null): Bitmap { + checkState(isAttachedToWindow, "View must be attached to a window") + checkState( + handler.looper.isCurrentThread, + "Must be called from view's handler thread. Current: ${Thread.currentThread().name}, view handler: ${handler.looper.thread.name}", + ) + + var bitmap: Bitmap? = null + val hardwareDrawingEnabled = HardwareRendererCompat.isDrawingEnabled() + HardwareRendererCompat.setDrawingEnabled(true) + try { + forceRedraw() + bitmap = generateBitmap(rect) + } finally { + HardwareRendererCompat.setDrawingEnabled(hardwareDrawingEnabled) + } + + return bitmap!! +} + +private fun getControlledLooper(): ControlledLooper { + return ServiceLoaderWrapper.loadSingleService(ControlledLooper::class.java) { + ControlledLooper.NO_OP_CONTROLLED_LOOPER + } +} + +/** A ListenableFuture variant of captureToBitmap intended for use from Java. */ +fun View.captureToBitmapAsync(rect: Rect? = null): ListenableFuture { + return SuspendToFutureAdapter.launchFuture(Dispatchers.Main) { captureToBitmap(rect) } +} + +/** + * Trigger a redraw of the given view. + * + * Should only be called on UI thread. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +suspend fun View.forceRedraw() { + checkState(handler.looper.isCurrentThread, "Must be called from view's handler thread") + if (!getControlledLooper().areDrawCallbacksSupported()) { + Log.i("ViewCapture", "Skipping forceRedraw as it is not supported") + return + } + + var drawListener: OnDrawListener? = null + try { + return suspendCancellableCoroutine { cont -> + if (Build.VERSION.SDK_INT >= 29 && isHardwareAccelerated) { + viewTreeObserver.registerFrameCommitCallback() { + Log.i("forceRedraw", "FrameCommitCallback complete") + cont.resume(Unit) + } + } else { + drawListener = + object : OnDrawListener { + var handled = false + + override fun onDraw() { + if (!handled) { + handled = true + cont.resume(Unit) + } + } + } + viewTreeObserver.addOnDrawListener(drawListener) + } + invalidate() + } + } finally { + if (drawListener != null) { + // post as async event to avoid 'cannot remove on draw listener inside of onDraw' error + handler.post { viewTreeObserver.removeOnDrawListener(drawListener) } + } + } +} + +private suspend fun View.generateBitmap(rect: Rect? = null): Bitmap { + val rectWidth = rect?.width() ?: width + val rectHeight = rect?.height() ?: height + val destBitmap = Bitmap.createBitmap(rectWidth, rectHeight, Bitmap.Config.ARGB_8888) + + return when { + Build.VERSION.SDK_INT < 26 -> generateBitmapFromDraw(destBitmap, rect) + Build.VERSION.SDK_INT >= 34 -> generateBitmapFromPixelCopy(destBitmap, rect) + this is SurfaceView -> generateBitmapFromSurfaceViewPixelCopy(destBitmap, rect) + else -> generateBitmapFromPixelCopy(this.getSurface(), destBitmap, rect) + } +} + +@RequiresApi(Build.VERSION_CODES.O) +private suspend fun SurfaceView.generateBitmapFromSurfaceViewPixelCopy( + destBitmap: Bitmap, + rect: Rect?, +): Bitmap { + return suspendCancellableCoroutine { cont -> + val onCopyFinished = + PixelCopy.OnPixelCopyFinishedListener { result -> + if (result == PixelCopy.SUCCESS) { + cont.resume(destBitmap) + } else { + cont.resumeWithException(RuntimeException(String.format("PixelCopy failed: %d", result))) + } + } + PixelCopy.request(this, rect, destBitmap, onCopyFinished, handler) + } +} + +internal fun View.generateBitmapFromDraw(destBitmap: Bitmap, rect: Rect?): Bitmap { + destBitmap.density = resources.displayMetrics.densityDpi + computeScroll() + val canvas = Canvas(destBitmap) + canvas.translate((-scrollX).toFloat(), (-scrollY).toFloat()) + if (rect != null) { + canvas.translate((-rect.left).toFloat(), (-rect.top).toFloat()) + } + + draw(canvas) + return destBitmap +} + +/** + * Generates a bitmap from the given surface using [PixelCopy]. + * + * This method is effectively the backwards compatibility version of android U's + * [PixelCopy.ofWindow(View)], and will be called when running on Android API levels O to T. + */ +@RequiresApi(Build.VERSION_CODES.O) +private suspend fun View.generateBitmapFromPixelCopy( + surface: Surface, + destBitmap: Bitmap, + rect: Rect?, +): Bitmap { + return suspendCancellableCoroutine { cont -> + var bounds = getBoundsInSurface() + if (rect != null) { + bounds = Rect(rect).apply { offset(bounds.left, bounds.top) } + } + val onCopyFinished = + PixelCopy.OnPixelCopyFinishedListener { result -> + if (result == PixelCopy.SUCCESS) { + cont.resume(destBitmap) + } else { + cont.resumeWithException(RuntimeException("PixelCopy failed: $result")) + } + } + PixelCopy.request(surface, bounds, destBitmap, onCopyFinished, Handler(Looper.getMainLooper())) + } +} + +/** Returns the Rect indicating the View's coordinates within its containing window. */ +private fun View.getBoundsInWindow(): Rect { + val locationInWindow = intArrayOf(0, 0) + getLocationInWindow(locationInWindow) + val x = locationInWindow[0] + val y = locationInWindow[1] + return Rect(x, y, x + width, y + height) +} + +/** Returns the Rect indicating the View's coordinates within its containing surface. */ +private fun View.getBoundsInSurface(): Rect { + val locationInSurface = intArrayOf(0, 0) + if (Build.VERSION.SDK_INT < 29) { + reflectivelyGetLocationInSurface(locationInSurface) + } else { + getLocationInSurface(locationInSurface) + } + val x = locationInSurface[0] + val y = locationInSurface[1] + val bounds = Rect(x, y, x + width, y + height) + + Log.d("ViewCapture", "getBoundsInSurface $bounds") + + return bounds +} + +private fun View.getSurface(): Surface { + // copy the implementation of API 34's PixelCopy.ofWindow to get the surface from view + val viewRootImpl = ReflectiveMethod(View::class.java, "getViewRootImpl").invoke(this) + return ReflectiveField("android.view.ViewRootImpl", "mSurface").get(viewRootImpl) +} + +/** + * The backwards compatible version of API 29's [View.getLocationInSurface]. + * + * It makes a best effort attempt to replicate the API 29 logic. + */ +@SuppressLint("NewApi") +private fun View.reflectivelyGetLocationInSurface(locationInSurface: IntArray) { + // copy the implementation of API 29+ getLocationInSurface + getLocationInWindow(locationInSurface) + if (Build.VERSION.SDK_INT < 28) { + val viewRootImpl = ReflectiveMethod(View::class.java, "getViewRootImpl").invoke(this) + val windowAttributes = + ReflectiveField("android.view.ViewRootImpl", "mWindowAttributes") + .get(viewRootImpl) + val surfaceInsets = + ReflectiveField(WindowManager.LayoutParams::class.java, "surfaceInsets") + .get(windowAttributes) + locationInSurface[0] += surfaceInsets.left + locationInSurface[1] += surfaceInsets.top + } else { + // ART restrictions introduced in API 29 disallow reflective access to mWindowAttributes + Log.w( + "ViewCapture", + "Could not calculate offset of view in surface on API 28, resulting image may have incorrect positioning", + ) + } +} + +@RequiresApi(34) +private suspend fun View.generateBitmapFromPixelCopy( + destBitmap: Bitmap, + rect: Rect? = null, +): Bitmap { + val boundsInWindow = getBoundsInWindow() + val sourceRect = + if (rect != null) { + Rect(rect).apply { offset(boundsInWindow.left, boundsInWindow.top) } + } else { + boundsInWindow + } + return suspendCancellableCoroutine { cont -> + val request = + PixelCopy.Request.Builder.ofWindow(this) + .setSourceRect(sourceRect) + .setDestinationBitmap(destBitmap) + .build() + val onCopyFinished = + Consumer { result -> + if (result.status == PixelCopy.SUCCESS) { + cont.resume(result.bitmap) + } else { + cont.resumeWithException( + RuntimeException("PixelCopy failed with status code: ${result.status}") + ) + } + } + PixelCopy.request(request, HandlerExecutor(handler), onCopyFinished) + } +} diff --git a/core/java/androidx/test/core/view/WindowCapture.kt b/core/java/androidx/test/core/view/WindowCapture.kt new file mode 100644 index 000000000..8bbc9206b --- /dev/null +++ b/core/java/androidx/test/core/view/WindowCapture.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2021 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. + */ +@file:JvmName("WindowCapture") + +package androidx.test.core.view + +import android.graphics.Bitmap +import android.graphics.Rect +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.PixelCopy +import android.view.Window +import androidx.annotation.RequiresApi +import androidx.concurrent.futures.SuspendToFutureAdapter +import androidx.test.platform.graphics.HardwareRendererCompat +import com.google.common.util.concurrent.ListenableFuture +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Suspend function that captures an image of the underlying window into a [Bitmap]. + * + * For devices below [Build.VERSION_CODES#O] the image is obtained using [View#draw] on the windows + * decorView. Otherwise, [PixelCopy] is used. + * + * This method will also enable [HardwareRendererCompat#setDrawingEnabled(boolean)] if required. + * + * This API is primarily intended for use in lower layer libraries or frameworks. For test authors, + * its recommended to use espresso or compose's captureToImage. + * + * This API must be called from the UI thread. + * + * The resulting image is captured after forcing the View to redraw, and waiting for the draw to + * operation complete. This is done as a means to improve the stability of the resulting image - + * especially in cases where hardware rendering drawing is off initially. + */ +suspend fun Window.captureRegionToBitmap(boundsInWindow: Rect? = null): Bitmap { + var bitmap: Bitmap? = null + + val hardwareDrawingEnabled = HardwareRendererCompat.isDrawingEnabled() + HardwareRendererCompat.setDrawingEnabled(true) + try { + decorView.forceRedraw() + bitmap = generateBitmap(boundsInWindow) + } finally { + HardwareRendererCompat.setDrawingEnabled(hardwareDrawingEnabled) + } + + return bitmap!! +} + +/** A ListenableFuture variant of captureRegionToBitmap intended for use from Java. */ +fun Window.captureRegionToBitmapAsync(boundsInWindow: Rect? = null): ListenableFuture { + return SuspendToFutureAdapter.launchFuture(Dispatchers.Main) { + captureRegionToBitmap(boundsInWindow) + } +} + +internal suspend fun Window.generateBitmap(boundsInWindow: Rect? = null): Bitmap { + val destBitmap = + Bitmap.createBitmap( + boundsInWindow?.width() ?: decorView.width, + boundsInWindow?.height() ?: decorView.height, + Bitmap.Config.ARGB_8888, + ) + when { + Build.VERSION.SDK_INT < 26 -> + // TODO: handle boundsInWindow + decorView.generateBitmapFromDraw(destBitmap, boundsInWindow) + else -> generateBitmapFromPixelCopy(boundsInWindow, destBitmap) + } + + return destBitmap +} + +@RequiresApi(Build.VERSION_CODES.O) +internal suspend fun Window.generateBitmapFromPixelCopy( + boundsInWindow: Rect? = null, + destBitmap: Bitmap, +): Bitmap { + return suspendCancellableCoroutine { cont -> + val onCopyFinished = + PixelCopy.OnPixelCopyFinishedListener { result -> + if (result == PixelCopy.SUCCESS) { + cont.resume(destBitmap, {}) + } else { + cont.resumeWithException(RuntimeException("PixelCopy failed: $result")) + } + } + + PixelCopy.request( + this, + boundsInWindow, + destBitmap, + onCopyFinished, + Handler(Looper.getMainLooper()), + ) + } +} diff --git a/core/javatests/androidx/test/core/AndroidManifest_target.xml b/core/javatests/androidx/test/core/AndroidManifest_target.xml deleted file mode 100644 index 502fe079d..000000000 --- a/core/javatests/androidx/test/core/AndroidManifest_target.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/javatests/androidx/test/core/BUILD.bazel b/core/javatests/androidx/test/core/BUILD.bazel deleted file mode 100644 index f82cdb361..000000000 --- a/core/javatests/androidx/test/core/BUILD.bazel +++ /dev/null @@ -1,84 +0,0 @@ -# Description: Tests for androidx.test.core - -load("//build_extensions:android_library_instrumentation_tests.bzl", "android_library_instrumentation_tests") -load("//build_extensions:android_library_local_tests.bzl", "android_library_local_tests") -load("//build_extensions:android_app_instrumentation_tests.bzl", "android_app_instrumentation_tests") -load("//build_extensions:test_devices.bzl", "devices") - -package( - default_testonly = 1, -) - -licenses(["notice"]) # Apache License 2.0 - -# ActivityScenarioTest is unique, in that we want to define the activity under test in a -# separate target binary, since that is how most users tests will be setup -android_binary( - name = "ActivityScenarioTest_target", - manifest = "AndroidManifest_target.xml", - deps = [ - "//core/javatests/androidx/test/core/app/testing", - "//core/javatests/androidx/test/core/app/testing:manifest", - ], -) - -android_app_instrumentation_tests( - name = "ActivityScenarioTest_instrumentation", - srcs = glob(["**/ActivityScenarioTest.java"]), - binary_target = ":ActivityScenarioTest_target", - target_devices = devices(), - deps = [ - "//:androidx_lifecycle_common", - "//core", - "//core/javatests/androidx/test/core/app/testing", - "//espresso/core/java/androidx/test/espresso", - "//ext/junit", - "//ext/truth", - "//runner/android_junit_runner", - "@maven//:com_google_guava_guava", - "@maven//:com_google_truth_truth", - "@maven//:junit_junit", - ], -) - -android_library_instrumentation_tests( - name = "instrumentation_tests", - srcs = glob( - ["**/*.java"], - exclude = ["**/ActivityScenario*Test.java"], - ), - target_devices = devices(), - deps = [ - "//:androidx_lifecycle_common", - "//core", - "//core/javatests/androidx/test/core/app/testing", - "//ext/junit", - "//runner/android_junit_runner", - "@maven//:com_google_guava_guava", - "@maven//:com_google_truth_truth", - "@maven//:junit_junit", - ], -) - -android_library_local_tests( - name = "local_tests", - srcs = glob( - ["**/*.java"], - exclude = [ - "**/ActivityScenarioTest.java", - "**/app/ActivityScenarioSharedTest.java", # currently broken due to AXT/Roboelctric version mismatch - ], - ), - deps = [ - "//:androidx_lifecycle_common", - "//core", - "//core/javatests/androidx/test/core/app/testing", - "//core/javatests/androidx/test/core/app/testing:manifest", - "//ext/junit", - "//ext/truth", - "//runner/monitor/java/androidx/test:monitor", - "@maven//:com_google_guava_guava", - "@maven//:com_google_truth_truth", - "@maven//:junit_junit", - ], -) diff --git a/core/javatests/androidx/test/core/app/ActivityScenarioTest.java b/core/javatests/androidx/test/core/app/ActivityScenarioTest.java index c7ec51771..df94679c2 100644 --- a/core/javatests/androidx/test/core/app/ActivityScenarioTest.java +++ b/core/javatests/androidx/test/core/app/ActivityScenarioTest.java @@ -19,6 +19,7 @@ import static android.app.Activity.RESULT_OK; import static androidx.test.ext.truth.content.IntentSubject.assertThat; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import android.app.Activity; @@ -43,7 +44,12 @@ import androidx.test.runner.lifecycle.Stage; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,6 +59,12 @@ */ @RunWith(AndroidJUnit4.class) public final class ActivityScenarioTest { + + @Before + public void setUp() { + RecordingActivity.clearCallbacks(); + } + @Test public void launchedActivityShouldBeResumed() throws Exception { try (ActivityScenario scenario = @@ -318,10 +330,25 @@ public void recreateResumedActivity() throws Exception { } } + @Ignore // TODO(hoisie): re-enable once github uses new robolectric + @Test + public void recreateIsChangingConfigurations() { + try (ActivityScenario activityScenario = + ActivityScenario.launch(RecordingActivity.class)) { + activityScenario.recreate(); + + activityScenario.onActivity( + activity -> + assertThat(activity.getCallbacks()) + .containsAtLeast("onPause true", "onStop true", "onDestroy true") + .inOrder()); + } + } + @Test public void activityResultWithNoResultData() throws Exception { try (ActivityScenario scenario = - ActivityScenario.launch(RecreationRecordingActivity.class)) { + ActivityScenario.launchActivityForResult(RecreationRecordingActivity.class)) { scenario.onActivity( activity -> { activity.setResult(RESULT_OK); @@ -335,7 +362,7 @@ public void activityResultWithNoResultData() throws Exception { @Test public void activityResultWithResultData() throws Exception { try (ActivityScenario scenario = - ActivityScenario.launch(RecreationRecordingActivity.class)) { + ActivityScenario.launchActivityForResult(RecreationRecordingActivity.class)) { scenario.onActivity( activity -> { activity.setResult(RESULT_OK, new Intent().setAction(Intent.ACTION_SEND)); @@ -349,7 +376,7 @@ public void activityResultWithResultData() throws Exception { @Test public void activityResultWithResultDataAfterRecreate() throws Exception { try (ActivityScenario scenario = - ActivityScenario.launch(RecreationRecordingActivity.class)) { + ActivityScenario.launchActivityForResult(RecreationRecordingActivity.class)) { scenario.recreate(); scenario.onActivity( activity -> { @@ -361,6 +388,19 @@ public void activityResultWithResultDataAfterRecreate() throws Exception { } } + @Test + public void scenarioResultAfterLaunch() throws Exception { + try (ActivityScenario scenario = + ActivityScenario.launch(RecreationRecordingActivity.class)) { + IllegalStateException e = assertThrows(IllegalStateException.class, scenario::getResult); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "You must start Activity first. Make sure you are using launchActivityForResult() to" + + " launch an Activity."); + } + } + @Test public void launch_unknownActivity() { Intent intent = new Intent(); @@ -416,68 +456,63 @@ public void perform(RecreationRecordingActivity activity) { } @Test - public void launch_callbackSequence() { - ActivityScenario activityScenario = - ActivityScenario.launch(RecordingActivity.class); - Espresso.onIdle(); - Espresso.onIdle(); - activityScenario.onActivity( - activity -> - assertThat(activity.getCallbacks()) - .containsExactly( - "onCreate", - "onStart", - "onPostCreate", - "onResume", - "onPostResume", - "onAttachedToWindow", - "onWindowFocusChanged true") - .inOrder()); + public void launch_callbackSequence() + throws ExecutionException, InterruptedException, TimeoutException { + try (ActivityScenario activityScenario = + ActivityScenario.launch(RecordingActivity.class)) { + + // windowFocus event is async, so wait a small amount of time for that + CountDownLatch windowFocusLatch = new CountDownLatch(1); + activityScenario.onActivity( + activity -> activity.listenForEvent(windowFocusLatch, "onWindowFocusChanged true")); + windowFocusLatch.await(1, TimeUnit.SECONDS); + + activityScenario.onActivity( + activity -> + assertThat(activity.getCallbacks()) + .containsExactly( + "onCreate", + "onStart", + "onPostCreate", + "onResume", + "onPostResume", + "onAttachedToWindow", + "onWindowFocusChanged true") + .inOrder()); + } } @Test public void launch_postingCallbackSequence() throws Exception { - ActivityScenario activityScenario = - ActivityScenario.launch(AsyncRecordingActivity.class); - Espresso.onIdle(); - Espresso.onIdle(); - - int maxRetry = 3; - AtomicBoolean activityHasFocus = new AtomicBoolean(false); - for (int attempt = 0; attempt < maxRetry; attempt++) { - activityScenario.onActivity(activity -> activityHasFocus.set(activity.hasWindowFocus())); - if (activityHasFocus.get()) { - break; - } - // Retry after the sleep. Window focus is the global state and there is a lag - // before onWindowFocusChanged is called after the activity is resumed. - // TODO(b/191072024): Find a beter way to monitor focus activity and remove the sleep. - Thread.sleep(500); + try (ActivityScenario activityScenario = + ActivityScenario.launch(AsyncRecordingActivity.class)) { + + CountDownLatch windowFocusLatch = new CountDownLatch(1); + activityScenario.onActivity( + activity -> activity.listenForEvent(windowFocusLatch, "onWindowFocusChanged true")); + windowFocusLatch.await(1, TimeUnit.SECONDS); + + // wait for windowFocus post + Espresso.onIdle(); + + activityScenario.onActivity( + activity -> + // just assert the first few events, The exact order for the full event sequence is + // not deterministic + assertThat(activity.getCallbacks()) + .containsAtLeast( + "onCreate", + "onStart", + "onPostCreate", + "onResume", + "onPostResume", + "post from onCreate") + .inOrder()); } + } - activityScenario.onActivity( - activity -> - assertThat(activity.getCallbacks()) - .containsExactly( - "onCreate", - "onStart", - "onPostCreate", - "onResume", - "onPostResume", - "post from onCreate", - "post from onStart", - "post from onPostCreate", - "post from onResume", - "post from onPostResume", - "onAttachedToWindow", - "post from onAttachedToWindow", - "onWindowFocusChanged true", - "post from onWindowFocusChanged true") - .inOrder()); - } - - @Test - @SdkSuppress(minSdkVersion = 16) // ActivityOptions is added in API 16. + @Test + @SdkSuppress(minSdkVersion = 28) // ActivityOptions is added in API 28. public void launch_withActivityOptionsBundle() throws Exception { try (ActivityScenario scenario = ActivityScenario.launch( @@ -496,6 +531,24 @@ public void launch_intentWithAction() { assertThat(activityScenario).isNotNull(); } + @Test + public void launch_intentWithPackageName() { + Intent intent = new Intent("custom.actions.intent.EXAMPLE_INTENT"); + intent.setPackage("androidx.test.core.app"); + + ActivityScenario activityScenario = ActivityScenario.launch(intent); + assertThat(activityScenario).isNotNull(); + } + + @Test + public void launchActivityForResult_intentWithAction() { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("scenario://test")); + + ActivityScenario activityScenario = + ActivityScenario.launchActivityForResult(intent); + assertThat(activityScenario).isNotNull(); + } + private static Stage lastLifeCycleTransition(Activity activity) { return ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity); } diff --git a/core/javatests/androidx/test/core/app/AndroidManifest_target.xml b/core/javatests/androidx/test/core/app/AndroidManifest_target.xml new file mode 100644 index 000000000..cc9ccbbbf --- /dev/null +++ b/core/javatests/androidx/test/core/app/AndroidManifest_target.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/javatests/androidx/test/core/app/AndroidManifest_test.xml b/core/javatests/androidx/test/core/app/AndroidManifest_test.xml new file mode 100644 index 000000000..8935f2d73 --- /dev/null +++ b/core/javatests/androidx/test/core/app/AndroidManifest_test.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/core/javatests/androidx/test/core/app/BUILD b/core/javatests/androidx/test/core/app/BUILD new file mode 100644 index 000000000..b6220c505 --- /dev/null +++ b/core/javatests/androidx/test/core/app/BUILD @@ -0,0 +1,119 @@ +# Description: Tests for androidx.test.core + +load("@build_bazel_rules_android//android:rules.bzl", "android_binary") +load("//build_extensions:android_library_test.bzl", "axt_android_library_test") +load("//build_extensions:axt_android_application_test.bzl", "axt_android_application_test") +load("//build_extensions:axt_android_local_test.bzl", "axt_android_local_test") +load("//build_extensions:phone_devices.bzl", "devices") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = 1, +) + +licenses(["notice"]) + +# ActivityScenarioTest is unique, in that we want to define the activity under test in a +# separate target binary, since that is how most users tests will be setup +android_binary( + name = "ActivityScenarioTest_target", + manifest = "AndroidManifest_target.xml", + deps = [ + "//core/javatests/androidx/test/core/app/testing", + "//core/javatests/androidx/test/core/app/testing:manifest", + ], +) + +axt_android_application_test( + name = "ActivityScenarioTest_instrumentation", + srcs = ["ActivityScenarioTest.java"], + args = [ + "--instrumentation_options=waitForActivitiesToComplete=false", + ], + instruments = ":ActivityScenarioTest_target", + manifest = "AndroidManifest_test.xml", + shard_count = 8, + deps = [ + "//core", + "//core/javatests/androidx/test/core/app/testing", + "//espresso/core/java/androidx/test/espresso", + "//ext/junit", + "//ext/truth", + "//runner/android_junit_runner", + "@maven//:androidx_lifecycle_lifecycle_common", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +) + +axt_android_application_test( + name = "ActivityScenarioSharedTest_instrumentation", + srcs = ["ActivityScenarioSharedTest.java"], + args = [ + "--instrumentation_options=waitForActivitiesToComplete=false", + # turn off video recording. Video recording won't work anyway on the upcoming slim device with skipDrawing enabled, + # and turning off video recording saves significant runtime and flakiness + "--record_test_video=NEVER", + ], + instruments = ":ActivityScenarioTest_target", + manifest = "AndroidManifest_test.xml", + deps = [ + "//core", + "//core/javatests/androidx/test/core/app/testing", + "//ext/junit", + "@maven//:androidx_lifecycle_lifecycle_common", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +) + +axt_android_local_test( + name = "ActivityScenarioTest", + srcs = ["ActivityScenarioTest.java"], + manifest_values = { + "applicationId": "androidx.test.core.app", + }, + deps = [ + "//core", + "//core/javatests/androidx/test/core/app/testing", + "//core/javatests/androidx/test/core/app/testing:manifest", + "//espresso/core/java/androidx/test/espresso", + "//ext/junit", + "//ext/truth", + "//runner/android_junit_runner", + "//runner/monitor/java/androidx/test:monitor", + "@maven//:androidx_lifecycle_lifecycle_common", + "@maven//:com_google_truth_truth", + ], +) + +axt_android_local_test( + name = "ApplicationProviderTest", + srcs = ["ApplicationProviderTest.java"], + manifest_values = { + "applicationId": "androidx.test.core", + }, + deps = [ + "//core", + "//ext/junit", + "@maven//:com_google_truth_truth", + ], +) + +# TakeScreenshotTest needs its own rule due to higher min sdk +axt_android_library_test( + name = "TakeScreenShotTest", + srcs = ["TakeScreenShotTest.kt"], + device_list = devices(), + deps = [ + "//core", + "//core/javatests/androidx/test/core/app/testing", + "//core/javatests/androidx/test/core/app/testing:manifest", + "//ext/junit", + "//ktx/core", + "//services/storage/java/androidx/test/services/storage", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + "@maven_listenablefuture//:com_google_guava_listenablefuture", + ], +) diff --git a/core/javatests/androidx/test/core/app/TakeScreenShotTest.kt b/core/javatests/androidx/test/core/app/TakeScreenShotTest.kt new file mode 100644 index 000000000..f37d6bb94 --- /dev/null +++ b/core/javatests/androidx/test/core/app/TakeScreenShotTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 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. + */ + +package androidx.test.core.app + +import androidx.test.core.app.testing.ActivityWithDialog +import androidx.test.core.app.testing.UiActivity +import androidx.test.core.graphics.writeToTestStorage +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestName +import org.junit.runner.RunWith + +/** Simple test for takeScreenshot */ +@RunWith(AndroidJUnit4::class) +class TakeScreenShotTest { + + @get:Rule val name = TestName() + + @Test + fun takeScreenshot_blank() { + val bitmap = takeScreenshot() + + assertThat(bitmap).isNotNull() + // arbitrary check to ensure bitmap is non empty. Contents need to be manually validated for now + assertThat(bitmap.byteCount).isGreaterThan(100) + + bitmap.writeToTestStorage(name.methodName) + } + + @Test + fun takeScreenshot_activity() { + launchActivity().use { + val bitmap = takeScreenshot() + + assertThat(bitmap).isNotNull() + // arbitrary check to ensure bitmap is non empty. Contents need to be manually validated for + // now + assertThat(bitmap.byteCount).isGreaterThan(100) + + bitmap.writeToTestStorage(name.methodName) + } + } + + @Test + fun takeScreenshot_activityWithDialog() { + launchActivity().use { + val bitmap = takeScreenshot() + + assertThat(bitmap).isNotNull() + // arbitrary check to ensure bitmap is non empty. Contents need to be manually validated for + // now + assertThat(bitmap.byteCount).isGreaterThan(100) + + bitmap.writeToTestStorage(name.methodName) + } + } +} diff --git a/core/javatests/androidx/test/core/app/testing/ActivityWithDialog.kt b/core/javatests/androidx/test/core/app/testing/ActivityWithDialog.kt new file mode 100644 index 000000000..1967091b0 --- /dev/null +++ b/core/javatests/androidx/test/core/app/testing/ActivityWithDialog.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 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. + */ +package androidx.test.core.app.testing + +import android.app.Activity +import android.app.AlertDialog +import android.os.Bundle + +class ActivityWithDialog : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.simple_activity) + + AlertDialog.Builder(this) + .setMessage("This is a dialog") + .setTitle("Dialog Title") + .create() + .show() + } +} diff --git a/core/javatests/androidx/test/core/app/testing/AndroidManifest.xml b/core/javatests/androidx/test/core/app/testing/AndroidManifest.xml index 28e829011..87340d09c 100644 --- a/core/javatests/androidx/test/core/app/testing/AndroidManifest.xml +++ b/core/javatests/androidx/test/core/app/testing/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" package="androidx.test.core.app.testing"> + android:minSdkVersion="23"/> + + + + + + + diff --git a/core/javatests/androidx/test/core/app/testing/AndroidManifest_empty.xml b/core/javatests/androidx/test/core/app/testing/AndroidManifest_empty.xml new file mode 100644 index 000000000..8af416be7 --- /dev/null +++ b/core/javatests/androidx/test/core/app/testing/AndroidManifest_empty.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/core/javatests/androidx/test/core/app/testing/BUILD b/core/javatests/androidx/test/core/app/testing/BUILD new file mode 100644 index 000000000..bee451c37 --- /dev/null +++ b/core/javatests/androidx/test/core/app/testing/BUILD @@ -0,0 +1,36 @@ +# Description: Stub classes for testing androidx.test.core.app + +load("@build_bazel_rules_android//android:rules.bzl", "android_library") +load("//build_extensions:kt_android_library.bzl", "kt_android_library") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = 1, +) + +licenses(["notice"]) + +kt_android_library( + name = "testing", + srcs = glob([ + "**/*.java", + "**/*.kt", + ]), + manifest = "AndroidManifest_empty.xml", + resource_files = glob(["res/**"]), + visibility = ["//visibility:public"], + deps = [ + "//opensource/androidx:annotation", + "@maven//:com_google_guava_guava", + "@maven//:junit_junit", + ], +) + +# keep the activity manifest entries in a separate target, so they are not +# present in both binary_under_test and test apk +android_library( + name = "manifest", + exports_manifest = 1, + manifest = "AndroidManifest.xml", + visibility = ["//visibility:public"], +) diff --git a/core/javatests/androidx/test/core/app/testing/BUILD.bazel b/core/javatests/androidx/test/core/app/testing/BUILD.bazel deleted file mode 100644 index b187122bf..000000000 --- a/core/javatests/androidx/test/core/app/testing/BUILD.bazel +++ /dev/null @@ -1,25 +0,0 @@ -# Description: Stub classes for testing androidx.test.core.app - -package( - default_testonly = 1, -) - -licenses(["notice"]) # Apache License 2.0 - -android_library( - name = "testing", - srcs = glob(["**/*.java"]), - visibility = ["//visibility:public"], - deps = [ - "//:androidx_annotation", - ], -) - -# keep the activity manifest entries in a separate target, so they are not -# present in both binary_under_test and test apk -android_library( - name = "manifest", - exports_manifest = 1, - manifest = "AndroidManifest.xml", - visibility = ["//visibility:public"], -) diff --git a/core/javatests/androidx/test/core/app/testing/RecordingActivity.java b/core/javatests/androidx/test/core/app/testing/RecordingActivity.java index 4e2ca4ef7..db4553e74 100644 --- a/core/javatests/androidx/test/core/app/testing/RecordingActivity.java +++ b/core/javatests/androidx/test/core/app/testing/RecordingActivity.java @@ -16,18 +16,24 @@ package androidx.test.core.app.testing; +import static org.junit.Assert.assertFalse; + import android.app.Activity; import android.content.Context; import android.os.Bundle; -import androidx.annotation.Nullable; import android.view.View; +import androidx.annotation.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; /** An Activity that records lifecycle states. */ public class RecordingActivity extends Activity { - protected final List callbacks = new ArrayList<>(); + protected static final List callbacks = new ArrayList<>(); + private final Map pendingEvents = new HashMap<>(); private class VisibilityRecordingView extends View { @@ -43,8 +49,21 @@ protected void onAttachedToWindow() { } } + public void listenForEvent(CountDownLatch latch, String stateDescription) { + if (callbacks.contains(stateDescription)) { + latch.countDown(); + } else { + assertFalse(pendingEvents.containsKey(stateDescription)); + pendingEvents.put(stateDescription, latch); + } + } + protected void onEvent(String newStateDescription) { callbacks.add(newStateDescription); + CountDownLatch latch = pendingEvents.remove(newStateDescription); + if (latch != null) { + latch.countDown(); + } } @Override @@ -82,13 +101,13 @@ public void onPostResume() { @Override public void onPause() { super.onPause(); - onEvent("onPause"); + onEvent("onPause " + isChangingConfigurations()); } @Override public void onStop() { super.onStop(); - onEvent("onStop"); + onEvent("onStop " + isChangingConfigurations()); } @Override @@ -100,7 +119,7 @@ public void onRestart() { @Override public void onDestroy() { super.onDestroy(); - onEvent("onDestroy"); + onEvent("onDestroy " + isChangingConfigurations()); } @Override @@ -112,4 +131,8 @@ public void onWindowFocusChanged(boolean hasFocus) { public List getCallbacks() { return callbacks; } + + public static void clearCallbacks() { + callbacks.clear(); + } } diff --git a/core/javatests/androidx/test/core/app/testing/RecreationRecordingActivity.java b/core/javatests/androidx/test/core/app/testing/RecreationRecordingActivity.java index de56c0cd3..e514921f1 100644 --- a/core/javatests/androidx/test/core/app/testing/RecreationRecordingActivity.java +++ b/core/javatests/androidx/test/core/app/testing/RecreationRecordingActivity.java @@ -18,8 +18,8 @@ import android.app.Activity; import android.os.Bundle; -import androidx.annotation.Nullable; import android.view.Window; +import androidx.annotation.Nullable; /** * A minimum activity to demonstrate unit testing relating to Activity's life cycle events. It diff --git a/core/javatests/androidx/test/core/app/testing/UiActivity.kt b/core/javatests/androidx/test/core/app/testing/UiActivity.kt new file mode 100644 index 000000000..1721a6cdf --- /dev/null +++ b/core/javatests/androidx/test/core/app/testing/UiActivity.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 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. + */ + +package androidx.test.core.app.testing + +import android.app.Activity +import android.os.Bundle + +/** + * A simple [Activity] that displays UI. + * + * Used for screenshot testing + */ +class UiActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.simple_activity) + } +} diff --git a/core/javatests/androidx/test/core/app/testing/res/layout/simple_activity.xml b/core/javatests/androidx/test/core/app/testing/res/layout/simple_activity.xml new file mode 100644 index 000000000..6baa4da56 --- /dev/null +++ b/core/javatests/androidx/test/core/app/testing/res/layout/simple_activity.xml @@ -0,0 +1,29 @@ + + + + + + +