From 07ae446f566472686949cddcccf2f7cdb7a87045 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 06:04:47 -0700 Subject: [PATCH 01/13] Upgrade build infrastructure (AGP 9.3.1, Gradle 9.6.1) Upgrades the project's build system: - Upgraded Android Gradle Plugin to 9.3.1 and Gradle Wrapper to 9.6.1. - Migrated all modules from 'kotlin.android' to AGP's built-in Kotlin support. - Replaced 'kotlin.kapt' with 'legacy-kapt' for compatibility with built-in Kotlin. - Upgraded Hilt to 2.59.2 for AGP 9 compatibility. - Updated compiler flags to pass -Xcontext-receivers under the new built-in Kotlin configuration block in 11 modules. - Updated README to reflect Android Studio Quail / AGP 9 requirements. - Enabled parallel Gradle sync in gradle.properties. --- README.md | 4 ++-- app/build.gradle.kts | 3 +-- benchmark/build.gradle.kts | 16 +++++++++------- build.gradle.kts | 3 +-- core/camera/build.gradle.kts | 8 +++----- .../effects/single-stream/build.gradle.kts | 11 ++++++----- .../low-light-playservices/build.gradle.kts | 3 +-- core/camera/low-light/build.gradle.kts | 3 +-- core/camera/postprocess/build.gradle.kts | 3 +-- core/camera/testing/build.gradle.kts | 1 - core/common/build.gradle.kts | 3 +-- core/common/testing/build.gradle.kts | 1 - core/model/build.gradle.kts | 3 +-- core/settings/build.gradle.kts | 3 +-- core/settings/datastore-prefs/build.gradle.kts | 1 - .../datastore-prefs/testing/build.gradle.kts | 1 - data/camera/build.gradle.kts | 6 ++---- data/camera/consumer-rules.pro | 2 ++ data/media/build.gradle.kts | 3 +-- data/media/testing/build.gradle.kts | 1 - data/settings/build.gradle.kts | 3 +-- data/settings/testing/build.gradle.kts | 1 - feature/permissions/build.gradle.kts | 3 +-- feature/postcapture/build.gradle.kts | 13 +++++-------- feature/preview/build.gradle.kts | 3 +-- feature/settings/build.gradle.kts | 3 +-- gradle.properties | 5 +++-- gradle/libs.versions.toml | 5 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 1 - ui/components/build.gradle.kts | 10 ++++------ ui/components/capture/build.gradle.kts | 12 ++++++------ ui/controller/build.gradle.kts | 12 ++++++------ ui/controller/impl/build.gradle.kts | 12 ++++++------ ui/controller/testing/build.gradle.kts | 1 - ui/debug/build.gradle.kts | 3 +-- ui/debug/testing/build.gradle.kts | 1 - ui/uistate/build.gradle.kts | 12 ++++++------ ui/uistate/capture/build.gradle.kts | 12 ++++++------ ui/uistate/postcapture/build.gradle.kts | 5 ++--- ui/uistateadapter/build.gradle.kts | 12 ++++++------ ui/uistateadapter/capture/build.gradle.kts | 12 ++++++------ ui/uistateadapter/postcapture/build.gradle.kts | 5 ++--- 43 files changed, 100 insertions(+), 127 deletions(-) create mode 100644 data/camera/consumer-rules.pro diff --git a/README.md b/README.md index a733c15c3..b13307a74 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ OEMs looking to validate their camera feature implementations. This project uses the gradle build system, and can be imported directly into Android Studio. -Currently, Jetpack Camera App is built using the Android Gradle Plugin 8.10.0, which is only -compatible with Android Studio Meerkat or newer. +Currently, Jetpack Camera App is built using the Android Gradle Plugin 9.3.1, which is only +compatible with Android Studio Quail or newer. ## Pre-push Hook (Recommended) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ad359692b..7246eef3c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index a1708658f..00c54f097 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.test) - alias(libs.plugins.kotlin.android) } android { @@ -24,12 +24,8 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } defaultConfig { @@ -69,6 +65,12 @@ android { experimentalProperties["android.experimental.self-instrumenting"] = true } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(libs.androidx.junit) implementation(libs.androidx.benchmark.macro.junit4) diff --git a/build.gradle.kts b/build.gradle.kts index 64fdc6b56..53a8212e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,9 +19,8 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.android.test) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.dagger.hilt.android) apply false - alias(libs.plugins.kotlin.kapt) apply false + alias(libs.plugins.android.legacy.kapt) apply false alias(libs.plugins.compose.compiler) apply false } diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index b87fb79ee..975955fd0 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -93,10 +92,9 @@ android { } kotlin { jvmToolchain(17) - } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } } diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index dacbf112c..ee42831de 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } @@ -34,10 +33,12 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) - } - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } } diff --git a/core/camera/low-light-playservices/build.gradle.kts b/core/camera/low-light-playservices/build.gradle.kts index 639fa3e51..5c6aab589 100644 --- a/core/camera/low-light-playservices/build.gradle.kts +++ b/core/camera/low-light-playservices/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/low-light/build.gradle.kts b/core/camera/low-light/build.gradle.kts index cade65c9e..9377565e4 100644 --- a/core/camera/low-light/build.gradle.kts +++ b/core/camera/low-light/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/postprocess/build.gradle.kts b/core/camera/postprocess/build.gradle.kts index ceedef0b7..67157f0be 100644 --- a/core/camera/postprocess/build.gradle.kts +++ b/core/camera/postprocess/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts index 38f70711f..0768c256e 100644 --- a/core/camera/testing/build.gradle.kts +++ b/core/camera/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 185217b27..5886b309b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/common/testing/build.gradle.kts b/core/common/testing/build.gradle.kts index 57c300b0e..6d21ce391 100644 --- a/core/common/testing/build.gradle.kts +++ b/core/common/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index d352566c4..f9e6a46d8 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) } android { diff --git a/core/settings/build.gradle.kts b/core/settings/build.gradle.kts index 35c256a56..021d3d4d8 100644 --- a/core/settings/build.gradle.kts +++ b/core/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/settings/datastore-prefs/build.gradle.kts b/core/settings/datastore-prefs/build.gradle.kts index c0ca9f8d4..107acd946 100644 --- a/core/settings/datastore-prefs/build.gradle.kts +++ b/core/settings/datastore-prefs/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/settings/datastore-prefs/testing/build.gradle.kts b/core/settings/datastore-prefs/testing/build.gradle.kts index 0e619ddbd..d6b25c0f3 100644 --- a/core/settings/datastore-prefs/testing/build.gradle.kts +++ b/core/settings/datastore-prefs/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/data/camera/build.gradle.kts b/data/camera/build.gradle.kts index 8a541423a..39c75b667 100644 --- a/data/camera/build.gradle.kts +++ b/data/camera/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } @@ -37,8 +36,7 @@ android { buildTypes { release { proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt") ) } create("benchmark") { diff --git a/data/camera/consumer-rules.pro b/data/camera/consumer-rules.pro new file mode 100644 index 000000000..c1a8cebff --- /dev/null +++ b/data/camera/consumer-rules.pro @@ -0,0 +1,2 @@ +# Add project specific consumer ProGuard rules here. +# More info: https://developer.android.com/studio/build/shrink-code diff --git a/data/media/build.gradle.kts b/data/media/build.gradle.kts index 91146f6e8..223daced0 100644 --- a/data/media/build.gradle.kts +++ b/data/media/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/data/media/testing/build.gradle.kts b/data/media/testing/build.gradle.kts index f05656de1..2c531124c 100644 --- a/data/media/testing/build.gradle.kts +++ b/data/media/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 7748c8d51..b7037b199 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index 14e7b1e30..a3ff5b281 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/feature/permissions/build.gradle.kts b/feature/permissions/build.gradle.kts index 6c977afeb..e101d4892 100644 --- a/feature/permissions/build.gradle.kts +++ b/feature/permissions/build.gradle.kts @@ -15,10 +15,9 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.compose.compiler) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index b1faaa055..d0754c11e 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } @@ -48,6 +47,9 @@ android { } kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true @@ -73,12 +75,7 @@ android { } } } - } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } -} + }} dependencies { // Compose diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index 54ab1c505..37e210f8e 100644 --- a/feature/preview/build.gradle.kts +++ b/feature/preview/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index d5c246d1f..10dadeda4 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/gradle.properties b/gradle.properties index 15ab33aba..23a6f2b56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,9 +39,10 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.nonFinalResIds=false # Properties to make gradle managed devices testing more stable (see https://issuetracker.google.com/287312019#comment41) android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 # Ensure we can run managed devices on servers that don't support hardware rendering -android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect \ No newline at end of file +android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2317d598..04676d579 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ accompanist = "0.37.3" # kotlinPlugin and composeCompiler are linked # See https://developer.android.com/jetpack/androidx/releases/compose-kotlin kotlinPlugin = "2.2.0" -androidGradlePlugin = "8.10.1" +androidGradlePlugin = "9.3.1" androidxActivityCompose = "1.10.1" @@ -43,7 +43,7 @@ composeMaterial = "1.5.0-alpha07" cmake = "3.22.1" kotlinxAtomicfu = "0.29.0" kotlinxCoroutines = "1.10.2" -hilt = "2.57" +hilt = "2.59.2" junit = "4.13.2" robolectric = "4.15.1" @@ -124,3 +124,4 @@ dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hi kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinPlugin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlinPlugin" } +android-legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "androidGradlePlugin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b965ecfd5..c72c372a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Mar 12 23:44:57 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index dfb9e6557..f1197bb76 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,7 +57,6 @@ include(":ui:uistate:capture") include(":ui:uistateadapter:capture") include(":ui:components") include(":ui:components:capture") -include(":data:model") include(":core:settings") include(":core:model") include(":ui:uistate:postcapture") diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index 02d3b2dca..b152876c8 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -48,15 +47,14 @@ android { } kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index d2f9fc5fa..0766452c4 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index 2690d28bb..aad8888a3 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 062e38746..66172ec61 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/testing/build.gradle.kts b/ui/controller/testing/build.gradle.kts index 402ca3715..f239cd132 100644 --- a/ui/controller/testing/build.gradle.kts +++ b/ui/controller/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/ui/debug/build.gradle.kts b/ui/debug/build.gradle.kts index d62d15e16..4406021e2 100644 --- a/ui/debug/build.gradle.kts +++ b/ui/debug/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } diff --git a/ui/debug/testing/build.gradle.kts b/ui/debug/testing/build.gradle.kts index 4abe6a083..f05e7f6bf 100644 --- a/ui/debug/testing/build.gradle.kts +++ b/ui/debug/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index e06c9e4c7..5ceda5ab4 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index 345628e27..b32a0c393 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistate/postcapture/build.gradle.kts b/ui/uistate/postcapture/build.gradle.kts index 4fda15af5..cd5412fb3 100644 --- a/ui/uistate/postcapture/build.gradle.kts +++ b/ui/uistate/postcapture/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -42,8 +41,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index 2826a1033..9fe67b430 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index fa52725fe..46ec7008a 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistateadapter/postcapture/build.gradle.kts b/ui/uistateadapter/postcapture/build.gradle.kts index 889e3a51c..8659e8e7a 100644 --- a/ui/uistateadapter/postcapture/build.gradle.kts +++ b/ui/uistateadapter/postcapture/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -42,8 +41,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } From d4664bf1034b11949d539c26ef334f88ae7b4254 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 07:58:15 -0700 Subject: [PATCH 02/13] Fix Gradle 9 test task failures by removing empty test suites Deletes the unused and duplicated test helper classes in :ui:components:capture. Gradle 9 fails the build if a module contains test sources but no actual @Test classes to execute, which was the case here since it only contained test utilities. --- .../capture/rules/MainDispatcherRule.kt | 39 --- .../workaround/ComposableCaptureToImage.kt | 257 ------------------ 2 files changed, 296 deletions(-) delete mode 100644 ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt delete mode 100644 ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt deleted file mode 100644 index 287fb9fff..000000000 --- a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 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 com.google.jetpackcamera.ui.components.capture.capture.rules - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -class MainDispatcherRule(private val dispatcher: CoroutineDispatcher) : TestRule { - @OptIn(ExperimentalCoroutinesApi::class) - override fun apply(base: Statement?, description: Description?) = object : Statement() { - override fun evaluate() { - Dispatchers.setMain(dispatcher) - try { - base!!.evaluate() - } finally { - Dispatchers.resetMain() - } - } - } -} diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt deleted file mode 100644 index 9805bcde9..000000000 --- a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 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. - */ -@file:SuppressLint("UseSdkSuppress") - -package com.google.jetpackcamera.ui.components.capture.capture.workaround - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper -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.View -import android.view.Window -import androidx.annotation.DoNotInline -import androidx.annotation.RequiresApi -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.ViewRootForTest -import androidx.compose.ui.semantics.SemanticsNode -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.window.DialogWindowProvider -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.platform.graphics.HardwareRendererCompat -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.math.roundToInt - -/** - * Workaround captureToImage method. - * - * Once composable + robolectric graphics bugs are fixed, this can be replaced with the actual - * [SemanticsNodeInteraction.captureToImage]. Alternative is to use - * instrumentations tests, but they are not run at github workflows. - * - * See [robolectric issue 8071](https://github.com/robolectric/robolectric/issues/8071) for details. - */ - -@OptIn(ExperimentalTestApi::class) -@RequiresApi(Build.VERSION_CODES.O) -fun SemanticsNodeInteraction.captureToImage(): ImageBitmap { - val node = fetchSemanticsNode("Failed to capture a node to bitmap.") - // Validate we are in popup - val popupParentMaybe = - node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsPopup) - } - if (popupParentMaybe != null) { - return processMultiWindowScreenshot(node) - } - - val view = (node.root as ViewRootForTest).view - - // If we are in dialog use its window to capture the bitmap - val dialogParentNodeMaybe = - node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsDialog) - } - var dialogWindow: Window? = null - if (dialogParentNodeMaybe != null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - // TODO(b/163023027) - throw IllegalArgumentException("Cannot currently capture dialogs on API lower than 28!") - } - - dialogWindow = findDialogWindowProviderInParent(view)?.window - ?: throw IllegalArgumentException( - "Could not find a dialog window provider to capture its bitmap" - ) - } - - val windowToUse = dialogWindow ?: view.context.getActivityWindow() - - val nodeBounds = node.boundsInRoot - val nodeBoundsRect = - Rect( - nodeBounds.left.roundToInt(), - nodeBounds.top.roundToInt(), - nodeBounds.right.roundToInt(), - nodeBounds.bottom.roundToInt() - ) - - val locationInWindow = intArrayOf(0, 0) - view.getLocationInWindow(locationInWindow) - val x = locationInWindow[0] - val y = locationInWindow[1] - - // Now these are bounds in window - nodeBoundsRect.offset(x, y) - - return windowToUse.captureRegionToImage(nodeBoundsRect) -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun SemanticsNode.findClosestParentNode( - includeSelf: Boolean = false, - selector: (SemanticsNode) -> Boolean -): SemanticsNode? { - var currentParent = if (includeSelf) this else parent - while (currentParent != null) { - if (selector(currentParent)) { - return currentParent - } else { - currentParent = currentParent.parent - } - } - - return null -} - -@ExperimentalTestApi -@RequiresApi(Build.VERSION_CODES.O) -private fun processMultiWindowScreenshot(node: SemanticsNode): ImageBitmap { - val nodePositionInScreen = findNodePosition(node) - val nodeBoundsInRoot = node.boundsInRoot - - val combinedBitmap = InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot() - - val finalBitmap = - Bitmap.createBitmap( - combinedBitmap, - (nodePositionInScreen.x + nodeBoundsInRoot.left).roundToInt(), - (nodePositionInScreen.y + nodeBoundsInRoot.top).roundToInt(), - nodeBoundsInRoot.width.roundToInt(), - nodeBoundsInRoot.height.roundToInt() - ) - return finalBitmap.asImageBitmap() -} - -private fun findNodePosition(node: SemanticsNode): Offset { - val view = (node.root as ViewRootForTest).view - val locationOnScreen = intArrayOf(0, 0) - view.getLocationOnScreen(locationOnScreen) - val x = locationOnScreen[0] - val y = locationOnScreen[1] - - return Offset(x.toFloat(), y.toFloat()) -} - -internal fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? { - if (view is DialogWindowProvider) { - return view - } - val parent = view.parent ?: return null - if (parent is View) { - return findDialogWindowProviderInParent(parent) - } - return null -} - -private fun Context.getActivityWindow(): Window { - fun Context.getActivity(): Activity { - return when (this) { - is Activity -> this - is ContextWrapper -> this.baseContext.getActivity() - else -> throw IllegalStateException( - "Context is not an Activity context, but a ${javaClass.simpleName} context. " + - "An Activity context is required to get a Window instance" - ) - } - } - return getActivity().window -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.captureRegionToImage(boundsInWindow: Rect): ImageBitmap { - // Turn on hardware rendering, if necessary - return withDrawingEnabled { - // Then we generate the bitmap - generateBitmap(boundsInWindow).asImageBitmap() - } -} - -private fun withDrawingEnabled(block: () -> R): R { - val wasDrawingEnabled = HardwareRendererCompat.isDrawingEnabled() - try { - if (!wasDrawingEnabled) { - HardwareRendererCompat.setDrawingEnabled(true) - } - return block.invoke() - } finally { - if (!wasDrawingEnabled) { - HardwareRendererCompat.setDrawingEnabled(false) - } - } -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.generateBitmap(boundsInWindow: Rect): Bitmap { - val destBitmap = - Bitmap.createBitmap( - boundsInWindow.width(), - boundsInWindow.height(), - Bitmap.Config.ARGB_8888 - ) - generateBitmapFromPixelCopy(boundsInWindow, destBitmap) - return destBitmap -} - -@RequiresApi(Build.VERSION_CODES.O) -private object PixelCopyHelper { - @DoNotInline - fun request( - source: Window, - srcRect: Rect?, - dest: Bitmap, - listener: PixelCopy.OnPixelCopyFinishedListener, - listenerThread: Handler - ) { - PixelCopy.request(source, srcRect, dest, listener, listenerThread) - } -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.generateBitmapFromPixelCopy(boundsInWindow: Rect, destBitmap: Bitmap) { - val latch = CountDownLatch(1) - var copyResult = 0 - val onCopyFinished = - PixelCopy.OnPixelCopyFinishedListener { result -> - copyResult = result - latch.countDown() - } - PixelCopyHelper.request( - this, - boundsInWindow, - destBitmap, - onCopyFinished, - Handler(Looper.getMainLooper()) - ) - - if (!latch.await(1, TimeUnit.SECONDS)) { - throw AssertionError("Failed waiting for PixelCopy!") - } - if (copyResult != PixelCopy.SUCCESS) { - throw AssertionError("PixelCopy failed!") - } -} From 6d828d971dbaed39d62aa7b03e295931cdd634dc Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 08:35:49 -0700 Subject: [PATCH 03/13] styling cleanups --- core/camera/build.gradle.kts | 2 -- core/camera/effects/single-stream/build.gradle.kts | 4 +--- feature/postcapture/build.gradle.kts | 4 ++-- ui/components/build.gradle.kts | 1 - ui/components/capture/build.gradle.kts | 5 +---- ui/controller/build.gradle.kts | 5 +---- ui/controller/impl/build.gradle.kts | 5 +---- ui/uistate/build.gradle.kts | 5 +---- ui/uistate/capture/build.gradle.kts | 5 +---- ui/uistateadapter/build.gradle.kts | 5 +---- ui/uistateadapter/capture/build.gradle.kts | 5 +---- 11 files changed, 10 insertions(+), 36 deletions(-) diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index 975955fd0..b587e0a73 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -125,7 +125,6 @@ dependencies { implementation(libs.camera.lifecycle) implementation(libs.camera.video) - // Tracing implementation(libs.androidx.tracing) implementation(libs.kotlinx.atomicfu) @@ -141,4 +140,3 @@ dependencies { } - diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index ee42831de..f46fb4666 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -33,10 +33,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index d0754c11e..e57a6f450 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -75,7 +74,8 @@ android { } } } - }} + } +} dependencies { // Compose diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index b152876c8..cc2306000 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 0766452c4..837f3861b 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index aad8888a3..836d755b3 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 66172ec61..18e226ba7 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index 5ceda5ab4..3f5fffafb 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index b32a0c393..14ccd3306 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index 9fe67b430..ae6489ddd 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index 46ec7008a..a598f51fb 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } From f74ec6dc211a26d2e7209c6132600dfecf7e1323 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 06:04:47 -0700 Subject: [PATCH 04/13] Migrate context receivers to Kotlin 2.2.0 context parameters Migrates deprecated context receivers to the new context parameters syntax: - Updated context receivers to the new Kotlin 2.2.0 context parameters syntax (context(c: CameraSessionContext)) in CameraSession and ConcurrentCameraSession, using the -Xcontext-parameters compiler flag. - Upgraded compiler flags from -Xcontext-receivers to -Xcontext-parameters in 11 modules. - Upgraded ktlint to 1.8.0 to support Kotlin 2.2.0 context parameters. # Conflicts: # core/camera/effects/single-stream/build.gradle.kts # ui/components/capture/build.gradle.kts # ui/controller/build.gradle.kts # ui/controller/impl/build.gradle.kts # ui/uistate/build.gradle.kts # ui/uistate/capture/build.gradle.kts # ui/uistateadapter/build.gradle.kts # ui/uistateadapter/capture/build.gradle.kts --- core/camera/build.gradle.kts | 2 +- .../effects/single-stream/build.gradle.kts | 4 +- .../core/camera/CameraSession.kt | 1522 +++++++++-------- .../core/camera/ConcurrentCameraSession.kt | 269 +-- feature/postcapture/build.gradle.kts | 2 +- gradle/init.gradle.kts | 7 +- ui/components/build.gradle.kts | 2 +- ui/components/capture/build.gradle.kts | 2 +- ui/controller/build.gradle.kts | 2 +- ui/controller/impl/build.gradle.kts | 2 +- ui/uistate/build.gradle.kts | 2 +- ui/uistate/capture/build.gradle.kts | 4 +- ui/uistateadapter/build.gradle.kts | 2 +- ui/uistateadapter/capture/build.gradle.kts | 2 +- 14 files changed, 941 insertions(+), 883 deletions(-) diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index b587e0a73..e39c6c137 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -93,7 +93,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } } diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index f46fb4666..e09440860 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -34,8 +34,8 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") } } } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 8dd60c4a7..ab2d48e6b 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -119,253 +119,260 @@ private val QUALITY_RANGE_MAP = mapOf( SD to Range.create(241, 719) ) -context(CameraSessionContext) @ExperimentalCamera2Interop +context(c: CameraSessionContext) internal suspend fun runSingleCameraSession( sessionSettings: PerpetualSessionSettings.SingleCamera, cameraConstraints: CameraConstraints?, // TODO(tm): ImageCapture should go through an event channel like VideoCapture onImageCaptureCreated: (ImageCapture) -> Unit = {} -) = coroutineScope { - Log.d(TAG, "Starting new single camera session") - val initialCameraSelector = transientSettings.filterNotNull().first() - .primaryLensFacing.toCameraSelector() - - // only create video use case in standard or video_only - val videoCaptureUseCase = when (sessionSettings.captureMode) { - CaptureMode.STANDARD, CaptureMode.VIDEO_ONLY -> - createVideoUseCase( - cameraProvider.getCameraInfo(initialCameraSelector), - sessionSettings.aspectRatio, - sessionSettings.targetFrameRate, - sessionSettings.stabilizationMode, - sessionSettings.dynamicRange, - sessionSettings.videoQuality, - backgroundDispatcher - ) - - else -> { - null - } - } +) = with(c) { + coroutineScope { + Log.d(TAG, "Starting new single camera session") + val initialCameraSelector = transientSettings.filterNotNull().first() + .primaryLensFacing.toCameraSelector() + + // only create video use case in standard or video_only + val videoCaptureUseCase = when (sessionSettings.captureMode) { + CaptureMode.STANDARD, CaptureMode.VIDEO_ONLY -> + createVideoUseCase( + cameraProvider.getCameraInfo(initialCameraSelector), + sessionSettings.aspectRatio, + sessionSettings.targetFrameRate, + sessionSettings.stabilizationMode, + sessionSettings.dynamicRange, + sessionSettings.videoQuality, + backgroundDispatcher + ) - launch { - processVideoControlEvents( - videoCaptureUseCase, - captureTypeSuffix = if (sessionSettings.activeCameraEffect != null) { - "SingleStream" - } else { - "MultiStream" + else -> { + null } - ) - } + } - transientSettings - .filterNotNull() - .distinctUntilChanged { old, new -> - ( - old.primaryLensFacing == new.primaryLensFacing && - !( - (old.flashMode == FlashMode.LOW_LIGHT_BOOST) xor - (new.flashMode == FlashMode.LOW_LIGHT_BOOST) - ) - ) + launch { + processVideoControlEvents( + videoCaptureUseCase, + captureTypeSuffix = if (sessionSettings.activeCameraEffect != null) { + "SingleStream" + } else { + "MultiStream" + } + ) } - .collectLatest { currentTransientSettings -> - coroutineScope sessionScope@{ - cameraProvider.unbindAll() - val currentCameraSelector = currentTransientSettings.primaryLensFacing - .toCameraSelector() - val cameraInfo = cameraProvider.getCameraInfo(currentCameraSelector) - val camera2Info = Camera2CameraInfo.from(cameraInfo) - val cameraId = camera2Info.cameraId - - var cameraEffect: CameraEffect? = null - var captureResults: MutableStateFlow? = null - if (currentTransientSettings.flashMode == FlashMode.LOW_LIGHT_BOOST) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && - cameraConstraints?.supportedIlluminants?.contains( - Illuminant.LOW_LIGHT_BOOST_CAMERA_EFFECT - ) == true && lowLightBoostEffectProvider != null - ) { - captureResults = MutableStateFlow(null) - cameraEffect = lowLightBoostEffectProvider.create( - cameraId = cameraId, - captureResults = captureResults, - coroutineScope = this@sessionScope, - onSceneBrightnessChanged = { boostStrength -> - val strength = LowLightBoostState.Active(strength = boostStrength) - currentCameraState.update { old -> - if (old.lowLightBoostState != strength) { - old.copy(lowLightBoostState = strength) - } else { - old + + transientSettings + .filterNotNull() + .distinctUntilChanged { old, new -> + ( + old.primaryLensFacing == new.primaryLensFacing && + !( + (old.flashMode == FlashMode.LOW_LIGHT_BOOST) xor + (new.flashMode == FlashMode.LOW_LIGHT_BOOST) + ) + ) + } + .collectLatest { currentTransientSettings -> + coroutineScope sessionScope@{ + cameraProvider.unbindAll() + val currentCameraSelector = currentTransientSettings.primaryLensFacing + .toCameraSelector() + val cameraInfo = cameraProvider.getCameraInfo(currentCameraSelector) + val camera2Info = Camera2CameraInfo.from(cameraInfo) + val cameraId = camera2Info.cameraId + + var cameraEffect: CameraEffect? = null + var captureResults: MutableStateFlow? = null + if (currentTransientSettings.flashMode == FlashMode.LOW_LIGHT_BOOST) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + cameraConstraints?.supportedIlluminants?.contains( + Illuminant.LOW_LIGHT_BOOST_CAMERA_EFFECT + ) == true && lowLightBoostEffectProvider != null + ) { + captureResults = MutableStateFlow(null) + cameraEffect = lowLightBoostEffectProvider.create( + cameraId = cameraId, + captureResults = captureResults, + coroutineScope = this@sessionScope, + onSceneBrightnessChanged = { boostStrength -> + val strength = + LowLightBoostState.Active(strength = boostStrength) + currentCameraState.update { old -> + if (old.lowLightBoostState != strength) { + old.copy(lowLightBoostState = strength) + } else { + old + } + } + }, + onLowLightBoostError = { e -> + Log.w(TAG, "Emitting LLB Error", e) + currentCameraState.update { old -> + old.copy(lowLightBoostState = LowLightBoostState.Error(e)) } } - }, - onLowLightBoostError = { e -> - Log.w(TAG, "Emitting LLB Error", e) - currentCameraState.update { old -> - old.copy(lowLightBoostState = LowLightBoostState.Error(e)) - } - } - ) - } - } - if (cameraEffect == null) { - sessionSettings.activeCameraEffect?.let { key -> - cameraEffect = cameraEffectProviders[key]?.get()?.create(this@sessionScope) - } - } - val useCaseGroup = createUseCaseGroup( - cameraInfo = cameraProvider.getCameraInfo(currentCameraSelector), - videoCaptureUseCase = videoCaptureUseCase, - initialTransientSettings = currentTransientSettings, - stabilizationMode = sessionSettings.stabilizationMode, - aspectRatio = sessionSettings.aspectRatio, - imageFormat = sessionSettings.imageFormat, - captureMode = sessionSettings.captureMode, - effect = cameraEffect, - captureResults = captureResults - - ).apply { - getImageCapture()?.let(onImageCaptureCreated) - } - - cameraProvider.runWith( - currentCameraSelector, - useCaseGroup - ) { camera -> - Log.d(TAG, "Camera session started") - launch { - processFocusMeteringEvents( - camera.cameraInfo, - camera.cameraControl - ) + ) + } } - - launch { - camera.cameraInfo.torchState.asFlow().collectLatest { torchState -> - currentCameraState.update { old -> - old.copy(isTorchEnabled = torchState == TorchState.ON) - } + if (cameraEffect == null) { + sessionSettings.activeCameraEffect?.let { key -> + cameraEffect = + cameraEffectProviders[key]?.get()?.create(this@sessionScope) } } + val useCaseGroup = createUseCaseGroup( + cameraInfo = cameraProvider.getCameraInfo(currentCameraSelector), + videoCaptureUseCase = videoCaptureUseCase, + initialTransientSettings = currentTransientSettings, + stabilizationMode = sessionSettings.stabilizationMode, + aspectRatio = sessionSettings.aspectRatio, + imageFormat = sessionSettings.imageFormat, + captureMode = sessionSettings.captureMode, + effect = cameraEffect, + captureResults = captureResults + + ).apply { + getImageCapture()?.let(onImageCaptureCreated) + } - if (videoCaptureUseCase != null) { - val videoQuality = getVideoQualityFromResolution( - videoCaptureUseCase.resolutionInfo?.resolution - ) - if (videoQuality != sessionSettings.videoQuality) { - Log.e( - TAG, - "Failed to select video quality: $sessionSettings.videoQuality. " + - "Fallback: $videoQuality" + cameraProvider.runWith( + currentCameraSelector, + useCaseGroup + ) { camera -> + Log.d(TAG, "Camera session started") + launch { + processFocusMeteringEvents( + camera.cameraInfo, + camera.cameraControl ) } + launch { - currentCameraState.update { old -> - old.copy( - videoQualityInfo = VideoQualityInfo( - videoQuality, - getWidthFromCropRect( - videoCaptureUseCase.resolutionInfo?.cropRect - ), - getHeightFromCropRect( - videoCaptureUseCase.resolutionInfo?.cropRect - ) - ) - ) + camera.cameraInfo.torchState.asFlow().collectLatest { torchState -> + currentCameraState.update { old -> + old.copy(isTorchEnabled = torchState == TorchState.ON) + } } } - } - // Update CameraState to reflect when camera is running - launch { - camera.cameraInfo.cameraState - .asFlow() - .filterNotNull() - .distinctUntilChanged() - .onCompletion { - currentCameraState.update { old -> - old.copy( - isCameraRunning = false - ) - } + if (videoCaptureUseCase != null) { + val videoQuality = getVideoQualityFromResolution( + videoCaptureUseCase.resolutionInfo?.resolution + ) + if (videoQuality != sessionSettings.videoQuality) { + Log.e( + TAG, + "Failed to select video quality: " + + "$sessionSettings.videoQuality. " + + "Fallback: $videoQuality" + ) } - .collectLatest { cameraState -> + launch { currentCameraState.update { old -> old.copy( - isCameraRunning = - cameraState.type == CXCameraState.Type.OPEN + videoQualityInfo = VideoQualityInfo( + videoQuality, + getWidthFromCropRect( + videoCaptureUseCase.resolutionInfo?.cropRect + ), + getHeightFromCropRect( + videoCaptureUseCase.resolutionInfo?.cropRect + ) + ) ) } } - } + } - // Update CameraState to mirror current ZoomState - launch { - camera.cameraInfo.zoomState - .asFlow() - .filterNotNull() - .distinctUntilChanged() - .onCompletion { - // reset current camera state when changing cameras. - currentCameraState.update { old -> - old.copy( - zoomRatios = emptyMap(), - linearZoomScales = emptyMap() - ) + // Update CameraState to reflect when camera is running + launch { + camera.cameraInfo.cameraState + .asFlow() + .filterNotNull() + .distinctUntilChanged() + .onCompletion { + currentCameraState.update { old -> + old.copy( + isCameraRunning = false + ) + } } - } - .collectLatest { zoomState -> - // TODO(b/405987189): remove checks after buggy zoomState is fixed - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - if (zoomState.zoomRatio != 1.0f || - zoomState.zoomRatio == currentTransientSettings - .zoomRatios[currentTransientSettings.primaryLensFacing] - ) { - currentCameraState.update { old -> - old.copy( - zoomRatios = old.zoomRatios - .toMutableMap() - .apply { - put( - camera.cameraInfo.appLensFacing, - zoomState.zoomRatio - ) - }.toMap(), - linearZoomScales = old.linearZoomScales - .toMutableMap() - .apply { - put( - camera.cameraInfo.appLensFacing, - zoomState.linearZoom - ) - }.toMap() - ) + .collectLatest { cameraState -> + currentCameraState.update { old -> + old.copy( + isCameraRunning = + cameraState.type == CXCameraState.Type.OPEN + ) + } + } + } + + // Update CameraState to mirror current ZoomState + launch { + camera.cameraInfo.zoomState + .asFlow() + .filterNotNull() + .distinctUntilChanged() + .onCompletion { + // reset current camera state when changing cameras. + currentCameraState.update { old -> + old.copy( + zoomRatios = emptyMap(), + linearZoomScales = emptyMap() + ) + } + } + .collectLatest { zoomState -> + // TODO(b/405987189): remove checks after buggy zoomState is fixed + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (zoomState.zoomRatio != 1.0f || + zoomState.zoomRatio == currentTransientSettings + .zoomRatios[ + currentTransientSettings.primaryLensFacing + ] + ) { + currentCameraState.update { old -> + old.copy( + zoomRatios = old.zoomRatios + .toMutableMap() + .apply { + put( + camera.cameraInfo.appLensFacing, + zoomState.zoomRatio + ) + }.toMap(), + linearZoomScales = old.linearZoomScales + .toMutableMap() + .apply { + put( + camera.cameraInfo.appLensFacing, + zoomState.linearZoom + ) + }.toMap() + ) + } } } } - } - } + } - applyDeviceRotation(currentTransientSettings.deviceRotation, useCaseGroup) - processTransientSettingEvents( - camera, - cameraConstraints, - useCaseGroup, - currentTransientSettings, - transientSettings, - sessionSettings - ) + applyDeviceRotation(currentTransientSettings.deviceRotation, useCaseGroup) + processTransientSettingEvents( + camera, + cameraConstraints, + useCaseGroup, + currentTransientSettings, + transientSettings, + sessionSettings + ) + } } } - } + } } -context(CameraSessionContext) @OptIn(ExperimentalCamera2Interop::class) +context(c: CameraSessionContext) internal suspend fun processTransientSettingEvents( camera: Camera, cameraConstraints: CameraConstraints?, @@ -374,98 +381,100 @@ internal suspend fun processTransientSettingEvents( transientSettings: StateFlow, sessionSettings: PerpetualSessionSettings.SingleCamera? ) { - // Immediately Apply camera zoom from current settings when opening a new camera - camera.cameraControl.setZoomRatio( - initialTransientSettings.zoomRatios[camera.cameraInfo.appLensFacing] ?: 1f - ) + with(c) { + // Immediately Apply camera zoom from current settings when opening a new camera + camera.cameraControl.setZoomRatio( + initialTransientSettings.zoomRatios[camera.cameraInfo.appLensFacing] ?: 1f + ) - val camera2OptionsBuilder = CaptureRequestOptions.Builder() - updateCamera2RequestOptions( - camera, - cameraConstraints, - null, - initialTransientSettings, - sessionSettings, - camera2OptionsBuilder - ) + val camera2OptionsBuilder = CaptureRequestOptions.Builder() + updateCamera2RequestOptions( + camera, + cameraConstraints, + null, + initialTransientSettings, + sessionSettings, + camera2OptionsBuilder + ) - var prevTransientSettings = initialTransientSettings - val isFrontFacing = camera.cameraInfo.appLensFacing == LensFacing.FRONT - var torchOn = false - fun setTorch(newTorchOn: Boolean) { - if (newTorchOn != torchOn) { - camera.cameraControl.enableTorch(newTorchOn) - torchOn = newTorchOn - } - } - combine( - transientSettings.filterNotNull(), - currentCameraState.asStateFlow().transform { emit(it.videoRecordingState) } - ) { newTransientSettings, videoRecordingState -> - return@combine Pair(newTransientSettings, videoRecordingState) - }.collect { transientPair -> - val newTransientSettings = transientPair.first - val videoRecordingState = transientPair.second - - // todo(): handle torch on Auto FlashMode - // enable torch only while recording is in progress - if ((videoRecordingState !is VideoRecordingState.Inactive) && - newTransientSettings.flashMode == FlashMode.ON && - !isFrontFacing - ) { - setTorch(true) - } else { - setTorch(false) + var prevTransientSettings = initialTransientSettings + val isFrontFacing = camera.cameraInfo.appLensFacing == LensFacing.FRONT + var torchOn = false + fun setTorch(newTorchOn: Boolean) { + if (newTorchOn != torchOn) { + camera.cameraControl.enableTorch(newTorchOn) + torchOn = newTorchOn + } } + combine( + transientSettings.filterNotNull(), + currentCameraState.asStateFlow().transform { emit(it.videoRecordingState) } + ) { newTransientSettings, videoRecordingState -> + return@combine Pair(newTransientSettings, videoRecordingState) + }.collect { transientPair -> + val newTransientSettings = transientPair.first + val videoRecordingState = transientPair.second + + // todo(): handle torch on Auto FlashMode + // enable torch only while recording is in progress + if ((videoRecordingState !is VideoRecordingState.Inactive) && + newTransientSettings.flashMode == FlashMode.ON && + !isFrontFacing + ) { + setTorch(true) + } else { + setTorch(false) + } - // apply camera torch mode to image capture - useCaseGroup.getImageCapture()?.let { imageCapture -> - if (prevTransientSettings.flashMode != newTransientSettings.flashMode) { - setFlashModeInternal( - imageCapture = imageCapture, - flashMode = newTransientSettings.flashMode, - isFrontFacing = camera.cameraInfo.appLensFacing == LensFacing.FRONT - ) + // apply camera torch mode to image capture + useCaseGroup.getImageCapture()?.let { imageCapture -> + if (prevTransientSettings.flashMode != newTransientSettings.flashMode) { + setFlashModeInternal( + imageCapture = imageCapture, + flashMode = newTransientSettings.flashMode, + isFrontFacing = camera.cameraInfo.appLensFacing == LensFacing.FRONT + ) + } } - } - if (prevTransientSettings.deviceRotation - != newTransientSettings.deviceRotation - ) { - Log.d( - TAG, - "Updating device rotation from " + - "${prevTransientSettings.deviceRotation} -> " + - "${newTransientSettings.deviceRotation}" - ) - applyDeviceRotation(newTransientSettings.deviceRotation, useCaseGroup) - } + if (prevTransientSettings.deviceRotation + != newTransientSettings.deviceRotation + ) { + Log.d( + TAG, + "Updating device rotation from " + + "${prevTransientSettings.deviceRotation} -> " + + "${newTransientSettings.deviceRotation}" + ) + applyDeviceRotation(newTransientSettings.deviceRotation, useCaseGroup) + } - // setzoomratio when the primary zoom value changes. - if (prevTransientSettings.primaryLensFacing == newTransientSettings.primaryLensFacing && - prevTransientSettings.zoomRatios[prevTransientSettings.primaryLensFacing] != - newTransientSettings.zoomRatios[newTransientSettings.primaryLensFacing] - ) { - newTransientSettings.primaryLensFacing.let { - camera.cameraControl.setZoomRatio(newTransientSettings.zoomRatios[it] ?: 1f) + // setzoomratio when the primary zoom value changes. + if (prevTransientSettings.primaryLensFacing == newTransientSettings.primaryLensFacing && + prevTransientSettings.zoomRatios[prevTransientSettings.primaryLensFacing] != + newTransientSettings.zoomRatios[newTransientSettings.primaryLensFacing] + ) { + newTransientSettings.primaryLensFacing.let { + camera.cameraControl.setZoomRatio(newTransientSettings.zoomRatios[it] ?: 1f) + } } - } - updateCamera2RequestOptions( - camera, - cameraConstraints, - prevTransientSettings, - newTransientSettings, - sessionSettings, - camera2OptionsBuilder - ) + updateCamera2RequestOptions( + camera, + cameraConstraints, + prevTransientSettings, + newTransientSettings, + sessionSettings, + camera2OptionsBuilder + ) - prevTransientSettings = newTransientSettings + prevTransientSettings = newTransientSettings + } } } -context(CameraSessionContext) @ExperimentalCamera2Interop +context(c: CameraSessionContext) private suspend fun updateCamera2RequestOptions( camera: Camera, cameraConstraints: CameraConstraints?, @@ -474,88 +483,99 @@ private suspend fun updateCamera2RequestOptions( sessionSettings: PerpetualSessionSettings.SingleCamera?, optionsBuilder: CaptureRequestOptions.Builder ) { - var needsUpdate = false - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && - prevTransientSettings?.flashMode != newTransientSettings.flashMode - ) { - when (newTransientSettings.flashMode) { - FlashMode.LOW_LIGHT_BOOST -> { - if (cameraConstraints?.supportedIlluminants?.contains( - Illuminant.LOW_LIGHT_BOOST_AE_MODE - ) == true - ) { - Log.d( - TAG, - "Setting LLB with " + - "CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY" - ) - val captureRequestOptions = CaptureRequestOptions.Builder() - .setCaptureRequestOption( - CaptureRequest.CONTROL_AE_MODE, - CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY + with(c) { + var needsUpdate = false + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && + prevTransientSettings?.flashMode != newTransientSettings.flashMode + ) { + when (newTransientSettings.flashMode) { + FlashMode.LOW_LIGHT_BOOST -> { + if (cameraConstraints?.supportedIlluminants?.contains( + Illuminant.LOW_LIGHT_BOOST_AE_MODE + ) == true + ) { + Log.d( + TAG, + "Setting LLB with " + + "CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY" ) - .build() + val captureRequestOptions = CaptureRequestOptions.Builder() + .setCaptureRequestOption( + CaptureRequest + .CONTROL_AE_MODE, + CameraMetadata + .CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY + ) + .build() - Camera2CameraControl.from(camera.cameraControl) - .addCaptureRequestOptions(captureRequestOptions) + Camera2CameraControl.from(camera.cameraControl) + .addCaptureRequestOptions(captureRequestOptions) + } } - } - else -> { - optionsBuilder.clearCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE) + else -> { + optionsBuilder.clearCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE) + } } + needsUpdate = true } - needsUpdate = true - } - val newTestPattern = newTransientSettings.testPattern - if (prevTransientSettings?.testPattern != newTestPattern) { - val (mode: Int?, data: IntArray?) = when (newTestPattern) { - TestPattern.Off -> Pair(null, null) - TestPattern.ColorBars -> Pair(CameraMetadata.SENSOR_TEST_PATTERN_MODE_COLOR_BARS, null) - TestPattern.ColorBarsFadeToGray -> Pair( - CameraMetadata.SENSOR_TEST_PATTERN_MODE_COLOR_BARS_FADE_TO_GRAY, - null - ) + val newTestPattern = newTransientSettings.testPattern + if (prevTransientSettings?.testPattern != newTestPattern) { + val (mode: Int?, data: IntArray?) = when (newTestPattern) { + TestPattern.Off -> Pair(null, null) + + TestPattern.ColorBars -> Pair( + CameraMetadata.SENSOR_TEST_PATTERN_MODE_COLOR_BARS, + null + ) + + TestPattern.ColorBarsFadeToGray -> Pair( + CameraMetadata.SENSOR_TEST_PATTERN_MODE_COLOR_BARS_FADE_TO_GRAY, + null + ) + + TestPattern.PN9 -> Pair(CameraMetadata.SENSOR_TEST_PATTERN_MODE_PN9, null) + + TestPattern.Custom1 -> Pair(CameraMetadata.SENSOR_TEST_PATTERN_MODE_CUSTOM1, null) - TestPattern.PN9 -> Pair(CameraMetadata.SENSOR_TEST_PATTERN_MODE_PN9, null) - TestPattern.Custom1 -> Pair(CameraMetadata.SENSOR_TEST_PATTERN_MODE_CUSTOM1, null) - is TestPattern.SolidColor -> { - Pair( - CameraMetadata.SENSOR_TEST_PATTERN_MODE_SOLID_COLOR, - intArrayOf( - newTestPattern.red.toInt(), - newTestPattern.greenEven.toInt(), - newTestPattern.greenOdd.toInt(), - newTestPattern.blue.toInt() + is TestPattern.SolidColor -> { + Pair( + CameraMetadata.SENSOR_TEST_PATTERN_MODE_SOLID_COLOR, + intArrayOf( + newTestPattern.red.toInt(), + newTestPattern.greenEven.toInt(), + newTestPattern.greenOdd.toInt(), + newTestPattern.blue.toInt() + ) ) + } + } + if (mode != null) { + optionsBuilder.setCaptureRequestOption( + CaptureRequest.SENSOR_TEST_PATTERN_MODE, + mode ) + } else { + optionsBuilder.clearCaptureRequestOption(CaptureRequest.SENSOR_TEST_PATTERN_MODE) } - } - if (mode != null) { - optionsBuilder.setCaptureRequestOption( - CaptureRequest.SENSOR_TEST_PATTERN_MODE, - mode - ) - } else { - optionsBuilder.clearCaptureRequestOption(CaptureRequest.SENSOR_TEST_PATTERN_MODE) - } - if (data != null) { - optionsBuilder.setCaptureRequestOption( - CaptureRequest.SENSOR_TEST_PATTERN_DATA, - data - ) - } else { - optionsBuilder.clearCaptureRequestOption(CaptureRequest.SENSOR_TEST_PATTERN_DATA) + if (data != null) { + optionsBuilder.setCaptureRequestOption( + CaptureRequest.SENSOR_TEST_PATTERN_DATA, + data + ) + } else { + optionsBuilder.clearCaptureRequestOption(CaptureRequest.SENSOR_TEST_PATTERN_DATA) + } + needsUpdate = true } - needsUpdate = true - } - if (needsUpdate) { - Camera2CameraControl.from(camera.cameraControl) - .setCaptureRequestOptions(optionsBuilder.build()) + if (needsUpdate) { + Camera2CameraControl.from(camera.cameraControl) + .setCaptureRequestOptions(optionsBuilder.build()) + } } } @@ -582,7 +602,7 @@ internal fun applyDeviceRotation(deviceRotation: DeviceRotation, useCaseGroup: U } } -context(CameraSessionContext) +context(c: CameraSessionContext) internal fun createUseCaseGroup( cameraInfo: CameraInfo, initialTransientSettings: TransientSessionSettings, @@ -594,49 +614,51 @@ internal fun createUseCaseGroup( effect: CameraEffect? = null, captureResults: MutableStateFlow? = null ): UseCaseGroup { - val previewUseCase = - createPreviewUseCase( - cameraInfo, - aspectRatio, - stabilizationMode, - captureResults - ) + with(c) { + val previewUseCase = + createPreviewUseCase( + cameraInfo, + aspectRatio, + stabilizationMode, + captureResults + ) - // only create image use case in image or standard - val imageCaptureUseCase = if (captureMode != CaptureMode.VIDEO_ONLY) { - createImageUseCase(cameraInfo, aspectRatio, imageFormat) - } else { - null - } + // only create image use case in image or standard + val imageCaptureUseCase = if (captureMode != CaptureMode.VIDEO_ONLY) { + createImageUseCase(cameraInfo, aspectRatio, imageFormat) + } else { + null + } - imageCaptureUseCase?.let { - setFlashModeInternal( - imageCapture = imageCaptureUseCase, - flashMode = initialTransientSettings.flashMode, - isFrontFacing = cameraInfo.appLensFacing == LensFacing.FRONT - ) - } + imageCaptureUseCase?.let { + setFlashModeInternal( + imageCapture = imageCaptureUseCase, + flashMode = initialTransientSettings.flashMode, + isFrontFacing = cameraInfo.appLensFacing == LensFacing.FRONT + ) + } - return UseCaseGroup.Builder().apply { - Log.d( - TAG, - "Setting initial device rotation to ${initialTransientSettings.deviceRotation}" - ) - setViewPort( - ViewPort.Builder( - Rational(aspectRatio.numerator, aspectRatio.denominator), - // Initialize rotation to Preview's rotation, which comes from Display rotation - previewUseCase.targetRotation - ).build() - ) - addUseCase(previewUseCase) + return UseCaseGroup.Builder().apply { + Log.d( + TAG, + "Setting initial device rotation to ${initialTransientSettings.deviceRotation}" + ) + setViewPort( + ViewPort.Builder( + Rational(aspectRatio.numerator, aspectRatio.denominator), + // Initialize rotation to Preview's rotation, which comes from Display rotation + previewUseCase.targetRotation + ).build() + ) + addUseCase(previewUseCase) - // image and video use cases are only created if supported by the configuration - imageCaptureUseCase?.let { addUseCase(imageCaptureUseCase) } - videoCaptureUseCase?.let { addUseCase(videoCaptureUseCase) } + // image and video use cases are only created if supported by the configuration + imageCaptureUseCase?.let { addUseCase(imageCaptureUseCase) } + videoCaptureUseCase?.let { addUseCase(videoCaptureUseCase) } - effect?.let { addEffect(it) } - }.build() + effect?.let { addEffect(it) } + }.build() + } } private fun getVideoQualityFromResolution(resolution: Size?): VideoQuality = @@ -719,7 +741,9 @@ internal fun createVideoUseCase( private fun getAspectRatioForUseCase(sensorLandscapeRatio: Float, aspectRatio: AspectRatio): Int = when (aspectRatio) { AspectRatio.THREE_FOUR -> androidx.camera.core.AspectRatio.RATIO_4_3 + AspectRatio.NINE_SIXTEEN -> androidx.camera.core.AspectRatio.RATIO_16_9 + else -> { // Choose the aspect ratio which maximizes FOV by being closest to the sensor ratio if ( @@ -733,44 +757,51 @@ private fun getAspectRatioForUseCase(sensorLandscapeRatio: Float, aspectRatio: A } } -context(CameraSessionContext) +context(c: CameraSessionContext) private fun createPreviewUseCase( cameraInfo: CameraInfo, aspectRatio: AspectRatio, stabilizationMode: StabilizationMode, captureResults: MutableStateFlow? = null -): Preview = Preview.Builder().apply { - updateCameraStateWithCaptureResults( - targetCameraInfo = cameraInfo, - captureResults = captureResults - ) +): Preview = with(c) { + Preview.Builder().apply { + updateCameraStateWithCaptureResults( + targetCameraInfo = cameraInfo, + captureResults = captureResults + ) + + // set preview stabilization + when (stabilizationMode) { + StabilizationMode.ON -> setPreviewStabilizationEnabled(true) + + StabilizationMode.OPTICAL -> setOpticalStabilizationModeEnabled(true) + + StabilizationMode.OFF -> { + setOpticalStabilizationModeEnabled(false) + // Setting this to false in Preview use case will disable video stabilization, even in + // IMAGE_ONLY mode where there isn't a video capture use case attached + setPreviewStabilizationEnabled(false) + } - // set preview stabilization - when (stabilizationMode) { - StabilizationMode.ON -> setPreviewStabilizationEnabled(true) - StabilizationMode.OPTICAL -> setOpticalStabilizationModeEnabled(true) - StabilizationMode.OFF -> { - setOpticalStabilizationModeEnabled(false) - // Setting this to false in Preview use case will disable video stabilization, even in - // IMAGE_ONLY mode where there isn't a video capture use case attached - setPreviewStabilizationEnabled(false) + StabilizationMode.HIGH_QUALITY -> {} + + // No-op. Handled by VideoCapture use case. + else -> throw UnsupportedOperationException( + "Unexpected stabilization mode: $stabilizationMode. Stabilization mode should " + + "always have an explicit mode, such as ON, OPTICAL, OFF or HIGH_QUALITY" + ) } - StabilizationMode.HIGH_QUALITY -> {} // No-op. Handled by VideoCapture use case. - else -> throw UnsupportedOperationException( - "Unexpected stabilization mode: $stabilizationMode. Stabilization mode should always " + - "an explicit mode, such as ON, OPTICAL, OFF or HIGH_QUALITY" - ) - } - setResolutionSelector( - getResolutionSelector(cameraInfo.sensorLandscapeRatio, aspectRatio) - ) -}.build() - .apply { - setSurfaceProvider { surfaceRequest -> - surfaceRequests.update { surfaceRequest } + setResolutionSelector( + getResolutionSelector(cameraInfo.sensorLandscapeRatio, aspectRatio) + ) + }.build() + .apply { + setSurfaceProvider { surfaceRequest -> + surfaceRequests.update { surfaceRequest } + } } - } +} @OptIn(ExperimentalCamera2Interop::class) private fun Preview.Builder.setOpticalStabilizationModeEnabled(enabled: Boolean): Preview.Builder { @@ -792,7 +823,9 @@ private fun getResolutionSelector( ): ResolutionSelector { val aspectRatioStrategy = when (aspectRatio) { AspectRatio.THREE_FOUR -> AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY + AspectRatio.NINE_SIXTEEN -> AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY + else -> { // Choose the resolution selector strategy which maximizes FOV by being closest // to the sensor aspect ratio @@ -809,56 +842,61 @@ private fun getResolutionSelector( return ResolutionSelector.Builder().setAspectRatioStrategy(aspectRatioStrategy).build() } -context(CameraSessionContext) +context(c: CameraSessionContext) private fun setFlashModeInternal( imageCapture: ImageCapture, flashMode: FlashMode, isFrontFacing: Boolean ) { - val isScreenFlashRequired = - isFrontFacing && (flashMode == FlashMode.ON || flashMode == FlashMode.AUTO) - - if (isScreenFlashRequired) { - imageCapture.screenFlash = object : ImageCapture.ScreenFlash { - override fun apply( - expirationTimeMillis: Long, - listener: ImageCapture.ScreenFlashListener - ) { - Log.d(TAG, "ImageCapture.ScreenFlash: apply") - screenFlashEvents.trySend( - CameraSystem.ScreenFlashEvent(CameraSystem.ScreenFlashEvent.Type.APPLY_UI) { - listener.onCompleted() - } - ) - } + with(c) { + val isScreenFlashRequired = + isFrontFacing && (flashMode == FlashMode.ON || flashMode == FlashMode.AUTO) + + if (isScreenFlashRequired) { + imageCapture.screenFlash = object : ImageCapture.ScreenFlash { + override fun apply( + expirationTimeMillis: Long, + listener: ImageCapture.ScreenFlashListener + ) { + Log.d(TAG, "ImageCapture.ScreenFlash: apply") + screenFlashEvents.trySend( + CameraSystem.ScreenFlashEvent(CameraSystem.ScreenFlashEvent.Type.APPLY_UI) { + listener.onCompleted() + } + ) + } - override fun clear() { - Log.d(TAG, "ImageCapture.ScreenFlash: clear") - screenFlashEvents.trySend( - CameraSystem.ScreenFlashEvent(CameraSystem.ScreenFlashEvent.Type.CLEAR_UI) {} - ) + override fun clear() { + Log.d(TAG, "ImageCapture.ScreenFlash: clear") + screenFlashEvents.trySend( + CameraSystem.ScreenFlashEvent(CameraSystem.ScreenFlashEvent.Type.CLEAR_UI) { + } + ) + } } } - } - imageCapture.flashMode = when (flashMode) { - FlashMode.OFF -> ImageCapture.FLASH_MODE_OFF // 2 + imageCapture.flashMode = when (flashMode) { + FlashMode.OFF -> ImageCapture.FLASH_MODE_OFF - FlashMode.ON -> if (isScreenFlashRequired) { - ImageCapture.FLASH_MODE_SCREEN // 3 - } else { - ImageCapture.FLASH_MODE_ON // 1 - } + // 2 - FlashMode.AUTO -> if (isScreenFlashRequired) { - ImageCapture.FLASH_MODE_SCREEN // 3 - } else { - ImageCapture.FLASH_MODE_AUTO // 0 - } + FlashMode.ON -> if (isScreenFlashRequired) { + ImageCapture.FLASH_MODE_SCREEN // 3 + } else { + ImageCapture.FLASH_MODE_ON // 1 + } + + FlashMode.AUTO -> if (isScreenFlashRequired) { + ImageCapture.FLASH_MODE_SCREEN // 3 + } else { + ImageCapture.FLASH_MODE_AUTO // 0 + } - FlashMode.LOW_LIGHT_BOOST -> ImageCapture.FLASH_MODE_OFF // 2 + FlashMode.LOW_LIGHT_BOOST -> ImageCapture.FLASH_MODE_OFF // 2 + } + Log.d(TAG, "Set flash mode to: ${imageCapture.flashMode}") } - Log.d(TAG, "Set flash mode to: ${imageCapture.flashMode}") } private fun getPendingRecording( @@ -999,8 +1037,8 @@ private fun getPendingRecording( } } -context(CameraSessionContext) @OptIn(ExperimentalPersistentRecording::class) +context(c: CameraSessionContext) private suspend fun startVideoRecordingInternal( isInitialAudioEnabled: Boolean, context: Context, @@ -1009,82 +1047,53 @@ private suspend fun startVideoRecordingInternal( initialRecordingSettings: InitialRecordingSettings, onVideoRecord: (OnVideoRecordEvent) -> Unit ): Recording { - // set the camerastate to starting - currentCameraState.update { old -> - old.copy(videoRecordingState = VideoRecordingState.Starting(initialRecordingSettings)) - } - - // ok. there is a difference between MUTING and ENABLING audio - // audio must be enabled in order to be muted - // if the video recording isn't started with audio enabled, you will not be able to un-mute it - // the toggle should only affect whether or not the audio is muted. - // the permission will determine whether or not the audio is enabled. - val isAudioGranted = checkSelfPermission( - context, - Manifest.permission.RECORD_AUDIO - ) == PackageManager.PERMISSION_GRANTED - - pendingRecord.apply { - if (isAudioGranted) { - withAudioEnabled(isInitialAudioEnabled) + with(c) { + // set the camerastate to starting + currentCameraState.update { old -> + old.copy(videoRecordingState = VideoRecordingState.Starting(initialRecordingSettings)) } - } - .asPersistentRecording() - - val callbackExecutor: Executor = - ( - currentCoroutineContext()[ContinuationInterceptor] as? - CoroutineDispatcher - )?.asExecutor() ?: ContextCompat.getMainExecutor(context) - return pendingRecord.start(callbackExecutor) { onVideoRecordEvent -> - Log.d(TAG, onVideoRecordEvent.toString()) - when (onVideoRecordEvent) { - is VideoRecordEvent.Start -> { - currentCameraState.update { old -> - old.copy( - videoRecordingState = VideoRecordingState.Active.Recording( - audioAmplitude = onVideoRecordEvent.recordingStats.audioStats - .audioAmplitude, - maxDurationMillis = maxDurationMillis, - elapsedTimeNanos = onVideoRecordEvent.recordingStats - .recordedDurationNanos - ) - ) - } - } - is VideoRecordEvent.Pause -> { - currentCameraState.update { old -> - old.copy( - videoRecordingState = VideoRecordingState.Active.Paused( - audioAmplitude = onVideoRecordEvent.recordingStats.audioStats - .audioAmplitude, - maxDurationMillis = maxDurationMillis, - elapsedTimeNanos = onVideoRecordEvent.recordingStats - .recordedDurationNanos - ) - ) - } + // ok. there is a difference between MUTING and ENABLING audio + // audio must be enabled in order to be muted + // if the video recording isn't started with audio enabled, you will not be able to un-mute it + // the toggle should only affect whether or not the audio is muted. + // the permission will determine whether or not the audio is enabled. + val isAudioGranted = checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) == PackageManager.PERMISSION_GRANTED + + pendingRecord.apply { + if (isAudioGranted) { + withAudioEnabled(isInitialAudioEnabled) } + } + .asPersistentRecording() - is VideoRecordEvent.Resume -> { - currentCameraState.update { old -> - old.copy( - videoRecordingState = VideoRecordingState.Active.Recording( - audioAmplitude = onVideoRecordEvent.recordingStats.audioStats - .audioAmplitude, - maxDurationMillis = maxDurationMillis, - elapsedTimeNanos = onVideoRecordEvent.recordingStats - .recordedDurationNanos + val callbackExecutor: Executor = + ( + currentCoroutineContext()[ContinuationInterceptor] as? + CoroutineDispatcher + )?.asExecutor() ?: ContextCompat.getMainExecutor(context) + return pendingRecord.start(callbackExecutor) { onVideoRecordEvent -> + Log.d(TAG, onVideoRecordEvent.toString()) + when (onVideoRecordEvent) { + is VideoRecordEvent.Start -> { + currentCameraState.update { old -> + old.copy( + videoRecordingState = VideoRecordingState.Active.Recording( + audioAmplitude = onVideoRecordEvent.recordingStats.audioStats + .audioAmplitude, + maxDurationMillis = maxDurationMillis, + elapsedTimeNanos = onVideoRecordEvent.recordingStats + .recordedDurationNanos + ) ) - ) + } } - } - is VideoRecordEvent.Status -> { - currentCameraState.update { old -> - // don't want to change state from paused to recording if status changes while paused - if (old.videoRecordingState is VideoRecordingState.Active.Paused) { + is VideoRecordEvent.Pause -> { + currentCameraState.update { old -> old.copy( videoRecordingState = VideoRecordingState.Active.Paused( audioAmplitude = onVideoRecordEvent.recordingStats.audioStats @@ -1094,7 +1103,11 @@ private suspend fun startVideoRecordingInternal( .recordedDurationNanos ) ) - } else { + } + } + + is VideoRecordEvent.Resume -> { + currentCameraState.update { old -> old.copy( videoRecordingState = VideoRecordingState.Active.Recording( audioAmplitude = onVideoRecordEvent.recordingStats.audioStats @@ -1106,71 +1119,98 @@ private suspend fun startVideoRecordingInternal( ) } } - } - is VideoRecordEvent.Finalize -> { - when (onVideoRecordEvent.error) { - ERROR_NONE -> { - // update recording state to inactive with the final values of the recording. - currentCameraState.update { old -> + is VideoRecordEvent.Status -> { + currentCameraState.update { old -> + // don't want to change state from paused to recording if status changes while paused + if (old.videoRecordingState is VideoRecordingState.Active.Paused) { old.copy( - videoRecordingState = VideoRecordingState.Inactive( - finalElapsedTimeNanos = onVideoRecordEvent.recordingStats + videoRecordingState = VideoRecordingState.Active.Paused( + audioAmplitude = onVideoRecordEvent.recordingStats.audioStats + .audioAmplitude, + maxDurationMillis = maxDurationMillis, + elapsedTimeNanos = onVideoRecordEvent.recordingStats .recordedDurationNanos ) ) - } - onVideoRecord( - OnVideoRecordEvent.OnVideoRecorded( - onVideoRecordEvent.outputResults.outputUri + } else { + old.copy( + videoRecordingState = VideoRecordingState.Active.Recording( + audioAmplitude = onVideoRecordEvent.recordingStats.audioStats + .audioAmplitude, + maxDurationMillis = maxDurationMillis, + elapsedTimeNanos = onVideoRecordEvent.recordingStats + .recordedDurationNanos + ) ) - ) + } } + } - ERROR_DURATION_LIMIT_REACHED -> { - currentCameraState.update { old -> - old.copy( - videoRecordingState = VideoRecordingState.Inactive( - finalElapsedTimeNanos = maxDurationMillis.milliseconds - .inWholeNanoseconds + is VideoRecordEvent.Finalize -> { + when (onVideoRecordEvent.error) { + ERROR_NONE -> { + // update recording state to inactive with the final values of the recording. + currentCameraState.update { old -> + old.copy( + videoRecordingState = VideoRecordingState.Inactive( + finalElapsedTimeNanos = onVideoRecordEvent.recordingStats + .recordedDurationNanos + ) + ) + } + onVideoRecord( + OnVideoRecordEvent.OnVideoRecorded( + onVideoRecordEvent.outputResults.outputUri ) ) } - onVideoRecord( - OnVideoRecordEvent.OnVideoRecorded( - onVideoRecordEvent.outputResults.outputUri - ) - ) - } + ERROR_DURATION_LIMIT_REACHED -> { + currentCameraState.update { old -> + old.copy( + videoRecordingState = VideoRecordingState.Inactive( + finalElapsedTimeNanos = maxDurationMillis.milliseconds + .inWholeNanoseconds + ) + ) + } - else -> { - onVideoRecord( - OnVideoRecordEvent.OnVideoRecordError( - RuntimeException( - "Recording finished with error: ${onVideoRecordEvent.error}", - onVideoRecordEvent.cause + onVideoRecord( + OnVideoRecordEvent.OnVideoRecorded( + onVideoRecordEvent.outputResults.outputUri ) ) - ) - currentCameraState.update { old -> - old.copy( - videoRecordingState = VideoRecordingState.Inactive( - finalElapsedTimeNanos = onVideoRecordEvent.recordingStats - .recordedDurationNanos + } + + else -> { + onVideoRecord( + OnVideoRecordEvent.OnVideoRecordError( + RuntimeException( + "Recording finished with error: ${onVideoRecordEvent.error}", + onVideoRecordEvent.cause + ) ) ) + currentCameraState.update { old -> + old.copy( + videoRecordingState = VideoRecordingState.Inactive( + finalElapsedTimeNanos = onVideoRecordEvent.recordingStats + .recordedDurationNanos + ) + ) + } } } } } + }.apply { + mute(!isInitialAudioEnabled) } - }.apply { - mute(!isInitialAudioEnabled) } } -context(CameraSessionContext) +context(c: CameraSessionContext) private suspend fun runVideoRecording( videoCapture: VideoCapture, captureTypeSuffix: String, @@ -1181,91 +1221,98 @@ private suspend fun runVideoRecording( videoControlEvents: Channel, onVideoRecord: (OnVideoRecordEvent) -> Unit, filePathGenerator: FilePathGenerator -) = coroutineScope { - var currentSettings = transientSettings.filterNotNull().first() - - getPendingRecording( - context, - videoCapture, - maxDurationMillis, - filePathGenerator, - captureTypeSuffix, - saveLocation, - onVideoRecord - )?.let { - startVideoRecordingInternal( - isInitialAudioEnabled = currentSettings.isAudioEnabled, - context = context, - pendingRecord = it, - maxDurationMillis = maxDurationMillis, - onVideoRecord = onVideoRecord, - initialRecordingSettings = InitialRecordingSettings( - isAudioEnabled = currentSettings.isAudioEnabled, - lensFacing = currentSettings.primaryLensFacing, - zoomRatios = currentSettings.zoomRatios - ) - ).use { recording -> - val recordingSettingsUpdater = launch { - fun TransientSessionSettings.isFlashModeOn() = flashMode == FlashMode.ON - - transientSettings.filterNotNull() - .collectLatest { newTransientSettings -> - if (currentSettings.isAudioEnabled != newTransientSettings.isAudioEnabled) { - recording.mute(newTransientSettings.isAudioEnabled) +) = with(c) { + coroutineScope { + var currentSettings = transientSettings.filterNotNull().first() + + getPendingRecording( + context, + videoCapture, + maxDurationMillis, + filePathGenerator, + captureTypeSuffix, + saveLocation, + onVideoRecord + )?.let { + startVideoRecordingInternal( + isInitialAudioEnabled = currentSettings.isAudioEnabled, + context = context, + pendingRecord = it, + maxDurationMillis = maxDurationMillis, + onVideoRecord = onVideoRecord, + initialRecordingSettings = InitialRecordingSettings( + isAudioEnabled = currentSettings.isAudioEnabled, + lensFacing = currentSettings.primaryLensFacing, + zoomRatios = currentSettings.zoomRatios + ) + ).use { recording -> + val recordingSettingsUpdater = launch { + fun TransientSessionSettings.isFlashModeOn() = flashMode == FlashMode.ON + + transientSettings.filterNotNull() + .collectLatest { newTransientSettings -> + if (currentSettings.isAudioEnabled != + newTransientSettings.isAudioEnabled + ) { + recording.mute(newTransientSettings.isAudioEnabled) + } + if (currentSettings.isFlashModeOn() != + newTransientSettings.isFlashModeOn() + ) { + currentSettings = newTransientSettings + } } - if (currentSettings.isFlashModeOn() != - newTransientSettings.isFlashModeOn() - ) { - currentSettings = newTransientSettings + } + + for (event in videoControlEvents) { + when (event) { + is VideoCaptureControlEvent.StartRecordingEvent -> + throw IllegalStateException("A recording is already in progress") + + VideoCaptureControlEvent.StopRecordingEvent -> { + recordingSettingsUpdater.cancel() + break } - } - } - for (event in videoControlEvents) { - when (event) { - is VideoCaptureControlEvent.StartRecordingEvent -> - throw IllegalStateException("A recording is already in progress") + VideoCaptureControlEvent.PauseRecordingEvent -> recording.pause() - VideoCaptureControlEvent.StopRecordingEvent -> { - recordingSettingsUpdater.cancel() - break + VideoCaptureControlEvent.ResumeRecordingEvent -> recording.resume() } - - VideoCaptureControlEvent.PauseRecordingEvent -> recording.pause() - VideoCaptureControlEvent.ResumeRecordingEvent -> recording.resume() } } } } } -context(CameraSessionContext) +context(c: CameraSessionContext) internal suspend fun processVideoControlEvents( videoCapture: VideoCapture?, captureTypeSuffix: String -) = coroutineScope { - for (event in videoCaptureControlEvents) { - when (event) { - is VideoCaptureControlEvent.StartRecordingEvent -> { - if (videoCapture == null) { - throw RuntimeException( - "Attempted video recording with null videoCapture" +) = with(c) { + coroutineScope { + for (event in videoCaptureControlEvents) { + when (event) { + is VideoCaptureControlEvent.StartRecordingEvent -> { + if (videoCapture == null) { + throw RuntimeException( + "Attempted video recording with null videoCapture" + ) + } + runVideoRecording( + videoCapture, + captureTypeSuffix, + context, + event.maxVideoDuration, + transientSettings, + event.saveLocation, + videoCaptureControlEvents, + event.onVideoRecord, + filePathGenerator ) } - runVideoRecording( - videoCapture, - captureTypeSuffix, - context, - event.maxVideoDuration, - transientSettings, - event.saveLocation, - videoCaptureControlEvents, - event.onVideoRecord, - filePathGenerator - ) - } - else -> {} + else -> {} + } } } } @@ -1273,129 +1320,136 @@ internal suspend fun processVideoControlEvents( /** * Applies a CaptureCallback to the provided image capture builder */ -context(CameraSessionContext) @OptIn(ExperimentalCamera2Interop::class) +context(c: CameraSessionContext) private fun Preview.Builder.updateCameraStateWithCaptureResults( targetCameraInfo: CameraInfo, captureResults: MutableStateFlow? = null ): Preview.Builder { - val isFirstFrameTimestampUpdated = atomic(false) - val targetCameraLogicalId = Camera2CameraInfo.from(targetCameraInfo).cameraId - Camera2Interop.Extender(this).setSessionCaptureCallback( - object : CameraCaptureSession.CaptureCallback() { - override fun onCaptureCompleted( - session: CameraCaptureSession, - request: CaptureRequest, - result: TotalCaptureResult - ) { - super.onCaptureCompleted(session, request, result) + with(c) { + val isFirstFrameTimestampUpdated = atomic(false) + val targetCameraLogicalId = Camera2CameraInfo.from(targetCameraInfo).cameraId + Camera2Interop.Extender(this@updateCameraStateWithCaptureResults).setSessionCaptureCallback( + object : CameraCaptureSession.CaptureCallback() { + override fun onCaptureCompleted( + session: CameraCaptureSession, + request: CaptureRequest, + result: TotalCaptureResult + ) { + super.onCaptureCompleted(session, request, result) - captureResults?.update { result } + captureResults?.update { result } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && - result.get(CaptureResult.CONTROL_AE_MODE) == - CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY - ) { - val nativeBoostState = result.get(CaptureResult.CONTROL_LOW_LIGHT_BOOST_STATE) - val boostStrength = when (nativeBoostState) { - CameraMetadata.CONTROL_LOW_LIGHT_BOOST_STATE_ACTIVE -> - LowLightBoostState.Active(LowLightBoostState.MAXIMUM_STRENGTH) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && + result.get(CaptureResult.CONTROL_AE_MODE) == + CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY + ) { + val nativeBoostState = + result.get(CaptureResult.CONTROL_LOW_LIGHT_BOOST_STATE) + val boostStrength = when (nativeBoostState) { + CameraMetadata.CONTROL_LOW_LIGHT_BOOST_STATE_ACTIVE -> + LowLightBoostState.Active(LowLightBoostState.MAXIMUM_STRENGTH) - else -> LowLightBoostState.Inactive - } - currentCameraState.update { old -> - if (old.lowLightBoostState != boostStrength) { - old.copy(lowLightBoostState = boostStrength) - } else { - old + else -> LowLightBoostState.Inactive + } + currentCameraState.update { old -> + if (old.lowLightBoostState != boostStrength) { + old.copy(lowLightBoostState = boostStrength) + } else { + old + } } } - } - val logicalCameraId = session.device.id + val logicalCameraId = session.device.id - // todo(b/405987189): remove completely after buggy zoomState is fixed - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && - logicalCameraId == targetCameraLogicalId - ) { - // update camerastate with zoom ratio - val newZoomRatio = result.get(CaptureResult.CONTROL_ZOOM_RATIO) - currentCameraState.update { old -> - if (newZoomRatio != null && - old.zoomRatios[targetCameraInfo.appLensFacing] != newZoomRatio - ) { - old.copy( - zoomRatios = old.zoomRatios - .toMutableMap() - .apply { - put(targetCameraInfo.appLensFacing, newZoomRatio) - }.toMap() - ) - } else { - old + // todo(b/405987189): remove completely after buggy zoomState is fixed + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + logicalCameraId == targetCameraLogicalId + ) { + // update camerastate with zoom ratio + val newZoomRatio = result.get(CaptureResult.CONTROL_ZOOM_RATIO) + currentCameraState.update { old -> + if (newZoomRatio != null && + old.zoomRatios[targetCameraInfo.appLensFacing] != newZoomRatio + ) { + old.copy( + zoomRatios = old.zoomRatios + .toMutableMap() + .apply { + put(targetCameraInfo.appLensFacing, newZoomRatio) + }.toMap() + ) + } else { + old + } } } - } - if (logicalCameraId != targetCameraLogicalId) return - try { - val physicalCameraId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - result.get(CaptureResult.LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID) - } else { - null - } - currentCameraState.update { old -> - if (old.debugInfo.logicalCameraId != logicalCameraId || - old.debugInfo.physicalCameraId != physicalCameraId - ) { - old.copy( - debugInfo = DebugInfo(logicalCameraId, physicalCameraId) - ) + if (logicalCameraId != targetCameraLogicalId) return + try { + val physicalCameraId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + result.get(CaptureResult.LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID) } else { - old + null } - } - if (!isFirstFrameTimestampUpdated.value) { currentCameraState.update { old -> - old.copy( - sessionFirstFrameTimestamp = SystemClock.elapsedRealtimeNanos() - ) + if (old.debugInfo.logicalCameraId != logicalCameraId || + old.debugInfo.physicalCameraId != physicalCameraId + ) { + old.copy( + debugInfo = DebugInfo(logicalCameraId, physicalCameraId) + ) + } else { + old + } + } + if (!isFirstFrameTimestampUpdated.value) { + currentCameraState.update { old -> + old.copy( + sessionFirstFrameTimestamp = SystemClock.elapsedRealtimeNanos() + ) + } + isFirstFrameTimestampUpdated.value = true } - isFirstFrameTimestampUpdated.value = true + // Publish stabilization state + publishStabilizationMode(result) + } catch (_: Exception) { } - // Publish stabilization state - publishStabilizationMode(result) - } catch (_: Exception) { } } - } - ) - return this + ) + return this@updateCameraStateWithCaptureResults + } } -context(CameraSessionContext) +context(c: CameraSessionContext) private fun publishStabilizationMode(result: TotalCaptureResult) { - val nativeVideoStabilizationMode = result.get(CaptureResult.CONTROL_VIDEO_STABILIZATION_MODE) - val stabilizationMode = when (nativeVideoStabilizationMode) { - CaptureResult.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION -> - StabilizationMode.ON + with(c) { + val nativeVideoStabilizationMode = + result.get(CaptureResult.CONTROL_VIDEO_STABILIZATION_MODE) + val stabilizationMode = when (nativeVideoStabilizationMode) { + CaptureResult.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION -> + StabilizationMode.ON - CaptureResult.CONTROL_VIDEO_STABILIZATION_MODE_ON -> StabilizationMode.HIGH_QUALITY - else -> { - result.get(CaptureResult.LENS_OPTICAL_STABILIZATION_MODE)?.let { - if (it == CaptureResult.LENS_OPTICAL_STABILIZATION_MODE_ON) { - StabilizationMode.OPTICAL - } else { - StabilizationMode.OFF - } - } ?: StabilizationMode.OFF + CaptureResult.CONTROL_VIDEO_STABILIZATION_MODE_ON -> StabilizationMode.HIGH_QUALITY + + else -> { + result.get(CaptureResult.LENS_OPTICAL_STABILIZATION_MODE)?.let { + if (it == CaptureResult.LENS_OPTICAL_STABILIZATION_MODE_ON) { + StabilizationMode.OPTICAL + } else { + StabilizationMode.OFF + } + } ?: StabilizationMode.OFF + } } - } - currentCameraState.update { old -> - if (old.stabilizationMode != stabilizationMode) { - old.copy(stabilizationMode = stabilizationMode) - } else { - old + currentCameraState.update { old -> + if (old.stabilizationMode != stabilizationMode) { + old.copy(stabilizationMode = stabilizationMode) + } else { + old + } } } } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index f0e3e5ae9..86b93449a 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -40,161 +40,164 @@ import kotlinx.coroutines.launch private const val TAG = "ConcurrentCameraSession" -context(CameraSessionContext) @SuppressLint("RestrictedApi") +context(c: CameraSessionContext) internal suspend fun runConcurrentCameraSession( sessionSettings: PerpetualSessionSettings.ConcurrentCamera, cameraConstraints: CameraConstraints? -) = coroutineScope { - val primaryLensFacing = sessionSettings.primaryCameraInfo.appLensFacing - val secondaryLensFacing = sessionSettings.secondaryCameraInfo.appLensFacing - Log.d( - TAG, - "Starting new concurrent camera session " + - "[primary: $primaryLensFacing, secondary: $secondaryLensFacing]" - ) - - val initialTransientSettings = transientSettings - .filterNotNull() - .first() - - val videoCapture = if (sessionSettings.captureMode != CaptureMode.IMAGE_ONLY) { - createVideoUseCase( - cameraProvider.getCameraInfo( - initialTransientSettings.primaryLensFacing.toCameraSelector() - ), - sessionSettings.aspectRatio, - TARGET_FPS_AUTO, - StabilizationMode.OFF, - DynamicRange.SDR, - VideoQuality.UNSPECIFIED, - backgroundDispatcher +) = with(c) { + coroutineScope { + val primaryLensFacing = sessionSettings.primaryCameraInfo.appLensFacing + val secondaryLensFacing = sessionSettings.secondaryCameraInfo.appLensFacing + Log.d( + TAG, + "Starting new concurrent camera session " + + "[primary: $primaryLensFacing, secondary: $secondaryLensFacing]" ) - } else { - null - } - val useCaseGroup = createUseCaseGroup( - cameraInfo = sessionSettings.primaryCameraInfo, - initialTransientSettings = initialTransientSettings, - stabilizationMode = StabilizationMode.OFF, - aspectRatio = sessionSettings.aspectRatio, - imageFormat = ImageOutputFormat.JPEG, - captureMode = sessionSettings.captureMode, - videoCaptureUseCase = videoCapture - ) + val initialTransientSettings = transientSettings + .filterNotNull() + .first() - val cameraConfigs = listOf( - Pair( - sessionSettings.primaryCameraInfo.cameraSelector, - CompositionSettings.Builder() - .setAlpha(1.0f) - .setOffset(0.0f, 0.0f) - .setScale(1.0f, 1.0f) - .build() - ), - Pair( - sessionSettings.secondaryCameraInfo.cameraSelector, - CompositionSettings.Builder() - .setAlpha(1.0f) - .setOffset(2 / 3f - 0.1f, -2 / 3f + 0.1f) - .setScale(1 / 3f, 1 / 3f) - .build() - ) - ) - - cameraProvider.runWithConcurrent(cameraConfigs, useCaseGroup) { concurrentCamera -> - Log.d(TAG, "Concurrent camera session started") - // todo: concurrent camera only ever lists one camera - val primaryCamera = concurrentCamera.cameras.first { - it.cameraInfo.appLensFacing == sessionSettings.primaryCameraInfo.appLensFacing - } - - launch { - processFocusMeteringEvents( - primaryCamera.cameraInfo, - primaryCamera.cameraControl + val videoCapture = if (sessionSettings.captureMode != CaptureMode.IMAGE_ONLY) { + createVideoUseCase( + cameraProvider.getCameraInfo( + initialTransientSettings.primaryLensFacing.toCameraSelector() + ), + sessionSettings.aspectRatio, + TARGET_FPS_AUTO, + StabilizationMode.OFF, + DynamicRange.SDR, + VideoQuality.UNSPECIFIED, + backgroundDispatcher ) + } else { + null } - launch { - processVideoControlEvents( - useCaseGroup.getVideoCapture(), - captureTypeSuffix = "DualCam" + val useCaseGroup = createUseCaseGroup( + cameraInfo = sessionSettings.primaryCameraInfo, + initialTransientSettings = initialTransientSettings, + stabilizationMode = StabilizationMode.OFF, + aspectRatio = sessionSettings.aspectRatio, + imageFormat = ImageOutputFormat.JPEG, + captureMode = sessionSettings.captureMode, + videoCaptureUseCase = videoCapture + ) + + val cameraConfigs = listOf( + Pair( + sessionSettings.primaryCameraInfo.cameraSelector, + CompositionSettings.Builder() + .setAlpha(1.0f) + .setOffset(0.0f, 0.0f) + .setScale(1.0f, 1.0f) + .build() + ), + Pair( + sessionSettings.secondaryCameraInfo.cameraSelector, + CompositionSettings.Builder() + .setAlpha(1.0f) + .setOffset(2 / 3f - 0.1f, -2 / 3f + 0.1f) + .setScale(1 / 3f, 1 / 3f) + .build() ) - } + ) - launch { - sessionSettings.primaryCameraInfo.torchState.asFlow().collectLatest { torchState -> - currentCameraState.update { old -> - old.copy(isTorchEnabled = torchState == TorchState.ON) - } + cameraProvider.runWithConcurrent(cameraConfigs, useCaseGroup) { concurrentCamera -> + Log.d(TAG, "Concurrent camera session started") + // todo: concurrent camera only ever lists one camera + val primaryCamera = concurrentCamera.cameras.first { + it.cameraInfo.appLensFacing == sessionSettings.primaryCameraInfo.appLensFacing + } + + launch { + processFocusMeteringEvents( + primaryCamera.cameraInfo, + primaryCamera.cameraControl + ) } - } - // Update CameraState to reflect when camera is running - launch { - primaryCamera.cameraInfo.cameraState - .asFlow() - .filterNotNull() - .distinctUntilChanged() - .onCompletion { + launch { + processVideoControlEvents( + useCaseGroup.getVideoCapture(), + captureTypeSuffix = "DualCam" + ) + } + + launch { + sessionSettings.primaryCameraInfo.torchState.asFlow().collectLatest { torchState -> currentCameraState.update { old -> - old.copy( - isCameraRunning = false - ) + old.copy(isTorchEnabled = torchState == TorchState.ON) } } - .collectLatest { cameraState -> - currentCameraState.update { old -> - old.copy( - isCameraRunning = cameraState.type == CXCameraState.Type.OPEN - ) + } + + // Update CameraState to reflect when camera is running + launch { + primaryCamera.cameraInfo.cameraState + .asFlow() + .filterNotNull() + .distinctUntilChanged() + .onCompletion { + currentCameraState.update { old -> + old.copy( + isCameraRunning = false + ) + } } - } - } + .collectLatest { cameraState -> + currentCameraState.update { old -> + old.copy( + isCameraRunning = cameraState.type == CXCameraState.Type.OPEN + ) + } + } + } - // update cameraState to mirror the current zoomState - launch { - primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().distinctUntilChanged() - .collectLatest { zoomState -> - val settings = transientSettings.value - // TODO(b/405987189): remove checks after buggy zoomState is fixed - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - if (zoomState.zoomRatio != 1.0f || - settings == null || - zoomState.zoomRatio == - settings.zoomRatios[primaryCamera.cameraInfo.appLensFacing] - ) { - currentCameraState.update { old -> - old.copy( - zoomRatios = old.zoomRatios.toMutableMap().apply { - put( - primaryCamera.cameraInfo.appLensFacing, - zoomState.zoomRatio - ) - }.toMap(), - linearZoomScales = old.linearZoomScales.toMutableMap().apply { - put( - primaryCamera.cameraInfo.appLensFacing, - zoomState.linearZoom - ) - }.toMap() - ) + // update cameraState to mirror the current zoomState + launch { + primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().distinctUntilChanged() + .collectLatest { zoomState -> + val settings = transientSettings.value + // TODO(b/405987189): remove checks after buggy zoomState is fixed + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (zoomState.zoomRatio != 1.0f || + settings == null || + zoomState.zoomRatio == + settings.zoomRatios[primaryCamera.cameraInfo.appLensFacing] + ) { + currentCameraState.update { old -> + old.copy( + zoomRatios = old.zoomRatios.toMutableMap().apply { + put( + primaryCamera.cameraInfo.appLensFacing, + zoomState.zoomRatio + ) + }.toMap(), + linearZoomScales = old.linearZoomScales.toMutableMap() + .apply { + put( + primaryCamera.cameraInfo.appLensFacing, + zoomState.linearZoom + ) + }.toMap() + ) + } } } } - } - } + } - applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup) - processTransientSettingEvents( - primaryCamera, - cameraConstraints, - useCaseGroup, - initialTransientSettings, - transientSettings, - null - ) + applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup) + processTransientSettingEvents( + primaryCamera, + cameraConstraints, + useCaseGroup, + initialTransientSettings, + transientSettings, + null + ) + } } } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index e57a6f450..bd8e9c6c9 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts index 8b5cab4ee..8b2579e1d 100644 --- a/gradle/init.gradle.kts +++ b/gradle/init.gradle.kts @@ -14,7 +14,7 @@ * limitations under the License. */ -val ktlintVersion = "1.2.1" +val ktlintVersion = "1.8.0" initscript { val spotlessVersion = "6.25.0" @@ -32,11 +32,12 @@ rootProject { subprojects { apply() extensions.configure { - // limit format enforcement to just the files changed by this feature branch ratchetFrom("origin/main") kotlin { target("**/*.kt") - targetExclude("**/build/**/*.kt") + targetExclude( + "**/build/**/*.kt" + ) ktlint(ktlintVersion) .setEditorConfigPath(rootProject.file(".editorconfig")) licenseHeaderFile(rootProject.file("spotless/copyright.kt")) diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index cc2306000..3294d9cca 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 837f3861b..816274fe9 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index 836d755b3..7be91fbb4 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 18e226ba7..1338fe53a 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index 3f5fffafb..489353eda 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index 14ccd3306..0806be6d7 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -46,8 +46,8 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index ae6489ddd..7a6a65451 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index a598f51fb..0cf037598 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -47,7 +47,7 @@ android { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.add("-Xcontext-parameters") } } buildFeatures { From 879518c8e60ddca1a69b9cac524576674d27caf2 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 09:52:20 -0700 Subject: [PATCH 05/13] upgrade kotlin to 2.4, hilt to 2.60.1, and removes experimental context parameter flags --- core/camera/build.gradle.kts | 3 --- core/camera/effects/single-stream/build.gradle.kts | 3 --- feature/postcapture/build.gradle.kts | 3 --- gradle/libs.versions.toml | 4 ++-- ui/components/build.gradle.kts | 3 --- ui/components/capture/build.gradle.kts | 3 --- ui/controller/build.gradle.kts | 3 --- ui/controller/impl/build.gradle.kts | 3 --- ui/uistate/build.gradle.kts | 3 --- ui/uistate/capture/build.gradle.kts | 3 --- ui/uistateadapter/build.gradle.kts | 3 --- ui/uistateadapter/capture/build.gradle.kts | 3 --- 12 files changed, 2 insertions(+), 35 deletions(-) diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index e39c6c137..eaa789c39 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -92,9 +92,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } } diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index e09440860..64c40462a 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -34,9 +34,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index bd8e9c6c9..02c89c359 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 04676d579..11e0994f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ composeBom = "2025.10.01" accompanist = "0.37.3" # kotlinPlugin and composeCompiler are linked # See https://developer.android.com/jetpack/androidx/releases/compose-kotlin -kotlinPlugin = "2.2.0" +kotlinPlugin = "2.4.10" androidGradlePlugin = "9.3.1" @@ -43,7 +43,7 @@ composeMaterial = "1.5.0-alpha07" cmake = "3.22.1" kotlinxAtomicfu = "0.29.0" kotlinxCoroutines = "1.10.2" -hilt = "2.59.2" +hilt = "2.60.1" junit = "4.13.2" robolectric = "4.15.1" diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index 3294d9cca..05de2cee8 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 816274fe9..8f933f597 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index 7be91fbb4..dd536be2a 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 1338fe53a..0362f83a8 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index 489353eda..4eb388c5e 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index 0806be6d7..463791bb6 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index 7a6a65451..88bef4268 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index 0cf037598..1222a5e28 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-parameters") - } } buildFeatures { buildConfig = true From 338d7c3f0b594bf828b99f86c62cfc76808b8659 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 06:04:47 -0700 Subject: [PATCH 06/13] Upgrade build infrastructure (AGP 9.3.1, Gradle 9.6.1) Upgrades the project's build system: - Upgraded Android Gradle Plugin to 9.3.1 and Gradle Wrapper to 9.6.1. - Migrated all modules from 'kotlin.android' to AGP's built-in Kotlin support. - Replaced 'kotlin.kapt' with 'legacy-kapt' for compatibility with built-in Kotlin. - Upgraded Hilt to 2.59.2 for AGP 9 compatibility. - Updated compiler flags to pass -Xcontext-receivers under the new built-in Kotlin configuration block in 11 modules. - Updated README to reflect Android Studio Quail / AGP 9 requirements. - Enabled parallel Gradle sync in gradle.properties. --- README.md | 4 ++-- app/build.gradle.kts | 3 +-- benchmark/build.gradle.kts | 16 +++++++++------- build.gradle.kts | 3 +-- core/camera/build.gradle.kts | 8 +++----- .../effects/single-stream/build.gradle.kts | 11 ++++++----- .../low-light-playservices/build.gradle.kts | 3 +-- core/camera/low-light/build.gradle.kts | 3 +-- core/camera/postprocess/build.gradle.kts | 3 +-- core/camera/testing/build.gradle.kts | 1 - core/common/build.gradle.kts | 3 +-- core/common/testing/build.gradle.kts | 1 - core/model/build.gradle.kts | 3 +-- core/settings/build.gradle.kts | 3 +-- core/settings/datastore-prefs/build.gradle.kts | 1 - .../datastore-prefs/testing/build.gradle.kts | 1 - data/camera/build.gradle.kts | 6 ++---- data/camera/consumer-rules.pro | 2 ++ data/media/build.gradle.kts | 3 +-- data/media/testing/build.gradle.kts | 1 - data/settings/build.gradle.kts | 3 +-- data/settings/testing/build.gradle.kts | 1 - feature/permissions/build.gradle.kts | 3 +-- feature/postcapture/build.gradle.kts | 13 +++++-------- feature/preview/build.gradle.kts | 3 +-- feature/settings/build.gradle.kts | 3 +-- gradle.properties | 5 +++-- gradle/libs.versions.toml | 5 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 1 - ui/components/build.gradle.kts | 10 ++++------ ui/components/capture/build.gradle.kts | 12 ++++++------ ui/controller/build.gradle.kts | 12 ++++++------ ui/controller/impl/build.gradle.kts | 12 ++++++------ ui/controller/testing/build.gradle.kts | 1 - ui/debug/build.gradle.kts | 3 +-- ui/debug/testing/build.gradle.kts | 1 - ui/uistate/build.gradle.kts | 12 ++++++------ ui/uistate/capture/build.gradle.kts | 12 ++++++------ ui/uistate/postcapture/build.gradle.kts | 5 ++--- ui/uistateadapter/build.gradle.kts | 12 ++++++------ ui/uistateadapter/capture/build.gradle.kts | 12 ++++++------ ui/uistateadapter/postcapture/build.gradle.kts | 5 ++--- 43 files changed, 100 insertions(+), 127 deletions(-) create mode 100644 data/camera/consumer-rules.pro diff --git a/README.md b/README.md index a733c15c3..b13307a74 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ OEMs looking to validate their camera feature implementations. This project uses the gradle build system, and can be imported directly into Android Studio. -Currently, Jetpack Camera App is built using the Android Gradle Plugin 8.10.0, which is only -compatible with Android Studio Meerkat or newer. +Currently, Jetpack Camera App is built using the Android Gradle Plugin 9.3.1, which is only +compatible with Android Studio Quail or newer. ## Pre-push Hook (Recommended) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ad359692b..7246eef3c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index a1708658f..00c54f097 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.test) - alias(libs.plugins.kotlin.android) } android { @@ -24,12 +24,8 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } defaultConfig { @@ -69,6 +65,12 @@ android { experimentalProperties["android.experimental.self-instrumenting"] = true } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(libs.androidx.junit) implementation(libs.androidx.benchmark.macro.junit4) diff --git a/build.gradle.kts b/build.gradle.kts index 64fdc6b56..53a8212e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,9 +19,8 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.android.test) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.dagger.hilt.android) apply false - alias(libs.plugins.kotlin.kapt) apply false + alias(libs.plugins.android.legacy.kapt) apply false alias(libs.plugins.compose.compiler) apply false } diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index b87fb79ee..975955fd0 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -93,10 +92,9 @@ android { } kotlin { jvmToolchain(17) - } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } } diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index dacbf112c..ee42831de 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } @@ -34,10 +33,12 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) - } - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } } diff --git a/core/camera/low-light-playservices/build.gradle.kts b/core/camera/low-light-playservices/build.gradle.kts index 639fa3e51..5c6aab589 100644 --- a/core/camera/low-light-playservices/build.gradle.kts +++ b/core/camera/low-light-playservices/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/low-light/build.gradle.kts b/core/camera/low-light/build.gradle.kts index cade65c9e..9377565e4 100644 --- a/core/camera/low-light/build.gradle.kts +++ b/core/camera/low-light/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/postprocess/build.gradle.kts b/core/camera/postprocess/build.gradle.kts index ceedef0b7..67157f0be 100644 --- a/core/camera/postprocess/build.gradle.kts +++ b/core/camera/postprocess/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts index 38f70711f..0768c256e 100644 --- a/core/camera/testing/build.gradle.kts +++ b/core/camera/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 185217b27..5886b309b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/common/testing/build.gradle.kts b/core/common/testing/build.gradle.kts index 57c300b0e..6d21ce391 100644 --- a/core/common/testing/build.gradle.kts +++ b/core/common/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index d352566c4..f9e6a46d8 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) } android { diff --git a/core/settings/build.gradle.kts b/core/settings/build.gradle.kts index 35c256a56..021d3d4d8 100644 --- a/core/settings/build.gradle.kts +++ b/core/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/settings/datastore-prefs/build.gradle.kts b/core/settings/datastore-prefs/build.gradle.kts index c0ca9f8d4..107acd946 100644 --- a/core/settings/datastore-prefs/build.gradle.kts +++ b/core/settings/datastore-prefs/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/settings/datastore-prefs/testing/build.gradle.kts b/core/settings/datastore-prefs/testing/build.gradle.kts index 0e619ddbd..d6b25c0f3 100644 --- a/core/settings/datastore-prefs/testing/build.gradle.kts +++ b/core/settings/datastore-prefs/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/data/camera/build.gradle.kts b/data/camera/build.gradle.kts index 8a541423a..39c75b667 100644 --- a/data/camera/build.gradle.kts +++ b/data/camera/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } @@ -37,8 +36,7 @@ android { buildTypes { release { proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt") ) } create("benchmark") { diff --git a/data/camera/consumer-rules.pro b/data/camera/consumer-rules.pro new file mode 100644 index 000000000..c1a8cebff --- /dev/null +++ b/data/camera/consumer-rules.pro @@ -0,0 +1,2 @@ +# Add project specific consumer ProGuard rules here. +# More info: https://developer.android.com/studio/build/shrink-code diff --git a/data/media/build.gradle.kts b/data/media/build.gradle.kts index 91146f6e8..223daced0 100644 --- a/data/media/build.gradle.kts +++ b/data/media/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/data/media/testing/build.gradle.kts b/data/media/testing/build.gradle.kts index f05656de1..2c531124c 100644 --- a/data/media/testing/build.gradle.kts +++ b/data/media/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 7748c8d51..b7037b199 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index 14e7b1e30..a3ff5b281 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/feature/permissions/build.gradle.kts b/feature/permissions/build.gradle.kts index 6c977afeb..e101d4892 100644 --- a/feature/permissions/build.gradle.kts +++ b/feature/permissions/build.gradle.kts @@ -15,10 +15,9 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.compose.compiler) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index b1faaa055..d0754c11e 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } @@ -48,6 +47,9 @@ android { } kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true @@ -73,12 +75,7 @@ android { } } } - } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } -} + }} dependencies { // Compose diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index 54ab1c505..37e210f8e 100644 --- a/feature/preview/build.gradle.kts +++ b/feature/preview/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index d5c246d1f..10dadeda4 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/gradle.properties b/gradle.properties index 15ab33aba..23a6f2b56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,9 +39,10 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.nonFinalResIds=false # Properties to make gradle managed devices testing more stable (see https://issuetracker.google.com/287312019#comment41) android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 # Ensure we can run managed devices on servers that don't support hardware rendering -android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect \ No newline at end of file +android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2317d598..04676d579 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ accompanist = "0.37.3" # kotlinPlugin and composeCompiler are linked # See https://developer.android.com/jetpack/androidx/releases/compose-kotlin kotlinPlugin = "2.2.0" -androidGradlePlugin = "8.10.1" +androidGradlePlugin = "9.3.1" androidxActivityCompose = "1.10.1" @@ -43,7 +43,7 @@ composeMaterial = "1.5.0-alpha07" cmake = "3.22.1" kotlinxAtomicfu = "0.29.0" kotlinxCoroutines = "1.10.2" -hilt = "2.57" +hilt = "2.59.2" junit = "4.13.2" robolectric = "4.15.1" @@ -124,3 +124,4 @@ dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hi kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinPlugin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlinPlugin" } +android-legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "androidGradlePlugin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b965ecfd5..c72c372a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Mar 12 23:44:57 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index dfb9e6557..f1197bb76 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,7 +57,6 @@ include(":ui:uistate:capture") include(":ui:uistateadapter:capture") include(":ui:components") include(":ui:components:capture") -include(":data:model") include(":core:settings") include(":core:model") include(":ui:uistate:postcapture") diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index 02d3b2dca..b152876c8 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -48,15 +47,14 @@ android { } kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index d2f9fc5fa..0766452c4 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index 2690d28bb..aad8888a3 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 062e38746..66172ec61 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/testing/build.gradle.kts b/ui/controller/testing/build.gradle.kts index 402ca3715..f239cd132 100644 --- a/ui/controller/testing/build.gradle.kts +++ b/ui/controller/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/ui/debug/build.gradle.kts b/ui/debug/build.gradle.kts index d62d15e16..4406021e2 100644 --- a/ui/debug/build.gradle.kts +++ b/ui/debug/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } diff --git a/ui/debug/testing/build.gradle.kts b/ui/debug/testing/build.gradle.kts index 4abe6a083..f05e7f6bf 100644 --- a/ui/debug/testing/build.gradle.kts +++ b/ui/debug/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index e06c9e4c7..5ceda5ab4 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index 345628e27..b32a0c393 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistate/postcapture/build.gradle.kts b/ui/uistate/postcapture/build.gradle.kts index 4fda15af5..cd5412fb3 100644 --- a/ui/uistate/postcapture/build.gradle.kts +++ b/ui/uistate/postcapture/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -42,8 +41,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index 2826a1033..9fe67b430 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index fa52725fe..46ec7008a 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistateadapter/postcapture/build.gradle.kts b/ui/uistateadapter/postcapture/build.gradle.kts index 889e3a51c..8659e8e7a 100644 --- a/ui/uistateadapter/postcapture/build.gradle.kts +++ b/ui/uistateadapter/postcapture/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -42,8 +41,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } From 6b8535055b025c3af87a3d575084e6dc01a5984a Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 07:58:15 -0700 Subject: [PATCH 07/13] Fix Gradle 9 test task failures by removing empty test suites Deletes the unused and duplicated test helper classes in :ui:components:capture. Gradle 9 fails the build if a module contains test sources but no actual @Test classes to execute, which was the case here since it only contained test utilities. --- .../capture/rules/MainDispatcherRule.kt | 39 --- .../workaround/ComposableCaptureToImage.kt | 257 ------------------ 2 files changed, 296 deletions(-) delete mode 100644 ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt delete mode 100644 ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt deleted file mode 100644 index 287fb9fff..000000000 --- a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 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 com.google.jetpackcamera.ui.components.capture.capture.rules - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -class MainDispatcherRule(private val dispatcher: CoroutineDispatcher) : TestRule { - @OptIn(ExperimentalCoroutinesApi::class) - override fun apply(base: Statement?, description: Description?) = object : Statement() { - override fun evaluate() { - Dispatchers.setMain(dispatcher) - try { - base!!.evaluate() - } finally { - Dispatchers.resetMain() - } - } - } -} diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt deleted file mode 100644 index 9805bcde9..000000000 --- a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 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. - */ -@file:SuppressLint("UseSdkSuppress") - -package com.google.jetpackcamera.ui.components.capture.capture.workaround - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper -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.View -import android.view.Window -import androidx.annotation.DoNotInline -import androidx.annotation.RequiresApi -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.ViewRootForTest -import androidx.compose.ui.semantics.SemanticsNode -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.window.DialogWindowProvider -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.platform.graphics.HardwareRendererCompat -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.math.roundToInt - -/** - * Workaround captureToImage method. - * - * Once composable + robolectric graphics bugs are fixed, this can be replaced with the actual - * [SemanticsNodeInteraction.captureToImage]. Alternative is to use - * instrumentations tests, but they are not run at github workflows. - * - * See [robolectric issue 8071](https://github.com/robolectric/robolectric/issues/8071) for details. - */ - -@OptIn(ExperimentalTestApi::class) -@RequiresApi(Build.VERSION_CODES.O) -fun SemanticsNodeInteraction.captureToImage(): ImageBitmap { - val node = fetchSemanticsNode("Failed to capture a node to bitmap.") - // Validate we are in popup - val popupParentMaybe = - node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsPopup) - } - if (popupParentMaybe != null) { - return processMultiWindowScreenshot(node) - } - - val view = (node.root as ViewRootForTest).view - - // If we are in dialog use its window to capture the bitmap - val dialogParentNodeMaybe = - node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsDialog) - } - var dialogWindow: Window? = null - if (dialogParentNodeMaybe != null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - // TODO(b/163023027) - throw IllegalArgumentException("Cannot currently capture dialogs on API lower than 28!") - } - - dialogWindow = findDialogWindowProviderInParent(view)?.window - ?: throw IllegalArgumentException( - "Could not find a dialog window provider to capture its bitmap" - ) - } - - val windowToUse = dialogWindow ?: view.context.getActivityWindow() - - val nodeBounds = node.boundsInRoot - val nodeBoundsRect = - Rect( - nodeBounds.left.roundToInt(), - nodeBounds.top.roundToInt(), - nodeBounds.right.roundToInt(), - nodeBounds.bottom.roundToInt() - ) - - val locationInWindow = intArrayOf(0, 0) - view.getLocationInWindow(locationInWindow) - val x = locationInWindow[0] - val y = locationInWindow[1] - - // Now these are bounds in window - nodeBoundsRect.offset(x, y) - - return windowToUse.captureRegionToImage(nodeBoundsRect) -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun SemanticsNode.findClosestParentNode( - includeSelf: Boolean = false, - selector: (SemanticsNode) -> Boolean -): SemanticsNode? { - var currentParent = if (includeSelf) this else parent - while (currentParent != null) { - if (selector(currentParent)) { - return currentParent - } else { - currentParent = currentParent.parent - } - } - - return null -} - -@ExperimentalTestApi -@RequiresApi(Build.VERSION_CODES.O) -private fun processMultiWindowScreenshot(node: SemanticsNode): ImageBitmap { - val nodePositionInScreen = findNodePosition(node) - val nodeBoundsInRoot = node.boundsInRoot - - val combinedBitmap = InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot() - - val finalBitmap = - Bitmap.createBitmap( - combinedBitmap, - (nodePositionInScreen.x + nodeBoundsInRoot.left).roundToInt(), - (nodePositionInScreen.y + nodeBoundsInRoot.top).roundToInt(), - nodeBoundsInRoot.width.roundToInt(), - nodeBoundsInRoot.height.roundToInt() - ) - return finalBitmap.asImageBitmap() -} - -private fun findNodePosition(node: SemanticsNode): Offset { - val view = (node.root as ViewRootForTest).view - val locationOnScreen = intArrayOf(0, 0) - view.getLocationOnScreen(locationOnScreen) - val x = locationOnScreen[0] - val y = locationOnScreen[1] - - return Offset(x.toFloat(), y.toFloat()) -} - -internal fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? { - if (view is DialogWindowProvider) { - return view - } - val parent = view.parent ?: return null - if (parent is View) { - return findDialogWindowProviderInParent(parent) - } - return null -} - -private fun Context.getActivityWindow(): Window { - fun Context.getActivity(): Activity { - return when (this) { - is Activity -> this - is ContextWrapper -> this.baseContext.getActivity() - else -> throw IllegalStateException( - "Context is not an Activity context, but a ${javaClass.simpleName} context. " + - "An Activity context is required to get a Window instance" - ) - } - } - return getActivity().window -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.captureRegionToImage(boundsInWindow: Rect): ImageBitmap { - // Turn on hardware rendering, if necessary - return withDrawingEnabled { - // Then we generate the bitmap - generateBitmap(boundsInWindow).asImageBitmap() - } -} - -private fun withDrawingEnabled(block: () -> R): R { - val wasDrawingEnabled = HardwareRendererCompat.isDrawingEnabled() - try { - if (!wasDrawingEnabled) { - HardwareRendererCompat.setDrawingEnabled(true) - } - return block.invoke() - } finally { - if (!wasDrawingEnabled) { - HardwareRendererCompat.setDrawingEnabled(false) - } - } -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.generateBitmap(boundsInWindow: Rect): Bitmap { - val destBitmap = - Bitmap.createBitmap( - boundsInWindow.width(), - boundsInWindow.height(), - Bitmap.Config.ARGB_8888 - ) - generateBitmapFromPixelCopy(boundsInWindow, destBitmap) - return destBitmap -} - -@RequiresApi(Build.VERSION_CODES.O) -private object PixelCopyHelper { - @DoNotInline - fun request( - source: Window, - srcRect: Rect?, - dest: Bitmap, - listener: PixelCopy.OnPixelCopyFinishedListener, - listenerThread: Handler - ) { - PixelCopy.request(source, srcRect, dest, listener, listenerThread) - } -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.generateBitmapFromPixelCopy(boundsInWindow: Rect, destBitmap: Bitmap) { - val latch = CountDownLatch(1) - var copyResult = 0 - val onCopyFinished = - PixelCopy.OnPixelCopyFinishedListener { result -> - copyResult = result - latch.countDown() - } - PixelCopyHelper.request( - this, - boundsInWindow, - destBitmap, - onCopyFinished, - Handler(Looper.getMainLooper()) - ) - - if (!latch.await(1, TimeUnit.SECONDS)) { - throw AssertionError("Failed waiting for PixelCopy!") - } - if (copyResult != PixelCopy.SUCCESS) { - throw AssertionError("PixelCopy failed!") - } -} From c5db5b5e13d8b3efe2cc5b29a8c1f47736197cf4 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 08:35:49 -0700 Subject: [PATCH 08/13] styling cleanups --- core/camera/build.gradle.kts | 2 -- core/camera/effects/single-stream/build.gradle.kts | 4 +--- feature/postcapture/build.gradle.kts | 4 ++-- ui/components/build.gradle.kts | 1 - ui/components/capture/build.gradle.kts | 5 +---- ui/controller/build.gradle.kts | 5 +---- ui/controller/impl/build.gradle.kts | 5 +---- ui/uistate/build.gradle.kts | 5 +---- ui/uistate/capture/build.gradle.kts | 5 +---- ui/uistateadapter/build.gradle.kts | 5 +---- ui/uistateadapter/capture/build.gradle.kts | 5 +---- 11 files changed, 10 insertions(+), 36 deletions(-) diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index 975955fd0..b587e0a73 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -125,7 +125,6 @@ dependencies { implementation(libs.camera.lifecycle) implementation(libs.camera.video) - // Tracing implementation(libs.androidx.tracing) implementation(libs.kotlinx.atomicfu) @@ -141,4 +140,3 @@ dependencies { } - diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index ee42831de..f46fb4666 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -33,10 +33,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index d0754c11e..e57a6f450 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -75,7 +74,8 @@ android { } } } - }} + } +} dependencies { // Compose diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index b152876c8..cc2306000 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 0766452c4..837f3861b 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index aad8888a3..836d755b3 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 66172ec61..18e226ba7 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index 5ceda5ab4..3f5fffafb 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index b32a0c393..14ccd3306 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index 9fe67b430..ae6489ddd 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index 46ec7008a..a598f51fb 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } From 402a7153a615a7bdee15c1ded81f4d29406ae86b Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 14:02:58 -0700 Subject: [PATCH 09/13] Remove deprecated -Xcontext-receivers compiler option --- .../core/camera/CameraSession.kt | 36 +++++++++---------- feature/postcapture/build.gradle.kts | 3 -- ui/components/build.gradle.kts | 3 -- ui/components/capture/build.gradle.kts | 3 -- ui/controller/build.gradle.kts | 3 -- ui/controller/impl/build.gradle.kts | 3 -- ui/uistate/build.gradle.kts | 3 -- ui/uistate/capture/build.gradle.kts | 3 -- ui/uistateadapter/build.gradle.kts | 3 -- ui/uistateadapter/capture/build.gradle.kts | 3 -- 10 files changed, 18 insertions(+), 45 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index f1d8cca2a..d9ef0f637 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -1071,9 +1071,9 @@ private suspend fun startVideoRecordingInternal( val callbackExecutor: Executor = ( - currentCoroutineContext()[ContinuationInterceptor] as? - CoroutineDispatcher - )?.asExecutor() ?: ContextCompat.getMainExecutor(context) + currentCoroutineContext()[ContinuationInterceptor] as? + CoroutineDispatcher + )?.asExecutor() ?: ContextCompat.getMainExecutor(context) return pendingRecord.start(callbackExecutor) { onVideoRecordEvent -> Log.d(TAG, onVideoRecordEvent.toString()) when (onVideoRecordEvent) { @@ -1182,22 +1182,23 @@ private suspend fun startVideoRecordingInternal( ) } - else -> { - onVideoRecord( - OnVideoRecordEvent.OnVideoRecordError( - RuntimeException( - "Recording finished with error: ${onVideoRecordEvent.error}", - onVideoRecordEvent.cause + else -> { + onVideoRecord( + OnVideoRecordEvent.OnVideoRecordError( + RuntimeException( + "Recording finished with error: ${onVideoRecordEvent.error}", + onVideoRecordEvent.cause + ) ) ) - ) - currentCameraState.update { old -> - old.copy( - videoRecordingState = VideoRecordingState.Inactive( - finalElapsedTimeNanos = onVideoRecordEvent.recordingStats - .recordedDurationNanos + currentCameraState.update { old -> + old.copy( + videoRecordingState = VideoRecordingState.Inactive( + finalElapsedTimeNanos = onVideoRecordEvent.recordingStats + .recordedDurationNanos + ) ) - ) + } } } } @@ -1250,8 +1251,7 @@ private suspend fun runVideoRecording( if (currentSettings.isAudioEnabled != newTransientSettings.isAudioEnabled ) { - // audio mute state will be inverse of - if audio is enabled. + // audio mute state will be inverse of if audio is enabled. recording.mute(!newTransientSettings.isAudioEnabled) } currentSettings = newTransientSettings diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index e57a6f450..02c89c359 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index cc2306000..05de2cee8 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 837f3861b..8f933f597 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index 836d755b3..dd536be2a 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 18e226ba7..0362f83a8 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index 3f5fffafb..4eb388c5e 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index 14ccd3306..463791bb6 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index ae6489ddd..88bef4268 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index a598f51fb..1222a5e28 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -46,9 +46,6 @@ android { } kotlin { jvmToolchain(17) - compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") - } } buildFeatures { buildConfig = true From 03113c3271ab1bd25c83e74289b721834683ea36 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 14:06:48 -0700 Subject: [PATCH 10/13] spotless --- .../google/jetpackcamera/core/camera/CameraSession.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index d9ef0f637..82e125d5c 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -1071,9 +1071,9 @@ private suspend fun startVideoRecordingInternal( val callbackExecutor: Executor = ( - currentCoroutineContext()[ContinuationInterceptor] as? - CoroutineDispatcher - )?.asExecutor() ?: ContextCompat.getMainExecutor(context) + currentCoroutineContext()[ContinuationInterceptor] as? + CoroutineDispatcher + )?.asExecutor() ?: ContextCompat.getMainExecutor(context) return pendingRecord.start(callbackExecutor) { onVideoRecordEvent -> Log.d(TAG, onVideoRecordEvent.toString()) when (onVideoRecordEvent) { @@ -1254,10 +1254,9 @@ private suspend fun runVideoRecording( // audio mute state will be inverse of if audio is enabled. recording.mute(!newTransientSettings.isAudioEnabled) } - currentSettings = newTransientSettings - } + currentSettings = newTransientSettings } - + } for (event in videoControlEvents) { when (event) { From 4fdb416d46936f7ba9613b0aee8009900efaa874 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 06:04:47 -0700 Subject: [PATCH 11/13] Upgrade build infrastructure (AGP 9.3.1, Gradle 9.6.1) Upgrades the project's build system: - Upgraded Android Gradle Plugin to 9.3.1 and Gradle Wrapper to 9.6.1. - Migrated all modules from 'kotlin.android' to AGP's built-in Kotlin support. - Replaced 'kotlin.kapt' with 'legacy-kapt' for compatibility with built-in Kotlin. - Upgraded Hilt to 2.59.2 for AGP 9 compatibility. - Updated compiler flags to pass -Xcontext-receivers under the new built-in Kotlin configuration block in 11 modules. - Updated README to reflect Android Studio Quail / AGP 9 requirements. - Enabled parallel Gradle sync in gradle.properties. --- README.md | 4 ++-- app/build.gradle.kts | 3 +-- benchmark/build.gradle.kts | 16 +++++++++------- build.gradle.kts | 3 +-- core/camera/build.gradle.kts | 8 +++----- .../effects/single-stream/build.gradle.kts | 11 ++++++----- .../low-light-playservices/build.gradle.kts | 3 +-- core/camera/low-light/build.gradle.kts | 3 +-- core/camera/postprocess/build.gradle.kts | 3 +-- core/camera/testing/build.gradle.kts | 1 - core/common/build.gradle.kts | 3 +-- core/common/testing/build.gradle.kts | 1 - core/model/build.gradle.kts | 3 +-- core/settings/build.gradle.kts | 3 +-- core/settings/datastore-prefs/build.gradle.kts | 1 - .../datastore-prefs/testing/build.gradle.kts | 1 - data/camera/build.gradle.kts | 6 ++---- data/camera/consumer-rules.pro | 2 ++ data/media/build.gradle.kts | 3 +-- data/media/testing/build.gradle.kts | 1 - data/settings/build.gradle.kts | 3 +-- data/settings/testing/build.gradle.kts | 1 - feature/permissions/build.gradle.kts | 3 +-- feature/postcapture/build.gradle.kts | 13 +++++-------- feature/preview/build.gradle.kts | 3 +-- feature/settings/build.gradle.kts | 3 +-- gradle.properties | 5 +++-- gradle/libs.versions.toml | 5 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 1 - ui/components/build.gradle.kts | 10 ++++------ ui/components/capture/build.gradle.kts | 12 ++++++------ ui/controller/build.gradle.kts | 12 ++++++------ ui/controller/impl/build.gradle.kts | 12 ++++++------ ui/controller/testing/build.gradle.kts | 1 - ui/debug/build.gradle.kts | 3 +-- ui/debug/testing/build.gradle.kts | 1 - ui/uistate/build.gradle.kts | 12 ++++++------ ui/uistate/capture/build.gradle.kts | 12 ++++++------ ui/uistate/postcapture/build.gradle.kts | 5 ++--- ui/uistateadapter/build.gradle.kts | 12 ++++++------ ui/uistateadapter/capture/build.gradle.kts | 12 ++++++------ ui/uistateadapter/postcapture/build.gradle.kts | 5 ++--- 43 files changed, 100 insertions(+), 127 deletions(-) create mode 100644 data/camera/consumer-rules.pro diff --git a/README.md b/README.md index a733c15c3..b13307a74 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ OEMs looking to validate their camera feature implementations. This project uses the gradle build system, and can be imported directly into Android Studio. -Currently, Jetpack Camera App is built using the Android Gradle Plugin 8.10.0, which is only -compatible with Android Studio Meerkat or newer. +Currently, Jetpack Camera App is built using the Android Gradle Plugin 9.3.1, which is only +compatible with Android Studio Quail or newer. ## Pre-push Hook (Recommended) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ad359692b..7246eef3c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index a1708658f..00c54f097 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.test) - alias(libs.plugins.kotlin.android) } android { @@ -24,12 +24,8 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } defaultConfig { @@ -69,6 +65,12 @@ android { experimentalProperties["android.experimental.self-instrumenting"] = true } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(libs.androidx.junit) implementation(libs.androidx.benchmark.macro.junit4) diff --git a/build.gradle.kts b/build.gradle.kts index 64fdc6b56..53a8212e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,9 +19,8 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.android.test) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.dagger.hilt.android) apply false - alias(libs.plugins.kotlin.kapt) apply false + alias(libs.plugins.android.legacy.kapt) apply false alias(libs.plugins.compose.compiler) apply false } diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index b87fb79ee..975955fd0 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -93,10 +92,9 @@ android { } kotlin { jvmToolchain(17) - } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } } diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index dacbf112c..ee42831de 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } @@ -34,10 +33,12 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) - } - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } } diff --git a/core/camera/low-light-playservices/build.gradle.kts b/core/camera/low-light-playservices/build.gradle.kts index 639fa3e51..5c6aab589 100644 --- a/core/camera/low-light-playservices/build.gradle.kts +++ b/core/camera/low-light-playservices/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/low-light/build.gradle.kts b/core/camera/low-light/build.gradle.kts index cade65c9e..9377565e4 100644 --- a/core/camera/low-light/build.gradle.kts +++ b/core/camera/low-light/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/postprocess/build.gradle.kts b/core/camera/postprocess/build.gradle.kts index ceedef0b7..67157f0be 100644 --- a/core/camera/postprocess/build.gradle.kts +++ b/core/camera/postprocess/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts index 38f70711f..0768c256e 100644 --- a/core/camera/testing/build.gradle.kts +++ b/core/camera/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 185217b27..5886b309b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/common/testing/build.gradle.kts b/core/common/testing/build.gradle.kts index 57c300b0e..6d21ce391 100644 --- a/core/common/testing/build.gradle.kts +++ b/core/common/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index d352566c4..f9e6a46d8 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) } android { diff --git a/core/settings/build.gradle.kts b/core/settings/build.gradle.kts index 35c256a56..021d3d4d8 100644 --- a/core/settings/build.gradle.kts +++ b/core/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/core/settings/datastore-prefs/build.gradle.kts b/core/settings/datastore-prefs/build.gradle.kts index c0ca9f8d4..107acd946 100644 --- a/core/settings/datastore-prefs/build.gradle.kts +++ b/core/settings/datastore-prefs/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/core/settings/datastore-prefs/testing/build.gradle.kts b/core/settings/datastore-prefs/testing/build.gradle.kts index 0e619ddbd..d6b25c0f3 100644 --- a/core/settings/datastore-prefs/testing/build.gradle.kts +++ b/core/settings/datastore-prefs/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/data/camera/build.gradle.kts b/data/camera/build.gradle.kts index 8a541423a..39c75b667 100644 --- a/data/camera/build.gradle.kts +++ b/data/camera/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } @@ -37,8 +36,7 @@ android { buildTypes { release { proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt") ) } create("benchmark") { diff --git a/data/camera/consumer-rules.pro b/data/camera/consumer-rules.pro new file mode 100644 index 000000000..c1a8cebff --- /dev/null +++ b/data/camera/consumer-rules.pro @@ -0,0 +1,2 @@ +# Add project specific consumer ProGuard rules here. +# More info: https://developer.android.com/studio/build/shrink-code diff --git a/data/media/build.gradle.kts b/data/media/build.gradle.kts index 91146f6e8..223daced0 100644 --- a/data/media/build.gradle.kts +++ b/data/media/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/data/media/testing/build.gradle.kts b/data/media/testing/build.gradle.kts index f05656de1..2c531124c 100644 --- a/data/media/testing/build.gradle.kts +++ b/data/media/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 7748c8d51..b7037b199 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index 14e7b1e30..a3ff5b281 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/feature/permissions/build.gradle.kts b/feature/permissions/build.gradle.kts index 6c977afeb..e101d4892 100644 --- a/feature/permissions/build.gradle.kts +++ b/feature/permissions/build.gradle.kts @@ -15,10 +15,9 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.compose.compiler) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index b1faaa055..d0754c11e 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -15,8 +15,7 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } @@ -48,6 +47,9 @@ android { } kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true @@ -73,12 +75,7 @@ android { } } } - } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } -} + }} dependencies { // Compose diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index 54ab1c505..37e210f8e 100644 --- a/feature/preview/build.gradle.kts +++ b/feature/preview/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index d5c246d1f..10dadeda4 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.compose.compiler) } diff --git a/gradle.properties b/gradle.properties index 15ab33aba..23a6f2b56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,9 +39,10 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.nonFinalResIds=false # Properties to make gradle managed devices testing more stable (see https://issuetracker.google.com/287312019#comment41) android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 # Ensure we can run managed devices on servers that don't support hardware rendering -android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect \ No newline at end of file +android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2317d598..04676d579 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ accompanist = "0.37.3" # kotlinPlugin and composeCompiler are linked # See https://developer.android.com/jetpack/androidx/releases/compose-kotlin kotlinPlugin = "2.2.0" -androidGradlePlugin = "8.10.1" +androidGradlePlugin = "9.3.1" androidxActivityCompose = "1.10.1" @@ -43,7 +43,7 @@ composeMaterial = "1.5.0-alpha07" cmake = "3.22.1" kotlinxAtomicfu = "0.29.0" kotlinxCoroutines = "1.10.2" -hilt = "2.57" +hilt = "2.59.2" junit = "4.13.2" robolectric = "4.15.1" @@ -124,3 +124,4 @@ dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hi kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinPlugin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlinPlugin" } +android-legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "androidGradlePlugin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b965ecfd5..c72c372a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Mar 12 23:44:57 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index dfb9e6557..f1197bb76 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,7 +57,6 @@ include(":ui:uistate:capture") include(":ui:uistateadapter:capture") include(":ui:components") include(":ui:components:capture") -include(":data:model") include(":core:settings") include(":core:model") include(":ui:uistate:postcapture") diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index 02d3b2dca..b152876c8 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -48,15 +47,14 @@ android { } kotlin { jvmToolchain(17) + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index d2f9fc5fa..0766452c4 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index 2690d28bb..aad8888a3 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 062e38746..66172ec61 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/controller/testing/build.gradle.kts b/ui/controller/testing/build.gradle.kts index 402ca3715..f239cd132 100644 --- a/ui/controller/testing/build.gradle.kts +++ b/ui/controller/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/ui/debug/build.gradle.kts b/ui/debug/build.gradle.kts index d62d15e16..4406021e2 100644 --- a/ui/debug/build.gradle.kts +++ b/ui/debug/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } diff --git a/ui/debug/testing/build.gradle.kts b/ui/debug/testing/build.gradle.kts index 4abe6a083..f05e7f6bf 100644 --- a/ui/debug/testing/build.gradle.kts +++ b/ui/debug/testing/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index e06c9e4c7..5ceda5ab4 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index 345628e27..b32a0c393 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistate/postcapture/build.gradle.kts b/ui/uistate/postcapture/build.gradle.kts index 4fda15af5..cd5412fb3 100644 --- a/ui/uistate/postcapture/build.gradle.kts +++ b/ui/uistate/postcapture/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -42,8 +41,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index 2826a1033..9fe67b430 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index fa52725fe..46ec7008a 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -16,8 +16,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.android.legacy.kapt) alias(libs.plugins.compose.compiler) } @@ -47,16 +46,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { + jvmToolchain(17) + + compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } buildFeatures { buildConfig = true compose = true } - - kotlinOptions { - freeCompilerArgs += "-Xcontext-receivers" - } } dependencies { diff --git a/ui/uistateadapter/postcapture/build.gradle.kts b/ui/uistateadapter/postcapture/build.gradle.kts index 889e3a51c..8659e8e7a 100644 --- a/ui/uistateadapter/postcapture/build.gradle.kts +++ b/ui/uistateadapter/postcapture/build.gradle.kts @@ -15,7 +15,6 @@ */ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) } android { @@ -42,8 +41,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } From bee3659d06533f3c7e23b0154e8729bab06da537 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 07:58:15 -0700 Subject: [PATCH 12/13] Fix Gradle 9 test task failures by removing empty test suites Deletes the unused and duplicated test helper classes in :ui:components:capture. Gradle 9 fails the build if a module contains test sources but no actual @Test classes to execute, which was the case here since it only contained test utilities. --- .../capture/rules/MainDispatcherRule.kt | 39 --- .../workaround/ComposableCaptureToImage.kt | 257 ------------------ 2 files changed, 296 deletions(-) delete mode 100644 ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt delete mode 100644 ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt deleted file mode 100644 index 287fb9fff..000000000 --- a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/rules/MainDispatcherRule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 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 com.google.jetpackcamera.ui.components.capture.capture.rules - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -class MainDispatcherRule(private val dispatcher: CoroutineDispatcher) : TestRule { - @OptIn(ExperimentalCoroutinesApi::class) - override fun apply(base: Statement?, description: Description?) = object : Statement() { - override fun evaluate() { - Dispatchers.setMain(dispatcher) - try { - base!!.evaluate() - } finally { - Dispatchers.resetMain() - } - } - } -} diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt deleted file mode 100644 index 9805bcde9..000000000 --- a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/workaround/ComposableCaptureToImage.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 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. - */ -@file:SuppressLint("UseSdkSuppress") - -package com.google.jetpackcamera.ui.components.capture.capture.workaround - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper -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.View -import android.view.Window -import androidx.annotation.DoNotInline -import androidx.annotation.RequiresApi -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.ViewRootForTest -import androidx.compose.ui.semantics.SemanticsNode -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.window.DialogWindowProvider -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.platform.graphics.HardwareRendererCompat -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.math.roundToInt - -/** - * Workaround captureToImage method. - * - * Once composable + robolectric graphics bugs are fixed, this can be replaced with the actual - * [SemanticsNodeInteraction.captureToImage]. Alternative is to use - * instrumentations tests, but they are not run at github workflows. - * - * See [robolectric issue 8071](https://github.com/robolectric/robolectric/issues/8071) for details. - */ - -@OptIn(ExperimentalTestApi::class) -@RequiresApi(Build.VERSION_CODES.O) -fun SemanticsNodeInteraction.captureToImage(): ImageBitmap { - val node = fetchSemanticsNode("Failed to capture a node to bitmap.") - // Validate we are in popup - val popupParentMaybe = - node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsPopup) - } - if (popupParentMaybe != null) { - return processMultiWindowScreenshot(node) - } - - val view = (node.root as ViewRootForTest).view - - // If we are in dialog use its window to capture the bitmap - val dialogParentNodeMaybe = - node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsDialog) - } - var dialogWindow: Window? = null - if (dialogParentNodeMaybe != null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - // TODO(b/163023027) - throw IllegalArgumentException("Cannot currently capture dialogs on API lower than 28!") - } - - dialogWindow = findDialogWindowProviderInParent(view)?.window - ?: throw IllegalArgumentException( - "Could not find a dialog window provider to capture its bitmap" - ) - } - - val windowToUse = dialogWindow ?: view.context.getActivityWindow() - - val nodeBounds = node.boundsInRoot - val nodeBoundsRect = - Rect( - nodeBounds.left.roundToInt(), - nodeBounds.top.roundToInt(), - nodeBounds.right.roundToInt(), - nodeBounds.bottom.roundToInt() - ) - - val locationInWindow = intArrayOf(0, 0) - view.getLocationInWindow(locationInWindow) - val x = locationInWindow[0] - val y = locationInWindow[1] - - // Now these are bounds in window - nodeBoundsRect.offset(x, y) - - return windowToUse.captureRegionToImage(nodeBoundsRect) -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun SemanticsNode.findClosestParentNode( - includeSelf: Boolean = false, - selector: (SemanticsNode) -> Boolean -): SemanticsNode? { - var currentParent = if (includeSelf) this else parent - while (currentParent != null) { - if (selector(currentParent)) { - return currentParent - } else { - currentParent = currentParent.parent - } - } - - return null -} - -@ExperimentalTestApi -@RequiresApi(Build.VERSION_CODES.O) -private fun processMultiWindowScreenshot(node: SemanticsNode): ImageBitmap { - val nodePositionInScreen = findNodePosition(node) - val nodeBoundsInRoot = node.boundsInRoot - - val combinedBitmap = InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot() - - val finalBitmap = - Bitmap.createBitmap( - combinedBitmap, - (nodePositionInScreen.x + nodeBoundsInRoot.left).roundToInt(), - (nodePositionInScreen.y + nodeBoundsInRoot.top).roundToInt(), - nodeBoundsInRoot.width.roundToInt(), - nodeBoundsInRoot.height.roundToInt() - ) - return finalBitmap.asImageBitmap() -} - -private fun findNodePosition(node: SemanticsNode): Offset { - val view = (node.root as ViewRootForTest).view - val locationOnScreen = intArrayOf(0, 0) - view.getLocationOnScreen(locationOnScreen) - val x = locationOnScreen[0] - val y = locationOnScreen[1] - - return Offset(x.toFloat(), y.toFloat()) -} - -internal fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? { - if (view is DialogWindowProvider) { - return view - } - val parent = view.parent ?: return null - if (parent is View) { - return findDialogWindowProviderInParent(parent) - } - return null -} - -private fun Context.getActivityWindow(): Window { - fun Context.getActivity(): Activity { - return when (this) { - is Activity -> this - is ContextWrapper -> this.baseContext.getActivity() - else -> throw IllegalStateException( - "Context is not an Activity context, but a ${javaClass.simpleName} context. " + - "An Activity context is required to get a Window instance" - ) - } - } - return getActivity().window -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.captureRegionToImage(boundsInWindow: Rect): ImageBitmap { - // Turn on hardware rendering, if necessary - return withDrawingEnabled { - // Then we generate the bitmap - generateBitmap(boundsInWindow).asImageBitmap() - } -} - -private fun withDrawingEnabled(block: () -> R): R { - val wasDrawingEnabled = HardwareRendererCompat.isDrawingEnabled() - try { - if (!wasDrawingEnabled) { - HardwareRendererCompat.setDrawingEnabled(true) - } - return block.invoke() - } finally { - if (!wasDrawingEnabled) { - HardwareRendererCompat.setDrawingEnabled(false) - } - } -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.generateBitmap(boundsInWindow: Rect): Bitmap { - val destBitmap = - Bitmap.createBitmap( - boundsInWindow.width(), - boundsInWindow.height(), - Bitmap.Config.ARGB_8888 - ) - generateBitmapFromPixelCopy(boundsInWindow, destBitmap) - return destBitmap -} - -@RequiresApi(Build.VERSION_CODES.O) -private object PixelCopyHelper { - @DoNotInline - fun request( - source: Window, - srcRect: Rect?, - dest: Bitmap, - listener: PixelCopy.OnPixelCopyFinishedListener, - listenerThread: Handler - ) { - PixelCopy.request(source, srcRect, dest, listener, listenerThread) - } -} - -@RequiresApi(Build.VERSION_CODES.O) -private fun Window.generateBitmapFromPixelCopy(boundsInWindow: Rect, destBitmap: Bitmap) { - val latch = CountDownLatch(1) - var copyResult = 0 - val onCopyFinished = - PixelCopy.OnPixelCopyFinishedListener { result -> - copyResult = result - latch.countDown() - } - PixelCopyHelper.request( - this, - boundsInWindow, - destBitmap, - onCopyFinished, - Handler(Looper.getMainLooper()) - ) - - if (!latch.await(1, TimeUnit.SECONDS)) { - throw AssertionError("Failed waiting for PixelCopy!") - } - if (copyResult != PixelCopy.SUCCESS) { - throw AssertionError("PixelCopy failed!") - } -} From 178f2661210d4fa1b2f250626857a2faa8f4c441 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Fri, 31 Jul 2026 08:35:49 -0700 Subject: [PATCH 13/13] styling cleanups --- core/camera/build.gradle.kts | 2 -- core/camera/effects/single-stream/build.gradle.kts | 4 +--- feature/postcapture/build.gradle.kts | 4 ++-- ui/components/build.gradle.kts | 1 - ui/components/capture/build.gradle.kts | 5 +---- ui/controller/build.gradle.kts | 5 +---- ui/controller/impl/build.gradle.kts | 5 +---- ui/uistate/build.gradle.kts | 5 +---- ui/uistate/capture/build.gradle.kts | 5 +---- ui/uistateadapter/build.gradle.kts | 5 +---- ui/uistateadapter/capture/build.gradle.kts | 5 +---- 11 files changed, 10 insertions(+), 36 deletions(-) diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index 975955fd0..b587e0a73 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -125,7 +125,6 @@ dependencies { implementation(libs.camera.lifecycle) implementation(libs.camera.video) - // Tracing implementation(libs.androidx.tracing) implementation(libs.kotlinx.atomicfu) @@ -141,4 +140,3 @@ dependencies { } - diff --git a/core/camera/effects/single-stream/build.gradle.kts b/core/camera/effects/single-stream/build.gradle.kts index ee42831de..f46fb4666 100644 --- a/core/camera/effects/single-stream/build.gradle.kts +++ b/core/camera/effects/single-stream/build.gradle.kts @@ -33,10 +33,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index d0754c11e..e57a6f450 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -75,7 +74,8 @@ android { } } } - }} + } +} dependencies { // Compose diff --git a/ui/components/build.gradle.kts b/ui/components/build.gradle.kts index b152876c8..cc2306000 100644 --- a/ui/components/build.gradle.kts +++ b/ui/components/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 0766452c4..837f3861b 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/controller/build.gradle.kts b/ui/controller/build.gradle.kts index aad8888a3..836d755b3 100644 --- a/ui/controller/build.gradle.kts +++ b/ui/controller/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 66172ec61..18e226ba7 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistate/build.gradle.kts b/ui/uistate/build.gradle.kts index 5ceda5ab4..3f5fffafb 100644 --- a/ui/uistate/build.gradle.kts +++ b/ui/uistate/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistate/capture/build.gradle.kts b/ui/uistate/capture/build.gradle.kts index b32a0c393..14ccd3306 100644 --- a/ui/uistate/capture/build.gradle.kts +++ b/ui/uistate/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistateadapter/build.gradle.kts b/ui/uistateadapter/build.gradle.kts index 9fe67b430..ae6489ddd 100644 --- a/ui/uistateadapter/build.gradle.kts +++ b/ui/uistateadapter/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } } diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index 46ec7008a..a598f51fb 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -32,7 +32,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - flavorDimensions += "flavor" productFlavors { create("stable") { @@ -46,10 +45,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) - - compilerOptions { + compilerOptions { freeCompilerArgs.add("-Xcontext-receivers") } }