diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a85fb9698..48d81b04a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,9 @@ plugins { alias(libs.plugins.kotlin.compose) } +// Opt-in instrumentation of the minified variant; production release signing is unchanged. +val releaseUiTests = providers.gradleProperty("releaseUiTests").orNull == "true" + val githubReleaseCertSha256 = providers .environmentVariable("BITCHAT_GITHUB_RELEASE_CERT_SHA256") .orElse(providers.gradleProperty("BITCHAT_GITHUB_RELEASE_CERT_SHA256")) @@ -25,6 +28,7 @@ android { namespace = "com.bitchat.android" compileSdk = libs.versions.compileSdk.get().toInt() buildToolsVersion = libs.versions.buildTools.get() + if (releaseUiTests) testBuildType = "release" defaultConfig { applicationId = "com.bitchat.droid" @@ -38,7 +42,11 @@ android { "\"$normalizedGithubReleaseCertSha256\"" ) - testInstrumentationRunner = "com.bitchat.android.ui.PerformanceTestRunner" + testInstrumentationRunner = if (releaseUiTests) { + "com.bitchat.android.ui.ReleaseScannerTestRunner" + } else { + "com.bitchat.android.ui.PerformanceTestRunner" + } vectorDrawables { useSupportLibrary = true } @@ -59,6 +67,10 @@ android { } } release { + if (releaseUiTests) { + signingConfig = signingConfigs.getByName("debug") + proguardFiles("release-instrumentation.pro") + } isMinifyEnabled = true isShrinkResources = true proguardFiles( @@ -192,6 +204,8 @@ dependencies { // Testing testImplementation(libs.bundles.testing) + testImplementation(platform(libs.androidx.compose.bom)) + testImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.test.runner) diff --git a/app/gradle.lockfile b/app/gradle.lockfile index 420310107..da7a264fb 100644 --- a/app/gradle.lockfile +++ b/app/gradle.lockfile @@ -2,178 +2,178 @@ # Manual edits can break the build and are not advised. # This file is expected to be part of source control. # To regenerate this file, run: ./gradlew :app:dependencies --write-locks -androidx.activity:activity-compose:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.activity:activity-ktx:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.activity:activity:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.annotation:annotation-jvm:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.annotation:annotation:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.appcompat:appcompat-resources:1.7.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.appcompat:appcompat:1.7.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.arch.core:core-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.autofill:autofill:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity-compose:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity-ktx:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-jvm:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.appcompat:appcompat-resources:1.7.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.appcompat:appcompat:1.7.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.autofill:autofill:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.camera.featurecombinationquery:featurecombinationquery:1.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.camera.viewfinder:viewfinder-compose:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.camera.viewfinder:viewfinder-core:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.camera.viewfinder:viewfinder-compose:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.camera.viewfinder:viewfinder-core:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.camera:camera-camera2-pipe:1.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.camera:camera-camera2:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.camera:camera-compose:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.camera:camera-core:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.camera:camera-lifecycle:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.collection:collection-jvm:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.collection:collection-ktx:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.collection:collection:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.animation:animation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.animation:animation-core-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.animation:animation-core:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.animation:animation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.foundation:foundation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.foundation:foundation-layout-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.foundation:foundation-layout:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.foundation:foundation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material3:material3-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material3:material3:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material:material-icons-core-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath +androidx.camera:camera-camera2:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.camera:camera-compose:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.camera:camera-core:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.camera:camera-lifecycle:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-jvm:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-ktx:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material3:material3-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material3:material3:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-core-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseRuntimeClasspath androidx.compose.material:material-icons-core-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath -androidx.compose.material:material-icons-core:1.7.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material:material-icons-extended-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-core:1.7.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-extended-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseRuntimeClasspath androidx.compose.material:material-icons-extended-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath -androidx.compose.material:material-icons-extended:1.7.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material:material-ripple-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material:material-ripple:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-annotation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-annotation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-retain-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-retain:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-saveable-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-saveable:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-geometry-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-geometry:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-graphics-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-graphics:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-test-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-test-junit4-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-test-junit4:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-test-manifest:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-test:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-text-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-text:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-extended:1.7.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-test-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-manifest:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +androidx.compose.ui:ui-text-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-text:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-tooling-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath androidx.compose.ui:ui-tooling-data-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath androidx.compose.ui:ui-tooling-data:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-tooling-preview-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-tooling-preview:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-preview-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-preview:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-tooling:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-unit-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-unit:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-util-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui-util:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.ui:ui:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose:compose-bom:2026.06.01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-unit-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-unit:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose:compose-bom:2026.06.01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.concurrent:concurrent-futures-ktx:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.concurrent:concurrent-futures-ktx:1.2.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.concurrent:concurrent-futures:1.2.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath androidx.core:core-backported-fixes:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.core:core-ktx:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.core:core:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.cursoradapter:cursoradapter:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.customview:customview-poolingcontainer:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.documentfile:documentfile:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.drawerlayout:drawerlayout:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.dynamicanimation:dynamicanimation:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-ktx:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.cursoradapter:cursoradapter:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.customview:customview-poolingcontainer:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.documentfile:documentfile:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.drawerlayout:drawerlayout:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.dynamicanimation:dynamicanimation:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.emoji2:emoji2-views-helper:1.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.exifinterface:exifinterface:1.4.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.fragment:fragment:1.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.graphics:graphics-path:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.interpolator:interpolator:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.legacy:legacy-support-core-utils:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.exifinterface:exifinterface:1.4.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.fragment:fragment:1.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.graphics:graphics-path:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.legacy:legacy-support-core-utils:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-common-java8:2.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-common-jvm:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-common:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-livedata-core:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-livedata:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-process:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime-ktx-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common-jvm:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-process:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.lifecycle:lifecycle-service:2.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigation:navigation-common-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigation:navigation-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigation:navigation-compose:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigation:navigation-runtime-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigation:navigation-runtime:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-common-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-compose:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-runtime-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-runtime:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.room:room-common:2.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.room:room-ktx:2.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.room:room-runtime:2.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.savedstate:savedstate-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.savedstate:savedstate-compose-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.savedstate:savedstate-compose:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.savedstate:savedstate-ktx:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.savedstate:savedstate:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.security:security-crypto:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-compose-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-compose:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-ktx:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.security:security-crypto:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.sqlite:sqlite-framework:2.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.sqlite:sqlite:2.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath -androidx.startup:startup-runtime:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.test.espresso:espresso-core:3.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath +androidx.startup:startup-runtime:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.test.espresso:espresso-core:3.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath androidx.test.espresso:espresso-core:3.7.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath androidx.test.espresso:espresso-idling-resource:3.7.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.test.ext:junit:1.1.5=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.ext:junit:1.1.5=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath androidx.test.ext:junit:1.3.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.test.services:storage:1.4.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.services:storage:1.4.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath androidx.test.services:storage:1.6.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.test:annotation:1.0.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath -androidx.test:core:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:annotation:1.0.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +androidx.test:core:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath androidx.test:core:1.7.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.test:monitor:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:monitor:1.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath androidx.test:monitor:1.8.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.test:runner:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:runner:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath androidx.test:runner:1.7.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -androidx.tracing:tracing-android:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.tracing:tracing-android:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.tracing:tracing-ktx:1.3.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.tracing:tracing:1.0.0=debugAndroidTestCompileClasspath +androidx.tracing:tracing:1.0.0=debugAndroidTestCompileClasspath,releaseAndroidTestCompileClasspath androidx.tracing:tracing:1.1.0=debugUnitTestCompileClasspath -androidx.tracing:tracing:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.transition:transition:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.vectordrawable:vectordrawable-animated:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.vectordrawable:vectordrawable:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.window:window-core-android:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.window:window-core:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.window:window:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.work:work-runtime-ktx:2.10.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.work:work-runtime:2.10.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.tracing:tracing:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.transition:transition:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.vectordrawable:vectordrawable-animated:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.vectordrawable:vectordrawable:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window-core-android:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window-core:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.work:work-runtime-ktx:2.10.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.work:work-runtime:2.10.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.almworks.sqlite4java:sqlite4java:1.0.392=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath com.android.tools.analytics-library:protos:32.3.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.analytics-library:shared:32.3.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle @@ -218,16 +218,16 @@ com.android.tools:play-sdk-proto:32.3.1=androidLintTool com.android.tools:repository:32.3.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools:sdk-common:32.3.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools:sdklib:32.3.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -com.google.accompanist:accompanist-permissions:0.37.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.datatransport:transport-api:2.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.datatransport:transport-backend-cct:2.3.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.datatransport:transport-runtime:2.2.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-base:18.9.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-basement:18.9.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-location:21.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-tasks:18.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.odml:image:1.0.0-beta1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.accompanist:accompanist-permissions:0.37.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.datatransport:transport-api:2.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.datatransport:transport-backend-cct:2.3.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.datatransport:transport-runtime:2.2.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-base:18.9.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-basement:18.9.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-location:21.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-tasks:18.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.odml:image:1.0.0-beta1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android:annotations:4.1.1.4=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-core com.google.api.grpc:proto-google-common-protos:2.17.0=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core com.google.api.grpc:proto-google-common-protos:2.48.0=unified-test-platform-android-test-plugin-host-emulator-control @@ -236,39 +236,39 @@ com.google.auto.service:auto-service:1.1.1=unified-test-platform-android-device- com.google.auto.value:auto-value-annotations:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath com.google.auto.value:auto-value-annotations:1.6.3=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.auto:auto-common:1.2.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle -com.google.code.findbugs:jsr305:3.0.2=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.code.findbugs:jsr305:3.0.2=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.code.gson:gson:2.10.1=unified-test-platform-core,unified-test-platform-gradle-work-action com.google.code.gson:gson:2.11.0=androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle -com.google.code.gson:gson:2.14.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.code.gson:gson:2.14.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.code.gson:gson:2.8.9=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher -com.google.crypto.tink:tink-android:1.23.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.crypto.tink:tink-android:1.23.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.crypto.tink:tink:1.18.0=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action com.google.dagger:dagger:2.48=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action com.google.dagger:dagger:2.59=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.errorprone:error_prone_annotation:2.38.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath com.google.errorprone:error_prone_annotations:2.23.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.errorprone:error_prone_annotations:2.36.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action -com.google.errorprone:error_prone_annotations:2.48.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.firebase:firebase-annotations:16.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.firebase:firebase-components:16.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.firebase:firebase-encoders-json:17.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.firebase:firebase-encoders:16.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.48.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.firebase:firebase-annotations:16.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.firebase:firebase-components:16.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.firebase:firebase-encoders-json:17.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.firebase:firebase-encoders:16.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.guava:failureaccess:1.0.1=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.guava:failureaccess:1.0.2=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action com.google.guava:failureaccess:1.0.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath com.google.guava:guava:32.0.1-jre=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.guava:guava:33.4.0-jre=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action com.google.guava:guava:33.4.8-jre=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.j2objc:j2objc-annotations:2.8=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action com.google.jimfs:jimfs:1.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -com.google.mlkit:barcode-scanning-common:17.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.mlkit:barcode-scanning:17.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.mlkit:common:18.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.mlkit:vision-common:17.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.mlkit:vision-interfaces:16.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.mlkit:barcode-scanning-common:17.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.mlkit:barcode-scanning:17.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.mlkit:common:18.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.mlkit:vision-common:17.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.mlkit:vision-interfaces:16.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.protobuf:protobuf-java-util:3.22.3=unified-test-platform-core com.google.protobuf:protobuf-java-util:4.28.3=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.protobuf:protobuf-java:3.25.5=androidLintTool @@ -281,14 +281,14 @@ com.google.testing.platform:core-proto:0.0.9-alpha04=unified-test-platform-andro com.google.testing.platform:core:0.0.9-alpha04=unified-test-platform-core com.google.testing.platform:launcher:0.0.9-alpha04=unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.testparameterinjector:test-parameter-injector:1.18=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -com.google.zxing:core:3.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.zxing:core:3.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.ibm.icu:icu4j:77.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath com.squareup.okhttp3:mockwebserver3:5.4.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -com.squareup.okhttp3:okhttp-android:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.squareup.okhttp3:okhttp:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.squareup.okio:okio-jvm:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.squareup.okio:okio:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.squareup:javawriter:2.1.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +com.squareup.okhttp3:okhttp-android:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okhttp3:okhttp:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okio:okio-jvm:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okio:okio:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup:javawriter:2.1.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath com.sun.istack:istack-commons-runtime:3.0.8=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.sun.xml.fastinfoset:FastInfoset:1.2.16=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle commons-codec:commons-codec:1.17.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle @@ -340,14 +340,14 @@ jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool,unified-test-pla jakarta.inject:jakarta.inject-api:2.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle javax.annotation:javax.annotation-api:1.3.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-test-plugin-host-emulator-control -javax.inject:javax.inject:1=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action -junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +javax.inject:javax.inject:1=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.17.7=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath net.bytebuddy:byte-buddy:1.17.7=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action net.java.dev.jna:jna:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action net.sf.kxml:kxml2:2.3.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action -no.nordicsemi.android:ble:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +no.nordicsemi.android:ble:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.apache.commons:commons-compress:1.27.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.apache.commons:commons-lang3:3.16.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.apache.httpcomponents:httpclient:4.5.6=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle @@ -355,7 +355,7 @@ org.apache.httpcomponents:httpcore:4.4.16=androidLintTool,unified-test-platform- org.apache.httpcomponents:httpmime:4.5.6=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.bouncycastle:bcpkix-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.bouncycastle:bcprov-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.bouncycastle:bcprov-jdk18on:1.85=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.85=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.bouncycastle:bcutil-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.checkerframework:checker-qual:3.33.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher org.checkerframework:checker-qual:3.43.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action @@ -365,34 +365,34 @@ org.codehaus.mojo:animal-sniffer-annotations:1.24=unified-test-platform-android- org.conscrypt:conscrypt-openjdk-uber:2.5.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.glassfish.jaxb:txw2:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -org.hamcrest:hamcrest-integration:1.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath -org.hamcrest:hamcrest-library:1.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +org.hamcrest:hamcrest-integration:1.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath +org.hamcrest:hamcrest-library:1.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestRuntimeClasspath org.jetbrains.kotlin:compose-group-mapping:2.4.10=composeMappingProducerClasspath org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath -org.jetbrains.kotlin:kotlin-build-tools-api:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath -org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-compiler-runner:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.4.10=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.4.10=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-daemon-client:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath -org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease -org.jetbrains.kotlin:kotlin-parcelize-compiler:2.4.10=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease -org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy -org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest +org.jetbrains.kotlin:kotlin-parcelize-compiler:2.4.10=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest +org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy +org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest org.jetbrains.kotlin:kotlin-reflect:1.8.21=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-reflect:2.1.20=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath -org.jetbrains.kotlin:kotlin-script-runtime:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-script-runtime:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=unified-test-platform-android-test-plugin,unified-test-platform-core org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib-common:2.2.10=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action @@ -408,35 +408,35 @@ org.jetbrains.kotlin:kotlin-stdlib:1.9.0=unified-test-platform-android-driver-in org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action org.jetbrains.kotlin:kotlin-stdlib:2.2.21=composeMappingProducerClasspath org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath -org.jetbrains.kotlin:kotlin-stdlib:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy +org.jetbrains.kotlin:kotlin-stdlib:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy org.jetbrains.kotlin:kotlin-tooling-core:2.4.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:atomicfu-jvm:0.28.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:atomicfu:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:atomicfu:0.28.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains:annotations:13.0=composeMappingProducerClasspath,debugRuntimeClasspathCopy,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseRuntimeClasspathCopy -org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher -org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains:annotations:13.0=composeMappingProducerClasspath,debugRuntimeClasspathCopy,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kotlinCompilerPluginClasspathReleaseAndroidTest,releaseRuntimeClasspathCopy +org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseAndroidTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jvnet.staxex:stax-ex:1.8.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.mockito.kotlin:mockito-kotlin:6.3.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.mockito:mockito-core:5.23.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -org.nanohttpd:nanohttpd:2.3.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.nanohttpd:nanohttpd:2.3.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseAndroidTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.objenesis:objenesis:3.3=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.ow2.asm:asm-analysis:9.9=androidLintTool org.ow2.asm:asm-commons:9.8=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath @@ -461,4 +461,4 @@ org.robolectric:shadows-framework:4.15=debugUnitTestCompileClasspath,debugUnitTe org.robolectric:utils-reflector:4.15=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.robolectric:utils:4.15=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.yaml:snakeyaml:2.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath -empty=androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues +empty=androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,lintChecks,lintPublish,releaseAndroidTestAnnotationProcessorClasspath,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 4363120a0..74d1a32e8 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -24,6 +24,14 @@ (); } +# ML Kit discovers Firebase component registrars from manifest metadata and calls +# their no-argument constructors reflectively. Keeping the class alone does not +# retain that constructor in R8 full mode, leaving BarcodeScanning without its +# registered components and crashing when the verification scanner opens. +-keepclassmembers class * implements com.google.firebase.components.ComponentRegistrar { + public (); +} + # Keep Tor implementation (always included) -keep class com.bitchat.android.net.RealTorProvider { *; } diff --git a/app/release-instrumentation.pro b/app/release-instrumentation.pro new file mode 100644 index 000000000..67142eb02 --- /dev/null +++ b/app/release-instrumentation.pro @@ -0,0 +1,9 @@ +# Keep only the real analyzer entry point for tests. Do not keep ML Kit internals: +# doing so would hide regressions in the production dependency-discovery graph. +-keep class com.bitchat.android.ui.QRCodeAnalyzer { + (kotlin.jvm.functions.Function1); + public void analyze(androidx.camera.core.ImageProxy); +} +# CameraX's public frame interfaces are implemented by the synthetic test source. +-keep interface androidx.camera.core.ImageProxy { *; } +-keep interface androidx.camera.core.ImageInfo { *; } diff --git a/app/src/androidTest/java/com/bitchat/android/ui/BarcodeScannerInstrumentedTest.kt b/app/src/androidTest/java/com/bitchat/android/ui/BarcodeScannerInstrumentedTest.kt new file mode 100644 index 000000000..819ddaf45 --- /dev/null +++ b/app/src/androidTest/java/com/bitchat/android/ui/BarcodeScannerInstrumentedTest.kt @@ -0,0 +1,122 @@ +package com.bitchat.android.ui + +import android.graphics.ImageFormat +import android.media.ImageReader +import android.media.ImageWriter +import android.os.Handler +import android.os.HandlerThread +import androidx.camera.core.ImageInfo +import androidx.camera.core.ImageProxy +import java.lang.reflect.Proxy +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Calls the same analyzer constructor as ScannerView, with minified ML Kit dependencies. */ +class BarcodeScannerInstrumentedTest { + @Test + fun scannerCreationCompletesAfterColdStart() { + QRCodeAnalyzer { } + } + + @Test + fun realAnalyzerDecodesSyntheticCameraFrameAndClosesIt() { + val decoded = AtomicReference() + val codeReceived = CountDownLatch(1) + val frameClosed = CountDownLatch(1) + val analyzer = QRCodeAnalyzer { + decoded.set(it) + codeReceived.countDown() + } + val thread = HandlerThread("synthetic-scanner-frame").apply { start() } + // A fixed, synthetic QR encodes "bitchat:test-scanner". No camera or permissions needed. + val modules = java.util.ArrayList() + val rows = java.util.StringTokenizer(QR_MODULES) + while (rows.hasMoreTokens()) modules.add(rows.nextToken()) + val scale = 8 + val size = modules.size * scale + val reader = ImageReader.newInstance(size, size, ImageFormat.YUV_420_888, 2) + val writer = ImageWriter.newInstance(reader.surface, 2) + try { + reader.setOnImageAvailableListener({ source -> + val image = source.acquireNextImage() ?: return@setOnImageAvailableListener + val info = Proxy.newProxyInstance(ImageInfo::class.java.classLoader, + arrayOf(ImageInfo::class.java)) { _, method, _ -> + when (method.name) { + "getRotationDegrees" -> 0 + else -> error("Unexpected frame info: ${method.name}") + } + } as ImageInfo + val proxy = Proxy.newProxyInstance(ImageProxy::class.java.classLoader, + arrayOf(ImageProxy::class.java)) { _, method, _ -> + when (method.name) { + "getImage" -> image + "getImageInfo" -> info + "close" -> { image.close(); frameClosed.countDown(); null } + else -> error("Unexpected frame access: ${method.name}") + } + } as ImageProxy + analyzer.analyze(proxy) + }, Handler(thread.looper)) + val input = writer.dequeueInputImage() + input.planes.forEachIndexed { planeIndex, plane -> + val planeSize = if (planeIndex == 0) size else size / 2 + for (y in 0 until planeSize) for (x in 0 until planeSize) { + val value = if (planeIndex != 0) 128 else + if (modules[y / scale][x / scale] == '1') 0 else 255 + plane.buffer.put(y * plane.rowStride + x * plane.pixelStride, value.toByte()) + } + } + writer.queueInputImage(input) + assertTrue("Real release scanner must decode the frame", codeReceived.await(20, TimeUnit.SECONDS)) + assertEquals("bitchat:test-scanner", decoded.get()) + assertTrue("Analyzer must release the camera frame", frameClosed.await(5, TimeUnit.SECONDS)) + } finally { + writer.close() + reader.close() + thread.quitSafely() + thread.join(5_000) + } + } + + private companion object { + const val QR_MODULES = """ + 000000000000000000000000000000000 + 000000000000000000000000000000000 + 000000000000000000000000000000000 + 000000000000000000000000000000000 + 000011111110110001001011111110000 + 000010000010000001000010000010000 + 000010111010100111010010111010000 + 000010111010110101100010111010000 + 000010111010111100111010111010000 + 000010000010010011100010000010000 + 000011111110101010101011111110000 + 000000000000010100000000000000000 + 000011110010100000101100111010000 + 000011001001110000101001000100000 + 000011101010000001101000100000000 + 000010001001000111011100011100000 + 000010100011110100010111111000000 + 000000110101011101111011100110000 + 000001101110100010100001110100000 + 000010111101011011000100100000000 + 000000101111110100001111101100000 + 000000000000101011011000100010000 + 000011111110000110001010110110000 + 000010000010010010011000110100000 + 000010111010010111001111100100000 + 000010111010100011100111110110000 + 000010111010101111011010110100000 + 000010000010110000000101011000000 + 000011111110101010110011001110000 + 000000000000000000000000000000000 + 000000000000000000000000000000000 + 000000000000000000000000000000000 + 000000000000000000000000000000000 + """ + } +} diff --git a/app/src/androidTest/java/com/bitchat/android/ui/ReleaseScannerTestRunner.kt b/app/src/androidTest/java/com/bitchat/android/ui/ReleaseScannerTestRunner.kt new file mode 100644 index 000000000..1ab8b64ed --- /dev/null +++ b/app/src/androidTest/java/com/bitchat/android/ui/ReleaseScannerTestRunner.kt @@ -0,0 +1,29 @@ +package com.bitchat.android.ui + +import android.app.Application +import android.app.Instrumentation +import android.content.Context +import android.os.Bundle +import org.junit.runner.JUnitCore + +/** Minimal runner avoids instrumentation-only tracing dependencies removed by target R8. */ +class ReleaseScannerTestRunner : Instrumentation() { + override fun newApplication(cl: ClassLoader, className: String, context: Context): Application = + super.newApplication(cl, Application::class.java.name, context) + + override fun onCreate(arguments: Bundle?) { + super.onCreate(arguments) + start() + } + + override fun onStart() { + val result = JUnitCore.runClasses(BarcodeScannerInstrumentedTest::class.java) + val report = Bundle().apply { + putString("stream", buildString { + append("Tests run: ${result.runCount}, failures: ${result.failureCount}\n") + result.failures.forEach { append(it.trace).append('\n') } + }) + } + finish(if (result.wasSuccessful()) -1 else 0, report) + } +} diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index dfb4f2cc4..0948b5ff7 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,398 +1,378 @@ Relay URL,Latitude,Longitude -inbox.scuba323.com,40.8218,-74.45 -relay.pornstr.net,53.3498,-6.26031 -relay.btcforplebs.com,43.6532,-79.3832 -nostr-01.yakihonne.com,1.32123,103.695 -relay.agentry.com,42.8864,-78.8784 -offchain.pub:443,39.1585,-94.5728 -dev.relay.stream,43.6532,-79.3832 -cyberspace.nostr1.com,40.7057,-74.0136 -relay.shadowbip.com,54.352,18.6466 -nip85-staging.nosfabrica.com,39.0997,-94.5786 -portal-relay.pareto.space,53.5488,9.98717 -relay.ru.ac.th,13.7607,100.627 -nostr-relay.corb.net,39.6478,-104.988 -nostrelay.circum.space,52.2245,8.826 -relay.satsapp.me,50.4754,12.3683 -nostr-01.uid.ovh,50.9871,2.12554 -nostr.dzo-hadar.ts.net,35.6764,139.65 -vault.iris.to:443,43.6532,-79.3832 -relay.nmail.li,50.9871,2.12554 -nostr.janx.com,43.6532,-79.3832 -relay.satmaxt.xyz:443,43.6532,-79.3832 -nos.lol:443,50.4754,12.3683 -nostr.mad-social.net,50.4754,12.3683 -relay.flashapp.me,43.6548,-79.3885 -relay.layer.systems,43.6532,-79.3832 -relay.novospes.com,43.6532,-79.3832 -relay.satmaxt.xyz,43.6532,-79.3832 -relay.getvia.xyz,60.1699,24.9384 -vidar.nestoris.online,52.3676,4.90414 -nostr.yutakobayashi.com,43.6532,-79.3832 -dev-relay.nostreon.com,60.1699,24.9384 -nostrcity-club.fly.dev:443,38.7946,-106.535 -nostr.oxtr.dev,50.4754,12.3683 -relay.lacrypta.ar,43.6532,-79.3832 -node.kommonzenze.de,49.4521,11.0767 -bitchat.nostr1.com,40.7057,-74.0136 -nostr.trepechov.com,49.4543,11.0746 -articles.layer3.news:443,37.3387,-121.885 -relay.ditto.pub,43.6532,-79.3832 -treuzkas.branruz.com,48.8575,2.35138 -nostr.plantroon.com:443,50.1013,8.62643 -nostr.mas-family.eu,60.3478,15.7505 -relay.nostrfy.org,35.6916,139.768 -relay.nostrmap.net,60.1699,24.9384 -woods-leader-permits-yamaha.trycloudflare.com,43.6532,-79.3832 -nostr.middling.mydns.jp,35.8099,140.12 -adre.su,59.9311,30.3609 -relay.s-w.art,43.6532,-79.3832 -nostr.thebiglake.org,32.71,-96.6745 -relay.satlantis.io,39.0438,-77.4874 -relay.routstr.com,59.4016,17.9455 -relay.nostr.space,43.6532,-79.3832 -relay.wavlake.com:443,41.2619,-95.8608 -relay.islandbitcoin.com,20.3898,78.0903 -nostr.tabordalab.com,60.1699,24.9384 -nostr-relay.cbrx.io,43.6532,-79.3832 -testnet.samt.st,43.6532,-79.3832 -temp.iris.to,43.6532,-79.3832 -freelay.sovbit.host,60.1699,24.9384 -relay.grammstr.com,38.7946,-106.535 -relay.nostronchain.com,43.6532,-79.3832 -relay.hazeem.xyz,43.6532,-79.3832 -relayone.geektank.ai,39.0997,-94.5786 -nostr-relay.corb.net:443,39.6478,-104.988 -relay.olas.app,60.1699,24.9384 -relay.44billion.net,43.6532,-79.3832 -relay.mitosystem.net,-23.5558,-46.6396 -relay.ltgnet.work:8443,51.0511,-114.075 -nostr.chaima.info:443,50.1109,8.68213 -nostrcity-club.fly.dev,38.7946,-106.535 -nostr.sathoarder.com,48.5734,7.75211 -relay.fountain.fm:443,43.6532,-79.3832 -nostr.txlyre.website,60.1699,24.9384 -nostr.planix.org,43.6532,-79.3832 -relay.chorus.community,50.1109,8.68213 -dev-relay.nostr.space,43.6532,-79.3832 -relay.satsmarkt.club,52.3676,4.90414 -relay.bullishbounty.com,43.6532,-79.3832 -relay.lanacoin-eternity.com,40.8302,-74.1299 -social.amanah.eblessing.co,48.1046,11.6002 -relay-arg.zombi.cloudrodion.com,1.35208,103.82 -relay.primal.net,43.6532,-79.3832 -nostr-relay.zimage.com,34.282,-118.439 -relay.veganostr.com:443,60.1699,24.9384 -relay.hivetalk.org,40.8302,-74.1299 -nostr.whitenode45.ddns.net,40.55,-74.4758 -nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 -relay.edufeed.org,49.4521,11.0767 -nostr.pbfs.io:443,50.4754,12.3683 -relay.substation.ninja,43.6532,-79.3832 -nostr.liberty.fans,36.8767,-89.5879 -nostr-verified.wellorder.net,45.5201,-122.99 -relay.hivescope.xyz,40.4332,-3.62431 -nostr2.girino.org:443,43.6532,-79.3832 -relay.cloistr.xyz,42.337,-77.3178 -relay.agora.social,50.7383,15.0648 -relayone.soundhsa.com,39.0997,-94.5786 -nostr.0x7e.xyz,47.4949,8.71954 -nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 -relay-na1.metanomalist.com,43.6532,-79.3832 -relay.bitmacro.cloud,43.6532,-79.3832 -relay.iyou.me,64.1466,-21.9426 -nostr.bitcoiner.social,47.6743,-117.112 -relay.wisp.talk:443,49.4543,11.0746 -relay.valleirug.nl,52.0274,5.62427 -chat-relay.zap-work.com,43.6532,-79.3832 -nostr.2b9t.xyz,34.0549,-118.243 -groups.beginningend.com,35.2227,-97.4786 -relay.edufeed.org:443,49.4521,11.0767 -nrs-02.darkcloudarcade.com,39.9526,-75.1652 -relay.angor.io,48.1046,11.6002 -relay.jmoose.rocks,32.7767,-96.797 -nostr.mom,50.4754,12.3683 -x.kojira.io,43.6532,-79.3832 -cs-relay.nostrdev.com:443,50.4754,12.3683 -nostr.computingcache.com,45.5341,-122.956 -relay-dev.satlantis.io,39.0438,-77.4874 -nostr.jcera.eu,43.6532,-79.3832 -relay.aarpia.com,37.3986,-121.964 -relay.nostrian-conquest.com,41.223,-111.974 -relay.fiatdenier.com,43.7787,-79.5393 -relayrs.notoshi.win:443,43.6532,-79.3832 -bread.nostrsms.com,40.8218,-74.45 -test.relays.nuts.cash,52.3676,4.90414 -nostrrelay.taylorperron.com,45.5029,-73.5723 -relay.jmoose.rocks:443,32.7767,-96.797 -relay.tapestry.ninja,40.8054,-74.0241 -nostr.hetuniversum.nl,43.6532,-79.3832 -public.crostr.com:443,43.6532,-79.3832 -nostr.trevena.info,43.6532,-79.3832 -relay.chorus.community:443,50.1109,8.68213 -relay.sharegap.net,43.6532,-79.3832 -relayone.geektank.ai:443,39.0997,-94.5786 -nip85.nosfabrica.com,39.0997,-94.5786 -yabu.me,35.6096,139.73 -nostr.bitcoiner.social:443,47.6743,-117.112 -support.flotilla.social,32.9483,-96.7299 -strfry.bonsai.com:443,39.0438,-77.4874 -nostr-relayflare.enpitsulin.workers.dev,43.6532,-79.3832 -relay.crovaxon.de,53.0777,7.41516 -relay.froth.zone,60.1699,24.9384 -r.senx.io,50.6924,3.20113 -relay2.nostrasia.net,43.6532,-79.3832 -relay.decentralia.fr,48.122,11.589 -relay.mappingbitcoin.com,43.6532,-79.3832 -nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 -chorus.mikedilger.com:444,-36.8906,174.794 -nostr.wild-vibes.ts.net,48.8566,2.35222 -spamspamspamspam.rest,43.6532,-79.3832 -relay.wisp.talk,49.4543,11.0746 -relay.bornheimer.app,50.1109,8.68213 -nostr-dev.wellorder.net,45.5201,-122.99 -strfry.shock.network:443,39.0438,-77.4874 -nostr.mikoshi.de,52.52,13.405 -nostr.christiansass.de,50.1109,8.68213 -relay.14898.hns.one,42.8864,-78.8784 -porchlight.social,43.6532,-79.3832 -nostr.twinkle.lol,51.902,7.6657 -nostr.pbfs.io,50.4754,12.3683 -zencrown.relay2aeon.quest,52.3061,4.6907 -21milionidinostr.duckdns.org,45.519,9.08615 -test-relay.uid.ovh,48.5734,7.75211 -nexus.libernet.app,43.6532,-79.3832 -bitcoiner.social,47.6743,-117.112 -syb.lol,43.6532,-79.3832 -relay.directsponsor.net,42.8864,-78.8784 -budabit.nostr1.com,40.7057,-74.0136 -relay.budabit.club,49.4543,11.0746 -relay.monostr.com,50.4754,12.3683 -relay.rebelbase.social,50.965,5.50082 -nostr.hifish.org,47.4009,8.57814 -nostr.21crypto.ch,47.5356,8.73209 -nostr.emanuelemiani.it,45.778,8.79414 -relay.kizuna-miki.com,35.694,139.754 -relay.binaryrobot.com:443,43.6532,-79.3832 -spurline.safebox.dev,43.6532,-79.3832 -buzz.cashu.space,43.6532,-79.3832 -relay.plebeian.market,50.1109,8.68213 -relay.mleku.dev,32.7767,-96.797 -nostr.purpura.cloud,43.6532,-79.3832 -relay.binaryrobot.com,43.6532,-79.3832 -relay.paulstephenborile.com,49.4543,11.0746 -relay2.fiatdenier.com,50.1013,8.62643 -cs-relay.nostrdev.com,50.4754,12.3683 -nostr.davenov.com,50.1109,8.68213 -relay.nostrhub.fr,48.1045,11.6004 -nostr.spaceshell.xyz,43.6532,-79.3832 -relay.gorrdy.cz,50.1109,8.68213 -relay.nostrmap.net:443,60.1699,24.9384 -relay.getsafebox.app,43.6532,-79.3832 -relay.lanavault.space,60.1699,24.9384 -relay.veganostr.com,60.1699,24.9384 -prl.plus,54.9486,21.7283 -relay.nostr.blockhenge.com,39.0438,-77.4874 -nostr.quali.chat,60.1699,24.9384 -nostr.nodesmap.com,59.3327,18.0656 -relay.beginningend.com,35.2227,-97.4786 -relay2.0xprivacy.online,43.6532,-79.3832 -relay1.privkey.io,25.7617,-80.1918 -relay-dev.gulugulu.moe:443,43.6532,-79.3832 -relay.lotek-distro.com,43.6532,-79.3832 -relay.noeudlibre.fr,50.6924,3.20113 -relay.cyberguy.fyi,52.3676,4.90414 -nostr-relay.xbytez.io,50.6924,3.20113 -relay.kaleidoswap.com,47.3769,8.54169 -relay.bnos.space,43.6532,-79.3832 -purplerelay.com,43.6532,-79.3832 -hasky.chat,40.7862,-74.0743 -relay.snort.social,53.3498,-6.26031 -nostr.linky.fit,50.1109,8.68213 -nostr.plantroon.com,50.1013,8.62643 -testr.nymble.world,40.8054,-74.0241 -nostr.tac.lol,47.4748,-122.273 -cdn.satellite.earth,40.8302,-74.1299 -dm-test-strfry-discovery.samt.st,43.6532,-79.3832 -wisp.djorivaltech.com,43.6532,-79.3832 -nostr.trcjr.xyz,35.3821,-78.5486 -offchain.pub,39.1585,-94.5728 -nostras.app,38.7946,-106.535 -relay.manneken.brussels,49.4543,11.0746 -relay.laantungir.net,-19.4692,-42.5315 -nostr-relay.xbytez.io:443,50.6924,3.20113 -relay.gulugulu.moe,43.6532,-79.3832 -relayrs.notoshi.win,43.6532,-79.3832 -vault.iris.to,43.6532,-79.3832 -relay.getsafebox.app:443,43.6532,-79.3832 -top.testrelay.top,43.6532,-79.3832 -relay.t6x.host,40.8302,-74.1299 -relay.vrtmrz.net:443,43.6532,-79.3832 -relay.ohstr.com,43.6532,-79.3832 -nostr.wecsats.io,43.6532,-79.3832 -ynostr.yael.at,60.1699,24.9384 -infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 -nostr.islandarea.net:443,35.4669,-97.6473 -relay.favillakey.io,-27.4705,153.026 -nostr.self-determined.de,51.4379,14.2355 -nostr-pub.wellorder.net,45.5201,-122.99 -relay.trotters.cc,43.6532,-79.3832 -nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -nostr2.girino.org,43.6532,-79.3832 -nostr.rtvslawenia.com,49.4543,11.0746 -nostr.stakey.net:443,52.6485,4.73787 -relay.artx.market,43.6548,-79.3885 -nos.lol,50.4754,12.3683 -relay.nearhood.co.uk,51.5134,-0.0890675 -nostr.pro.aw,43.6532,-79.3832 -relay.piazza.today,48.122,11.589 -relay.underorion.se,51.4998,-0.011072 -nostr.oxtr.dev:443,50.4754,12.3683 -relay.bitos.space,43.6532,-79.3832 -nostrelay.circum.space:443,52.2245,8.826 -relay.klabo.world,47.674,-122.122 -nostr.spicyz.io,43.6532,-79.3832 -relay.pyramid.li,47.4093,8.46503 -strfry.shock.network,39.0438,-77.4874 -relay.lightning.pub:443,39.0438,-77.4874 -nostr.snowbla.de,50.4754,12.3683 -relay.wavlake.com,41.2619,-95.8608 -relay2.orangesync.tech,40.7128,-74.006 -relay.mostro.network,40.8302,-74.1299 -relay-dev.gulugulu.moe,43.6532,-79.3832 -relay.orangesync.tech,40.7128,-74.006 -chat-relay.zap-work.com:443,43.6532,-79.3832 -relay.wasabiwallet.io,43.6532,-79.3832 -relay.kilombino.com,43.6532,-79.3832 -bridge.tagomago.me,42.3601,-71.0589 -relay.loveisbitcoin.com,43.6532,-79.3832 -relay.staging.plebeian.market,51.5072,-0.127586 -relay.nostrcade.com,37.79,-122.401 -relay.nostr.com,50.1109,8.68213 -relay02.lnfi.network,35.6764,139.65 -nostr.debate.report,50.1109,8.68213 -relay.gulugulu.moe:443,43.6532,-79.3832 -n.bogs.ccwu.cc,43.6532,-79.3832 -relay.twentyone.ist,43.7696,-79.3309 -nostr.mom:443,50.4754,12.3683 -relay.czas.plus,50.1109,8.68213 -relay.bullishbounty.com:443,43.6532,-79.3832 -no.str.cr,8.96171,-83.5246 -nostr.88mph.life,52.1941,-2.21905 -relay.trotters.cc:443,43.6532,-79.3832 -nostr.unkn0wn.world,47.4464,8.21073 -relay.dyne.org,49.0291,8.35705 -nostr.n7ekb.net,47.4941,-122.294 -nostr.sathoarder.com:443,48.5734,7.75211 -relay.guggero.org,46.5971,9.59652 -relay.bowlafterbowl.com,32.9483,-96.7299 -wheat.oslim.dev,43.6532,-79.3832 -nostr.myshosholoza.co.za,52.3676,4.90414 -nostr.tagomago.me,42.3601,-71.0589 -relay.snotr.nl:49999,51.9758,4.31389 -relay.lightning.pub,39.0438,-77.4874 -0x-nostr-relay.fly.dev,38.7946,-106.535 -relay.nostrconnect.com,41.5695,-83.9786 -relay-can.zombi.cloudrodion.com,43.6532,-79.3832 -relay.nostr.dev.br,48.1046,11.6002 -relay.mrmave.work,43.6532,-79.3832 -nostr.rtvslawenia.com:443,49.4543,11.0746 -nostr.data.haus,50.4754,12.3683 -nostr.chaima.info,50.1109,8.68213 -nostr.relay.hedwig.sh,60.1699,24.9384 -social.hns.one,42.8864,-78.8784 -relay.pacsarcade.org,32.7767,-96.797 -auth.nostr1.com,40.7057,-74.0136 -relayone.soundhsa.com:443,39.0997,-94.5786 -nostr.azzamo.net,52.2633,21.0283 -nostr.eu.freedomrelay.dev,49.0524,14.4347 -basspistol.org,49.0291,8.35696 -relay.nostrops.com,32.71,-96.6745 -dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 -schnorr.me,43.6532,-79.3832 -no.str.cr:443,8.96171,-83.5246 -nostr.snowbla.de:443,50.4754,12.3683 -relay.openspecs.uid.ovh,50.9871,2.12554 -nostr.nestoris.online,55.568,37.5853 -relay.plebeian.market:443,50.1109,8.68213 -relay.mwaters.net,50.9871,2.12554 -relay.conduit.market,38.7946,-106.535 -relay.wellorder.net,45.5201,-122.99 -relay.nostriot.com,41.5695,-83.9786 -nostr.tbxnetworx.de,47.8226,10.0064 -relay.zone667.com,60.1699,24.9384 -public.crostr.com,43.6532,-79.3832 -relay.colombiap2p.com,43.6532,-79.3832 -soloco.nl,43.6532,-79.3832 -relay.orly.dev,32.7767,-96.797 -relay01.lnfi.network,35.6764,139.65 -relay-fra.zombi.cloudrodion.com,48.8566,2.35222 -nostr.notribe.net,40.8302,-74.1299 -relay.shychat.lol,39.8965,-80.1792 -relay.cypherguide.org,1.32123,103.695 -strfry.bonsai.com,39.0438,-77.4874 -relay.decentnewsroom.com,50.4754,12.3683 -relay.atsocy.com,43.6532,-79.3832 -scores.brainstorm.world,39.0997,-94.5786 -relay.nostr.bond,40.1885,29.061 -nostr.fullstackcash.net,45.5201,-122.99 -nostr.islandarea.net,35.4669,-97.6473 -relay.illuminodes.com,43.6532,-79.3832 -relay.samt.st,40.8302,-74.1299 -relay.pprgb.app,52.52,13.405 -relay.jcera.eu,43.6532,-79.3832 -relay.nostreon.com,60.1699,24.9384 -myvoiceourstory.org,44.8484,-123.234 -relay.mypathtofire.de,42.8864,-78.8784 -nostr.stakey.net,52.6485,4.73787 -schnorr.me:443,43.6532,-79.3832 -conduit-congee.fly.dev,38.7946,-106.535 -offchain.bostr.online,43.6532,-79.3832 -staging.yabu.me,35.6096,139.73 nostr.synalysis.com,50.4754,12.3683 -relay.scuba323.com,40.8218,-74.45 -nostr.hoppe-relay.it.com,42.8864,-78.8784 -relay.paulstephenborile.com:443,49.4543,11.0746 -memlay.v0l.io,53.3498,-6.26031 -nostr-02.uid.ovh,50.9871,2.12554 -articles.layer3.news,37.3387,-121.885 -nostr.2b9t.xyz:443,34.0549,-118.243 -relay.chatbett.de,40.7128,-74.006 -relay.staging.plebeian.market:443,51.5072,-0.127586 -relay.nostr.black,43.6532,-79.3832 -relay.solomonstr.com,43.6532,-79.3832 -relay.nostr.net,43.6532,-79.3832 -dm-test-strfry-generic.samt.st,43.6532,-79.3832 -nostr.girino.org,43.6532,-79.3832 -bcast.girino.org,43.6532,-79.3832 -n.kojira.io,43.6532,-79.3832 -relay.sigit.io,50.4754,12.3683 -bruh.samt.st,43.6532,-79.3832 -relay.i9.eti.br,43.6532,-79.3832 -relay.degmods.com,50.4754,12.3683 -pymd.kamp.site,38.9072,-77.0369 -relay.internationalright-wing.org,-22.4692,-48.9875 -relay.earthly.city,34.1749,-118.54 -relay-us.zombi.cloudrodion.com,40.7862,-74.0743 -relay.yoinekodo.jp,43.6532,-79.3832 -cache.trustr.ing,43.6548,-79.3885 -relay2.angor.io,48.1046,11.6002 -relay.lanacoin-eternity.com:443,40.8302,-74.1299 -relay.nostar.org,43.6532,-79.3832 -nostr-kyomu-haskell.onrender.com,37.7775,-122.397 -relay.nostx.io,43.6532,-79.3832 -relay.sincensura.org,43.6532,-79.3832 -relay.qstr.app,50.1109,8.68213 -relay2.veganostr.com,60.1699,24.9384 -denimroad.feeds.relay.tools,38.6327,-90.1961 +relay.nostr-check.me,43.6532,-79.3832 +nostr.eu.freedomrelay.dev,49.0524,14.4347 +relay.44billion.net,43.6532,-79.3832 +nostr.debate.report,50.1109,8.68213 +relayrs.notoshi.win:443,43.6532,-79.3832 +top.testrelay.top,43.6532,-79.3832 +relay.satsmarkt.club,52.3676,4.90414 +nostr.plantroon.com:443,50.1013,8.62643 +relay.lightning.pub,39.0438,-77.4874 +nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 +auth.nostr1.com,40.7057,-74.0136 +offchain.pub:443,39.1585,-94.5728 relay.fundstr.me,42.3601,-71.0589 -openrelay.ziomc.com,41.8967,12.4822 -nostrride.io,37.3986,-121.964 -relay.vrtmrz.net,43.6532,-79.3832 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -relay.keykeeper.world,40.7824,-74.0711 -relay.minibolt.info,43.6532,-79.3832 +relay.pacsarcade.org,32.7767,-96.797 +relay.tapestry.ninja,40.8054,-74.0241 +nostr.mom:443,50.4754,12.3683 +strfry.bonsai.com,39.0438,-77.4874 +relay.zone667.com,60.1699,24.9384 +relay.directsponsor.net,42.8864,-78.8784 +relay.veganostr.com,60.1699,24.9384 +relay.nostrmap.net:443,60.1699,24.9384 +relay.nostrconnect.com,41.5695,-83.9786 +nostr.hifish.org,47.4009,8.57814 +nostr.dzo-hadar.ts.net,35.6764,139.65 +relay.openspecs.uid.ovh,50.9871,2.12554 +no.str.cr,8.96171,-83.5246 +nostr.mikoshi.de,51.2829,6.7827 +nostr.chaima.info,50.1109,8.68213 +relay.wavlake.com:443,41.2619,-95.8608 +nostr-relay.xbytez.io,50.6924,3.20113 +relay5.bitransfer.org,43.6532,-79.3832 +relay.degmods.com,50.4754,12.3683 +nostr-relay.xbytez.io:443,50.6924,3.20113 +relay.lanacoin-eternity.com,40.8302,-74.1299 +relay.illuminodes.com,43.6532,-79.3832 +nostr.overmind.lol,43.6532,-79.3832 +nostr.hetuniversum.nl,43.6532,-79.3832 +relay.samt.st,40.8302,-74.1299 +nos.lol,50.4754,12.3683 +relay.valleirug.nl,52.0274,5.62427 +relay.nostar.org,43.6532,-79.3832 +wheat.oslim.dev,43.6532,-79.3832 +nostr.thebiglake.org,32.71,-96.6745 +relay.budabit.club,49.4543,11.0746 +cdn.satellite.earth,40.8302,-74.1299 +n.kojira.io,43.6532,-79.3832 +dm-test-strfry-discovery.samt.st,43.6532,-79.3832 +relay.nostrfy.org,35.6916,139.768 +nostr.janx.com,43.6532,-79.3832 +nostr.mad-social.net,50.4754,12.3683 +nostr.girino.org,43.6532,-79.3832 +relay.jmoose.rocks,32.7767,-96.797 +nostr.2b9t.xyz:443,34.0549,-118.243 +chat-relay.zap-work.com,43.6532,-79.3832 +relayrs.notoshi.win,43.6532,-79.3832 +relay1.privkey.io,25.7617,-80.1918 +nostr.emanuelemiani.it,45.778,8.79414 +relay.snort.social,53.3498,-6.26031 +relay.ohstr.com,43.6532,-79.3832 +relay.lightning.pub:443,39.0438,-77.4874 +nostr.islandarea.net:443,35.4669,-97.6473 +nostr.tagomago.me,42.3601,-71.0589 +fanfares.nostr1.com:443,40.7057,-74.0136 +nostr.snowbla.de:443,50.4754,12.3683 +vault.iris.to:443,43.6532,-79.3832 +test.relays.nuts.cash,52.3676,4.90414 +relay-us.zombi.cloudrodion.com,40.7862,-74.0743 +relay.nostrmap.net,60.1699,24.9384 +relay.nostrcade.com,37.79,-122.401 +relay.crovaxon.de,53.0777,7.41516 +relay.flashapp.me,43.6548,-79.3885 +nostr.oxtr.dev:443,50.4754,12.3683 +relay.decentnewsroom.com,50.4754,12.3683 +relay.liberbitworld.org,43.6532,-79.3832 +relayone.soundhsa.com:443,39.0997,-94.5786 bucket.coracle.social,37.7775,-122.397 -nrl.ceskar.xyz,50.5145,16.0119 +relay.lanavault.space,60.1699,24.9384 +public.crostr.com,43.6532,-79.3832 +relay.thehakon.com,43.6532,-79.3832 +relay.wisp.talk,49.4543,11.0746 +nostr.trevena.info,43.6532,-79.3832 +relay.qstr.app,50.1109,8.68213 +relay.chorus.community:443,50.1109,8.68213 +relay.plebeian.market:443,50.1109,8.68213 +relay.nostrops.com,32.71,-96.6745 +nostr.chaima.info:443,50.1109,8.68213 +relay.nostr.dev.br,48.1046,11.6002 +relay.camelus.app,45.5201,-122.99 +nostr-pub.wellorder.net,45.5201,-122.99 +cyberspace.nostr1.com,40.7057,-74.0136 +relay2.angor.io,48.1046,11.6002 +relay.edufeed.org:443,49.4521,11.0767 +relay.bnos.space,43.6532,-79.3832 +groups.beginningend.com,35.2227,-97.4786 +relay.gulugulu.moe:443,43.6532,-79.3832 +relayone.soundhsa.com,39.0997,-94.5786 +relay.noeudlibre.fr,50.6924,3.20113 +nostr.christiansass.de,50.1109,8.68213 +chat-relay.zap-work.com:443,43.6532,-79.3832 +conduit-congee.fly.dev,38.7946,-106.535 +relay.satmaxt.xyz:443,43.6532,-79.3832 +relay.rebelbase.social,50.965,5.50082 +nostr.relay.hedwig.sh,60.1699,24.9384 +dev.relay.stream,43.6532,-79.3832 +relay.edufeed.org,49.4521,11.0767 +relay.s-w.art,43.6532,-79.3832 +nostr.wecsats.io,43.6532,-79.3832 +relay.nostreon.com,60.1699,24.9384 +relay.agentry.com,42.8864,-78.8784 +denimroad.feeds.relay.tools,38.6327,-90.1961 +relay.artx.market,43.6548,-79.3885 +nostr.unkn0wn.world,47.4464,8.21073 +relay.mappingbitcoin.com,43.6532,-79.3832 +relay-na1.metanomalist.com,43.6532,-79.3832 +nostr.2b9t.xyz,34.0549,-118.243 +relay.artio.inf.unibe.ch,46.9501,7.43678 +relay.mostro.network,40.8302,-74.1299 +relay.nmail.li,50.9871,2.12554 +bruh.samt.st,43.6532,-79.3832 +relay.conduit.market,38.7946,-106.535 +relay.mwaters.net,50.9871,2.12554 +nostr.twinkle.lol,51.902,7.6657 +relay.selfrule.co.za,43.6532,-79.3832 +relay.btcforplebs.com,43.6532,-79.3832 +relay.plebeian.market,50.1109,8.68213 +nostr.n7ekb.net,47.4941,-122.294 +nostr.easycryptosend.it,43.6532,-79.3832 +relay.primal.net,43.6532,-79.3832 +dev-relay.nostreon.com,60.1699,24.9384 +nostr-kyomu-haskell.onrender.com,37.7775,-122.397 +relay.kilombino.com,43.6532,-79.3832 +nostr.bitcoiner.social:443,47.6743,-117.112 +relay.underorion.se,51.4998,-0.011072 +relay.paulstephenborile.com:443,49.4543,11.0746 +relay.nostr.net,43.6532,-79.3832 +syb.lol,43.6532,-79.3832 +articles.layer3.news,37.3387,-121.885 +ec1.f7z.io,60.1699,24.9384 +relay.manneken.brussels,49.4543,11.0746 +nostr.tac.lol,47.4748,-122.273 +relay.agora.social,50.7383,15.0648 +nostr.oxtr.dev,50.4754,12.3683 +relay.hivetalk.org,40.8302,-74.1299 +income-strongly-active-systematic.trycloudflare.com,43.6532,-79.3832 +nostr.jcera.eu,43.6532,-79.3832 +relay-dev.satlantis.io,39.0438,-77.4874 +nostr.sovereignservices.xyz,43.6532,-79.3832 +relay.lacrypta.ar,43.6532,-79.3832 +nostr.data.haus,50.4754,12.3683 +relay.monostr.com,50.4754,12.3683 +relay.nostrhub.fr,48.1045,11.6004 +relay.loveisbitcoin.com,43.6532,-79.3832 +portal-relay.pareto.space,53.5488,9.98717 +relay.minibolt.info,43.6532,-79.3832 +relay.czas.plus,50.1109,8.68213 +relay2.veganostr.com,60.1699,24.9384 +relay.gorrdy.cz,50.1109,8.68213 +relay.paulstephenborile.com,49.4543,11.0746 +temp.iris.to,43.6532,-79.3832 +relay.formstr.app,43.6532,-79.3832 +talon.quest,43.6532,-79.3832 +relay.ltgnet.work:8443,51.0511,-114.075 +relay.guggero.org,46.5971,9.59652 +relay.nostr.bond,40.1885,29.061 +bitchat.nostr1.com,40.7057,-74.0136 +prl.plus,54.9486,21.7283 +relay.sharegap.net,43.6532,-79.3832 +nostr.21crypto.ch,47.5356,8.73209 +relay.jcera.eu,43.6532,-79.3832 +r.senx.io,50.6924,3.20113 +porchlight.social,43.6532,-79.3832 +bread.nostrsms.com,40.8218,-74.45 +relay.nostrian-conquest.com,41.223,-111.974 +relay.getsafebox.app:443,43.6532,-79.3832 +nostr.quali.chat,60.1699,24.9384 +relay.laantungir.net,-19.4692,-42.5315 +relay.hivescope.xyz,40.4332,-3.62431 +nostr.fullstackcash.net,45.5201,-122.99 +nip85-staging.nosfabrica.com,39.0997,-94.5786 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 +relay.nostr.black,43.6532,-79.3832 +nostr.trepechov.com,49.4543,11.0746 +relay.binaryrobot.com:443,43.6532,-79.3832 +basspistol.org,49.0291,8.35696 +free.relayted.de,50.1109,8.68213 +relay.beginningend.com,35.2227,-97.4786 +nostrelay.circum.space:443,52.2245,8.826 +strfry.bonsai.com:443,39.0438,-77.4874 +relay.wasabiwallet.io,43.6532,-79.3832 +relay.scuba323.com,40.8218,-74.45 +vidar.nestoris.online,52.3676,4.90414 +nostr.pbfs.io:443,50.4754,12.3683 +relay.pyramid.li,47.4093,8.46503 +relay.olas.app,60.1699,24.9384 +relay.internationalright-wing.org,-22.4692,-48.9875 +nostr-relay.corb.net,39.6478,-104.988 +relay.plebdevs.com,40.7128,-74.006 +relay.ditto.pub,43.6532,-79.3832 +relay.i9.eti.br,43.6532,-79.3832 +nostr.pro.aw,43.6532,-79.3832 +relay.hazeem.xyz,43.6532,-79.3832 +zencrown.relay2aeon.quest,52.3061,4.6907 +relay.npubhaus.com,43.6532,-79.3832 +relay.nostriot.com,41.5695,-83.9786 +cs-relay.nostrdev.com,50.4754,12.3683 +relay.substation.ninja,43.6532,-79.3832 +relay1.nostrchat.io,60.1699,24.9384 +nostr-relay.cbrx.io,43.6532,-79.3832 +nostr.stakey.net:443,52.6485,4.73787 +relayone.geektank.ai:443,39.0997,-94.5786 +adre.su,59.9311,30.3609 +nostr.islandarea.net,35.4669,-97.6473 +relay.cypherguide.org,1.32123,103.695 +fanfares.nostr1.com,40.7057,-74.0136 +nostrride.io,43.6532,-79.3832 relay.littlebitstudios.com,34.8688,-86.5502 +relay.snotr.nl:49999,51.9758,4.31389 +relay02.lnfi.network,35.6764,139.65 +nostr.rtvslawenia.com,49.4543,11.0746 +relay.staging.plebeian.market:443,51.5072,-0.127586 +relay.earthly.city,34.1749,-118.54 +relay.binaryrobot.com,43.6532,-79.3832 +relay.artiostr.ch,43.6532,-79.3832 +nostr.nestoris.online,55.568,37.5853 +nostr.liberty.fans,36.8767,-89.5879 +n.bogs.ccwu.cc,43.6532,-79.3832 +relay.t6x.host,40.8302,-74.1299 +relay.nostrverse.net,43.6532,-79.3832 +relay.bullishbounty.com:443,43.6532,-79.3832 +nostr.mom,50.4754,12.3683 +scores.brainstorm.world,39.0997,-94.5786 +nostr.tabordalab.com,60.1699,24.9384 +relay.favillakey.io,-27.4705,153.026 +relay.nostr.blockhenge.com,39.0438,-77.4874 +relay.bullishbounty.com,43.6532,-79.3832 +vm-1734.lnvps.cloud,53.3498,-6.26031 +nostr.bitcoiner.social,47.6743,-117.112 +budabit.nostr1.com,40.7057,-74.0136 +soloco.nl,43.6532,-79.3832 +relay.iyou.me,64.1466,-21.9426 +dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 +relay.cloistr.xyz,42.337,-77.3178 +relay.nearhood.co.uk,51.5134,-0.0890675 +offchain.bostr.online,43.6532,-79.3832 +relay.keykeeper.world,40.7824,-74.0711 +nostras.app,38.7946,-106.535 +nostr.wild-vibes.ts.net,48.8566,2.35222 +nostr-relayflare.enpitsulin.workers.dev,43.6532,-79.3832 +relay.lanacoin-eternity.com:443,40.8302,-74.1299 +greenart7c3.feeds.relay.tools,38.6327,-90.1961 +relay.mitosystem.net,-23.5558,-46.6396 +nostr.snowbla.de,50.4754,12.3683 +cs-relay.nostrdev.com:443,50.4754,12.3683 +nostrelay.circum.space,52.2245,8.826 +0x-nostr-relay.fly.dev,38.7946,-106.535 +nip85.nosfabrica.com,39.0997,-94.5786 +chorus.mikedilger.com:444,-36.8906,174.794 +nostr.rtvslawenia.com:443,49.4543,11.0746 +yabu.me,35.6096,139.73 +nostr.planix.org,43.6532,-79.3832 +relay2.orangesync.tech,40.7128,-74.006 +relay.dyne.org,49.0291,8.35705 +relay.aarpia.com,37.3986,-121.964 +relay.fiatdenier.com,43.7787,-79.5393 +relay.wisp.talk:443,49.4543,11.0746 +nostr.azzamo.net,52.2633,21.0283 +relay.satmaxt.xyz,43.6532,-79.3832 +relay.jmoose.rocks:443,32.7767,-96.797 +nostr-01.uid.ovh,50.9871,2.12554 +nostr-relay.corb.net:443,39.6478,-104.988 +buzz.cashu.space,43.6532,-79.3832 +dev-relay.nostr.space,43.6532,-79.3832 +relay.cyberguy.fyi,52.3676,4.90414 +freelay.sovbit.host,60.1699,24.9384 +hasky.chat,40.7862,-74.0743 +treuzkas.branruz.com,48.8575,2.35138 +nostr-verified.wellorder.net,45.5201,-122.99 +nexus.libernet.app,43.6532,-79.3832 +relay.gulugulu.moe,43.6532,-79.3832 +bitcoiner.social,47.6743,-117.112 +nostr.stakey.net,52.6485,4.73787 +schnorr.me,43.6532,-79.3832 +nos.xmark.cc,50.6916,3.20151 +vault.iris.to,43.6532,-79.3832 +nostr.nodesmap.com,59.3327,18.0656 +nostr.88mph.life,52.1941,-2.21905 +nostr.yutakobayashi.com,43.6532,-79.3832 +relay.layer.systems,43.6532,-79.3832 +relay-can.zombi.cloudrodion.com,43.6532,-79.3832 +bcast.girino.org,43.6532,-79.3832 +relay.chorus.community,50.1109,8.68213 +strfry.shock.network,39.0438,-77.4874 +ynostr.yael.at,60.1699,24.9384 +relay.staging.plebeian.market,51.5072,-0.127586 +cache.trustr.ing,43.6548,-79.3885 +nos.lol:443,50.4754,12.3683 +relay.shadowbip.com,54.352,18.6466 +nostr.computingcache.com,45.5341,-122.956 +memlay.v0l.io,53.3498,-6.26031 +relay.colombiap2p.com,43.6532,-79.3832 +dm-test-strfry-generic.samt.st,43.6532,-79.3832 +borgar.lol,52.3676,4.90414 +relay.wellorder.net,45.5201,-122.99 +node.kommonzenze.de,49.4521,11.0767 +relay.ru.ac.th,13.7607,100.627 +nostr.davenov.com,50.1109,8.68213 +nostr.linky.fit,50.1109,8.68213 +nostr-relay.zimage.com,34.282,-118.439 +relay.nostx.io,43.6532,-79.3832 +relay.dwadziesciajeden.pl,52.2297,21.0122 +relay.pornstr.net,53.3498,-6.26031 +nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 +schnorr.me:443,43.6532,-79.3832 +nostr.plantroon.com,50.1013,8.62643 +relay.routstr.com,59.4016,17.9455 +relay.shychat.lol,39.8965,-80.1792 +nr.yay.so,46.2126,6.1154 +pymd.kamp.site,38.9072,-77.0369 +relay.decentralia.fr,48.122,11.589 +relay.klabo.world,47.674,-122.122 +spurline.safebox.dev,43.6532,-79.3832 +bridge.tagomago.me,42.3601,-71.0589 +inbox.scuba323.com,40.8218,-74.45 +no.str.cr:443,8.96171,-83.5246 +nostr2.girino.org:443,43.6532,-79.3832 +nostr-dev.wellorder.net,45.5201,-122.99 +relay.mypathtofire.de,42.8864,-78.8784 +nostr-02.uid.ovh,50.9871,2.12554 +support.flotilla.social,32.9483,-96.7299 +relay.mccormick.cx,52.3563,4.95714 +relay.getsafebox.app,43.6532,-79.3832 +relay.bitos.space,43.6532,-79.3832 +nostrcity-club.fly.dev,38.7946,-106.535 +spamspamspamspam.rest,43.6532,-79.3832 +relay.solomonstr.com,43.6532,-79.3832 +relay.angor.io,48.1046,11.6002 +relay.islandbitcoin.com,20.3898,78.0903 +relay.bornheimer.app,50.1109,8.68213 +relay.atsocy.com,43.6532,-79.3832 +relay.wavlake.com,41.2619,-95.8608 +relay.nostrcheck.me,43.6532,-79.3832 +relay.bowlafterbowl.com,32.9483,-96.7299 +relay.novospes.com,43.6532,-79.3832 +nostr.spaceshell.xyz,43.6532,-79.3832 +purplerelay.com,43.6532,-79.3832 +relay.satsapp.me,50.4754,12.3683 +nostr.pbfs.io,50.4754,12.3683 +relay2.nostrasia.net,43.6532,-79.3832 +relay.piazza.today,48.122,11.589 +relay.nostr.space,43.6532,-79.3832 relay.duck1123.com,43.6532,-79.3832 +relay.kizuna-miki.com,35.694,139.754 +relay.dreamith.to,43.6532,-79.3832 +relay.chatbett.de,40.7128,-74.006 +public.crostr.com:443,43.6532,-79.3832 +relay.satlantis.io,39.0438,-77.4874 +nostr2.girino.org,43.6532,-79.3832 +relay.lotek-distro.com,43.6532,-79.3832 +nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 +nostrrelay.taylorperron.com,45.5029,-73.5723 +nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 +nostr.spicyz.io,43.6532,-79.3832 +x.kojira.io,43.6532,-79.3832 +relay.veganostr.com:443,60.1699,24.9384 +nostr.alex71btc.com,43.6532,-79.3832 +relay.thewired.app,39.0438,-77.4874 +relay.getvia.xyz,60.1699,24.9384 +relayone.geektank.ai,39.0997,-94.5786 +nostr.hoppe-relay.it.com,42.8864,-78.8784 +relay2.0xprivacy.online,43.6532,-79.3832 +nostr-01.yakihonne.com,1.32123,103.695 +offchain.pub,39.1585,-94.5728 +nostrcity-club.fly.dev:443,38.7946,-106.535 +testr.nymble.world,40.8054,-74.0241 +relay.fountain.fm,43.6532,-79.3832 +relay.nostr.com,50.1109,8.68213 diff --git a/app/src/main/java/com/bitchat/android/BitchatApplication.kt b/app/src/main/java/com/bitchat/android/BitchatApplication.kt index 7b5926efc..b03bdd664 100644 --- a/app/src/main/java/com/bitchat/android/BitchatApplication.kt +++ b/app/src/main/java/com/bitchat/android/BitchatApplication.kt @@ -45,6 +45,9 @@ class BitchatApplication : Application() { com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(this) } catch (_: Exception) { } + // Restore the spam filter toggle before transports start admitting public messages + com.bitchat.android.spam.SpamFilterPreferences.init(this) + // Initialize theme preference ThemePreferenceManager.init(this) diff --git a/app/src/main/java/com/bitchat/android/core/ui/component/sheet/BitchatSheetTopBar.kt b/app/src/main/java/com/bitchat/android/core/ui/component/sheet/BitchatSheetTopBar.kt index b8a027dc3..fe0e9c06c 100644 --- a/app/src/main/java/com/bitchat/android/core/ui/component/sheet/BitchatSheetTopBar.kt +++ b/app/src/main/java/com/bitchat/android/core/ui/component/sheet/BitchatSheetTopBar.kt @@ -34,7 +34,8 @@ fun BitchatSheetTopBar( val dismiss = LocalSheetDismiss.current CloseButton( onClick = { dismiss?.invoke() ?: onClose() }, - modifier = Modifier.padding(horizontal = 16.dp) + // 2.dp from any preceding action, matching the conversation header's action spacing. + modifier = Modifier.padding(start = 2.dp, end = 16.dp) ) }, colors = TopAppBarDefaults.topAppBarColors( @@ -64,7 +65,8 @@ fun BitchatSheetCenterTopBar( val dismiss = LocalSheetDismiss.current CloseButton( onClick = { dismiss?.invoke() ?: onClose() }, - modifier = Modifier.padding(horizontal = 16.dp) + // 2.dp from any preceding action, matching the conversation header's action spacing. + modifier = Modifier.padding(start = 2.dp, end = 16.dp) ) }, colors = TopAppBarDefaults.topAppBarColors( diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 6fab9f031..19dd66d8a 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -97,8 +97,10 @@ class SecurityManager(private val encryptionService: EncryptionService, private // Record only authenticated packets. Recording an attacker-controlled // invalid packet first would let it poison duplicate detection for a // later legitimate packet with the same timestamp and payload. - processedMessages.add(messageID) - messageTimestamps[messageID] = currentTime + synchronized(messageTimestamps) { + processedMessages.add(messageID) + messageTimestamps[messageID] = currentTime + } return true } @@ -136,8 +138,10 @@ class SecurityManager(private val encryptionService: EncryptionService, private // flow and reports whether this exact frame completed authentication. Never infer that // from ambient session state: a rejected replacement may leave the old session active. val result = encryptionService.processHandshakeMessageWithResult(packet.payload, peerID) - processedKeyExchanges.add(exchangeKey) - keyExchangeTimestamps[exchangeKey] = System.currentTimeMillis() + synchronized(keyExchangeTimestamps) { + processedKeyExchanges.add(exchangeKey) + keyExchangeTimestamps[exchangeKey] = System.currentTimeMillis() + } if (result.response != null) { // Send handshake response through delegate @@ -389,42 +393,45 @@ class SecurityManager(private val encryptionService: EncryptionService, private internal fun cleanupOldData(nowMs: Long = System.currentTimeMillis()) { val cutoffTime = nowMs - MESSAGE_TIMEOUT - // Clean up old message timestamps and corresponding processed messages - val messagesToRemove = messageTimestamps.entries.filter { (_, timestamp) -> - timestamp < cutoffTime - }.map { it.key } - - messagesToRemove.forEach { messageId -> - messageTimestamps.remove(messageId) - processedMessages.remove(messageId) + // Synchronized collection wrappers protect individual operations, not iteration. + // Keep each timestamp/set pair locked through expiry and capacity eviction so + // cleanup cannot remove a freshly recorded entry or traverse a mutating set. + synchronized(messageTimestamps) { + synchronized(processedMessages) { + val messagesToRemove = messageTimestamps.entries.filter { (_, timestamp) -> + timestamp < cutoffTime + }.map { it.key } + messagesToRemove.forEach { messageId -> + messageTimestamps.remove(messageId) + processedMessages.remove(messageId) + } + if (processedMessages.size > MAX_PROCESSED_MESSAGES) { + val excess = processedMessages.size - MAX_PROCESSED_MESSAGES + val toRemove = processedMessages.take(excess) + processedMessages.removeAll(toRemove.toSet()) + removeFromMessageTimestamps(toRemove) + } + } } - // Limit the size of processed messages set - if (processedMessages.size > MAX_PROCESSED_MESSAGES) { - val excess = processedMessages.size - MAX_PROCESSED_MESSAGES - val toRemove = processedMessages.take(excess) - processedMessages.removeAll(toRemove.toSet()) - removeFromMessageTimestamps(toRemove) - } - - // Expire handshake dedup entries by time so a delayed same-ephemeral delivery - // (e.g. a re-handshake retry after a failed attempt) is not blocked forever. - val keyExchangeCutoff = nowMs - KEY_EXCHANGE_DEDUP_TIMEOUT - val keyExchangesToRemove = keyExchangeTimestamps.entries.filter { (_, timestamp) -> - timestamp < keyExchangeCutoff - }.map { it.key } - - keyExchangesToRemove.forEach { exchangeKey -> - keyExchangeTimestamps.remove(exchangeKey) - processedKeyExchanges.remove(exchangeKey) - } - - // Limit the size of processed key exchanges set - if (processedKeyExchanges.size > MAX_PROCESSED_KEY_EXCHANGES) { - val excess = processedKeyExchanges.size - MAX_PROCESSED_KEY_EXCHANGES - val toRemove = processedKeyExchanges.take(excess) - processedKeyExchanges.removeAll(toRemove.toSet()) - toRemove.forEach { keyExchangeTimestamps.remove(it) } + synchronized(keyExchangeTimestamps) { + synchronized(processedKeyExchanges) { + // Expire delayed handshake retries as well as bounding the cache size. + val keyExchangeCutoff = nowMs - KEY_EXCHANGE_DEDUP_TIMEOUT + val keyExchangesToRemove = keyExchangeTimestamps.entries.filter { (_, timestamp) -> + timestamp < keyExchangeCutoff + }.map { it.key } + keyExchangesToRemove.forEach { exchangeKey -> + keyExchangeTimestamps.remove(exchangeKey) + processedKeyExchanges.remove(exchangeKey) + } + if (processedKeyExchanges.size > MAX_PROCESSED_KEY_EXCHANGES) { + val excess = processedKeyExchanges.size - MAX_PROCESSED_KEY_EXCHANGES + val toRemove = processedKeyExchanges.take(excess) + processedKeyExchanges.removeAll(toRemove.toSet()) + toRemove.forEach { keyExchangeTimestamps.remove(it) } + } + } } } @@ -441,10 +448,14 @@ class SecurityManager(private val encryptionService: EncryptionService, private * Clear all security data */ fun clearAllData() { - processedMessages.clear() - processedKeyExchanges.clear() - messageTimestamps.clear() - keyExchangeTimestamps.clear() + synchronized(messageTimestamps) { + processedMessages.clear() + messageTimestamps.clear() + } + synchronized(keyExchangeTimestamps) { + processedKeyExchanges.clear() + keyExchangeTimestamps.clear() + } } /** diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt index a43194cbf..06ca4a053 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt @@ -3,6 +3,7 @@ package com.bitchat.android.nostr import android.app.Application import android.util.Log import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.spam.SpamFilter import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -14,7 +15,7 @@ import java.util.Date * GeohashMessageHandler * - Processes kind=20000 Nostr events for geohash channels * - Updates repository for participants + nicknames - * - Emits messages to MessageManager + * - Hides spam via [SpamFilter], then emits messages to MessageManager */ class GeohashMessageHandler( private val application: Application, @@ -22,7 +23,9 @@ class GeohashMessageHandler( private val scope: CoroutineScope, private val dataManager: com.bitchat.android.ui.DataManager, private val addChannelMessage: (String, BitchatMessage) -> Unit, - private val signatureVerificationDispatcher: CoroutineDispatcher = Dispatchers.Default + private val signatureVerificationDispatcher: CoroutineDispatcher = Dispatchers.Default, + // The foreground handler runs on the main thread; the spam checks are CPU work. + private val spamFilterDispatcher: CoroutineDispatcher = signatureVerificationDispatcher ) { companion object { private const val TAG = "GeohashMessageHandler" } @@ -111,6 +114,13 @@ class GeohashMessageHandler( if (hasNonce) NostrProofOfWork.calculateDifficulty(event.id).takeIf { it > 0 } else null } catch (_: Exception) { null } ) + val hide = withContext(spamFilterDispatcher) { + SpamFilter.shared.shouldHide(msg, SpamFilter.nostrSenderKey(pubkey)) + } + if (hide) { + Log.d(TAG, "Hiding geohash spam ${event.id.take(8)}...") + return@launch + } addChannelMessage("geo:$subscribedGeohash", msg) } catch (e: Exception) { Log.e(TAG, "onEvent error: ${e.message}") diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt b/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt index 81c1cac6a..18f4952a4 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt @@ -424,7 +424,7 @@ class NostrRelayManager private constructor() { liveLocationConnectionJobs.forEach(Job::cancel) liveLocationConnectionJobs.clear() - val sockets = connections.values.toList() + val sockets = buildList { connections.values.forEach { add(it) } } connections.clear() sockets.forEach { webSocket -> webSocket.close(1000, "Manual disconnect") @@ -514,7 +514,10 @@ class NostrRelayManager private constructor() { scope.launch { if (!isNetworkActionAllowed(subscriptionInfo.liveLocationToken)) return@launch - val targetRelays = subscriptionInfo.targetRelayUrls?.toList() ?: connections.keys.toList() + // A concurrent view can shrink between Collection.size and iterator.next. + // Build by iteration instead of Kotlin's size-based toList fast paths. + val targetRelays = subscriptionInfo.targetRelayUrls?.toList() + ?: buildList { connections.keys.forEach { add(it) } } targetRelays.forEach { relayUrl -> val webSocket = connections[relayUrl] diff --git a/app/src/main/java/com/bitchat/android/services/IncomingMessageAdmission.kt b/app/src/main/java/com/bitchat/android/services/IncomingMessageAdmission.kt index dc76a57dc..de0b0797f 100644 --- a/app/src/main/java/com/bitchat/android/services/IncomingMessageAdmission.kt +++ b/app/src/main/java/com/bitchat/android/services/IncomingMessageAdmission.kt @@ -1,6 +1,7 @@ package com.bitchat.android.services import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.spam.SpamFilter import kotlinx.coroutines.runBlocking /** @@ -8,7 +9,8 @@ import kotlinx.coroutines.runBlocking * * Private-message admission is authoritative: a duplicate or a message rejected while panic mode * is wiping state must not continue to UI delegates, unread tracking, haptics, or notifications. - * Public and channel messages retain their existing best-effort behavior if state reflection fails. + * Public and channel messages retain their existing best-effort behavior if state reflection fails, + * and are refused (no timeline row, no UI callback) when the spam filter hides them. */ internal object IncomingMessageAdmission { fun admitToAppState(message: BitchatMessage): Boolean = try { @@ -24,6 +26,8 @@ internal object IncomingMessageAdmission { } } + isHiddenAsSpam(message) -> false + message.channel != null -> { AppStateStore.addChannelMessage(message.channel, message) true @@ -39,4 +43,9 @@ internal object IncomingMessageAdmission { // bypass private-message admission when persistence or canonicalization fails. !message.isPrivate } + + private fun isHiddenAsSpam(message: BitchatMessage): Boolean { + if (SpamFilter.isLocalNotice(message)) return false + return SpamFilter.shared.shouldHide(message, SpamFilter.meshSenderKey(message)) + } } diff --git a/app/src/main/java/com/bitchat/android/spam/LinkCounter.kt b/app/src/main/java/com/bitchat/android/spam/LinkCounter.kt new file mode 100644 index 000000000..b36b90bc5 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/spam/LinkCounter.kt @@ -0,0 +1,14 @@ +package com.bitchat.android.spam + +/** + * Counts links the way the chat linkifies them: scheme URLs, `www.` hosts and bare domains + * such as `example.com/path`. Pure Kotlin so it also runs on the watch and in JVM tests. + */ +internal object LinkCounter { + + private val LINK = Regex( + """(?i)(?]+|www\.[^\s<>]+|[a-z0-9\-]+(?:\.[a-z0-9\-]+)*\.[a-z]{2,}(?:/[^\s<>]*)?)(?![\w@])""" + ) + + fun count(text: String): Int = LINK.findAll(text).count() +} diff --git a/app/src/main/java/com/bitchat/android/spam/RepetitionDetector.kt b/app/src/main/java/com/bitchat/android/spam/RepetitionDetector.kt new file mode 100644 index 000000000..9132360cb --- /dev/null +++ b/app/src/main/java/com/bitchat/android/spam/RepetitionDetector.kt @@ -0,0 +1,187 @@ +package com.bitchat.android.spam + +import java.text.Normalizer + +/** + * Character autocorrelation measures for repeated-content spam. + * + * Text is compared as Unicode code points after [normalize], so case, spacing, look-alike + * letters, accents and invisible characters don't hide a repeat. + */ +internal object RepetitionDetector { + + /** + * Folds a message to what a reader sees, for comparison only (never for display): NFKD + * turns fullwidth and math-styled letters into plain ones and splits off accents, then + * combining marks and invisible characters are dropped, whitespace runs become one space + * and the result is lowercased and capped to [maxLength] code points. + */ + fun normalize(text: String, maxLength: Int): IntArray { + // Disguises at most a few code units per visible character; bound the work up front. + val head = if (text.length > maxLength * 4) text.substring(0, maxLength * 4) else text + val decomposed = Normalizer.normalize(head, Normalizer.Form.NFKD) + val out = IntArray(minOf(maxLength, decomposed.length)) + var size = 0 + var pendingSpace = false + var offset = 0 + while (offset < decomposed.length && size < out.size) { + val point = decomposed.codePointAt(offset) + offset += Character.charCount(point) + when { + isMark(point) || isInvisible(point) -> continue + isWhitespace(point) -> pendingSpace = size > 0 + else -> { + if (pendingSpace && size < out.size - 1) out[size++] = ' '.code + pendingSpace = false + out[size++] = Character.toLowerCase(point) + } + } + } + return out.copyOf(size) + } + + private fun isWhitespace(point: Int): Boolean = + Character.isWhitespace(point) || Character.isSpaceChar(point) + + /** Zero-width and direction controls, soft hyphens and other non-printing characters. */ + private fun isInvisible(point: Int): Boolean = + Character.getType(point) == Character.FORMAT.toInt() || + (Character.isISOControl(point) && !isWhitespace(point)) + + private fun isMark(point: Int): Boolean = when (Character.getType(point)) { + Character.NON_SPACING_MARK.toInt(), + Character.COMBINING_SPACING_MARK.toInt(), + Character.ENCLOSING_MARK.toInt() -> true + else -> false + } + + /** + * Highest autocorrelation of [text] with itself over shifts 1..n/2: the fraction of + * overlapping positions where `text[i] == text[i + lag]`. A pattern repeated back to back + * ("aaaa", "buy now buy now") scores 1.0; ordinary prose stays well below 0.3. + */ + fun selfSimilarity(text: IntArray): Double { + val n = text.size + var best = 0.0 + for (lag in 1..n / 2) { + val overlap = n - lag + var matches = 0 + for (i in 0 until overlap) if (text[i] == text[i + lag]) matches++ + val score = matches.toDouble() / overlap + if (score > best) best = score + } + return best + } + + /** One diagonal of the cross-correlation: `b[j]` lined up under `a[j + shift]`. */ + data class Peak(val shift: Int, val matches: Int) + + /** How two texts line up: the peaks used and the share of the longer text they cover. */ + data class Alignment(val peaks: List, val matches: Int, val score: Double) + + fun crossSimilarity(a: IntArray, b: IntArray, maxPeaks: Int = 3, minRun: Int = 3): Double = + align(a, b, maxPeaks, minRun).score + + /** + * Cross-correlation of two texts that tolerates small edits. + * + * The first peak is the classic correlation maximum, the shift where the most characters + * match. An inserted or deleted character moves the rest of the text onto a neighboring + * shift, so up to [maxPeaks] - 1 further peaks add matches from other shifts. Those count + * only runs of at least [minRun] consecutive characters, so coincidental single letters + * don't add up, and no character of either text is used twice. The score is the matched + * characters over the longer length: 1.0 for identical texts, and a short text never + * matches a long one. + */ + fun align(a: IntArray, b: IntArray, maxPeaks: Int = 3, minRun: Int = 3): Alignment { + if (a.isEmpty() || b.isEmpty()) return Alignment(emptyList(), 0, 0.0) + val usedA = BooleanArray(a.size) + val usedB = BooleanArray(b.size) + val peaks = ArrayList() + var total = 0 + while (peaks.size < maxPeaks) { + val run = if (peaks.isEmpty()) 1 else minRun + var bestGain = 0 + var bestShift = 0 + for (shift in -(b.size - 1) until a.size) { + val from = maxOf(0, -shift) + val to = minOf(b.size, a.size - shift) + if (to - from <= bestGain) continue + val gain = runMatches(a, b, shift, from, to, usedA, usedB, run, mark = false) + if (gain > bestGain) { + bestGain = gain + bestShift = shift + } + } + if (bestGain < run) break + val from = maxOf(0, -bestShift) + val to = minOf(b.size, a.size - bestShift) + runMatches(a, b, bestShift, from, to, usedA, usedB, run, mark = true) + peaks += Peak(bestShift, bestGain) + total += bestGain + } + return Alignment(peaks, total, total.toDouble() / maxOf(a.size, b.size)) + } + + /** Unused matches at [shift] that sit in runs of at least [minRun]; [mark] claims them. */ + private fun runMatches( + a: IntArray, + b: IntArray, + shift: Int, + from: Int, + to: Int, + usedA: BooleanArray, + usedB: BooleanArray, + minRun: Int, + mark: Boolean + ): Int { + var gain = 0 + var runStart = -1 + for (j in from..to) { + val hit = j < to && !usedB[j] && !usedA[j + shift] && a[j + shift] == b[j] + if (hit) { + if (runStart < 0) runStart = j + } else if (runStart >= 0) { + val length = j - runStart + if (length >= minRun) { + gain += length + if (mark) for (k in runStart until j) { + usedB[k] = true + usedA[k + shift] = true + } + } + runStart = -1 + } + } + return gain + } + + /** Distinct character trigrams as sorted hashes, for a cheap pre-check before [align]. */ + fun trigrams(text: IntArray): IntArray { + if (text.size < 3) return IntArray(0) + return IntArray(text.size - 2) { i -> (text[i] * 31 + text[i + 1]) * 31 + text[i + 2] } + .distinct() + .sorted() + .toIntArray() + } + + /** + * Shared trigrams over the larger trigram set. Near-copies share most of theirs, unrelated + * texts few, so pairs below a low bar can skip the quadratic [align]. Texts too short to + * have trigrams always pass. + */ + fun trigramOverlap(a: IntArray, b: IntArray): Double { + if (a.isEmpty() || b.isEmpty()) return 1.0 + var i = 0 + var j = 0 + var shared = 0 + while (i < a.size && j < b.size) { + when { + a[i] == b[j] -> { shared++; i++; j++ } + a[i] < b[j] -> i++ + else -> j++ + } + } + return shared.toDouble() / maxOf(a.size, b.size) + } +} diff --git a/app/src/main/java/com/bitchat/android/spam/SpamFilter.kt b/app/src/main/java/com/bitchat/android/spam/SpamFilter.kt new file mode 100644 index 000000000..c3eb50946 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/spam/SpamFilter.kt @@ -0,0 +1,246 @@ +package com.bitchat.android.spam + +import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.model.BitchatMessageType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlin.math.abs + +enum class SpamVerdict { + ALLOWED, + MESSAGE_RATE, + CHARACTER_RATE, + LINK_RATE, + REPEATED_MESSAGE, + REPEATED_ACROSS_USERS, + REPEATED_CHARACTERS; + + val isSpam: Boolean get() = this != ALLOWED +} + +/** + * Decides whether an incoming public message should be hidden. + * + * Each sender gets three token buckets (messages, characters, links; see [SpamFilterConfig]) + * plus three repetition checks: a message that repeats itself internally, a near-copy of the + * sender's own recent history, and a near-copy of what *other* senders posted recently. The last + * one catches spam spread over many throwaway identities, which per-sender limits cannot see. + * Hidden messages never spend tokens, but they do enter the repetition history so a spammer + * cannot cycle the same text. + * + * Verdicts are remembered by message ID, so the same message arriving over BLE and Wi-Fi Aware, + * or through both geohash handlers, is judged once and counted once. + */ +class SpamFilter( + config: SpamFilterConfig = SpamFilterConfig(), + private val clock: () -> Long = System::currentTimeMillis +) { + @Volatile + var enabled: Boolean = true + + /** Changing the limits forgets every sender, so new budgets start full. */ + @Volatile + var config: SpamFilterConfig = config + set(value) { + synchronized(this) { + field = value + senders.clear() + verdicts.clear() + recentTexts.clear() + } + } + + /** Debug: let spam through and record its verdict in [flagged] so the chat can mark it. */ + @Volatile + var debugBypass: Boolean = false + + private val _flagged = MutableStateFlow>(emptyMap()) + + /** Verdicts of spam shown because of [debugBypass], by message ID. */ + val flagged: StateFlow> = _flagged.asStateFlow() + + /** A message prepared for comparison: its leading characters and their trigrams. */ + private class Fingerprint(val timeMs: Long, val text: IntArray, val trigrams: IntArray) + + private class SharedEntry(val senderKey: String, val fingerprint: Fingerprint) + + private inner class SenderState { + val rate = TokenBucketSchedule( + listOf(config.messageBucket, config.characterBucket, config.linkBucket) + ) + val history = ArrayDeque() + } + + private val senders = object : LinkedHashMap(64, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = + size > config.maxTrackedSenders + } + + /** Recent texts from every sender, for the cross-user repeat check. */ + private val recentTexts = ArrayDeque() + + private val verdicts = object : LinkedHashMap(256, 0.75f, false) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = + size > config.maxRememberedVerdicts + } + + /** + * Returns true when [message] must be kept out of the chat. With [debugBypass] on, spam is + * let through and recorded in [flagged] instead. + */ + fun shouldHide(message: BitchatMessage, senderKey: String): Boolean { + val verdict = check(message, senderKey) + if (!verdict.isSpam) return false + if (!debugBypass) return true + _flagged.update { current -> + if (current[message.id] == verdict) current + else (current + (message.id to verdict)).entries + .toList() + .takeLast(MAX_FLAGGED) + .associate { it.key to it.value } + } + return false + } + + /** Checks a received message; [senderKey] must identify the author stably within a transport. */ + fun check(message: BitchatMessage, senderKey: String): SpamVerdict = check( + senderKey = senderKey, + messageId = message.id, + content = message.content, + timestampMs = message.timestamp.time, + isText = message.type == BitchatMessageType.Message + ) + + fun check( + senderKey: String, + messageId: String, + content: String, + timestampMs: Long, + isText: Boolean = true + ): SpamVerdict { + if (!enabled) return SpamVerdict.ALLOWED + synchronized(this) { + verdicts[messageId]?.let { return it } + + val now = clock() + val oldestTrusted = now - config.maxBackdateMs + val time = timestampMs.coerceIn(oldestTrusted, now) + val state = senders.getOrPut(senderKey) { SenderState() } + state.rate.prune(oldestTrusted) + + val repetition = config.repetition + val text = if (isText) RepetitionDetector.normalize(content, repetition.maxAnalyzedLength) else null + val fingerprint = text?.let { fingerprint(it, time) } + val verdict = when { + text != null && hasRepeatedCharacters(text) -> SpamVerdict.REPEATED_CHARACTERS + text != null && repeatsHistory(state, text.size, fingerprint!!) -> SpamVerdict.REPEATED_MESSAGE + text != null && repeatsOtherSenders(senderKey, text.size, fingerprint!!) -> + SpamVerdict.REPEATED_ACROSS_USERS + else -> admitRate(state, content, time, isText) + } + + if (text != null && fingerprint != null) { + state.history.addLast(fingerprint) + while (state.history.size > repetition.historySize) state.history.removeFirst() + if (text.size >= repetition.crossUserMinLength) { + recentTexts.addLast(SharedEntry(senderKey, fingerprint)) + while (recentTexts.size > repetition.crossUserHistorySize) recentTexts.removeFirst() + } + } + verdicts[messageId] = verdict + return verdict + } + } + + /** Forgets all senders, e.g. when the filter is toggled or the app is wiped. */ + fun reset() { + synchronized(this) { + senders.clear() + verdicts.clear() + recentTexts.clear() + } + _flagged.value = emptyMap() + } + + private fun hasRepeatedCharacters(text: IntArray): Boolean { + val repetition = config.repetition + return text.size >= repetition.minRepeatedCharactersLength && + RepetitionDetector.selfSimilarity(text) >= repetition.repeatedCharactersThreshold + } + + private fun fingerprint(text: IntArray, timeMs: Long): Fingerprint { + val max = config.repetition.maxComparedLength + val compared = if (text.size > max) text.copyOf(max) else text + return Fingerprint(timeMs, compared, RepetitionDetector.trigrams(compared)) + } + + private fun repeatsHistory(state: SenderState, length: Int, message: Fingerprint): Boolean { + val repetition = config.repetition + if (length < repetition.minRepeatedMessageLength) return false + val copies = state.history.count { isNearCopy(it, message) } + return copies > repetition.allowedRepeats + } + + private fun repeatsOtherSenders(senderKey: String, length: Int, message: Fingerprint): Boolean { + val repetition = config.repetition + if (length < repetition.crossUserMinLength) return false + var copies = 0 + for (entry in recentTexts) { + if (entry.senderKey == senderKey || !isNearCopy(entry.fingerprint, message)) continue + if (++copies > repetition.crossUserAllowedRepeats) return true + } + return false + } + + private fun isNearCopy(earlier: Fingerprint, message: Fingerprint): Boolean { + val repetition = config.repetition + return abs(earlier.timeMs - message.timeMs) <= repetition.historyWindowMs && + lengthsCanMatch(earlier.text.size, message.text.size) && + RepetitionDetector.trigramOverlap(earlier.trigrams, message.trigrams) >= repetition.minSharedTrigrams && + RepetitionDetector.crossSimilarity( + earlier.text, + message.text, + repetition.maxAlignmentPeaks, + repetition.minPeakRun + ) >= repetition.repeatedMessageSimilarity + } + + // Cross-similarity is bounded by shorter/longer, so skip pairs that can never reach it. + private fun lengthsCanMatch(a: Int, b: Int): Boolean = + minOf(a, b).toDouble() / maxOf(a, b) >= config.repetition.repeatedMessageSimilarity + + private fun admitRate(state: SenderState, content: String, timeMs: Long, isText: Boolean): SpamVerdict { + val characters = if (isText) content.codePointCount(0, content.length).toDouble() else 0.0 + val links = if (isText) LinkCounter.count(content).toDouble() else 0.0 + return when (state.rate.tryAdmit(timeMs, doubleArrayOf(1.0, characters, links))) { + -1 -> SpamVerdict.ALLOWED + 0 -> SpamVerdict.MESSAGE_RATE + 1 -> SpamVerdict.CHARACTER_RATE + else -> SpamVerdict.LINK_RATE + } + } + + companion object { + private const val MAX_FLAGGED = 500 + + /** Process-wide filter shared by every transport and geohash handler. */ + val shared = SpamFilter() + + /** + * True for notices this device generated itself, such as "X favorited you". Every message + * read off the wire carries the sender's peer ID; only local notices have none. The + * nickname is no signal, since any peer can call itself "system". + */ + fun isLocalNotice(message: BitchatMessage): Boolean = + message.senderPeerID == null && message.sender == "system" + + /** Key for public mesh and mesh channel messages. */ + fun meshSenderKey(message: BitchatMessage): String = + "mesh:" + (message.senderPeerID ?: message.sender) + + /** Key for geohash messages, by the sender's per-geohash Nostr pubkey. */ + fun nostrSenderKey(pubkeyHex: String): String = "nostr:" + pubkeyHex.lowercase() + } +} diff --git a/app/src/main/java/com/bitchat/android/spam/SpamFilterConfig.kt b/app/src/main/java/com/bitchat/android/spam/SpamFilterConfig.kt new file mode 100644 index 000000000..8a50c6e7b --- /dev/null +++ b/app/src/main/java/com/bitchat/android/spam/SpamFilterConfig.kt @@ -0,0 +1,74 @@ +package com.bitchat.android.spam + +/** + * Tunables for [SpamFilter]. These are code-level knobs only; the user can just turn the + * filter on or off. + */ +data class SpamFilterConfig( + /** Messages per sender: a burst of 6, then one more every 10 seconds. */ + val messageBucket: TokenBucketConfig = TokenBucketConfig( + capacity = 6.0, + refillPerSecond = 1.0 / 10.0 + ), + /** Characters per sender: 1200 per minute, refilled continuously (20 per second). */ + val characterBucket: TokenBucketConfig = TokenBucketConfig( + capacity = 1200.0, + refillPerSecond = 1200.0 / 60.0 + ), + /** Links per sender: 2 per minute, refilled continuously (one every 30 seconds). */ + val linkBucket: TokenBucketConfig = TokenBucketConfig( + capacity = 2.0, + refillPerSecond = 2.0 / 60.0 + ), + val repetition: RepetitionConfig = RepetitionConfig(), + /** + * How far in the past a message timestamp is trusted. Relay backfill (geohash subscribes to + * the last hour) and mesh sync deliver history in bursts, so buckets run on message time; + * older or future timestamps are clamped into [now - maxBackdateMs, now]. + */ + val maxBackdateMs: Long = 60 * 60 * 1000L, + /** Least recently active senders beyond this are forgotten. */ + val maxTrackedSenders: Int = 1_000, + /** Verdicts remembered by message ID so multi-transport duplicates are judged once. */ + val maxRememberedVerdicts: Int = 4_000 +) + +data class TokenBucketConfig( + val capacity: Double, + val refillPerSecond: Double +) + +data class RepetitionConfig( + /** Messages shorter than this (in characters) are never checked for repeated characters. */ + val minRepeatedCharactersLength: Int = 32, + /** Hide when some shift of the message matches itself at this fraction of positions. */ + val repeatedCharactersThreshold: Double = 0.85, + /** Messages shorter than this are never compared with the sender's history. */ + val minRepeatedMessageLength: Int = 10, + /** Two messages count as the same when their best alignment matches this fraction. */ + val repeatedMessageSimilarity: Double = 0.8, + /** How many near-copies of one text a sender may post within [historyWindowMs]. */ + val allowedRepeats: Int = 1, + /** + * How many near-copies of a text *other* senders may already have posted within + * [historyWindowMs] before it is hidden. Catches spam spread over many identities. + */ + val crossUserAllowedRepeats: Int = 2, + /** Messages shorter than this are never compared across senders; short replies coincide. */ + val crossUserMinLength: Int = 16, + /** Recent messages from all senders kept for the cross-user comparison. */ + val crossUserHistorySize: Int = 200, + /** Recent messages per sender kept for comparison. */ + val historySize: Int = 10, + val historyWindowMs: Long = 10 * 60 * 1000L, + /** Longer messages are analyzed on their first N characters only, bounding CPU per message. */ + val maxAnalyzedLength: Int = 1_000, + /** Messages are compared with others on their first N characters only. */ + val maxComparedLength: Int = 280, + /** Correlation peaks combined per comparison; extra peaks absorb inserted or deleted text. */ + val maxAlignmentPeaks: Int = 3, + /** Extra peaks only count runs of at least this many matching characters. */ + val minPeakRun: Int = 3, + /** Pairs sharing fewer trigrams than this fraction skip the full comparison. */ + val minSharedTrigrams: Double = 0.25 +) diff --git a/app/src/main/java/com/bitchat/android/spam/SpamFilterPreferences.kt b/app/src/main/java/com/bitchat/android/spam/SpamFilterPreferences.kt new file mode 100644 index 000000000..f3191c9e7 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/spam/SpamFilterPreferences.kt @@ -0,0 +1,115 @@ +package com.bitchat.android.spam + +import android.content.Context +import android.content.SharedPreferences +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Persists the user's on/off choice for [SpamFilter.shared] (on by default), plus the debug + * settings: tuned limits and the bypass that marks spam instead of hiding it. + */ +object SpamFilterPreferences { + + private const val PREFS_NAME = "spam_filter_preferences" + private const val KEY_ENABLED = "spam_filter_enabled" + private const val KEY_DEBUG_BYPASS = "spam_filter_debug_bypass" + private const val KEY_MESSAGE_BURST = "spam_filter_message_burst" + private const val KEY_SECONDS_PER_MESSAGE = "spam_filter_seconds_per_message" + private const val KEY_CHARACTERS_PER_MINUTE = "spam_filter_characters_per_minute" + private const val KEY_LINKS_PER_MINUTE = "spam_filter_links_per_minute" + private const val KEY_ALLOWED_REPEATS = "spam_filter_allowed_repeats" + private const val KEY_CROSS_USER_ALLOWED_REPEATS = "spam_filter_cross_user_allowed_repeats" + private const val KEY_CROSS_USER_MIN_LENGTH = "spam_filter_cross_user_min_length" + private const val KEY_REPEAT_SIMILARITY = "spam_filter_repeat_similarity" + private const val KEY_REPEAT_WINDOW_MINUTES = "spam_filter_repeat_window_minutes" + private const val KEY_SELF_MATCH_THRESHOLD = "spam_filter_self_match_threshold" + private const val KEY_SELF_MATCH_MIN_LENGTH = "spam_filter_self_match_min_length" + private const val DEFAULT_ENABLED = true + + private val _enabled = MutableStateFlow(DEFAULT_ENABLED) + val enabled: StateFlow = _enabled.asStateFlow() + + private val _debugBypass = MutableStateFlow(false) + val debugBypass: StateFlow = _debugBypass.asStateFlow() + + private val _tuning = MutableStateFlow(SpamTuning.DEFAULT) + val tuning: StateFlow = _tuning.asStateFlow() + + @Volatile + private var sharedPrefs: SharedPreferences? = null + + fun init(context: Context) { + if (sharedPrefs != null) return + val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + sharedPrefs = prefs + applyEnabled(prefs.getBoolean(KEY_ENABLED, DEFAULT_ENABLED)) + applyDebugBypass(prefs.getBoolean(KEY_DEBUG_BYPASS, false)) + applyTuning(readTuning(prefs)) + } + + fun setEnabled(enabled: Boolean) { + if (_enabled.value == enabled) return + sharedPrefs?.edit()?.putBoolean(KEY_ENABLED, enabled)?.apply() + applyEnabled(enabled) + // Start from a clean slate so budgets don't carry over from before the toggle. + SpamFilter.shared.reset() + } + + fun setDebugBypass(enabled: Boolean) { + sharedPrefs?.edit()?.putBoolean(KEY_DEBUG_BYPASS, enabled)?.apply() + applyDebugBypass(enabled) + } + + fun setTuning(tuning: SpamTuning) { + if (_tuning.value == tuning) return + sharedPrefs?.edit()?.apply { + putInt(KEY_MESSAGE_BURST, tuning.messageBurst) + putInt(KEY_SECONDS_PER_MESSAGE, tuning.secondsPerMessage) + putInt(KEY_CHARACTERS_PER_MINUTE, tuning.charactersPerMinute) + putInt(KEY_LINKS_PER_MINUTE, tuning.linksPerMinute) + putInt(KEY_ALLOWED_REPEATS, tuning.allowedRepeats) + putInt(KEY_CROSS_USER_ALLOWED_REPEATS, tuning.crossUserAllowedRepeats) + putInt(KEY_CROSS_USER_MIN_LENGTH, tuning.crossUserMinLength) + putInt(KEY_REPEAT_SIMILARITY, tuning.repeatSimilarityPercent) + putInt(KEY_REPEAT_WINDOW_MINUTES, tuning.repeatWindowMinutes) + putInt(KEY_SELF_MATCH_THRESHOLD, tuning.selfMatchPercent) + putInt(KEY_SELF_MATCH_MIN_LENGTH, tuning.selfMatchMinLength) + apply() + } + applyTuning(tuning) + } + + private fun readTuning(prefs: SharedPreferences): SpamTuning { + val d = SpamTuning.DEFAULT + return SpamTuning( + messageBurst = prefs.getInt(KEY_MESSAGE_BURST, d.messageBurst), + secondsPerMessage = prefs.getInt(KEY_SECONDS_PER_MESSAGE, d.secondsPerMessage), + charactersPerMinute = prefs.getInt(KEY_CHARACTERS_PER_MINUTE, d.charactersPerMinute), + linksPerMinute = prefs.getInt(KEY_LINKS_PER_MINUTE, d.linksPerMinute), + allowedRepeats = prefs.getInt(KEY_ALLOWED_REPEATS, d.allowedRepeats), + crossUserAllowedRepeats = prefs.getInt(KEY_CROSS_USER_ALLOWED_REPEATS, d.crossUserAllowedRepeats), + crossUserMinLength = prefs.getInt(KEY_CROSS_USER_MIN_LENGTH, d.crossUserMinLength), + repeatSimilarityPercent = prefs.getInt(KEY_REPEAT_SIMILARITY, d.repeatSimilarityPercent), + repeatWindowMinutes = prefs.getInt(KEY_REPEAT_WINDOW_MINUTES, d.repeatWindowMinutes), + selfMatchPercent = prefs.getInt(KEY_SELF_MATCH_THRESHOLD, d.selfMatchPercent), + selfMatchMinLength = prefs.getInt(KEY_SELF_MATCH_MIN_LENGTH, d.selfMatchMinLength) + ) + } + + private fun applyEnabled(enabled: Boolean) { + _enabled.value = enabled + SpamFilter.shared.enabled = enabled + } + + private fun applyDebugBypass(enabled: Boolean) { + _debugBypass.value = enabled + SpamFilter.shared.debugBypass = enabled + } + + private fun applyTuning(tuning: SpamTuning) { + _tuning.value = tuning + SpamFilter.shared.config = tuning.toConfig() + } +} diff --git a/app/src/main/java/com/bitchat/android/spam/SpamTuning.kt b/app/src/main/java/com/bitchat/android/spam/SpamTuning.kt new file mode 100644 index 000000000..1da951ee2 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/spam/SpamTuning.kt @@ -0,0 +1,64 @@ +package com.bitchat.android.spam + +/** + * The [SpamFilterConfig] knobs exposed in debug settings, in whole units that suit sliders. + * [DEFAULT] matches the code defaults in [SpamFilterConfig]. + */ +data class SpamTuning( + val messageBurst: Int, + val secondsPerMessage: Int, + val charactersPerMinute: Int, + val linksPerMinute: Int, + val allowedRepeats: Int, + val crossUserAllowedRepeats: Int, + val crossUserMinLength: Int, + val repeatSimilarityPercent: Int, + val repeatWindowMinutes: Int, + val selfMatchPercent: Int, + val selfMatchMinLength: Int +) { + fun toConfig(): SpamFilterConfig { + val base = SpamFilterConfig() + return base.copy( + messageBucket = TokenBucketConfig( + capacity = messageBurst.toDouble(), + refillPerSecond = 1.0 / secondsPerMessage.coerceAtLeast(1) + ), + characterBucket = TokenBucketConfig( + capacity = charactersPerMinute.toDouble(), + refillPerSecond = charactersPerMinute / 60.0 + ), + linkBucket = TokenBucketConfig( + capacity = linksPerMinute.toDouble(), + refillPerSecond = linksPerMinute / 60.0 + ), + repetition = base.repetition.copy( + allowedRepeats = allowedRepeats, + crossUserAllowedRepeats = crossUserAllowedRepeats, + crossUserMinLength = crossUserMinLength, + repeatedMessageSimilarity = repeatSimilarityPercent / 100.0, + historyWindowMs = repeatWindowMinutes * 60_000L, + repeatedCharactersThreshold = selfMatchPercent / 100.0, + minRepeatedCharactersLength = selfMatchMinLength + ) + ) + } + + companion object { + val DEFAULT: SpamTuning = SpamFilterConfig().let { config -> + SpamTuning( + messageBurst = config.messageBucket.capacity.toInt(), + secondsPerMessage = Math.round(1.0 / config.messageBucket.refillPerSecond).toInt(), + charactersPerMinute = config.characterBucket.capacity.toInt(), + linksPerMinute = config.linkBucket.capacity.toInt(), + allowedRepeats = config.repetition.allowedRepeats, + crossUserAllowedRepeats = config.repetition.crossUserAllowedRepeats, + crossUserMinLength = config.repetition.crossUserMinLength, + repeatSimilarityPercent = Math.round(config.repetition.repeatedMessageSimilarity * 100).toInt(), + repeatWindowMinutes = (config.repetition.historyWindowMs / 60_000L).toInt(), + selfMatchPercent = Math.round(config.repetition.repeatedCharactersThreshold * 100).toInt(), + selfMatchMinLength = config.repetition.minRepeatedCharactersLength + ) + } + } +} diff --git a/app/src/main/java/com/bitchat/android/spam/TokenBucketSchedule.kt b/app/src/main/java/com/bitchat/android/spam/TokenBucketSchedule.kt new file mode 100644 index 000000000..622fda6f2 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/spam/TokenBucketSchedule.kt @@ -0,0 +1,77 @@ +package com.bitchat.android.spam + +/** + * A set of token buckets for one sender, evaluated on message time instead of arrival order. + * + * Relay backfill and mesh sync hand over a sender's recent history in one burst, often newest + * first, so a plain arrival-time bucket would hide an ordinary conversation. Instead each + * admitted message stays on a timeline, and a candidate is admitted only if replaying every + * bucket over the timeline with the candidate inserted never runs dry. For live, in-order + * traffic this behaves exactly like a classic token bucket. Rejected messages cost nothing. + * + * Entries older than the backdate window are folded into a checkpoint, and nothing can be + * inserted before the checkpoint, so the replay stays short and exact. + */ +internal class TokenBucketSchedule(private val buckets: List) { + + private class Entry(val timeMs: Long, val costs: DoubleArray) + + private val entries = ArrayList() + private var checkpointMs: Long? = null + private val checkpointTokens = DoubleArray(buckets.size) { buckets[it].capacity } + + /** + * Admits and records the message when every bucket can pay [costs] at [timeMs]. + * Returns -1 on success, otherwise the index of the first bucket that ran dry. + */ + fun tryAdmit(timeMs: Long, costs: DoubleArray): Int { + require(costs.size == buckets.size) + val time = checkpointMs?.let { maxOf(it, timeMs) } ?: timeMs + var insertAt = entries.size + while (insertAt > 0 && entries[insertAt - 1].timeMs > time) insertAt-- + val candidate = Entry(time, costs) + + val tokens = checkpointTokens.copyOf() + var lastMs = checkpointMs + for (i in 0..entries.size) { + val entry = when { + i < insertAt -> entries[i] + i == insertAt -> candidate + else -> entries[i - 1] + } + refill(tokens, lastMs, entry.timeMs) + lastMs = entry.timeMs + for (b in buckets.indices) { + tokens[b] -= entry.costs[b] + if (tokens[b] < -EPSILON) return b + } + } + entries.add(insertAt, candidate) + return -1 + } + + /** Folds entries before [cutoffMs] into the checkpoint. */ + fun prune(cutoffMs: Long) { + var folded = 0 + while (folded < entries.size && entries[folded].timeMs < cutoffMs) { + val entry = entries[folded] + refill(checkpointTokens, checkpointMs, entry.timeMs) + for (b in buckets.indices) checkpointTokens[b] -= entry.costs[b] + checkpointMs = entry.timeMs + folded++ + } + if (folded > 0) entries.subList(0, folded).clear() + } + + private fun refill(tokens: DoubleArray, fromMs: Long?, toMs: Long) { + if (fromMs == null || toMs <= fromMs) return + val seconds = (toMs - fromMs) / 1000.0 + for (b in buckets.indices) { + tokens[b] = minOf(buckets[b].capacity, tokens[b] + seconds * buckets[b].refillPerSecond) + } + } + + private companion object { + const val EPSILON = 1e-9 + } +} diff --git a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt index 54ab03185..4fc570293 100644 --- a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.Security +import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.Speed import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -77,6 +78,7 @@ import com.bitchat.android.net.TorMode import com.bitchat.android.net.TorPreferenceManager import com.bitchat.android.nostr.NostrProofOfWork import com.bitchat.android.nostr.PoWPreferenceManager +import com.bitchat.android.spam.SpamFilterPreferences import com.bitchat.android.ui.theme.BitchatMotion import com.bitchat.android.ui.theme.LocalBitchatPalette import com.bitchat.android.util.ShareableApkVariant @@ -508,6 +510,7 @@ fun AboutSheet( LaunchedEffect(Unit) { PoWPreferenceManager.init(context) } val powEnabled by PoWPreferenceManager.powEnabled.collectAsState() val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsState() + val spamFilterEnabled by SpamFilterPreferences.enabled.collectAsState() var backgroundEnabled by remember { mutableStateOf(com.bitchat.android.service.MeshServicePreferences.isBackgroundEnabled(true)) } var liveVoiceEnabled by remember { mutableStateOf(com.bitchat.android.features.voice.LiveVoicePreferences.isEnabled(context)) @@ -582,6 +585,21 @@ fun AboutSheet( color = colorScheme.outlineVariant ) + // Spam Filter Toggle + SettingsToggleRow( + icon = Icons.Filled.FilterAlt, + title = stringResource(R.string.about_spam_filter_title), + subtitle = stringResource(R.string.about_spam_filter_desc), + checked = spamFilterEnabled, + onCheckedChange = { SpamFilterPreferences.setEnabled(it) } + ) + + HorizontalDivider( + modifier = Modifier.padding(start = 54.dp), + thickness = 1.dp, + color = colorScheme.outlineVariant + ) + // Tor Toggle SettingsToggleRow( icon = Icons.Filled.Security, diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index a4b9eb0ad..2a689d236 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -930,10 +930,8 @@ private fun ChatDialogs( isPresented = showMeshPeerListSheet, viewModel = viewModel, onDismiss = onMeshPeerListDismiss, - onShowVerification = { - onMeshPeerListDismiss() - viewModel.showVerificationSheet(fromSidebar = true) - } + // Stacks on top of the network sheet, so dismissing it lands back here. + onShowVerification = viewModel::showVerificationSheet ) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 762aabb60..b7f38eb07 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -1300,21 +1300,12 @@ class ChatViewModel( notificationManager.clearMeshMentionNotifications() } - private var reopenSidebarAfterVerification = false - - fun showVerificationSheet(fromSidebar: Boolean = false) { - if (fromSidebar) { - reopenSidebarAfterVerification = true - } + fun showVerificationSheet() { state.setShowVerificationSheet(true) } fun hideVerificationSheet() { state.setShowVerificationSheet(false) - if (reopenSidebarAfterVerification) { - reopenSidebarAfterVerification = false - state.setShowMeshPeerList(true) - } } fun showSecurityVerificationSheet() { @@ -1490,6 +1481,9 @@ class ChatViewModel( } catch (_: Exception) { wipeComplete = false } + + // The spam filter keeps recent message text and sender keys for its repeat checks + com.bitchat.android.spam.SpamFilter.shared.reset() // Clear all cryptographic data if (!clearAllCryptographicData()) wipeComplete = false diff --git a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt index c6233e1c1..6ef54f806 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt @@ -464,8 +464,8 @@ fun MeshPeerListSheet( Icon( imageVector = Icons.Outlined.QrCode, contentDescription = stringResource(R.string.verify_title), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(22.dp) + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(HeaderIconSize) ) } } diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index d398c397d..0e9b3762a 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -87,6 +87,8 @@ import com.bitchat.android.mesh.MeshService import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessageType import com.bitchat.android.model.DeliveryStatus +import com.bitchat.android.spam.SpamFilter +import com.bitchat.android.spam.SpamVerdict import com.bitchat.android.ui.media.FileMessageItem import com.bitchat.android.ui.theme.BASE_FONT_SIZE import com.bitchat.android.ui.theme.BitchatMotion @@ -253,6 +255,8 @@ fun MessagesList( // Collected once here so individual rows never subscribe to the preference flow; a mode // switch simply recomposes the list against the new layout. val bubbles by ChatUiModeManager.modeFlow.collectAsState() + // Spam let through by the debug bypass; empty unless that debug setting is on. + val spamFlags by SpamFilter.shared.flagged.collectAsState() // A fresh scroll position per conversation. Sharing one state meant a switch inherited the // previous channel's offset and then had to correct itself, which is what the jump was. @@ -268,8 +272,11 @@ fun MessagesList( var hasScrolledToInitialPosition by remember(conversationKey) { mutableStateOf(false) } var followIncomingMessages by remember(conversationKey) { mutableStateOf(true) } - // Smart scroll: auto-scroll to bottom for initial load, then follow unless user scrolls away - LaunchedEffect(messages.size) { + // Smart scroll: auto-scroll to bottom for initial load, then follow unless user scrolls away. + // + // Keyed on the newest message as well as the count: a timeline at its retention cap evicts its + // oldest message for every arrival, so its size stops changing while messages keep coming in. + LaunchedEffect(conversationKey, messages.size, messages.lastOrNull()?.id) { if (messages.isNotEmpty()) { val isFirstLoad = !hasScrolledToInitialPosition if (isFirstLoad || followIncomingMessages) { @@ -384,6 +391,7 @@ fun MessagesList( mentionPeerIdentities = resolvedMentionPeerIdentities, showSender = !isGrouped, bubbles = bubbles.isBubbles, + spamVerdict = spamFlags[message.id], topSpacing = MessageGrouping.topSpacingFor( isGrouped = isGrouped, isFirstInList = originalIndex == 0 @@ -418,6 +426,8 @@ fun MessageItem( mentionPeerIdentities: Map = emptyMap(), showSender: Boolean = true, bubbles: Boolean = false, + /** Set when the spam filter's debug bypass let this message through; tints the row red. */ + spamVerdict: SpamVerdict? = null, topSpacing: Dp = 0.dp, onNicknameClick: ((String) -> Unit)? = null, onMessageLongPress: ((BitchatMessage) -> Unit)? = null, @@ -427,11 +437,19 @@ fun MessageItem( ) { val colorScheme = MaterialTheme.colorScheme val timeFormatter = remember { SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()) } + val spamModifier = if (spamVerdict != null) { + Modifier + .background(colorScheme.error.copy(alpha = 0.18f), RoundedCornerShape(8.dp)) + .padding(horizontal = 6.dp, vertical = 4.dp) + } else { + Modifier + } Column( modifier = modifier .fillMaxWidth() - .padding(top = topSpacing), + .padding(top = topSpacing) + .then(spamModifier), verticalArrangement = Arrangement.spacedBy(0.dp) ) { Box(modifier = Modifier.fillMaxWidth()) { @@ -497,9 +515,29 @@ fun MessageItem( } // Link previews removed; links are now highlighted inline and clickable within the message text + + if (spamVerdict != null) { + Text( + text = "spam filter would hide this: ${spamVerdict.debugLabel()}", + fontFamily = BitchatFontFamily, + fontSize = 11.sp, + color = colorScheme.error, + modifier = Modifier.padding(top = 2.dp) + ) + } } } +private fun SpamVerdict.debugLabel(): String = when (this) { + SpamVerdict.ALLOWED -> "allowed" + SpamVerdict.MESSAGE_RATE -> "too many messages" + SpamVerdict.CHARACTER_RATE -> "too many characters" + SpamVerdict.LINK_RATE -> "too many links" + SpamVerdict.REPEATED_MESSAGE -> "repeated message" + SpamVerdict.REPEATED_ACROSS_USERS -> "same text from other users" + SpamVerdict.REPEATED_CHARACTERS -> "repeated characters" +} + @OptIn(ExperimentalFoundationApi::class) @Composable private fun MessageTextWithClickableNicknames( diff --git a/app/src/main/java/com/bitchat/android/ui/VerificationSheet.kt b/app/src/main/java/com/bitchat/android/ui/VerificationSheet.kt index a84c3c947..fca163999 100644 --- a/app/src/main/java/com/bitchat/android/ui/VerificationSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/VerificationSheet.kt @@ -512,7 +512,7 @@ private fun bitmapFromMatrix(matrix: BitMatrix): Bitmap { return bitmap } -private class QRCodeAnalyzer( +internal class QRCodeAnalyzer( private val onCode: (String) -> Unit ) : ImageAnalysis.Analyzer { private val scanner = BarcodeScanning.getClient( diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt index 9c8c48b5e..4be7a992b 100644 --- a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt @@ -355,6 +355,9 @@ fun DebugSettingsSheet( item { DistributionInfoSection(distributionInfo) } + item { + SpamFilterDebugSection() + } // Verbose logging toggle item { Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) { diff --git a/app/src/main/java/com/bitchat/android/ui/debug/SpamFilterDebugSection.kt b/app/src/main/java/com/bitchat/android/ui/debug/SpamFilterDebugSection.kt new file mode 100644 index 000000000..0a97a8999 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/ui/debug/SpamFilterDebugSection.kt @@ -0,0 +1,139 @@ +package com.bitchat.android.ui.debug + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bitchat.android.spam.SpamFilter +import com.bitchat.android.spam.SpamFilterPreferences +import com.bitchat.android.spam.SpamTuning +import com.bitchat.android.ui.theme.BitchatFontFamily +import kotlin.math.roundToInt + +/** Debug controls for the spam filter: live limit tuning and the mark-instead-of-hide bypass. */ +@Composable +fun SpamFilterDebugSection() { + val colorScheme = MaterialTheme.colorScheme + val filterEnabled by SpamFilterPreferences.enabled.collectAsState() + val bypass by SpamFilterPreferences.debugBypass.collectAsState() + val tuning by SpamFilterPreferences.tuning.collectAsState() + val flagged by SpamFilter.shared.flagged.collectAsState() + val update: (SpamTuning) -> Unit = SpamFilterPreferences::setTuning + + Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon(Icons.Filled.FilterAlt, contentDescription = null, tint = colorScheme.error) + Text("Spam filter", fontFamily = BitchatFontFamily, fontSize = 14.sp, fontWeight = FontWeight.Medium) + Spacer(Modifier.weight(1f)) + Switch(checked = filterEnabled, onCheckedChange = SpamFilterPreferences::setEnabled) + } + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Column(Modifier.weight(1f)) { + Text("Show spam in red instead of hiding it", fontFamily = BitchatFontFamily, fontSize = 12.sp) + Text( + "Bypass for debugging. ${flagged.size} marked since the last reset or limit change.", + fontFamily = BitchatFontFamily, + fontSize = 11.sp, + color = colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + Switch(checked = bypass, onCheckedChange = SpamFilterPreferences::setDebugBypass, enabled = filterEnabled) + } + + Text( + "Limits per sender. Changing one starts every sender with a full budget.", + fontFamily = BitchatFontFamily, + fontSize = 11.sp, + color = colorScheme.onSurface.copy(alpha = 0.7f) + ) + TuningSlider("Message burst", "${tuning.messageBurst} messages", tuning.messageBurst, 1..30) { + update(tuning.copy(messageBurst = it)) + } + TuningSlider("Message refill", "1 every ${tuning.secondsPerMessage} s", tuning.secondsPerMessage, 1..60) { + update(tuning.copy(secondsPerMessage = it)) + } + TuningSlider("Characters", "${tuning.charactersPerMinute} per minute", tuning.charactersPerMinute, 100..3000, step = 50) { + update(tuning.copy(charactersPerMinute = it)) + } + TuningSlider("Links", "${tuning.linksPerMinute} per minute", tuning.linksPerMinute, 0..10) { + update(tuning.copy(linksPerMinute = it)) + } + TuningSlider("Repeats allowed", "${tuning.allowedRepeats} near-copies", tuning.allowedRepeats, 0..10) { + update(tuning.copy(allowedRepeats = it)) + } + TuningSlider("Copies from other users allowed", "${tuning.crossUserAllowedRepeats} near-copies", tuning.crossUserAllowedRepeats, 0..10) { + update(tuning.copy(crossUserAllowedRepeats = it)) + } + TuningSlider("Compare across users from", "${tuning.crossUserMinLength} chars", tuning.crossUserMinLength, 4..100) { + update(tuning.copy(crossUserMinLength = it)) + } + TuningSlider("Near-copy match", "≥ ${tuning.repeatSimilarityPercent}%", tuning.repeatSimilarityPercent, 50..100) { + update(tuning.copy(repeatSimilarityPercent = it)) + } + TuningSlider("Repeat window", "${tuning.repeatWindowMinutes} min", tuning.repeatWindowMinutes, 1..60) { + update(tuning.copy(repeatWindowMinutes = it)) + } + TuningSlider("Repeated characters", "≥ ${tuning.selfMatchPercent}% self-match", tuning.selfMatchPercent, 50..100) { + update(tuning.copy(selfMatchPercent = it)) + } + TuningSlider("Repeated characters from", "${tuning.selfMatchMinLength} chars", tuning.selfMatchMinLength, 8..200, step = 4) { + update(tuning.copy(selfMatchMinLength = it)) + } + + Row { + Spacer(Modifier.weight(1f)) + TextButton(onClick = { update(SpamTuning.DEFAULT) }, enabled = tuning != SpamTuning.DEFAULT) { + Text("Reset to defaults", fontFamily = BitchatFontFamily, fontSize = 12.sp) + } + } + } + } +} + +@Composable +private fun TuningSlider( + label: String, + valueText: String, + value: Int, + range: IntRange, + step: Int = 1, + onChange: (Int) -> Unit +) { + val colorScheme = MaterialTheme.colorScheme + Column { + Row { + Text(label, fontFamily = BitchatFontFamily, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f)) + Spacer(Modifier.weight(1f)) + Text(valueText, fontFamily = BitchatFontFamily, fontSize = 11.sp) + } + Slider( + value = value.toFloat(), + onValueChange = { raw -> + val snapped = range.first + ((raw - range.first) / step).roundToInt() * step + onChange(snapped.coerceIn(range)) + }, + valueRange = range.first.toFloat()..range.last.toFloat(), + steps = ((range.last - range.first) / step - 1).coerceAtLeast(0) + ) + } +} diff --git a/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt b/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt index 1f3f567ed..131d1605b 100644 --- a/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt +++ b/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt @@ -111,7 +111,7 @@ fun VoiceNotePlayer( onSeek = seekTo ) val durText = if (durationMs > 0) String.format("%02d:%02d", (durationMs / 1000) / 60, (durationMs / 1000) % 60) else "--:--" - Text(text = durText, fontFamily = BitchatFontFamily, fontSize = 12.sp) + Text(text = durText, fontFamily = BitchatFontFamily, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurface) } } diff --git a/app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt b/app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt index 64261d47a..8c598a62a 100644 --- a/app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt +++ b/app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt @@ -2,6 +2,7 @@ package com.bitchat.android.ui.media import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshots.SnapshotStateList @@ -50,6 +51,10 @@ fun WaveformPreview( path: String, sendProgress: Float?, playbackProgress: Float?, + // Unplayed bars follow the surrounding text colour so they read on any bubble tint or + // surface; the played portion uses the theme accent, which is tuned per light/dark scheme. + baseColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f), + playedColor: Color = MaterialTheme.colorScheme.primary, onLoaded: ((FloatArray) -> Unit)? = null, onSeek: ((Float) -> Unit)? = null ) { @@ -74,10 +79,10 @@ fun WaveformPreview( modifier = modifier, samples = stateSamples, fillProgress = if (stateSamples.isEmpty()) 0f else progress, - baseColor = Color(0x2200FF7F), + baseColor = baseColor, fillColor = when { sendProgress != null -> Color(0xFF1E88E5) // blue while sending - else -> Color(0xFF00C851) // green during playback + else -> playedColor }, onSeek = onSeek ) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 772e48a5e..4b4d1fa77 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -393,4 +393,6 @@ لغة التطبيق إعداد النظام الافتراضي اختر اللغة + فلتر الرسائل المزعجة + إخفاء الرسائل العامة من الأشخاص الذين يرسلون بسرعة مفرطة أو يكررون رسائلهم أو يغرقون الدردشة بالروابط. diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index b6557fe2d..746ca763f 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -380,4 +380,6 @@ অ্যাপের ভাষা সিস্টেম ডিফল্ট ভাষা নির্বাচন করুন + স্প্যাম ফিল্টার + যারা খুব দ্রুত বার্তা পাঠান, একই কথা বারবার বলেন বা অতিরিক্ত লিঙ্ক পাঠান, তাঁদের প্রকাশ্য বার্তা লুকান। diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 75cc8927e..fe7933126 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -394,4 +394,6 @@ App-Sprache Systemstandard Sprache auswählen + Spamfilter + Öffentliche Nachrichten von Personen ausblenden, die zu schnell schreiben, sich wiederholen oder zu viele Links senden. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3f08ead06..e427714ff 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -393,4 +393,6 @@ Idioma de la app Predeterminado del sistema Seleccionar idioma + Filtro de spam + Oculta los mensajes públicos de quienes publican demasiado rápido, se repiten o envían demasiados enlaces. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index af49e1638..7e9c21894 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -619,4 +619,10 @@ Aplikazioaren hizkuntza Sistemaren lehenetsia Hautatu hizkuntza + bitchat + 📄 %1$s + v%1$s + image/* + Spam-iragazkia + Ezkutatu azkarregi idazten duten, mezuak errepikatzen dituzten edo esteka gehiegi bidaltzen dituzten pertsonen mezu publikoak. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 1e092b641..fa86d3d41 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -380,4 +380,6 @@ زبان برنامه پیش‌فرض سیستم انتخاب زبان + فیلتر هرزنامه + پیام‌های عمومی افرادی را که خیلی سریع پیام می‌فرستند، خود را تکرار می‌کنند یا پیوندهای زیادی می‌فرستند پنهان کن. diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index fa4aea0cf..283079381 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -391,4 +391,6 @@ Wika ng app Default ng system Pumili ng wika + Filter ng spam + Itago ang mga pampublikong mensahe mula sa mga taong masyadong mabilis mag-post, paulit-ulit ang sinasabi, o nagpapadala ng napakaraming link. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a884d35e1..4f5b0d287 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -405,4 +405,6 @@ Langue de l’appli Langue du système Choisir la langue + Filtre antispam + Masquer les messages publics des personnes qui publient trop vite, se répètent ou inondent le chat de liens. diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index f742042ef..da4fd7fde 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -392,4 +392,6 @@ שפת האפליקציה ברירת המחדל של המערכת בחירת שפה + מסנן ספאם + הסתר הודעות ציבוריות מאנשים ששולחים מהר מדי, חוזרים על עצמם או מציפים בקישורים. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 4cb18e38c..42a5db6b6 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -393,4 +393,6 @@ ऐप की भाषा सिस्टम डिफ़ॉल्ट भाषा चुनें + स्पैम फ़िल्टर + बहुत तेज़ी से संदेश भेजने, एक ही बात दोहराने या बहुत अधिक लिंक भेजने वाले लोगों के सार्वजनिक संदेश छिपाएँ। diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 02c86a683..48f1056bf 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -393,4 +393,6 @@ Bahasa aplikasi Default sistem Pilih bahasa + Filter spam + Sembunyikan pesan publik dari orang yang mengirim terlalu cepat, mengulang pesan, atau membanjiri obrolan dengan tautan. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index af59fad3e..489013089 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -420,4 +420,6 @@ Lingua dell’app Predefinita di sistema Seleziona lingua + Filtro antispam + Nascondi i messaggi pubblici di chi scrive troppo velocemente, si ripete o invia troppi link. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 17841dcc5..24c0ab07f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -393,4 +393,6 @@ アプリの言語 システムのデフォルト 言語を選択 + スパムフィルター + 短時間に大量のメッセージを送信したり、同じ内容を繰り返したり、リンクを大量に投稿したりする人の公開メッセージを非表示にします。 diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 8fe186cc7..b62b40465 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -380,4 +380,6 @@ აპის ენა სისტემის ნაგულისხმევი ენის არჩევა + სპამის ფილტრი + დამალე იმ ადამიანების საჯარო შეტყობინებები, რომლებიც ზედმეტად სწრაფად წერენ, იმეორებენ შეტყობინებებს ან აგზავნიან ზედმეტად ბევრ ბმულს. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 030165d6f..f1c095258 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -393,4 +393,6 @@ 앱 언어 시스템 기본값 언어 선택 + 스팸 필터 + 너무 빠르게 메시지를 보내거나, 같은 내용을 반복하거나, 링크를 과도하게 보내는 사람의 공개 메시지를 숨깁니다. diff --git a/app/src/main/res/values-mg/strings.xml b/app/src/main/res/values-mg/strings.xml index b7182e096..0475af611 100644 --- a/app/src/main/res/values-mg/strings.xml +++ b/app/src/main/res/values-mg/strings.xml @@ -400,4 +400,6 @@ Fitenin’ny app Fitenin’ny rafitra Safidio ny fiteny + Sivana spam + Afeno ny hafatra ampahibemaso avy amin’ireo olona mandefa haingana loatra, mamerimberina hafatra, na mandefa rohy be loatra. diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 79bf5b1c2..74bf035f2 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -433,4 +433,6 @@ Bahasa aplikasi Lalai sistem Pilih bahasa + Penapis spam + Sembunyikan mesej awam daripada orang yang menghantar terlalu pantas, mengulang mesej atau membanjiri perbualan dengan pautan. diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 59d528532..87b2f1e22 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -391,4 +391,6 @@ एपको भाषा प्रणालीको पूर्वनिर्धारित भाषा छान्नुहोस् + स्प्याम फिल्टर + धेरै छिटो सन्देश पठाउने, एउटै कुरा दोहोर्‍याउने वा धेरै लिङ्क पठाउने व्यक्तिका सार्वजनिक सन्देशहरू लुकाउनुहोस्। diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1e46f196f..6637e3869 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -418,4 +418,6 @@ App-taal Systeemstandaard Taal selecteren + Spamfilter + Verberg openbare berichten van mensen die te snel posten, zichzelf herhalen of te veel links sturen. diff --git a/app/src/main/res/values-pa-rPK/strings.xml b/app/src/main/res/values-pa-rPK/strings.xml index a3f677355..3e37dfbf9 100644 --- a/app/src/main/res/values-pa-rPK/strings.xml +++ b/app/src/main/res/values-pa-rPK/strings.xml @@ -380,4 +380,6 @@ ایپ دی بولی سسٹم دی ڈیفالٹ بولی چݨو + سپیم فلٹر + اوہناں لوکاں دے عوامی پیغام لُکاؤ جو بہت تیزی نال پیغام بھیجدے، گلّاں دُہراوندے یا بہت سارے لنک بھیجدے نیں۔ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3ec84dd46..331134801 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -398,4 +398,6 @@ Język aplikacji Domyślny systemu Wybierz język + Filtr spamu + Ukrywaj publiczne wiadomości od osób, które piszą zbyt szybko, powtarzają się lub wysyłają zbyt wiele linków. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 423408577..3f2fbfc0f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -392,4 +392,6 @@ Idioma do app Padrão do sistema Selecionar idioma + Filtro de spam + Ocultar mensagens públicas de pessoas que enviam mensagens rápido demais, se repetem ou enviam links em excesso. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0e01bb078..0127bf346 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -393,4 +393,6 @@ Idioma da aplicação Predefinição do sistema Selecionar idioma + Filtro de spam + Ocultar mensagens públicas de pessoas que publicam demasiado depressa, se repetem ou enviam demasiadas ligações. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 02b1175de..e7738dc6b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -380,4 +380,6 @@ Язык приложения Системный язык Выбрать язык + Спам-фильтр + Скрывать публичные сообщения от тех, кто пишет слишком быстро, повторяется или отправляет слишком много ссылок. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index b958faad6..2cc58f35e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -378,4 +378,6 @@ Appspråk Systemstandard Välj språk + Spamfilter + Dölj offentliga meddelanden från personer som skriver för snabbt, upprepar sig eller skickar för många länkar. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 243f21128..6b1b6f2da 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -390,4 +390,6 @@ பயன்பாட்டு மொழி கணினி இயல்புநிலை மொழியைத் தேர்ந்தெடுக்கவும் + ஸ்பேம் வடிகட்டி + மிக வேகமாகச் செய்திகளை அனுப்புபவர்கள், ஒரே கருத்தைத் திரும்பத் திரும்பக் கூறுபவர்கள் அல்லது அதிக இணைப்புகளை அனுப்புபவர்களின் பொதுச் செய்திகளை மறைக்கவும். diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index b21049c13..a6e22fde8 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -380,4 +380,6 @@ ภาษาของแอป ค่าเริ่มต้นของระบบ เลือกภาษา + ตัวกรองสแปม + ซ่อนข้อความสาธารณะจากผู้ที่ส่งข้อความเร็วเกินไป ส่งข้อความซ้ำ หรือส่งลิงก์จำนวนมากเกินไป diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 251d6a462..e2787768c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -378,4 +378,6 @@ Uygulama dili Sistem varsayılanı Dil seçin + Spam filtresi + Çok hızlı mesaj gönderen, kendini tekrarlayan veya çok fazla bağlantı paylaşan kişilerin herkese açık mesajlarını gizle. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f248ff71d..8ac22faca 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -400,4 +400,6 @@ Мова застосунку Системна мова Вибрати мову + Спам-фільтр + Приховувати публічні повідомлення від тих, хто пише надто швидко, повторюється або надсилає забагато посилань. diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 785338fbe..4be8ca54b 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -393,4 +393,6 @@ ایپ کی زبان سسٹم ڈیفالٹ زبان منتخب کریں + اسپام فلٹر + ایسے لوگوں کے عوامی پیغامات چھپائیں جو بہت تیزی سے پیغامات بھیجتے ہیں، اپنی بات دہراتے ہیں یا بہت زیادہ لنکس بھیجتے ہیں۔ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b97c3fc57..10073675e 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -380,4 +380,6 @@ Ngôn ngữ ứng dụng Mặc định hệ thống Chọn ngôn ngữ + Bộ lọc thư rác + Ẩn tin nhắn công khai từ những người gửi quá nhanh, lặp lại nội dung hoặc gửi quá nhiều liên kết. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fca6b07b7..b2bac216c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -388,4 +388,6 @@ 应用语言 跟随系统 选择语言 + 垃圾消息过滤器 + 隐藏发送过快、重复发言或发送过多链接的用户的公开消息。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f427dbaf9..ed78c193e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -389,4 +389,6 @@ 應用程式語言 跟隨系統 選擇語言 + 垃圾訊息篩選器 + 隱藏傳送過快、重複發言或傳送過多連結的使用者的公開訊息。 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 35f7477af..6fc8dce19 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -404,4 +404,6 @@ 应用语言 跟随系统 选择语言 + 垃圾消息过滤器 + 隐藏发送过快、重复发言或发送过多链接的用户的公开消息。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00268a54b..7f20c1eb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -209,6 +209,8 @@ PoW Off PoW On Add proof of work to geohash messages for spam deterrence. + Spam Filter + Hide public messages from people who post too fast, repeat themselves, or flood links. Difficulty: %1$d bits (~%2$s) Difficulty %1$d requires ~%2$s hash attempts No proof of work required diff --git a/app/src/test/java/com/bitchat/android/spam/SpamFilterTest.kt b/app/src/test/java/com/bitchat/android/spam/SpamFilterTest.kt new file mode 100644 index 000000000..3c5feabd5 --- /dev/null +++ b/app/src/test/java/com/bitchat/android/spam/SpamFilterTest.kt @@ -0,0 +1,432 @@ +package com.bitchat.android.spam + +import com.bitchat.android.model.BitchatMessage +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.random.Random + +class SpamFilterTest { + + private var now = 1_000_000_000L + private var nextId = 0 + private val random = Random(7) + private val filter = SpamFilter(clock = { now }) + + private fun send( + content: String = prose(), + sender: String = "alice", + timestampMs: Long = now, + id: String = "m${nextId++}" + ) = filter.check(sender, id, content, timestampMs) + + /** Unrelated synthetic words, so only the rule under test can fire. */ + private fun prose(length: Int = 40): String = buildString { + while (this.length < length) { + if (isNotEmpty()) append(' ') + repeat(random.nextInt(2, 8)) { append('a' + random.nextInt(26)) } + } + }.take(length) + + private fun advanceSeconds(seconds: Int) { + now += seconds * 1000L + } + + @Test + fun `sixth message in a burst is shown and seventh is hidden`() { + repeat(6) { assertEquals(SpamVerdict.ALLOWED, send()) } + assertEquals(SpamVerdict.MESSAGE_RATE, send()) + } + + @Test + fun `message budget refills one token every ten seconds`() { + repeat(6) { send() } + advanceSeconds(9) + assertEquals(SpamVerdict.MESSAGE_RATE, send()) + advanceSeconds(1) + assertEquals(SpamVerdict.ALLOWED, send()) + assertEquals(SpamVerdict.MESSAGE_RATE, send()) + } + + @Test + fun `hidden messages do not drain the budget of a flooding sender`() { + var shown = 0 + repeat(120) { + if (send() == SpamVerdict.ALLOWED) shown++ + advanceSeconds(1) + } + // A burst of 6, then one every 10 seconds for the remaining two minutes. + assertEquals(6 + 11, shown) + } + + @Test + fun `character budget allows twelve hundred characters per minute`() { + assertEquals(SpamVerdict.ALLOWED, send(prose(600))) + assertEquals(SpamVerdict.ALLOWED, send(prose(600))) + assertEquals(SpamVerdict.CHARACTER_RATE, send(prose(20))) + advanceSeconds(2) + assertEquals(SpamVerdict.ALLOWED, send(prose(20))) + } + + @Test + fun `message longer than the character budget is hidden`() { + assertEquals(SpamVerdict.CHARACTER_RATE, send(prose(1201))) + } + + @Test + fun `characters are counted as code points`() { + val emoji = "😀" + val sentence = (0 until 1200).joinToString("") { if (it % 3 == 0) emoji else "${'a' + random.nextInt(26)}" } + assertEquals(SpamVerdict.ALLOWED, send(sentence)) + } + + @Test + fun `third link within a minute is hidden`() { + assertEquals(SpamVerdict.ALLOWED, send("see https://example.org/a for details")) + assertEquals(SpamVerdict.ALLOWED, send("mirror at www.example.net too")) + assertEquals(SpamVerdict.LINK_RATE, send("and example.com/promo as well")) + assertEquals(SpamVerdict.ALLOWED, send("no link in this one at all")) + advanceSeconds(30) + assertEquals(SpamVerdict.ALLOWED, send("ok one more https://example.org/b")) + } + + @Test + fun `message with three links is hidden even for a fresh sender`() { + assertEquals( + SpamVerdict.LINK_RATE, + send("https://alpha.example/one and bravo.example plus www.charlie.example/three") + ) + } + + @Test + fun `link counter ignores email addresses and abbreviations`() { + assertEquals(0, LinkCounter.count("mail me at someone@example.com e.g. today")) + assertEquals(0, LinkCounter.count("pi is 3.14, version v1.2.3")) + assertEquals(3, LinkCounter.count("https://a.example/x, www.b.example and c.example.")) + } + + @Test + fun `third copy of a message within ten minutes is hidden`() { + val text = "anyone near the north gate tonight?" + assertEquals(SpamVerdict.ALLOWED, send(text)) + advanceSeconds(60) + assertEquals(SpamVerdict.ALLOWED, send(text)) + advanceSeconds(60) + assertEquals(SpamVerdict.REPEATED_MESSAGE, send(text)) + } + + @Test + fun `near copies with a changing suffix count as repeats`() { + assertEquals(SpamVerdict.ALLOWED, send("FREE SATS at the link below 101")) + advanceSeconds(20) + assertEquals(SpamVerdict.ALLOWED, send("free sats at the link below 202")) + advanceSeconds(20) + assertEquals(SpamVerdict.REPEATED_MESSAGE, send("free sats at the link below 303")) + } + + @Test + fun `repeats spread beyond the history window are allowed`() { + val text = "good morning everyone, coffee anyone?" + send(text) + advanceSeconds(11 * 60) + send(text) + advanceSeconds(11 * 60) + assertEquals(SpamVerdict.ALLOWED, send(text)) + } + + @Test + fun `short replies may repeat freely`() { + repeat(5) { + assertEquals(SpamVerdict.ALLOWED, send("lol yes")) + advanceSeconds(10) + } + } + + @Test + fun `message repeating a character is hidden`() { + assertEquals(SpamVerdict.REPEATED_CHARACTERS, send("A".repeat(40))) + assertEquals(SpamVerdict.REPEATED_CHARACTERS, send("hey " + "!".repeat(60))) + } + + @Test + fun `message repeating a phrase is hidden`() { + assertEquals(SpamVerdict.REPEATED_CHARACTERS, send("buy now ".repeat(6))) + } + + @Test + fun `ordinary long sentence is not mistaken for repetition`() { + val text = "The market by the old bridge opens at nine, bring cash because the card reader is flaky." + assertTrue(RepetitionDetector.selfSimilarity(RepetitionDetector.normalize(text, 1000)) < 0.3) + assertEquals(SpamVerdict.ALLOWED, send(text)) + } + + @Test + fun `senders have independent budgets`() { + repeat(6) { send(sender = "alice") } + assertEquals(SpamVerdict.MESSAGE_RATE, send(sender = "alice")) + assertEquals(SpamVerdict.ALLOWED, send(sender = "bob")) + } + + @Test + fun `same message ID arriving twice is judged once`() { + repeat(5) { send() } + assertEquals(SpamVerdict.ALLOWED, send(id = "dup")) + assertEquals(SpamVerdict.ALLOWED, send(id = "dup")) + assertEquals(SpamVerdict.MESSAGE_RATE, send()) + } + + @Test + fun `backfilled history delivered newest first is shown`() { + // Ten messages, one per minute over the last ten minutes, arriving in one burst. + val verdicts = (0 until 10).map { minutesAgo -> + send(timestampMs = now - minutesAgo * 60_000L) + } + assertTrue(verdicts.all { it == SpamVerdict.ALLOWED }) + } + + @Test + fun `backfilled burst is still rate limited on its own timestamps`() { + val start = now - 5 * 60_000L + val verdicts = (0 until 10).map { send(timestampMs = start + it * 1000L) } + assertEquals(6, verdicts.count { it == SpamVerdict.ALLOWED }) + } + + @Test + fun `backfill cannot be squeezed in between already shown messages`() { + repeat(6) { send(timestampMs = now - 60_000L) } + // A message claiming to be from just before the burst would overdraw the bucket. + assertEquals(SpamVerdict.MESSAGE_RATE, send(timestampMs = now - 61_000L)) + } + + @Test + fun `timestamps older than the backdate window are clamped`() { + // Spreading fake timestamps over days must not buy extra budget. + val verdicts = (0 until 10).map { send(timestampMs = now - (10 - it) * 86_400_000L) } + assertEquals(6, verdicts.count { it == SpamVerdict.ALLOWED }) + } + + @Test + fun `future timestamps are clamped to now`() { + val verdicts = (0 until 10).map { send(timestampMs = now + it * 60_000L) } + assertEquals(6, verdicts.count { it == SpamVerdict.ALLOWED }) + } + + @Test + fun `disabled filter allows everything`() { + filter.enabled = false + repeat(20) { assertEquals(SpamVerdict.ALLOWED, send("A".repeat(40))) } + } + + @Test + fun `limits are configurable`() { + val strict = SpamFilter( + config = SpamFilterConfig(messageBucket = TokenBucketConfig(capacity = 2.0, refillPerSecond = 1.0)), + clock = { now } + ) + assertEquals(SpamVerdict.ALLOWED, strict.check("a", "1", prose(), now)) + assertEquals(SpamVerdict.ALLOWED, strict.check("a", "2", prose(), now)) + assertEquals(SpamVerdict.MESSAGE_RATE, strict.check("a", "3", prose(), now)) + advanceSeconds(1) + assertEquals(SpamVerdict.ALLOWED, strict.check("a", "4", prose(), now)) + } + + @Test + fun `debug bypass shows spam and records why it would be hidden`() { + filter.debugBypass = true + val messages = (1..7).map { + BitchatMessage(id = "b$it", sender = "flooder", content = "hi $it", timestamp = java.util.Date(now)) + } + + val hidden = messages.map { filter.shouldHide(it, "flooder") } + + assertTrue(hidden.none { it }) + assertEquals(mapOf("b7" to SpamVerdict.MESSAGE_RATE), filter.flagged.value) + } + + @Test + fun `spam is hidden without the debug bypass`() { + val messages = (1..7).map { + BitchatMessage(id = "h$it", sender = "flooder", content = "hi $it", timestamp = java.util.Date(now)) + } + + assertEquals(listOf(false, false, false, false, false, false, true), messages.map { filter.shouldHide(it, "flooder") }) + assertTrue(filter.flagged.value.isEmpty()) + } + + @Test + fun `changing limits starts every sender with a full budget`() { + repeat(6) { send() } + assertEquals(SpamVerdict.MESSAGE_RATE, send()) + + filter.config = SpamTuning.DEFAULT.copy(messageBurst = 10).toConfig() + + repeat(10) { assertEquals(SpamVerdict.ALLOWED, send()) } + assertEquals(SpamVerdict.MESSAGE_RATE, send()) + } + + @Test + fun `default tuning matches the code defaults`() { + assertEquals(SpamFilterConfig(), SpamTuning.DEFAULT.toConfig()) + assertFalse(SpamTuning.DEFAULT.copy(linksPerMinute = 5).toConfig() == SpamFilterConfig()) + } + + @Test + fun `same text from many throwaway identities is hidden after three copies`() { + val text = "Free sats for everyone at freesats dot example" + val verdicts = (1..6).map { send(text, sender = "npub$it") } + + assertEquals( + listOf(SpamVerdict.ALLOWED, SpamVerdict.ALLOWED, SpamVerdict.ALLOWED) + + List(3) { SpamVerdict.REPEATED_ACROSS_USERS }, + verdicts + ) + } + + @Test + fun `near copies with small changes from different identities are hidden`() { + // Each variant is one small edit (a suffix, an insertion, a substitution) off the first. + val variants = listOf( + "Free sats for everyone, claim yours now", + "free sats for everyone, claim yours now 1", + "Free sats for everyone!! claim yours now", + "free sats 4 everyone, claim yours now", + "free sats for everyone, claim yours now 55" + ) + val verdicts = variants.mapIndexed { i, text -> send(text, sender = "npub$i") } + + assertEquals(List(4) { SpamVerdict.ALLOWED } + SpamVerdict.REPEATED_ACROSS_USERS, verdicts) + } + + @Test + fun `short common replies from many users are not compared`() { + repeat(8) { assertEquals(SpamVerdict.ALLOWED, send("good morning!", sender = "user$it")) } + } + + @Test + fun `different messages on the same topic from many users are allowed`() { + val messages = listOf( + "is the bakery on the corner still open?", + "the bakery closes at eight I think", + "bakery was still open when I walked by", + "anyone know if the bakery has bread left?", + "the corner bakery is open until eight" + ) + messages.forEachIndexed { i, text -> assertEquals(SpamVerdict.ALLOWED, send(text, sender = "user$i")) } + } + + @Test + fun `copies from other users outside the window do not count`() { + val text = "anyone up for chess at the tables later?" + repeat(3) { + send(text, sender = "user$it") + advanceSeconds(4 * 60) + } + // Only the copy from 4 minutes ago and the one from 8 minutes ago are in the window. + assertEquals(SpamVerdict.ALLOWED, send(text, sender = "late")) + } + + @Test + fun `a sender's own copies do not count toward the cross-user limit`() { + val text = "anyone near the north gate tonight?" + send(text, sender = "alice") + advanceSeconds(60) + send(text, sender = "alice") + assertEquals(SpamVerdict.ALLOWED, send(text, sender = "bob")) + assertEquals(SpamVerdict.REPEATED_ACROSS_USERS, send(text, sender = "carol")) + } + + @Test + fun `best alignment reports where the texts line up`() { + val a = RepetitionDetector.normalize("xx hello world", 1000) + val b = RepetitionDetector.normalize("hello world", 1000) + val alignment = RepetitionDetector.align(a, b) + assertEquals(3, alignment.peaks.first().shift) + assertEquals(11, alignment.matches) + } + + @Test + fun `an inserted character does not break the match`() { + val a = RepetitionDetector.normalize("free sats for everyone, claim yours now", 1000) + val b = RepetitionDetector.normalize("free sats for everyone!!, claim yours now", 1000) + assertTrue(RepetitionDetector.align(a, b, maxPeaks = 1).score < 0.8) + assertTrue(RepetitionDetector.align(a, b).score > 0.9) + } + + @Test + fun `unrelated sentences stay far below the near-copy threshold`() { + val a = RepetitionDetector.normalize("the bakery closes at eight I think", 1000) + val b = RepetitionDetector.normalize("the corner bakery is open until eight", 1000) + assertTrue(RepetitionDetector.align(a, b).score < 0.6) + } + + private val disguiseBase = "Free sats for everyone, claim yours now" + + /** The same text a reader sees, written so that a plain comparison would miss it. */ + private val disguises = mapOf( + "zero-width spaces" to disguiseBase.map { "$it​" }.joinToString(""), + "mixed invisible characters" to disguiseBase.mapIndexed { i, c -> + "$c" + listOf("‌", "‍", "⁠", "­", "‎")[i % 5] + }.joinToString(""), + "an accent on every letter" to disguiseBase.map { if (it.isLetter()) "$it́" else "$it" }.joinToString(""), + "math bold letters" to disguiseBase.map { c -> + when (c) { + in 'a'..'z' -> String(Character.toChars(0x1D41A + (c - 'a'))) + in 'A'..'Z' -> String(Character.toChars(0x1D400 + (c - 'A'))) + else -> "$c" + } + }.joinToString(""), + "fullwidth letters" to disguiseBase.map { c -> if (c in '!'..'~') (c.code + 0xFEE0).toChar() else c }.joinToString(""), + "non-breaking spaces" to disguiseBase.replace(' ', ' ') + ) + + @Test + fun `normalization folds disguises to the text a reader sees`() { + val plain = RepetitionDetector.normalize(disguiseBase, 1000).toList() + disguises.forEach { (label, text) -> + assertEquals(label, plain, RepetitionDetector.normalize(text, 1000).toList()) + } + } + + @Test + fun `disguised copies from other users are still caught`() { + disguises.forEach { (label, text) -> + val fresh = SpamFilter(clock = { now }) + repeat(3) { fresh.check("npub$it", "$label-$it", disguiseBase, now) } + assertEquals(label, SpamVerdict.REPEATED_ACROSS_USERS, fresh.check("npub9", "$label-x", text, now)) + } + } + + @Test + fun `disguised copies from the same user are still caught`() { + disguises.forEach { (label, text) -> + val fresh = SpamFilter(clock = { now }) + fresh.check("alice", "$label-1", disguiseBase, now) + fresh.check("alice", "$label-2", disguiseBase, now) + assertEquals(label, SpamVerdict.REPEATED_MESSAGE, fresh.check("alice", "$label-3", text, now)) + } + } + + @Test + fun `invisible characters cannot break up a repeated character run`() { + assertEquals(SpamVerdict.REPEATED_CHARACTERS, send("A​".repeat(40))) + } + + @Test + fun `ordinary text with accents and other scripts is still allowed`() { + listOf( + "Tiếng Việt có dấu, chúng tôi đang ở quán cà phê gần chợ", + "क्या कोई पास में चाय की दुकान जानता है? मैं स्टेशन के पास हूँ", + "Café au lait, crème brûlée, s'il vous plaît", + "家族と一緒に駅の近くのカフェにいます、誰か来ますか" + ).forEachIndexed { i, text -> assertEquals(text, SpamVerdict.ALLOWED, send(text, sender = "user$i")) } + } + + @Test + fun `different sentences in another script are not mistaken for copies`() { + assertEquals(SpamVerdict.ALLOWED, send("क्या कोई पास में चाय की दुकान जानता है?", sender = "a")) + assertEquals(SpamVerdict.ALLOWED, send("मैं स्टेशन के पास हूँ, बारिश हो रही है", sender = "b")) + assertEquals(SpamVerdict.ALLOWED, send("आज बाज़ार कितने बजे बंद होगा?", sender = "c")) + assertEquals(SpamVerdict.ALLOWED, send("क्या किसी के पास छाता है जो मैं ले सकूँ?", sender = "d")) + } +} diff --git a/app/src/test/java/com/bitchat/android/ui/MessagesListFollowTest.kt b/app/src/test/java/com/bitchat/android/ui/MessagesListFollowTest.kt new file mode 100644 index 000000000..b31fa43df --- /dev/null +++ b/app/src/test/java/com/bitchat/android/ui/MessagesListFollowTest.kt @@ -0,0 +1,118 @@ +package com.bitchat.android.ui + +import android.app.Application +import android.content.ComponentName +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasScrollToIndexAction +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.unit.dp +import com.bitchat.android.mesh.MeshService +import com.bitchat.android.model.BitchatMessage +import java.util.Date +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExternalResource +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +/** + * The message list follows new arrivals while the reader is at the latest message. + * + * Public, channel and geohash timelines are capped, so once one is full every arrival evicts the + * oldest message and the list size stays constant. Following must not depend on the size changing. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35], application = Application::class) +class MessagesListFollowTest { + + private val compose = createComposeRule() + + // Unit tests run without merged Android resources, so Robolectric does not know the host + // activity the compose rule launches. Register it before the rule starts. + @get:Rule + val rules: RuleChain = RuleChain + .outerRule(object : ExternalResource() { + override fun before() { + val app = RuntimeEnvironment.getApplication() + shadowOf(app.packageManager) + .addActivityIfNotPresent(ComponentName(app, ComponentActivity::class.java)) + } + }) + .around(compose) + + private val meshService = mock { on { myPeerID } doReturn "0000000000000000" } + private var messages by mutableStateOf(List(TimelineSize) { msg(it) }) + private var scrolledUp: Boolean? = null + + private fun setContent() { + compose.setContent { + MessagesList( + messages = messages, + currentUserNickname = "me", + meshService = meshService, + modifier = Modifier.size(360.dp, 640.dp), + conversationKey = "geo:test", + onScrolledUpChanged = { scrolledUp = it } + ) + } + compose.waitForIdle() + } + + /** Mirrors the timeline retention cap: the oldest message makes room for the new one. */ + private fun arriveAtCapacity(count: Int) { + repeat(count) { + messages = messages.drop(1) + msg(TimelineSize + it) + compose.waitForIdle() + } + } + + @Test + fun `a full timeline keeps following arrivals that evict the oldest message`() { + setContent() + + arriveAtCapacity(10) + + assertFalse("following stopped once the size stopped changing", scrolledUp ?: false) + compose.onNodeWithText(body(TimelineSize + 9), substring = true).assertIsDisplayed() + } + + @Test + fun `a full timeline does not pull a reader back down after they scroll away`() { + setContent() + compose.onNode(hasScrollToIndexAction()).performScrollToIndex(TimelineSize - 1) + compose.waitForIdle() + + arriveAtCapacity(3) + + assertTrue("reader was yanked back to the latest message", scrolledUp ?: false) + } + + private fun msg(index: Int) = BitchatMessage( + id = "m$index", + sender = "alice", + content = body(index), + timestamp = Date(index * 1_000L), + ) + + private fun body(index: Int) = "synthetic message $index" + + private companion object { + const val TimelineSize = 40 + } +} diff --git a/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt b/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt index e43e11b59..43ad0f874 100644 --- a/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt @@ -10,6 +10,10 @@ import com.bitchat.android.noise.NoiseSessionError import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType import kotlinx.coroutines.runBlocking +import java.util.Collections +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -99,6 +103,75 @@ class SecurityManagerTest { } } + @Test + fun `cleanup serializes timestamp traversal with concurrent packet tracking`() { + assertCleanupSerializesWrites("messageTimestamps") + } + + @Test + fun `cleanup serializes timestamp traversal with concurrent handshake tracking`() { + assertCleanupSerializesWrites("keyExchangeTimestamps") + } + + private fun assertCleanupSerializesWrites(fieldName: String) { + val allowWrite = CountDownLatch(1) + val writeFinished = CountDownLatch(1) + lateinit var timestamps: MutableMap + val backing = object : LinkedHashMap() { + override val entries: MutableSet> + get() { + val original = super.entries + return object : AbstractMutableSet>() { + override val size get() = original.size + override fun add(element: MutableMap.MutableEntry) = + error("Not used") + override fun iterator(): MutableIterator> { + val iterator = original.iterator() + var first = true + return object : MutableIterator> { + override fun hasNext() = iterator.hasNext() + override fun remove() = iterator.remove() + override fun next(): MutableMap.MutableEntry { + if (first) { + first = false + allowWrite.countDown() + // A correct synchronized traversal blocks the writer. An + // unguarded traversal deterministically exposes the CME. + if (!Thread.holdsLock(timestamps)) { + assertTrue(writeFinished.await(5, TimeUnit.SECONDS)) + } + } + return iterator.next() + } + } + } + } + } + } + timestamps = Collections.synchronizedMap(backing) + timestamps["expired-a"] = 1L + timestamps["expired-b"] = 2L + SecurityManager::class.java.getDeclaredField(fieldName).apply { + isAccessible = true + set(securityManager, timestamps) + } + val now = 10_000_000L + val executor = Executors.newSingleThreadExecutor() + val writer = executor.submit { + check(allowWrite.await(5, TimeUnit.SECONDS)) + timestamps["fresh"] = now + writeFinished.countDown() + } + try { + securityManager.cleanupOldData(now) + writer.get(5, TimeUnit.SECONDS) + assertTrue(timestamps.keys == setOf("fresh")) + } finally { + allowWrite.countDown() + executor.shutdownNow() + } + } + @Test fun `validatePacket - rejects packet with missing signature`() { val packet = BitchatPacket( diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSpamTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSpamTest.kt new file mode 100644 index 000000000..fac5b94c9 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSpamTest.kt @@ -0,0 +1,79 @@ +package com.bitchat.android.nostr + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import com.bitchat.android.spam.SpamFilter +import com.bitchat.android.ui.ChatState +import com.bitchat.android.ui.DataManager +import com.bitchat.android.ui.MessageManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class GeohashMessageHandlerSpamTest { + + private val application: Application = ApplicationProvider.getApplicationContext() + + @OptIn(ExperimentalCoroutinesApi::class) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val geohash = "zzzzzz" + + private lateinit var chatState: ChatState + private lateinit var handler: GeohashMessageHandler + + @Before + fun setup() { + com.bitchat.android.services.AppStateStore.clear() + SpamFilter.shared.reset() + chatState = ChatState(scope = testScope) + val dataManager = DataManager(context = application) + handler = GeohashMessageHandler( + application = application, + repo = GeohashRepository(application, chatState, dataManager, testScope), + scope = testScope, + dataManager = dataManager, + addChannelMessage = MessageManager(state = chatState)::addChannelMessage, + signatureVerificationDispatcher = testDispatcher, + spamFilterDispatcher = testDispatcher + ) + } + + @After + fun tearDown() { + SpamFilter.shared.reset() + com.bitchat.android.services.AppStateStore.clear() + } + + private fun event(identity: NostrIdentity, content: String) = NostrEvent( + pubkey = identity.publicKeyHex, + createdAt = (System.currentTimeMillis() / 1000).toInt(), + kind = NostrKind.EPHEMERAL_EVENT, + tags = listOf(listOf("g", geohash)), + content = content + ).sign(identity.privateKeyHex) + + @Test + fun `flood from one pubkey is cut off after the burst`() { + val flooder = NostrIdentity.generate() + (1..8).forEach { handler.onEvent(event(flooder, "hi $it"), geohash) } + + assertEquals(6, chatState.getChannelMessagesValue()["geo:$geohash"]?.size) + } + + @Test + fun `same text from many pubkeys is cut off after three copies`() { + repeat(5) { + handler.onEvent(event(NostrIdentity.generate(), "Free sats for everyone, claim yours now"), geohash) + } + + assertEquals(3, chatState.getChannelMessagesValue()["geo:$geohash"]?.size) + } +} diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt index 152d4ad0a..598ec6553 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt @@ -3,6 +3,7 @@ package com.bitchat.android.nostr import android.app.Application import androidx.test.core.app.ApplicationProvider import com.bitchat.android.services.AppStateStore +import com.bitchat.android.spam.SpamFilter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -23,12 +24,17 @@ class NostrBackgroundEventProcessorTest { @Before fun setUp() { AppStateStore.clear() + // This test floods one sender to measure throughput; that is exactly what the spam + // filter hides, so it is covered by SpamFilterTest instead. + SpamFilter.shared.enabled = false scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) } @After fun tearDown() { scope.cancel() + SpamFilter.shared.enabled = true + SpamFilter.shared.reset() AppStateStore.clear() } diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrRelaySnapshotTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrRelaySnapshotTest.kt new file mode 100644 index 000000000..f3334c47d --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrRelaySnapshotTest.kt @@ -0,0 +1,55 @@ +package com.bitchat.android.nostr + +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import okhttp3.WebSocket +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class NostrRelaySnapshotTest { + @Test + fun `subscription tolerates last relay disconnecting between size and iteration`() { + val manager = NostrRelayManager::class.java.getDeclaredConstructor().apply { + isAccessible = true + }.newInstance() + val errors = mutableListOf() + val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob() + + CoroutineExceptionHandler { _, error -> errors.add(error) }) + val connections = object : ConcurrentHashMap() { + override val size: Int + get() { + val previous = super.size + // The real concurrent key view delegates size() to its backing map. + // Disconnect exactly after observing the last connection. + clear() + return previous + } + } + NostrRelayManager::class.java.getDeclaredField("scope").apply { + isAccessible = true + (get(manager) as CoroutineScope).cancel() + set(manager, scope) + } + NostrRelayManager::class.java.getDeclaredField("connections").apply { + isAccessible = true + set(manager, connections) + } + connections["wss://relay.example"] = mock() + try { + manager.subscribe(NostrFilter(kinds = listOf(NostrKind.TEXT_NOTE)), + id = "synthetic-subscription", handler = {}) + assertTrue("Subscription coroutine failed: $errors", errors.isEmpty()) + } finally { + scope.cancel() + connections.clear() + } + } +} diff --git a/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt b/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt index a430eb9ab..444a52011 100644 --- a/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt @@ -276,6 +276,65 @@ class IncomingMessageAdmissionTest { assertTrue(state.getPrivateChatsValue().isEmpty()) } + @Test + fun `public flood from one peer is hidden while private messages still arrive`() { + val peer = "flood-${UUID.randomUUID()}" + val admitted = (1..7).map { index -> + IncomingMessageAdmission.admitToAppState( + BitchatMessage( + id = "$peer-$index", + sender = "flooder", + content = "hi $index", + timestamp = Date(), + senderPeerID = peer + ) + ) + } + + assertEquals(listOf(true, true, true, true, true, true, false), admitted) + assertFalse(AppStateStore.publicMessages.value.any { it.id == "$peer-7" }) + assertTrue( + IncomingMessageAdmission.admitToAppState( + privateMessage(id = "$peer-dm").copy(senderPeerID = peer) + ) + ) + } + + @Test + fun `peer calling itself system is still spam filtered`() { + val peer = "system-nick-${UUID.randomUUID()}" + val admitted = (1..7).map { index -> + IncomingMessageAdmission.admitToAppState( + BitchatMessage( + id = "$peer-$index", + sender = "system", + content = "hi $index", + timestamp = Date(), + senderPeerID = peer + ) + ) + } + + assertFalse(admitted.last()) + } + + @Test + fun `local system notices are never spam filtered`() { + val run = UUID.randomUUID().toString() + val admitted = (1..10).map { index -> + IncomingMessageAdmission.admitToAppState( + BitchatMessage( + id = "notice-$run-$index", + sender = "system", + content = "peer$index favorited you — favorite back to continue DMs later.", + timestamp = Date() + ) + ) + } + + assertTrue(admitted.all { it }) + } + private fun privateMessage(id: String) = BitchatMessage( id = id, sender = "alice", diff --git a/docs/release-gate-runbook.md b/docs/release-gate-runbook.md index 8de1e0637..a8e10c87d 100644 --- a/docs/release-gate-runbook.md +++ b/docs/release-gate-runbook.md @@ -298,6 +298,7 @@ python3 tools/release_gate/mesh_lab.py scenario all \ | `dm` | Noise handshake both ways, encrypted DM round trips with content match | | `favorite_verification` | favorite signal, orange-outline/filled mutual state, and peer fingerprint verification | | `broadcast` | public mesh message A→B | +| `broadcast_spam` | spam filter on B: two near-copies from A shown, the third hidden, A's next ordinary message shown (B needs the filter on, default limits, debug bypass off) | | `ptt_dm` | Noise-encrypted 440 Hz PTT in both directions; asserts real-time capture, zero sequence gaps, decoded PCM duration/energy/continuity, and finalized-note absorption | | `ptt_broadcast` | signed public 440 Hz PTT with the same bidirectional packet and decoded-audio quality assertions | | `file` | 1 KB broadcast file, receiver SHA-256 matches fixture | diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a11f1a73d..5c36ce874 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -39,6 +39,11 @@ + + + + + @@ -64,6 +69,11 @@ + + + + + @@ -1760,6 +1770,11 @@ + + + + + @@ -4784,6 +4799,11 @@ + + + + + @@ -4794,6 +4814,11 @@ + + + + + diff --git a/tools/release_gate/mesh_lab.py b/tools/release_gate/mesh_lab.py index 7abe8085b..2ddb4200a 100644 --- a/tools/release_gate/mesh_lab.py +++ b/tools/release_gate/mesh_lab.py @@ -532,6 +532,30 @@ def scenario_broadcast(a: Device, b: Device) -> dict: return {"send": send_result, "recv": recv_result} +def scenario_broadcast_spam(a: Device, b: Device) -> dict: + """Spam filter on B: two near-copies from A are shown, the third is hidden, and A's next + ordinary message is still shown. Needs B's spam filter on, default limits, bypass off.""" + id_a = whoami(a)["peer_id"] + # A fresh 32-character body per run keeps earlier runs out of B's ten-minute repeat window. + token = uuid.uuid4().hex[:16] + copies = [f"{token}{token} copy {mark}" for mark in "ABC"] + control = f"control {token} with different words, still delivered" + expected = [True, True, False, True] + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool: + recvs = [pool.submit(b.cmd, "msg_recv", 45_000, contains=text) for text in [*copies, control]] + time.sleep(2) + for text in [*copies, control]: + a.cmd_ok("broadcast_msg", 30_000, content=text) + results = [recv.result() for recv in recvs] + delivered = [result.get("status") == "ok" for result in results] + if delivered != expected: + raise MeshLabError(f"expected delivery {expected} for copies A-C and control, got {delivered}") + for result in results: + if result.get("status") == "ok": + assert result["from"] == id_a, result + return {"delivered": delivered} + + def _ptt_one_way( sender: Device, receiver: Device, @@ -869,6 +893,7 @@ SCENARIOS = { "dm": scenario_dm, "favorite_verification": scenario_favorite_verification, "broadcast": scenario_broadcast, + "broadcast_spam": scenario_broadcast_spam, "ptt_dm": scenario_ptt_dm, "ptt_broadcast": scenario_ptt_broadcast, # Broadcast transfers are receiver-capped at 256 fragments (~120 KB); only @@ -899,6 +924,7 @@ WATCH_SCENARIOS = [ "dm", "favorite_verification", "broadcast", + "broadcast_spam", "ptt_dm", "ptt_broadcast", "raw", diff --git a/tools/release_gate/scanner_release_regression.sh b/tools/release_gate/scanner_release_regression.sh new file mode 100644 index 000000000..a190b3484 --- /dev/null +++ b/tools/release_gate/scanner_release_regression.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Reproduce/verify the real scanner entry point after production R8 shrinking. +# Usage: bash tools/release_gate/scanner_release_regression.sh +# Requires JDK 21, the pinned Android SDK, and adb on PATH. Never use publishable +# signing keys here: -PreleaseUiTests=true uses the debug key for test APKs only. +set -euo pipefail + +if [[ $# != 1 || "$1" != emulator-* ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi +scanner_emulator="$1" +if [[ "$(adb -s "$scanner_emulator" shell getprop ro.kernel.qemu | tr -d '\r')" != 1 ]]; then + echo "Refusing to install: target is not an emulator." >&2 + exit 2 +fi +scanner_abi="$(adb -s "$scanner_emulator" shell getprop ro.product.cpu.abi | tr -d '\r')" +case "$scanner_abi" in + arm64-v8a|x86_64|armeabi-v7a|x86) ;; + *) echo "Unsupported emulator ABI." >&2; exit 2 ;; +esac + +cd "$(git rev-parse --show-toplevel)" +# This must be a disposable emulator. Keep the fixture offline and location off; +# no real camera, identities, messages, or transport startup are involved. +adb -s "$scanner_emulator" shell svc wifi disable +adb -s "$scanner_emulator" shell svc data disable +adb -s "$scanner_emulator" shell settings put secure location_mode 0 +./gradlew -PreleaseUiTests=true :app:assembleRelease :app:assembleReleaseAndroidTest +adb -s "$scanner_emulator" install -r "app/build/outputs/apk/release/app-${scanner_abi}-release.apk" +adb -s "$scanner_emulator" install -r app/build/outputs/apk/androidTest/release/app-release-androidTest.apk + +scanner_evidence="$(mktemp -d "${TMPDIR:-/tmp}/scanner-release.XXXXXX")" +chmod 700 "$scanner_evidence" +adb -s "$scanner_emulator" shell am instrument -w -r \ + com.bitchat.droid.test/com.bitchat.android.ui.ReleaseScannerTestRunner \ + > "$scanner_evidence/result.txt" +cat "$scanner_evidence/result.txt" +# adb itself exits successfully even when instrumentation tests fail. +grep -q '^INSTRUMENTATION_RESULT: stream=Tests run: 2, failures: 0' "$scanner_evidence/result.txt" +grep -q '^INSTRUMENTATION_CODE: -1' "$scanner_evidence/result.txt" +echo "Release scanner creation, synthetic QR decoding, and frame cleanup passed." diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index e6d4d9779..4f0414323 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -105,6 +105,7 @@ val sharedSourceIncludes = listOf( "com/bitchat/android/services/VerificationService.kt", "com/bitchat/android/services/meshgraph/**", "com/bitchat/android/service/TransportBridgeService.kt", + "com/bitchat/android/spam/**", "com/bitchat/android/nostr/Bech32.kt", "com/bitchat/android/nostr/GeohashAliasRegistry.kt", "com/bitchat/android/features/file/FileUtils.kt", @@ -119,6 +120,8 @@ val sharedSourceIncludes = listOf( ) val sharedSourceExcludes = listOf( "com/bitchat/android/model/FileSharingManager.kt", + // The watch has no spam filter toggle, so it keeps the filter's default (on). + "com/bitchat/android/spam/SpamFilterPreferences.kt", // Legacy phone monolith and Wi-Fi Aware multiplexer; the watch composes its own service // (MeshCore-style) in M2 instead of reusing these. "com/bitchat/android/mesh/BluetoothMeshService.kt", diff --git a/wear/src/main/java/com/bitchat/watch/MainActivity.kt b/wear/src/main/java/com/bitchat/watch/MainActivity.kt index dc07a949c..41de1a11a 100644 --- a/wear/src/main/java/com/bitchat/watch/MainActivity.kt +++ b/wear/src/main/java/com/bitchat/watch/MainActivity.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable @@ -39,6 +40,8 @@ import androidx.wear.compose.material3.OutlinedButton import com.bitchat.watch.mesh.WearMeshService import com.bitchat.watch.notification.WearNotificationCoordinator import com.bitchat.watch.service.WearMeshForegroundService +import com.bitchat.watch.ui.ChatHomePager +import com.bitchat.watch.ui.ChatMenuScreen import com.bitchat.watch.ui.ChatScreen import com.bitchat.watch.ui.DmScreen import com.bitchat.watch.ui.NicknameSetupScreen @@ -110,6 +113,7 @@ class MainActivity : ComponentActivity() { ) else -> WearNavHost( launchRequest = pendingLaunchRequest, + onQuit = ::quit, onLaunchRequestHandled = { requestID -> if (pendingLaunchRequest?.id == requestID) { pendingLaunchRequest = null @@ -177,6 +181,15 @@ class MainActivity : ComponentActivity() { startForegroundService(Intent(this, WearMeshForegroundService::class.java)) } + /** + * Quit from the in-app menu: stopping the foreground service tears the mesh down (see + * [WearMeshForegroundService.onDestroy]); the next launch starts it again. + */ + private fun quit() { + stopService(Intent(this, WearMeshForegroundService::class.java)) + finishAndRemoveTask() + } + private fun consumePrivateMessagePeer(intent: Intent?): String? { val peerID = privateMessagePeerFromIntent(intent) intent?.removeExtra(WearNotificationCoordinator.EXTRA_OPEN_DM) @@ -370,6 +383,7 @@ internal class WearNavigationState( @Composable internal fun WearNavHost( launchRequest: WearLaunchRequest?, + onQuit: () -> Unit, onLaunchRequestHandled: (Long) -> Unit ) { val navigation = rememberSaveable(saver = WearNavigationState.Saver) { @@ -397,10 +411,20 @@ internal fun WearNavHost( label = "screenTransition" ) { current -> when (current) { - is WearScreen.Chat -> ChatScreen( - onOpenPeople = { navigation.navigate(WearScreen.People) }, - onOpenTextInput = { navigation.navigate(WearScreen.TextInput(null)) } - ) + is WearScreen.Chat -> { + var chatOwnsGesture by remember { mutableStateOf(false) } + ChatHomePager( + swipeEnabled = !chatOwnsGesture, + menu = { ChatMenuScreen(onQuit = onQuit) }, + chat = { + ChatScreen( + onOpenPeople = { navigation.navigate(WearScreen.People) }, + onOpenTextInput = { navigation.navigate(WearScreen.TextInput(null)) }, + onHorizontalGestureActive = { chatOwnsGesture = it } + ) + } + ) + } is WearScreen.People -> PeopleScreen( onOpenDm = { navigation.navigate(WearScreen.Dm(it)) }, onEditNickname = { navigation.navigate(WearScreen.Nickname) } diff --git a/wear/src/main/java/com/bitchat/watch/mesh/WearMeshService.kt b/wear/src/main/java/com/bitchat/watch/mesh/WearMeshService.kt index eeb09295b..86fe86a1f 100644 --- a/wear/src/main/java/com/bitchat/watch/mesh/WearMeshService.kt +++ b/wear/src/main/java/com/bitchat/watch/mesh/WearMeshService.kt @@ -13,6 +13,7 @@ import com.bitchat.android.mesh.MeshTransport import com.bitchat.android.model.RoutedPacket import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.services.AppStateStore +import com.bitchat.android.spam.SpamFilter import com.bitchat.android.sync.GossipSyncManager import com.bitchat.android.util.AppConstants import kotlinx.coroutines.CoroutineScope @@ -238,6 +239,8 @@ class WearMeshService private constructor(private val context: Context) { try { onPrivateMessage?.invoke(message) } catch (_: Exception) { } true } + !SpamFilter.isLocalNotice(message) && + SpamFilter.shared.shouldHide(message, SpamFilter.meshSenderKey(message)) -> false message.channel != null -> { AppStateStore.addChannelMessage(message.channel, message) true diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatMenuScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatMenuScreen.kt new file mode 100644 index 000000000..8f3fb7f55 --- /dev/null +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatMenuScreen.kt @@ -0,0 +1,153 @@ +package com.bitchat.watch.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PowerSettingsNew +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.foundation.pager.HorizontalPager +import androidx.wear.compose.foundation.pager.rememberPagerState +import androidx.wear.compose.material3.AlertDialog +import androidx.wear.compose.material3.AlertDialogDefaults +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.HorizontalPagerScaffold +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.Text +import com.bitchat.android.services.AppStateStore +import com.bitchat.watch.ui.theme.ChatVisualTokens +import com.bitchat.watch.ui.theme.LocalBitchatPalette + +private const val MENU_PAGE = 0 +private const val CHAT_PAGE = 1 + +/** + * Home of the app: the public mesh chat, with an app menu one page to its left. Swiping the + * chat to the right reveals the menu (the Wear pattern used by Fitbit, Health and similar + * apps); swiping right again from the menu's left edge falls through to the system + * swipe-to-dismiss, so leaving the app still works as everywhere else on the watch. + * + * [swipeEnabled] is false while the chat owns a horizontal gesture (push-to-talk drag, + * full-screen image), so the pager never steals the pointer mid-gesture. + */ +@Composable +fun ChatHomePager( + swipeEnabled: Boolean, + menu: @Composable () -> Unit, + chat: @Composable () -> Unit, +) { + val pagerState = rememberPagerState(initialPage = CHAT_PAGE) { 2 } + // No page indicator: the menu is a peek-behind page, not a peer of the chat, and the + // dots would crowd the chat's edge action bar. + HorizontalPagerScaffold(pagerState = pagerState, pageIndicator = null) { + HorizontalPager( + state = pagerState, + userScrollEnabled = swipeEnabled, + // The crown scrolls chat history, never pages. + rotaryScrollableBehavior = null, + ) { page -> + when (page) { + MENU_PAGE -> menu() + CHAT_PAGE -> chat() + } + } + } +} + +@Composable +fun ChatMenuScreen(onQuit: () -> Unit) { + val palette = LocalBitchatPalette.current + val peers by AppStateStore.peers.collectAsState() + val listState = rememberScalingLazyListState() + var confirmQuit by rememberSaveable { mutableStateOf(false) } + + ScreenScaffold(scrollState = listState) { scaffoldPadding -> + ScalingLazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = scaffoldPadding, + ) { + item { + ListHeader { + Text( + text = "bitchat", + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + ) + } + } + item { + Text( + text = if (peers.isEmpty()) "Mesh running" else meshPeersText(peers.size), + style = ChatVisualTokens.SystemActionStyle, + color = palette.textTertiary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp), + ) + } + item { + FilledTonalButton( + onClick = { confirmQuit = true }, + modifier = Modifier.fillMaxWidth(), + icon = { + Icon( + imageVector = Icons.Filled.PowerSettingsNew, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + }, + label = { Text("Quit bitchat") }, + secondaryLabel = { + Text( + text = "Stop the mesh", + color = palette.textTertiary, + ) + }, + ) + } + } + } + + AlertDialog( + visible = confirmQuit, + onDismissRequest = { confirmQuit = false }, + icon = { + Icon( + imageVector = Icons.Filled.PowerSettingsNew, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(32.dp), + ) + }, + title = { Text("Quit bitchat?") }, + confirmButton = { + AlertDialogDefaults.ConfirmButton( + onClick = { + confirmQuit = false + onQuit() + }, + ) + }, + dismissButton = { + AlertDialogDefaults.DismissButton(onClick = { confirmQuit = false }) + }, + ) +} + +private fun meshPeersText(count: Int): String = + if (count == 1) "Mesh running · 1 peer" else "Mesh running · $count peers" diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt index d576cd03c..1c0dde16b 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt @@ -57,7 +57,11 @@ import java.util.Date import java.util.Locale @Composable -fun ChatScreen(onOpenPeople: () -> Unit, onOpenTextInput: () -> Unit) { +fun ChatScreen( + onOpenPeople: () -> Unit, + onOpenTextInput: () -> Unit, + onHorizontalGestureActive: (Boolean) -> Unit = {}, +) { val context = LocalContext.current val messages by AppStateStore.publicMessages.collectAsState() val peers by AppStateStore.peers.collectAsState() @@ -82,6 +86,11 @@ fun ChatScreen(onOpenPeople: () -> Unit, onOpenTextInput: () -> Unit) { } ) { path -> mesh?.let { sendVoiceNote(it, null, path) } } + // Push-to-talk drags and the image viewer own the whole screen; the home pager must not + // turn them into a page swipe. + val horizontalGestureActive = voice.recording || viewerPath != null + LaunchedEffect(horizontalGestureActive) { onHorizontalGestureActive(horizontalGestureActive) } + androidx.compose.runtime.DisposableEffect(liveVoiceManager) { liveVoiceManager.showPublicMesh() onDispose { liveVoiceManager.clearVisibleConversation() }