Implement CameraX GPG QR transfer on Android (#106)
Android ARM verification / rust (push) Failing after 47m42s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 23s

This commit is contained in:
hugo committed 2026-10-12 09:49:38 +02:00
1 parent 6669b77f4e
commit d466b170ab
33 files changed
+1616 -162

No files matched your search

+2
View File
@@ -8,6 +8,8 @@ UniFFI API. Local-first and HTTPS onboarding is described in
revision-checked [adaptive navigation](../docs/android/password-navigation.md);
Home renders Rust-owned [Git activity and HTTPS synchronization](../docs/android/home-sync.md).
Passwords and Preferences share [Rust-owned biometric authentication](../docs/android/authentication.md).
CameraX GPG scanning and QR export are described in
[key transfer](../docs/android/key-transfer.md).
Remaining feature screens retain their milestone issues. The full [CI and emulator workflow](../docs/android/ci.md) documents the
ARM-only runner/toolchain policy and artifact audits.
+29 -13
View File
@@ -16,6 +16,14 @@ androidx.arch.core:core-common:2.2.0=benchmarkCompileClasspath,benchmarkLintChec
androidx.arch.core:core-runtime:2.2.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.autofill:autofill:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.biometric:biometric:1.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera.featurecombinationquery:featurecombinationquery:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera.viewfinder:viewfinder-core:1.5.1=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-camera2-pipe:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-camera2:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-core:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-lifecycle:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-video:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-view:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.cardview:cardview:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.collection:collection-jvm:1.5.0=benchmarkCompileClasspath,debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,profileCompileClasspath,releaseCompileClasspath
androidx.collection:collection-jvm:1.6.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -67,10 +75,12 @@ androidx.compose.ui:ui-util-android:1.12.1=benchmarkCompileClasspath,benchmarkLi
androidx.compose.ui:ui-util:1.12.1=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui:1.12.1=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose:compose-bom:2026.09.00=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath
androidx.concurrent:concurrent-futures:1.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath
androidx.coordinatorlayout:coordinatorlayout:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.core:core-backported-fixes:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-ktx:1.18.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-viewtree:1.0.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core:1.18.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -79,6 +89,7 @@ androidx.customview:customview-poolingcontainer:1.0.0=benchmarkLintChecksClasspa
androidx.customview:customview:1.0.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.drawerlayout:drawerlayout:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.emoji2:emoji2:1.4.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.exifinterface:exifinterface:1.4.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.fragment:fragment:1.9.1=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.graphics:graphics-path:1.0.1=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.interpolator:interpolator:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -103,6 +114,9 @@ androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=benchmarkCompil
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel:2.11.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.loader:loader:1.0.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.media3:media3-common:1.9.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.media3:media3-container:1.9.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.media3:media3-muxer:1.9.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigation3:navigation3-runtime-android:1.2.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigation3:navigation3-runtime:1.2.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigation3:navigation3-ui-android:1.2.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -128,6 +142,7 @@ androidx.test:monitor:1.8.0=debugAndroidTestCompileClasspath,debugAndroidTestLin
androidx.test:rules:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.test:runner:1.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.tracing:tracing-android:2.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.tracing:tracing-ktx:2.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.tracing:tracing:1.1.0=debugAndroidTestCompileClasspath
androidx.tracing:tracing:2.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.transition:transition:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
@@ -174,6 +189,7 @@ com.github.ajalt.clikt:clikt-jvm:5.0.3=ktlint
com.github.ajalt.clikt:clikt:5.0.3=ktlint
com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:4.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
com.google.android.material:material:1.2.0-rc01=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
com.google.auto.value:auto-value-annotations:1.6.3=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.code.findbugs:jsr305:3.0.2=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.code.gson:gson:2.11.0=androidLintTool
com.google.dagger:dagger-compiler:2.60.1=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
@@ -185,20 +201,19 @@ com.google.dagger:hilt-compiler:2.60.1=_agp_internal_benchmark_kspClasspath,_agp
com.google.dagger:hilt-core:2.60.1=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.devtools.ksp:symbol-processing-api:2.3.12=kotlinCompilerPluginClasspathBenchmark,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathProfile,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable
com.google.devtools.ksp:symbol-processing-api:2.3.7=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
com.google.errorprone:error_prone_annotations:2.30.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.28.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.30.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath
com.google.errorprone:error_prone_annotations:2.36.0=androidLintTool
com.google.errorprone:error_prone_annotations:2.47.0=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
com.google.googlejavaformat:google-java-format:1.33.0=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
com.google.guava:failureaccess:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
com.google.guava:failureaccess:1.0.2=androidLintTool
com.google.guava:failureaccess:1.0.2=androidLintTool,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.guava:failureaccess:1.0.3=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
com.google.guava:guava:31.0.1-android=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
com.google.guava:guava:33.3.1-android=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.guava:guava:33.4.0-jre=androidLintTool
com.google.guava:guava:33.6.0-jre=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
com.google.guava:listenablefuture:1.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestLintChecksClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
com.google.j2objc:j2objc-annotations:1.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool
com.google.guava:listenablefuture:1.0=benchmarkCompileClasspath,debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,profileCompileClasspath,releaseCompileClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.j2objc:j2objc-annotations:3.1=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
com.google.jimfs:jimfs:1.1=androidLintTool
com.google.protobuf:protobuf-java:3.25.5=androidLintTool,unified-test-platform-gradle-work-action
@@ -252,15 +267,14 @@ org.bouncycastle:bcpkix-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcprov-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcutil-jdk18on:1.80.2=androidLintTool
org.checkerframework:checker-compat-qual:2.5.3=_agp_internal_benchmark_kspClasspath,_agp_internal_debug_kspClasspath,_agp_internal_profile_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorBenchmark,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorProfile,hiltAnnotationProcessorRelease,kspBenchmarkKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspProfileKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
org.checkerframework:checker-compat-qual:2.5.5=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
org.checkerframework:checker-qual:3.22.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
org.checkerframework:checker-qual:3.43.0=androidLintTool
org.checkerframework:checker-qual:3.43.0=androidLintTool,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.ec4j.core:ec4j-core:1.1.1=ktlint,ktlintBaselineReporter,ktlintRuleset
org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.hamcrest:hamcrest-library:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
org.jetbrains.kotlin:compose-group-mapping:2.4.21=composeMappingProducerClasspath
org.jetbrains.kotlin:kotlin-bom:1.8.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
@@ -298,6 +312,8 @@ org.jetbrains.kotlin:kotlin-stdlib:2.3.21=_agp_internal_benchmark_kspClasspath,_
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:atomicfu-jvm:0.28.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:atomicfu:0.28.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
@@ -333,4 +349,4 @@ org.ow2.asm:asm:9.9=androidLintTool
org.ow2.asm:asm:9.9.1=composeMappingProducerClasspath
org.slf4j:slf4j-api:2.0.7=ktlint
rustls:rustls-platform-verifier:0.1.1=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
empty=_agp_internal_debugAndroidTest_kspClasspath,_agp_internal_debugUnitTest_kspClasspath,androidApis,androidJdkImage,androidTestUtil,benchmarkAnnotationProcessorClasspath,benchmarkImplementationDependenciesMetadata,benchmarkReverseMetadataValues,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,hiltCompileOnlyBenchmark,hiltCompileOnlyDebug,hiltCompileOnlyDebugAndroidTest,hiltCompileOnlyDebugUnitTest,hiltCompileOnlyProfile,hiltCompileOnlyRelease,kotlinCompilerPluginClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,lintChecks,lintPublish,profileAnnotationProcessorClasspath,profileReverseMetadataValues,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues
empty=_agp_internal_debugAndroidTest_kspClasspath,_agp_internal_debugUnitTest_kspClasspath,androidApis,androidJdkImage,androidTestUtil,benchmarkAnnotationProcessorClasspath,benchmarkImplementationDependenciesMetadata,benchmarkReverseMetadataValues,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,hiltCompileOnlyBenchmark,hiltCompileOnlyDebug,hiltCompileOnlyDebugAndroidTest,hiltCompileOnlyDebugUnitTest,hiltCompileOnlyProfile,hiltCompileOnlyRelease,kotlinCompilerPluginClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,lintChecks,lintPublish,profileAnnotationProcessorClasspath,profileImplementationDependenciesMetadata,profileReverseMetadataValues,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues
@@ -8,11 +8,13 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import androidx.lifecycle.ViewModelProvider
import de.rfc1437.ironstorage.core.OnboardingRepository
import de.rfc1437.ironstorage.core.SetupKey
import de.rfc1437.ironstorage.core.SetupStatus
import de.rfc1437.ironstorage.features.OnboardingScreen
import de.rfc1437.ironstorage.features.SetupFormViewModel
import de.rfc1437.ironstorage.features.ShellViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -32,7 +34,16 @@ class OnboardingScreenTest {
val secrets = SetupFormViewModel()
compose.activity.runOnUiThread {
compose.activity.setProtectedTestContent {
MaterialTheme { OnboardingScreen(repository, secrets) }
MaterialTheme {
OnboardingScreen(
repository,
secrets,
SettingsScreenTest.Fixture(),
androidx.lifecycle.ViewModelProvider(
compose.activity
)[de.rfc1437.ironstorage.features.ShellViewModel::class.java].authentication
)
}
}
}
compose.waitForIdle()
@@ -60,6 +71,45 @@ class OnboardingScreenTest {
)
}
@Test fun closingQrReviewReloadsPreparedOnboardingKeysWithoutCreatingAStore() {
val original = FixtureRepository()
val settings = SettingsScreenTest.Fixture()
val secrets = SetupFormViewModel()
var staged = false
val repository = object : OnboardingRepository by original {
override fun refresh() {
if (staged) {
original.setup.value = original.setup.value.copy(
keys = listOf(
SetupKey("SCANNED-SETUP-KEY", listOf("Scanned setup fixture"))
),
selectedKey = "SCANNED-SETUP-KEY"
)
}
}
}
val model = ViewModelProvider(compose.activity)[ShellViewModel::class.java]
val authentication = model.authentication
compose.activity.runOnUiThread {
compose.activity.setProtectedTestContent {
MaterialTheme {
OnboardingScreen(repository, secrets, settings, authentication)
}
}
}
compose.onNodeWithText("Create local store").performClick()
compose.onNodeWithText("Import private GPG key").performScrollTo().performClick()
compose.onNodeWithText("Scan GPG QR codes").performScrollTo().performClick()
compose.runOnIdle { staged = true }
compose.onNodeWithText("Done").performClick()
compose.runOnIdle { assertEquals("SCANNED-SETUP-KEY", original.setup.value.selectedKey) }
compose.onNodeWithText("Back to setup choices").performScrollTo().performClick()
compose.onNodeWithText("Create local store").performClick()
compose.onNodeWithText("SCANNED-SETUP-KEY").performScrollTo().assertIsDisplayed()
assertTrue(!original.setup.value.configured)
assertTrue(original.calls.isEmpty())
}
private class FixtureRepository : OnboardingRepository {
override val setup = MutableStateFlow(SetupStatus())
val calls = mutableListOf<String>()
@@ -6,6 +6,8 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.test.hasText
@@ -24,6 +26,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.core.AuthenticationRepository
import de.rfc1437.ironstorage.core.AuthenticationState
import de.rfc1437.ironstorage.core.CameraPreview
import de.rfc1437.ironstorage.core.KeyTransferState
import de.rfc1437.ironstorage.core.ProtectedPresentation
import de.rfc1437.ironstorage.core.ProviderStatus
@@ -31,7 +34,9 @@ import de.rfc1437.ironstorage.core.SettingsPage
import de.rfc1437.ironstorage.core.SettingsRepository
import de.rfc1437.ironstorage.core.SettingsState
import de.rfc1437.ironstorage.core.TransferFrame
import de.rfc1437.ironstorage.core.TransferKey
import de.rfc1437.ironstorage.features.ApplicationTheme
import de.rfc1437.ironstorage.features.R as FeatureR
import de.rfc1437.ironstorage.features.SettingsScreen
import de.rfc1437.ironstorage.features.SetupFormViewModel
import de.rfc1437.ironstorage.features.ShellViewModel
@@ -164,6 +169,69 @@ class SettingsScreenTest {
authentication.presentation.clear()
}
}
@Test fun privateQrActionsRequireFingerprintConfirmationAndExplicitClosing() {
val original = ViewModelProvider(compose.activity)[ShellViewModel::class.java]
val repository = Fixture()
val authentication = object : AuthenticationRepository by original.authentication {
override val presentation = ProtectedPresentation()
}
compose.activity.runOnUiThread {
compose.activity.setProtectedTestContent(authentication.presentation) {
ApplicationTheme(authentication) {
de.rfc1437.ironstorage.features.SettingsScreen(
repository,
authentication,
original.onboarding,
ViewModelProvider(compose.activity)[SetupFormViewModel::class.java]
) {}
}
}
}
compose.onNodeWithTag(
"settings-list"
).performScrollToNode(hasText("Import or export GPG QR codes"))
compose.onNodeWithText("Import or export GPG QR codes").performClick()
compose.onNodeWithText("Export private key").performScrollTo().performClick()
compose.onNodeWithText(
"Confirm and export selected key"
).performScrollTo().assertIsNotEnabled()
compose.onNodeWithText(
"I checked this fingerprint and authorize displaying its private QR codes."
)
.performScrollTo().performClick()
compose.onNodeWithText(
"Confirm and export selected key"
).performScrollTo().assertIsEnabled().performClick()
assertEquals(1, repository.exports)
compose.runOnIdle {
repository.state.value = repository.state.value.copy(
transfer = repository.state.value.transfer.copy(
ready = TransferKey(
"SCANNED-FINGERPRINT",
"Scanned fixture",
true
),
received = 2u,
total = 2u
)
)
}
compose.onNodeWithText(
"Confirm and import reconstructed key"
).performScrollTo().assertIsNotEnabled()
compose.onNodeWithText("SCANNED-FINGERPRINT").performScrollTo().assertIsDisplayed()
compose.onNodeWithText(
"I checked this fingerprint and authorize importing its private key material."
)
.performScrollTo().performClick()
compose.onNodeWithText(
"Confirm and import reconstructed key"
).performScrollTo().assertIsEnabled().performClick()
assertEquals(1, repository.imports)
compose.onNodeWithText("Done").performClick()
assertFalse(repository.state.value.transfer.open)
}
internal class Fixture : SettingsRepository {
override val state = MutableStateFlow(
SettingsState(
@@ -193,6 +261,8 @@ class SettingsScreenTest {
var removals = 0
var reloads = 0
var choseImages = false
var exports = 0
var imports = 0
override fun reload() {
reloads += 1
}
@@ -211,15 +281,41 @@ class SettingsScreenTest {
override fun openCredentials() = Unit
override fun beginTransfer() {
state.value =
state.value.copy(error = null, transfer = KeyTransferState(open = true))
state.value.copy(
error = null,
transfer = KeyTransferState(
open = true,
keys = listOf(
TransferKey(
"PUBLIC-FINGERPRINT",
"Fixture GPG key",
true
)
)
)
)
}
override fun beginSetupTransfer() {
beginTransfer()
state.value =
state.value.copy(
transfer = state.value.transfer.copy(setup = true, keys = emptyList())
)
}
override fun cameraPreview(): CameraPreview? = null
override fun exportKey(fingerprint: String, privateKey: Boolean, passphrase: String) {
exports +=
1
}
override fun exportKey(fingerprint: String, privateKey: Boolean, passphrase: String) = Unit
override fun chooseQrImages() {
choseImages = true
}
override fun addQrImages(uris: List<String>) = Unit
override fun addQrPayload(payload: String) = Unit
override fun importKey(passphrase: String, makeDefault: Boolean, overwrite: Boolean) = Unit
override fun importKey(passphrase: String, makeDefault: Boolean, overwrite: Boolean) {
imports +=
1
}
override fun closeTransfer() {
state.value = state.value.copy(transfer = KeyTransferState())
}
+5 -1
View File
@@ -1,6 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<application
android:name=".IronStorageApplication"
android:allowBackup="false"
@@ -438,6 +438,7 @@ class NativeEntryViewerTest {
keystore: MemoryKeystore
) {
var image = ByteArray(0)
var cameraReceiver: ((CameraFrame) -> Unit)? = null
val platform = object : SettingsPlatform {
override fun autofill() = ProviderStatus(true, false, false)
override fun credentials() =
@@ -446,6 +447,13 @@ class NativeEntryViewerTest {
override fun version() = "native-settings-fixture"
override fun openAutofill() = true
override fun openCredentials() = true
override fun cameraPreview(
frame: (CameraFrame) -> Unit,
failure: () -> Unit
): CameraPreview? {
cameraReceiver = frame
return null
}
override fun chooseQrImages() = true
override suspend fun readQrImage(uri: String) = image.copyOf()
}
@@ -533,6 +541,18 @@ class NativeEntryViewerTest {
image = output.toByteArray()
request { settings.closeTransfer() }
assertTrue(frame.modules.all { it.toInt() == 0 })
assertNull(request { settings.beginTransfer() }.error)
withContext(Dispatchers.Main.immediate) { settings.cameraPreview() }
val luminance = ByteArray(size * size) { index ->
if (pixels[index] == android.graphics.Color.BLACK) 0 else 255.toByte()
}
requireNotNull(
cameraReceiver
)(CameraFrame(luminance, size.toUInt(), size.toUInt(), size.toUInt(), 1u, 90u))
withTimeout(30000) { settings.state.first { it.transfer.ready != null } }
assertEquals(fingerprint, settings.state.value.transfer.ready!!.fingerprint)
assertTrue(luminance.all { it.toInt() == 0 })
request { settings.closeTransfer() }
val qrStart = android.os.SystemClock.elapsedRealtime()
assertNull(request { settings.addQrImages(listOf("content://public-fixture/qr")) }.error)
val qrMs = android.os.SystemClock.elapsedRealtime() - qrStart
@@ -559,10 +579,14 @@ class NativeEntryViewerTest {
assertNotNull(request { settings.setDefaultKey(bob.fingerprint) }.error)
assertEquals(fingerprint, settings.state.value.page!!.defaultKeyFingerprint)
assertNull(request { settings.beginTransfer() }.error)
assertNull(request { settings.exportKey(fingerprint, false, "") }.error)
assertNull(request { settings.exportKey(fingerprint, true, passphrase) }.error)
assertTrue(settings.state.value.transfer.exportedKey!!.privateKey)
val privateFrames = settings.state.value.transfer.frames
assertTrue(privateFrames.isNotEmpty())
withContext(Dispatchers.Main.immediate) { authentication.presentation.clear() }
assertFalse(settings.state.value.transfer.open)
assertTrue(settings.state.value.transfer.frames.isEmpty())
assertTrue(privateFrames.all { frame -> frame.modules.all { it.toInt() == 0 } })
assertNull(request { settings.setDefaultKey(fingerprint) }.error)
assertFalse(authentication.state.value.unlocked)
assertEquals(fingerprint, settings.state.value.page!!.defaultKeyFingerprint)
@@ -2,6 +2,7 @@ package de.rfc1437.ironstorage.core
import de.rfc1437.ironstorage.bridge.NativeBridgeException
import de.rfc1437.ironstorage.bridge.generated.MobileAuthenticationFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileCameraFrame
import de.rfc1437.ironstorage.bridge.generated.MobileKeyTransfer
import de.rfc1437.ironstorage.bridge.generated.MobileKeyTransferFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileKeyTransferImport
@@ -10,7 +11,9 @@ import de.rfc1437.ironstorage.bridge.generated.MobileKeyTransferProgress
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileTotpOperation
import de.rfc1437.ironstorage.bridge.generated.mobileKeyTransfer
import de.rfc1437.ironstorage.bridge.generated.mobileLocalKeyTransferImporter
import de.rfc1437.ironstorage.bridge.generated.mobileTotpOperation
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -31,10 +34,12 @@ class NativeSettingsRepository(
private val mutable = MutableStateFlow(SettingsState())
override val state: StateFlow<SettingsState> = mutable
private val operation = Mutex()
private val cameraBusy = AtomicBoolean()
@Volatile private var epoch = 0L
@Volatile private var discovery: MobileTotpOperation? = null
private var visibility: Job? = null
private var transfer: MobileKeyTransfer? = null
private var importer: MobileKeyTransferImport? = null
init {
@@ -126,19 +131,27 @@ class NativeSettingsRepository(
)
}
}
override fun beginTransfer() = run { ticket ->
override fun beginTransfer() = begin(false)
override fun beginSetupTransfer() = begin(true)
private fun begin(setup: Boolean) = run { ticket ->
withContext(Dispatchers.IO) {
session.storage()
transfer = mobileKeyTransfer()
importer = requireNotNull(transfer).importer()
releaseTransfer()
if (setup) {
importer = mobileLocalKeyTransferImporter()
} else {
transfer = mobileKeyTransfer()
importer = requireNotNull(transfer).importer()
}
}
val keys = withContext(Dispatchers.IO) { requireNotNull(transfer).keys() }
val keys = withContext(Dispatchers.IO) { transfer?.keys().orEmpty() }
if (ticket ==
epoch
) {
mutable.value = mutable.value.copy(
transfer = KeyTransferState(
open = true,
setup = setup,
keys = keys.map {
TransferKey(
it.fingerprint,
@@ -152,6 +165,12 @@ class NativeSettingsRepository(
}
override fun exportKey(fingerprint: String, privateKey: Boolean, passphrase: String) =
run { ticket ->
visibility?.cancel()
mutable.value.transfer.frames.forEach { it.modules.fill(0) }
mutable.value =
mutable.value.copy(
transfer = mutable.value.transfer.copy(frames = emptyList(), exportedKey = null)
)
val result = withContext(Dispatchers.IO) {
requireNotNull(transfer).export(
fingerprint,
@@ -165,11 +184,28 @@ class NativeSettingsRepository(
mutable.value.transfer.frames.forEach { it.modules.fill(0) }
mutable.value = mutable.value.copy(
transfer = mutable.value.transfer.copy(
exportedKey = TransferKey(
result.key.fingerprint,
result.key.title,
result.key.kind == MobileKeyTransferKind.PRIVATE
),
frames = result.frames.map {
TransferFrame(it.sequence, it.total, it.width, it.modules)
}
)
)
visibility = scope.launch {
while (ticket == epoch) {
kotlinx.coroutines.delay(250)
val visible = operation.withLock {
withContext(Dispatchers.IO) { transfer?.exportVisible() == true }
}
if (ticket == epoch && !visible) {
closeTransfer()
break
}
}
}
} else {
result.frames.forEach { it.modules.fill(0) }
}
@@ -177,9 +213,11 @@ class NativeSettingsRepository(
private fun progress(ticket: Long, progress: MobileKeyTransferProgress) {
if (ticket != epoch) return
mutable.value = mutable.value.copy(
error = null,
transfer = mutable.value.transfer.copy(
received = progress.received,
total = progress.total,
duplicate = progress.duplicate,
ready = progress.key?.let {
TransferKey(
it.fingerprint,
@@ -190,6 +228,65 @@ class NativeSettingsRepository(
)
)
}
override fun cameraPreview(): CameraPreview? {
val ticket = epoch
return platform.cameraPreview({ frame ->
if (!cameraBusy.compareAndSet(false, true)) {
frame.pixels.fill(0)
} else {
scope.launch {
try {
operation.withLock {
if (ticket != epoch || !mutable.value.transfer.open ||
mutable.value.transfer.ready != null || mutable.value.busy
) {
return@withLock
}
val result = withContext(Dispatchers.IO) {
requireNotNull(importer).addCamera(
MobileCameraFrame(
frame.width,
frame.height,
frame.rowStride,
frame.pixelStride,
frame.pixels
),
frame.rotationDegrees
)
}
frame.pixels.fill(0)
if (result != null) progress(ticket, result)
}
} catch (error: MobileKeyTransferFfiException.Failed) {
if (ticket ==
epoch
) {
mutable.value = mutable.value.copy(error = error.rustMessage)
}
} catch (_: NativeBridgeException) {
if (ticket ==
epoch
) {
mutable.value =
mutable.value.copy(error = session.text(R.string.core_update_retry))
}
} finally {
frame.pixels.fill(0)
cameraBusy.set(false)
}
}
}
}, {
scope.launch {
if (ticket ==
epoch
) {
mutable.value =
mutable.value.copy(error = session.text(R.string.core_camera_unavailable))
}
}
})
}
override fun chooseQrImages() {
if (!platform.chooseQrImages()) {
mutable.value =
@@ -246,6 +343,15 @@ class NativeSettingsRepository(
progress(ticket, withContext(Dispatchers.IO) { requireNotNull(importer).addFrame(payload) })
}
override fun importKey(passphrase: String, makeDefault: Boolean, overwrite: Boolean) = run {
if (mutable.value.transfer.setup) {
withContext(Dispatchers.IO) {
session.storage().androidOnboarding(session.backend()).use {
it.importScanned(requireNotNull(importer), passphrase.ifEmpty { null })
}
}
closeTransfer()
return@run
}
val result = withContext(Dispatchers.IO) {
requireNotNull(importer).import(passphrase.ifEmpty { null }, makeDefault, overwrite)
}
@@ -264,6 +370,8 @@ class NativeSettingsRepository(
transfer = null
}
override fun closeTransfer() {
visibility?.cancel()
visibility = null
epoch += 1
mutable.value.transfer.frames.forEach { it.modules.fill(0) }
mutable.value = mutable.value.copy(transfer = KeyTransferState(), busy = false)
@@ -295,7 +403,11 @@ class NativeSettingsRepository(
mutable.value.copy(error = error.title + "\n" + error.detail)
}
} catch (error: MobileKeyTransferFfiException.Failed) {
if (ticket == epoch) mutable.value = mutable.value.copy(error = error.message)
if (ticket ==
epoch
) {
mutable.value = mutable.value.copy(error = error.rustMessage)
}
} catch (_: MobilePrivateStorageFfiException) {
if (ticket ==
epoch
@@ -12,6 +12,20 @@ interface SettingsPlatform {
fun openCredentials(): Boolean
fun chooseQrImages(): Boolean
suspend fun readQrImage(uri: String): ByteArray
fun cameraPreview(frame: (CameraFrame) -> Unit, failure: () -> Unit): CameraPreview?
}
data class CameraFrame(
val pixels: ByteArray,
val width: UInt,
val height: UInt,
val rowStride: UInt,
val pixelStride: UInt,
val rotationDegrees: UInt
) {
override fun toString() = "CameraFrame([REDACTED])"
}
interface CameraPreview : AutoCloseable {
val view: android.view.View
}
data class SettingsPage(
val repositoryTitle: String,
@@ -45,7 +59,10 @@ data class KeyTransferState(
val frames: List<TransferFrame> = emptyList(),
val received: UInt = 0u,
val total: UInt = 0u,
val ready: TransferKey? = null
val ready: TransferKey? = null,
val duplicate: Boolean = false,
val setup: Boolean = false,
val exportedKey: TransferKey? = null
) {
override fun toString() = "KeyTransferState([REDACTED])"
}
@@ -69,6 +86,8 @@ interface SettingsRepository {
fun openAutofill()
fun openCredentials()
fun beginTransfer()
fun beginSetupTransfer()
fun cameraPreview(): CameraPreview?
fun exportKey(fingerprint: String, privateKey: Boolean, passphrase: String)
fun chooseQrImages()
fun addQrImages(uris: List<String>)
@@ -38,4 +38,5 @@
<string name="core_copied">Copied. The clipboard will be cleared automatically.</string>
<string name="core_password_recovery">Unlock the device or recover the private store and refresh.</string>
<string name="core_password_storage">Private storage is unavailable. Unlock the device and refresh.</string>
<string name="core_camera_unavailable">The camera is unavailable. Pause scanning and retry, or choose QR images.</string>
</resources>
+25 -1
View File
@@ -176,7 +176,31 @@
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0": "Apache-2.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0": "Apache-2.0"
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0": "Apache-2.0",
"androidx.camera.featurecombinationquery:featurecombinationquery:1.6.2": "Apache-2.0",
"androidx.camera.viewfinder:viewfinder-core:1.5.1": "Apache-2.0",
"androidx.camera:camera-camera2-pipe:1.6.2": "Apache-2.0",
"androidx.camera:camera-camera2:1.6.2": "Apache-2.0",
"androidx.camera:camera-core:1.6.2": "Apache-2.0 AND BSD-3-Clause",
"androidx.camera:camera-lifecycle:1.6.2": "Apache-2.0",
"androidx.camera:camera-video:1.6.2": "Apache-2.0",
"androidx.camera:camera-view:1.6.2": "Apache-2.0",
"androidx.concurrent:concurrent-futures-ktx:1.1.0": "Apache-2.0",
"androidx.core:core-backported-fixes:1.0.0": "Apache-2.0",
"androidx.exifinterface:exifinterface:1.4.2": "Apache-2.0",
"androidx.media3:media3-common:1.9.0": "Apache-2.0",
"androidx.media3:media3-container:1.9.0": "Apache-2.0",
"androidx.media3:media3-muxer:1.9.0": "Apache-2.0",
"androidx.tracing:tracing-ktx:2.0.0": "Apache-2.0",
"com.google.auto.value:auto-value-annotations:1.6.3": "Apache-2.0",
"com.google.dagger:dagger:2.59": "Apache-2.0",
"com.google.errorprone:error_prone_annotations:2.28.0": "Apache-2.0",
"com.google.guava:failureaccess:1.0.2": "Apache-2.0",
"com.google.guava:guava:33.3.1-android": "Apache-2.0",
"com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava": "Apache-2.0",
"com.google.j2objc:j2objc-annotations:3.0.0": "Apache-2.0",
"org.checkerframework:checker-qual:3.43.0": "MIT",
"org.jetbrains.kotlinx:atomicfu-jvm:0.28.0": "Apache-2.0"
},
"rust_licenses": [
"(Apache-2.0 OR MIT) AND BSD-3-Clause",
@@ -19,6 +19,7 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@@ -54,9 +55,17 @@ class SetupFormViewModel @Inject constructor() : ViewModel() {
fun OnboardingScreen(
repository: OnboardingRepository,
secrets: SetupFormViewModel,
settings: de.rfc1437.ironstorage.core.SettingsRepository,
authentication: de.rfc1437.ironstorage.core.AuthenticationRepository,
modifier: Modifier = Modifier,
scroll: Boolean = true
) {
val transfer by settings.state.collectAsStateWithLifecycle()
DisposableEffect(transfer.transfer.open) {
val wasOpen = transfer.transfer.open
onDispose { if (wasOpen) repository.refresh() }
}
if (transfer.transfer.open) KeyTransferDialog(settings, authentication)
ProtectedInputSurface()
val state by repository.setup.collectAsStateWithLifecycle()
var step by rememberSaveable { mutableStateOf("choice") }
@@ -183,6 +192,9 @@ fun OnboardingScreen(
}
"import" -> {
Text(stringResource(R.string.setup_import_help))
TextButton(onClick = settings::beginSetupTransfer) {
Text(stringResource(R.string.qr_scan))
}
SecretField(
stringResource(R.string.setup_armor),
secrets.armor,
@@ -0,0 +1,95 @@
package de.rfc1437.ironstorage.features
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import de.rfc1437.ironstorage.core.SettingsRepository
@Composable
internal fun QrCameraPane(repository: SettingsRepository) {
val context = LocalContext.current
fun allowed() = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
var permitted by remember { mutableStateOf(allowed()) }
var scanning by remember { mutableStateOf(false) }
val permission =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
permitted = it
scanning = it
}
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { permitted = allowed() }
if (permitted && scanning) {
val camera = remember(repository) { repository.cameraPreview() }
DisposableEffect(camera) { onDispose { camera?.close() } }
if (camera == null) {
Text(stringResource(R.string.qr_camera_missing))
} else {
AndroidView(factory = {
camera.view
}, modifier = Modifier.fillMaxWidth().height(280.dp).clipToBounds())
}
TextButton(onClick = { scanning = false }) { Text(stringResource(R.string.qr_pause)) }
} else {
TextButton(onClick = {
if (permitted) {
scanning = true
} else {
permission.launch(Manifest.permission.CAMERA)
}
}) { Text(stringResource(if (permitted) R.string.qr_resume else R.string.qr_scan)) }
if (!permitted) {
Text(stringResource(R.string.qr_camera_denied))
TextButton(onClick = {
context.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
"package:${context.packageName}".toUri()
)
)
}) { Text(stringResource(R.string.qr_camera_settings)) }
}
}
}
@Composable
internal fun QrDisplayWindow() {
val view = androidx.compose.ui.platform.LocalView.current
val window = (view.parent as? androidx.compose.ui.window.DialogWindowProvider)?.window
DisposableEffect(window, view) {
val brightness = window?.attributes?.screenBrightness
val keep = view.keepScreenOn
view.keepScreenOn = true
window?.let { it.attributes = it.attributes.apply { screenBrightness = 1f } }
onDispose {
view.keepScreenOn = keep
if (brightness !=
null
) {
window.attributes = window.attributes.apply { screenBrightness = brightness }
}
}
}
}
@@ -169,6 +169,8 @@ fun SettingsScreen(
OnboardingScreen(
onboarding,
setup,
repository,
authentication,
scroll = false
)
}
@@ -290,7 +292,7 @@ fun SettingsScreen(
SettingsEditDialog(repository, authentication, action) { edit = null }
}
}
if (state.transfer.open) KeyTransferDialog(repository, authentication)
if (state.transfer.open && !showSetup) KeyTransferDialog(repository, authentication)
if (licenses) {
val resources = LocalResources.current
val loading = stringResource(R.string.settings_license_loading)
@@ -422,7 +424,7 @@ private fun SettingsEditDialog(
}
@Composable
private fun KeyTransferDialog(
internal fun KeyTransferDialog(
repository: SettingsRepository,
authentication: AuthenticationRepository
) {
@@ -436,6 +438,15 @@ private fun KeyTransferDialog(
var frame by remember { mutableStateOf(0) }
var passphrase by rememberProtectedInput(authentication.presentation)
var payload by rememberProtectedInput(authentication.presentation)
var exportConfirmed by remember(fingerprint, privateKey) { mutableStateOf(false) }
var importConfirmed by remember(state.transfer.ready) { mutableStateOf(false) }
var animate by remember { mutableStateOf(false) }
LaunchedEffect(animate, state.transfer.frames) {
while (animate && state.transfer.frames.size > 1) {
kotlinx.coroutines.delay(1200)
frame = (frame + 1) % state.transfer.frames.size
}
}
ProtectedWindow()
AlertDialog(
properties = DialogProperties(
@@ -454,91 +465,126 @@ private fun KeyTransferDialog(
ProtectedInputSurface(authentication.presentation)
state.error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
if (state.busy) CircularProgressIndicator()
state.transfer.keys.forEach { key ->
Row(
Modifier.fillMaxWidth().selectable(
selected = fingerprint == key.fingerprint,
enabled = !state.busy,
role = Role.RadioButton,
onClick = { fingerprint = key.fingerprint }
)
) {
RadioButton(selected = fingerprint == key.fingerprint, onClick = null)
Column(Modifier.weight(1f)) {
Text(key.title)
Text(key.fingerprint)
}
}
}
TextButton(
enabled =
!state.busy &&
state.transfer.keys.any {
it.fingerprint == fingerprint && it.privateKey
},
onClick = { repository.setDefaultKey(fingerprint) }
) { Text(stringResource(R.string.settings_default_key_select)) }
Text(stringResource(R.string.settings_private_warning))
SettingsToggle(stringResource(R.string.settings_private_export), privateKey) {
privateKey =
it
}
SecretField(stringResource(R.string.setup_passphrase), passphrase) {
passphrase = it
}
TextButton(enabled = !state.busy && fingerprint.isNotEmpty(), onClick = {
repository.exportKey(fingerprint, privateKey, passphrase)
passphrase = ""
frame =
0
}) { Text(stringResource(R.string.settings_export_confirm)) }
state.transfer.frames.getOrNull(frame)?.let { qr ->
val description =
stringResource(
R.string.settings_qr_frame,
qr.sequence.toInt(),
qr.total.toInt()
)
Text(description)
Canvas(
Modifier.fillMaxWidth().height(280.dp).semantics {
contentDescription =
description
}
) {
drawRect(Color.White)
val width = qr.width.toInt()
val scale = minOf(size.width, size.height) / (width + 8)
val x = (size.width - scale * width) / 2
val y = (size.height - scale * width) / 2
for (row in 0 until width) {
for (column in 0 until width) {
if (qr.modules[
row *
width +
column
].toInt() ==
1
) {
drawRect(
Color.Black,
Offset(x + column * scale, y + row * scale),
Size(scale, scale)
)
}
if (!state.transfer.setup) {
state.transfer.keys.forEach { key ->
Row(
Modifier.fillMaxWidth().selectable(
selected = fingerprint == key.fingerprint,
enabled = !state.busy,
role = Role.RadioButton,
onClick = { fingerprint = key.fingerprint }
)
) {
RadioButton(selected = fingerprint == key.fingerprint, onClick = null)
Column(Modifier.weight(1f)) {
Text(key.title)
Text(key.fingerprint)
}
}
}
FlowRow {
TextButton(enabled = frame > 0, onClick = {
frame -= 1
}) { Text(stringResource(R.string.settings_previous)) }
TextButton(enabled = frame < state.transfer.frames.lastIndex, onClick = {
frame +=
1
}) { Text(stringResource(R.string.settings_next)) }
TextButton(
enabled =
!state.busy &&
state.transfer.keys.any {
it.fingerprint == fingerprint && it.privateKey
},
onClick = { repository.setDefaultKey(fingerprint) }
) { Text(stringResource(R.string.settings_default_key_select)) }
Text(stringResource(R.string.settings_private_warning))
SettingsToggle(stringResource(R.string.settings_private_export), privateKey) {
privateKey =
it
}
SecretField(stringResource(R.string.setup_passphrase), passphrase) {
passphrase = it
}
if (privateKey) {
SettingsToggle(
stringResource(R.string.qr_export_private_confirm),
exportConfirmed
) {
exportConfirmed =
it
}
}
TextButton(
enabled =
!state.busy && fingerprint.isNotEmpty() && (!privateKey || exportConfirmed),
onClick = {
repository.exportKey(fingerprint, privateKey, passphrase)
passphrase = ""
frame =
0
}
) { Text(stringResource(R.string.settings_export_confirm)) }
state.transfer.frames.getOrNull(frame)?.let { qr ->
state.transfer.exportedKey?.let { exported ->
Text(exported.title)
Text(exported.fingerprint)
}
QrDisplayWindow()
val description =
stringResource(
R.string.settings_qr_frame,
qr.sequence.toInt(),
qr.total.toInt()
)
Text(description)
Canvas(
Modifier.fillMaxWidth().height(280.dp).semantics {
contentDescription =
description
}
) {
drawRect(Color.White)
val width = qr.width.toInt()
val scale = minOf(size.width, size.height) / (width + 8)
val x = (size.width - scale * width) / 2
val y = (size.height - scale * width) / 2
for (row in 0 until width) {
for (column in 0 until width) {
if (qr.modules[
row *
width +
column
].toInt() ==
1
) {
drawRect(
Color.Black,
Offset(x + column * scale, y + row * scale),
Size(scale, scale)
)
}
}
}
}
FlowRow {
TextButton(enabled = frame > 0, onClick = {
frame -= 1
}) { Text(stringResource(R.string.settings_previous)) }
TextButton(
enabled = frame < state.transfer.frames.lastIndex,
onClick = {
frame +=
1
}
) { Text(stringResource(R.string.settings_next)) }
}
}
if (state.transfer.frames.size > 1) {
SettingsToggle(stringResource(R.string.qr_animate), animate) {
animate = it
}
}
}
if (state.transfer.setup) {
SecretField(stringResource(R.string.setup_passphrase), passphrase) {
passphrase =
it
}
}
if (state.transfer.ready == null && !state.busy) QrCameraPane(repository)
TextButton(enabled = !state.busy, onClick = repository::chooseQrImages) {
Text(stringResource(R.string.settings_qr_images))
}
@@ -564,22 +610,50 @@ private fun KeyTransferDialog(
state.transfer.total.toInt()
)
)
if (state.transfer.duplicate) Text(stringResource(R.string.qr_duplicate))
state.transfer.ready?.let {
Text(it.title)
Text(it.fingerprint)
SettingsToggle(stringResource(R.string.settings_make_default), makeDefault) {
makeDefault =
it
if (!state.transfer.setup) {
SettingsToggle(
stringResource(R.string.settings_make_default),
makeDefault
) {
makeDefault =
it
}
SettingsToggle(stringResource(R.string.settings_replace_key), overwrite) {
overwrite =
it
}
}
SettingsToggle(stringResource(R.string.settings_replace_key), overwrite) {
overwrite =
it
if (it.privateKey) {
SettingsToggle(
stringResource(R.string.qr_import_private_confirm),
importConfirmed
) {
importConfirmed =
it
}
}
TextButton(
enabled = !state.busy && (!it.privateKey || importConfirmed),
onClick = {
repository.importKey(passphrase, makeDefault, overwrite)
passphrase =
""
}
) {
Text(
stringResource(
if (state.transfer.setup) {
R.string.qr_setup_import
} else {
R.string.settings_import_confirm
}
)
)
}
TextButton(enabled = !state.busy, onClick = {
repository.importKey(passphrase, makeDefault, overwrite)
passphrase =
""
}) { Text(stringResource(R.string.settings_import_confirm)) }
}
}
},
@@ -171,7 +171,13 @@ private fun ShellDestination(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OnboardingScreen(viewModel.onboarding, setupForm, scroll = false)
OnboardingScreen(
viewModel.onboarding,
setupForm,
viewModel.settings,
viewModel.authentication,
scroll = false
)
}
}
ready && route == ShellRoute.Passwords -> {
@@ -224,4 +224,15 @@
<item quantity="other">%1$d results</item>
</plurals>
<string name="settings_license_loading">Loading license…</string>
<string name="qr_scan">Scan GPG QR codes</string>
<string name="qr_pause">Pause camera</string>
<string name="qr_resume">Resume camera</string>
<string name="qr_camera_denied">Camera access is required to scan. If permission was denied, allow it in app settings or choose QR images.</string>
<string name="qr_camera_settings">Open camera permission settings</string>
<string name="qr_camera_missing">The camera is unavailable. Choose QR images or paste an armored key.</string>
<string name="qr_duplicate">Duplicate frame; progress is unchanged.</string>
<string name="qr_import_private_confirm">I checked this fingerprint and authorize importing its private key material.</string>
<string name="qr_export_private_confirm">I checked this fingerprint and authorize displaying its private QR codes.</string>
<string name="qr_animate">Automatically advance frames</string>
<string name="qr_setup_import">Prepare the scanned key</string>
</resources>
+4
View File
@@ -1,4 +1,5 @@
[versions]
camera = "1.6.2"
agp = "9.4.1"
kotlin = "2.4.21"
ksp = "2.3.12"
@@ -28,6 +29,9 @@ biometric = "1.1.0"
fragment = "1.9.1"
[libraries]
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" }
benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark" }
rustls-verifier = { group = "rustls", name = "rustls-platform-verifier", version.ref = "rustlsVerifier" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose" }
+260
View File
@@ -183,6 +183,11 @@
<sha256 value="03fb659177c8618e47425925c11bc91f384703ea26c265be9b25cd79292b511c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.1.0">
<artifact name="appcompat-1.1.0.pom">
<sha256 value="340d617121f8ef8e02a6680c8f357aa3e542276d0c8a1cdcb6fd98984b2cb7b9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.2.0">
<artifact name="appcompat-1.2.0.aar">
<sha256 value="3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70" origin="Generated by Gradle"/>
@@ -284,6 +289,70 @@
<sha256 value="4999f205b2b38e31180ad58111e32d564190c2024fbfd58527e2da8bfb67ff65" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera" name="camera-camera2" version="1.6.2">
<artifact name="camera-camera2-1.6.2.aar">
<sha256 value="5a55014ed96feda81a37799faf9f96150025a4434699d55caa1cdebf2ae07e20" origin="Generated by Gradle"/>
</artifact>
<artifact name="camera-camera2-1.6.2.module">
<sha256 value="4694b995fbb4934e5e1e3cedabef426e799a510cfc179b659c4e9632f6b2fe75" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera" name="camera-camera2-pipe" version="1.6.2">
<artifact name="camera-camera2-pipe-1.6.2.aar">
<sha256 value="b357bd94fd4273ff88f453f7e712a22e100adc932324fc6b51fbe9ef9cf45515" origin="Generated by Gradle"/>
</artifact>
<artifact name="camera-camera2-pipe-1.6.2.module">
<sha256 value="5470741b0a6fe88e560e7d929097e2ec60e7406aef09b48ab900b0befa102c0f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera" name="camera-core" version="1.6.2">
<artifact name="camera-core-1.6.2.aar">
<sha256 value="ebd66f090414f4c12f23a55b5023aeee1b096da8a06c02a0b6fb3520c4ddadf6" origin="Generated by Gradle"/>
</artifact>
<artifact name="camera-core-1.6.2.module">
<sha256 value="bee75b4ef561862030702a51f692057e034ae1f600f3091b2a5e5a89856ab410" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera" name="camera-lifecycle" version="1.6.2">
<artifact name="camera-lifecycle-1.6.2.aar">
<sha256 value="b7b701a3c6838bc7c4acd1372140e889fdf9681bf833ebfc638fdbe28ed5fa32" origin="Generated by Gradle"/>
</artifact>
<artifact name="camera-lifecycle-1.6.2.module">
<sha256 value="bd7fd00cb9de0f2ce8c3400b52504cf7691590bb29a1550a71b58255e483ff47" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera" name="camera-video" version="1.6.2">
<artifact name="camera-video-1.6.2.aar">
<sha256 value="dee568da7d1ce009506a538b2916cce6f23473a835b0aeb4e8193483ff1174d0" origin="Generated by Gradle"/>
</artifact>
<artifact name="camera-video-1.6.2.module">
<sha256 value="cf1e3fb9362af9c020f1126f8c56e7578f73a96b6f670136cc756a907182733c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera" name="camera-view" version="1.6.2">
<artifact name="camera-view-1.6.2.aar">
<sha256 value="2f258b88422b925717d2d1d1714d5572e48e2194336be227b35fc60000f6e166" origin="Generated by Gradle"/>
</artifact>
<artifact name="camera-view-1.6.2.module">
<sha256 value="c84dc907bff8a46133631d1e93edcef78ac608c83abd07b51056f9cca4ae800c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera.featurecombinationquery" name="featurecombinationquery" version="1.6.2">
<artifact name="featurecombinationquery-1.6.2.aar">
<sha256 value="2321cfd29678e3128284d1622bb6b06e0fecd2c2e69140471ab00e8616e622c2" origin="Generated by Gradle"/>
</artifact>
<artifact name="featurecombinationquery-1.6.2.module">
<sha256 value="4f6edda1492eaa928cfe7516d8efd097ed2f59ee4cedf1bec2d3345a3d6137fd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.camera.viewfinder" name="viewfinder-core" version="1.5.1">
<artifact name="viewfinder-core-1.5.1.aar">
<sha256 value="31179ed5509e4d6d8d667efb91a039cac897b2bfd800d4c6e0b5bed376b4fa69" origin="Generated by Gradle"/>
</artifact>
<artifact name="viewfinder-core-1.5.1.module">
<sha256 value="31407c1af39b26a59bebdcd61a39b7aa1a8b7f51c63aa61127d4f977a36d877c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.cardview" name="cardview" version="1.0.0">
<artifact name="cardview-1.0.0.aar">
<sha256 value="1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7" origin="Generated by Gradle"/>
@@ -848,6 +917,14 @@
<sha256 value="afd7094d3a07422be2e991bc87048542ae9724a5c42bd54b5cae3029c7970a1d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.concurrent" name="concurrent-futures-ktx" version="1.1.0">
<artifact name="concurrent-futures-ktx-1.1.0.jar">
<sha256 value="1968bf52039e38636aa6f114cd17d7256919d1e8997417716fef9d1da1f24d85" origin="Generated by Gradle"/>
</artifact>
<artifact name="concurrent-futures-ktx-1.1.0.module">
<sha256 value="69b79724566d49140846700690b8d2165231c577e93e66726a443e8f976bbe19" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.concurrent" name="concurrent-futures-ktx" version="1.2.0">
<artifact name="concurrent-futures-ktx-1.2.0.jar">
<sha256 value="e1f3e17bb4358ccd6c77ca45f70635c9aba237261f19eaa4f64a0218c00e2a3e" origin="Generated by Gradle"/>
@@ -903,6 +980,11 @@
<sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.7.0">
<artifact name="core-1.7.0.module">
<sha256 value="988f820899d5a4982e5c878ca1cd417970ace332ea2ff72f5be19b233fa0e788" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.8.0">
<artifact name="core-1.8.0.module">
<sha256 value="505f1838869611519d65ec7feb650e88856e3682e37e42a2a24b73e655a98d74" origin="Generated by Gradle"/>
@@ -916,6 +998,14 @@
<sha256 value="8674ade1d12962469dd2d1e6edc03b465f20d180b0840a129bc40b351e8651d1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core-backported-fixes" version="1.0.0">
<artifact name="core-backported-fixes-1.0.0.aar">
<sha256 value="874722634db5f60745ab2f64d734dd81d3ee4eb71f175263e6565dabea54713d" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-backported-fixes-1.0.0.module">
<sha256 value="05aef3e2e15168190894999d4b14907c84d066837964ec900f7a2f0e4d9354ac" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.1.0">
<artifact name="core-ktx-1.1.0.pom">
<sha256 value="729d11bad16d87c6212dc9ab80e9c770d333115062136e0732164a6b1696e11c" origin="Generated by Gradle"/>
@@ -1042,6 +1132,14 @@
<sha256 value="92be16cccb757f807bddd92f1fca95a038a810ccca051c4149c11682478954b8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.exifinterface" name="exifinterface" version="1.4.2">
<artifact name="exifinterface-1.4.2.aar">
<sha256 value="6bb2b68682a2023c55f35f941444ffea0157947b061e4fd7606ba935e2e77425" origin="Generated by Gradle"/>
</artifact>
<artifact name="exifinterface-1.4.2.module">
<sha256 value="6365165eb5aacc700a34b1650a2910ea01ebd819821b89af7689fa41274fb00b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.fragment" name="fragment" version="1.2.5">
<artifact name="fragment-1.2.5.pom">
<sha256 value="df0aca46b62bb47cc662cbcee63372db6d2a2859478ee38b594fba3433fe30a5" origin="Generated by Gradle"/>
@@ -1100,6 +1198,11 @@
<sha256 value="2baaf7f17551765524bd1e4d2f954a2a0178a112039cf549ba8b7d4b41e66931" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.1.0">
<artifact name="lifecycle-common-2.1.0.pom">
<sha256 value="692c2a60437a95220c8fbb6e8943196f255f535d1a7c58714e6c46530782f276" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.10.0">
<artifact name="lifecycle-common-2.10.0.module">
<sha256 value="5d1247b1edfbdc9ddedcbe525c9d2629567b1c538cb7a9c620cd044a124b5c73" origin="Generated by Gradle"/>
@@ -1170,6 +1273,11 @@
<sha256 value="1d1837f3942b33ea30aa2301fdcea2639408a0fd6fd4368c22478fa8153aebbf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.1.0">
<artifact name="lifecycle-livedata-2.1.0.pom">
<sha256 value="1e4c580c995e41cd18455cfb87e3e43955be9db9ec63a8fb3c7259f01b7c4e3c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.10.0">
<artifact name="lifecycle-livedata-2.10.0.aar">
<sha256 value="3e8c009fc88da1c52e4ed05a81269d5ac9bd405046f300307f5e3a17e201a6df" origin="Generated by Gradle"/>
@@ -1186,6 +1294,11 @@
<sha256 value="d990bc920ae43729d702d80077328e8a59781ea5a35b24c02bc47d326ae73d7d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.3.1">
<artifact name="lifecycle-livedata-2.3.1.module">
<sha256 value="93494e78d5049dff897fde50f477e7b965e9a3de4a1ed453b5f0cbc409eb4e4c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.6.1">
<artifact name="lifecycle-livedata-2.6.1.aar">
<sha256 value="67359f609dfc2bf65da1270b23033f856064ec279f058e0a70c715f7c9003031" origin="Generated by Gradle"/>
@@ -1194,6 +1307,11 @@
<sha256 value="e2659e87e890b52b66f300471828b387a205e5387a392713190166f399ecf641" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.1.0">
<artifact name="lifecycle-livedata-core-2.1.0.pom">
<sha256 value="6839d2e68c8fee4cc3bbf04e6b291a6ae842403fcd5f8d944c27814d296deff3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.10.0">
<artifact name="lifecycle-livedata-core-2.10.0.aar">
<sha256 value="12dd61a9843ccebb4547d3ebe2f6d030ca8f6988d22f8fad19c0af93b4a97e95" origin="Generated by Gradle"/>
@@ -1215,6 +1333,11 @@
<sha256 value="aa51621be3817d1f5d21336b0c0525fe96ae899cec656c13d8ea18a51ea32c2f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.3.1">
<artifact name="lifecycle-livedata-core-2.3.1.module">
<sha256 value="b1e095d550d39a7d6c815761d6dbe3fd64eb31a39dc28146e78bbf2c01ba8f41" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.6.1">
<artifact name="lifecycle-livedata-core-2.6.1.aar">
<sha256 value="2256780a3cff4a1e57fbb3d442557c17dc363ab8af105bcaf5261d8e2d5db949" origin="Generated by Gradle"/>
@@ -1563,6 +1686,30 @@
<sha256 value="c978d550808b47434aa49a63164110a50b55b0bcc6160a93a2e37d5110df8c5e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.media3" name="media3-common" version="1.9.0">
<artifact name="media3-common-1.9.0.aar">
<sha256 value="714114bf8a56fe44f0f78b499ae7ea3bed52391313794c8abdc1b687700d42aa" origin="Generated by Gradle"/>
</artifact>
<artifact name="media3-common-1.9.0.module">
<sha256 value="02d42f8fd7cc53c385256f417df73a5cf30e8b7c5635ca5786148518ce711b75" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.media3" name="media3-container" version="1.9.0">
<artifact name="media3-container-1.9.0.aar">
<sha256 value="f73cea4fe8a2bf9eb6462bfb7a547d1aaf55395d0007eed6fadb129b56927c0d" origin="Generated by Gradle"/>
</artifact>
<artifact name="media3-container-1.9.0.module">
<sha256 value="5a5f4d796aa34a3480ea75df73892d68eba7cd4bc239c116eee3816ce973d469" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.media3" name="media3-muxer" version="1.9.0">
<artifact name="media3-muxer-1.9.0.aar">
<sha256 value="452b182d84cfe20d1154d999107b9aac7c218ad8ee208d35d067916a92a02031" origin="Generated by Gradle"/>
</artifact>
<artifact name="media3-muxer-1.9.0.module">
<sha256 value="e22678eed7457db37ceb4f14d96c761fa5055a114b4750892cf42b820a791428" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.navigation3" name="navigation3-runtime" version="1.2.0">
<artifact name="navigation3-runtime-1.2.0.module">
<sha256 value="ff14442bf489768bbd2126536921a4d007bdf2d117b2c0451be67cd58eb66a21" origin="Generated by Gradle"/>
@@ -1989,6 +2136,19 @@
<sha256 value="6ec8aa998df9b964424c7ab215738237a9b2cc4301da135bb9b8cdc62325fb3e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.tracing" name="tracing-ktx" version="1.2.0">
<artifact name="tracing-ktx-1.2.0.module">
<sha256 value="39288b23c71a4391132aec9951f5e27f41727a2cdff69c6e4b4ece49f63e540c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.tracing" name="tracing-ktx" version="2.0.0">
<artifact name="tracing-ktx-2.0.0.aar">
<sha256 value="7e7cf1386841b11310138723eb9f36b5eaf6c8caec896b16f99e579d220d6a0c" origin="Generated by Gradle"/>
</artifact>
<artifact name="tracing-ktx-2.0.0.module">
<sha256 value="f30609942614488befb6edaa36aa4d3b4fb1c1b7b79780630d3ca3a02b08c0a5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.tracing" name="tracing-perfetto" version="1.0.1">
<artifact name="tracing-perfetto-1.0.1.aar">
<sha256 value="5387298b5bdd8f7d98315fe8720a757056ba0b9b5d9544d2e3fabc26b9ac55a6" origin="Generated by Gradle"/>
@@ -2551,6 +2711,11 @@
<sha256 value="05f740c6648165db00cf618dd56c200c4725e358e6d54f5853e0bec15734ea0a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.auto" name="auto-parent" version="7">
<artifact name="auto-parent-7.pom">
<sha256 value="a46426fccb5d32705ad9cbbc996f786bd048cc8cbdd21db046500169f15a4356" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.auto.value" name="auto-value-annotations" version="1.6.2">
<artifact name="auto-value-annotations-1.6.2.jar">
<sha256 value="b48b04ddba40e8ac33bf036f06fc43995fc5084bd94bdaace807ce27d3bea3fb" origin="Generated by Gradle"/>
@@ -2559,11 +2724,24 @@
<sha256 value="1c76cd462fc96e7aa96dc70ce82f0d54063d6df16db35c9c7d9cc0d1a99d3fff" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.auto.value" name="auto-value-annotations" version="1.6.3">
<artifact name="auto-value-annotations-1.6.3.jar">
<sha256 value="0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0" origin="Generated by Gradle"/>
</artifact>
<artifact name="auto-value-annotations-1.6.3.pom">
<sha256 value="e1fc780f7ee025e662b3da72723dbe2ac8dac0a2f8920f265315c4e1be3d765c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.auto.value" name="auto-value-parent" version="1.6.2">
<artifact name="auto-value-parent-1.6.2.pom">
<sha256 value="27b640c82179f5cff62009c0b72033d9bc60f60e9902a66802274b7fe37fc81c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.auto.value" name="auto-value-parent" version="1.6.3">
<artifact name="auto-value-parent-1.6.3.pom">
<sha256 value="e59df5732b4cb34d5727181446f9ded9ce4425131a0a1062519eea196de4d8a1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.findbugs" name="jsr305" version="3.0.2">
<artifact name="jsr305-3.0.2.jar">
<sha256 value="766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7" origin="Generated by Gradle"/>
@@ -2601,6 +2779,14 @@
<sha256 value="265ba959a8e13c3a06133f04b539169c1018daffd4d33f53c453ab4cb386f570" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.dagger" name="dagger" version="2.59">
<artifact name="dagger-2.59.jar">
<sha256 value="94d5b23812cc12fa903c9ac4a42978e7e7b2622e6b51c17f8b12037ceece43cd" origin="Generated by Gradle"/>
</artifact>
<artifact name="dagger-2.59.pom">
<sha256 value="0e47846618c7a1b7a8124fd20623a667643d73cbe0a561bb87056151aa967538" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.dagger" name="dagger" version="2.60.1">
<artifact name="dagger-2.60.1.jar">
<sha256 value="b012d0df527b51bfcd8953d5fcb4d022a45ebe66010d6f181ede68d1572b5cb2" origin="Generated by Gradle"/>
@@ -2716,6 +2902,9 @@
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.18.0">
<artifact name="error_prone_annotations-2.18.0.jar">
<sha256 value="9e6814cb71816988a4fd1b07a993a8f21bb7058d522c162b1de849e19bea54ae" origin="Generated by Gradle"/>
</artifact>
<artifact name="error_prone_annotations-2.18.0.pom">
<sha256 value="920135797dcca5917b5a5c017642a58d340a4cd1bcd12f56f892a5663bd7bddc" origin="Generated by Gradle"/>
</artifact>
@@ -2725,6 +2914,14 @@
<sha256 value="4ca5a35d61235e16549ac346d1e34551cdf0fe27e84aa57e03cbeb255ea4e5da" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.28.0">
<artifact name="error_prone_annotations-2.28.0.jar">
<sha256 value="f3fc8a3a0a4020706a373b00e7f57c2512dd26d1f83d28c7d38768f8682b231e" origin="Generated by Gradle"/>
</artifact>
<artifact name="error_prone_annotations-2.28.0.pom">
<sha256 value="0ce909f13a568148476e5ee200f380f98c75ba08971aaf15db295eb77598ef3a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.3.1">
<artifact name="error_prone_annotations-2.3.1.pom">
<sha256 value="3edce6b711ba368efe16b9b7aacb0214fbd648414cb9b965953a2e7ed89a819a" origin="Generated by Gradle"/>
@@ -2764,6 +2961,11 @@
<sha256 value="fa81829d049559df6927f9c9a6fd6bbd09f8b50e6d4736ae72c8300b6c3d7654" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.28.0">
<artifact name="error_prone_parent-2.28.0.pom">
<sha256 value="accefdbb5416cef5fcd2ddc37db4f1fcb34a2193cc1a55dfe5970a131b3e7683" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.3.1">
<artifact name="error_prone_parent-2.3.1.pom">
<sha256 value="767525d9a81129cd081968382980336327be4162b1e2251a182911daa733c123" origin="Generated by Gradle"/>
@@ -2837,6 +3039,25 @@
<sha256 value="13d4e2998aac652fc6d7b9eaf3e495fa2952bf98ef68a2c49aa5eaa0bbe731a9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava" version="32.0.1-android">
<artifact name="guava-32.0.1-android.jar">
<sha256 value="12429ff9ac33b7afd8b1a5071179757cf4d3c47f0f4099bc0384e644ecbf82dd" origin="Generated by Gradle"/>
</artifact>
<artifact name="guava-32.0.1-android.pom">
<sha256 value="cf26ce63461fb585cad25dc6932765883122643c3beec5e3269f667c73295ac4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava" version="33.3.1-android">
<artifact name="guava-33.3.1-android.jar">
<sha256 value="2c3e41d1b380f2044d257947a3aa82dabf3ae4b978622745254aa18b6cf89ab0" origin="Generated by Gradle"/>
</artifact>
<artifact name="guava-33.3.1-android.module">
<sha256 value="6971614ddeee003e54d2b69c6b0c94ce8a5b5eba980d16455c81df9089a7acb7" origin="Generated by Gradle"/>
</artifact>
<artifact name="guava-33.3.1-jre.jar">
<sha256 value="4bf0e2c5af8e4525c96e8fde17a4f7307f97f8478f11c4c8e35a0e3298ae4e90" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava" version="33.4.0-jre">
<artifact name="guava-33.4.0-jre.jar">
<sha256 value="b918c98a7e44dbe94ebd9fe3e40cddaadb5a93e6a78eb6008b42df237241e538" origin="Generated by Gradle"/>
@@ -2863,6 +3084,16 @@
<sha256 value="30a51d34b075c9b4eb6a2da3fba6c1f5047b45225a3a03aebcefc0b7880e5735" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava-parent" version="32.0.1-android">
<artifact name="guava-parent-32.0.1-android.pom">
<sha256 value="cddb3a941d71be5e871f39a239ba3bb8883c88c7763d8a3d2d342b09a1d5da30" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava-parent" version="33.3.1-android">
<artifact name="guava-parent-33.3.1-android.pom">
<sha256 value="6e11986ea7250b51f847157e2dc937f32a306804dfce0007a5e81ddb9b95c579" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava-parent" version="33.4.0-android">
<artifact name="guava-parent-33.4.0-android.pom">
<sha256 value="7220ede61026596fbc720ee4b93246fa2d14f328058532b59ef053de397c7d83" origin="Generated by Gradle"/>
@@ -2903,6 +3134,9 @@
</artifact>
</component>
<component group="com.google.j2objc" name="j2objc-annotations" version="2.8">
<artifact name="j2objc-annotations-2.8.jar">
<sha256 value="f02a95fa1a5e95edb3ed859fd0fb7df709d121a35290eff8b74dce2ab7f4d6ed" origin="Generated by Gradle"/>
</artifact>
<artifact name="j2objc-annotations-2.8.pom">
<sha256 value="37f87798b18385113c918bfa9e1276fe50735ef8fa849b5800c519d54dbf11f8" origin="Generated by Gradle"/>
</artifact>
@@ -3610,6 +3844,14 @@
<sha256 value="66f563d8a2a1ee395a1283992fd646347f42054d071592d1f477523816e397fa" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.checkerframework" name="checker-qual" version="3.33.0">
<artifact name="checker-qual-3.33.0.jar">
<sha256 value="e316255bbfcd9fe50d165314b85abb2b33cb2a66a93c491db648e498a82c2de1" origin="Generated by Gradle"/>
</artifact>
<artifact name="checker-qual-3.33.0.module">
<sha256 value="e8521d75625d41272c767d262a153ac163cc505b66644a2ef705fa8949ffb4e5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.checkerframework" name="checker-qual" version="3.43.0">
<artifact name="checker-qual-3.43.0.jar">
<sha256 value="3fbc2e98f05854c3df16df9abaa955b91b15b3ecac33623208ed6424640ef0f6" origin="Generated by Gradle"/>
@@ -3768,6 +4010,11 @@
<sha256 value="3090b4e873054cdc29991ae94c92fa45afb4c145d460441d803baf6ef165bbb8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-bom" version="1.8.0">
<artifact name="kotlin-bom-1.8.0.pom">
<sha256 value="681f9878548c0a149c678092fd8b1f4cc7c5c41ba1b24397101c1073a6c76dda" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-bom" version="2.3.21">
<artifact name="kotlin-bom-2.3.21.pom">
<sha256 value="950704bad04c5143656cfca1d2c574f25fcad9cac33905ab22a57e9fdf7bfd88" origin="Generated by Gradle"/>
@@ -4400,6 +4647,19 @@
<sha256 value="ee66712b1006e971636f11554af63c56835caf315382ce23e54f59f5b8b9d4f9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="atomicfu" version="0.28.0">
<artifact name="atomicfu-0.28.0.module">
<sha256 value="535568c9291a3da7705c4b40535eacedf86b248df570ac52669c907d4bb8558b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="atomicfu-jvm" version="0.28.0">
<artifact name="atomicfu-jvm-0.28.0.jar">
<sha256 value="39576ec46b83412ff7a2909d24cccbba90400e91f9aa67b250309bc0872ecf52" origin="Generated by Gradle"/>
</artifact>
<artifact name="atomicfu-jvm-0.28.0.module">
<sha256 value="34b623af9c25ca7f2e52406f10495c46e6f410835d7a058c60ba36923d689635" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.11.0">
<artifact name="kotlinx-coroutines-android-1.11.0.jar">
<sha256 value="c2cb206d27017c7d1bf5ff179787397543d13748dbabb0d7237e1585e0b29044" origin="Generated by Gradle"/>
+26 -5
View File
@@ -22,6 +22,14 @@ androidx.benchmark:benchmark-macro:1.5.0=benchmarkCompileClasspath,benchmarkLint
androidx.benchmark:benchmark-traceprocessor-android:1.5.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.benchmark:benchmark-traceprocessor:1.5.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.biometric:biometric:1.1.0=benchmarkTestedApks,debugTestedApks
androidx.camera.featurecombinationquery:featurecombinationquery:1.6.2=benchmarkTestedApks,debugTestedApks
androidx.camera.viewfinder:viewfinder-core:1.5.1=benchmarkTestedApks,debugTestedApks
androidx.camera:camera-camera2-pipe:1.6.2=benchmarkTestedApks,debugTestedApks
androidx.camera:camera-camera2:1.6.2=benchmarkTestedApks,debugTestedApks
androidx.camera:camera-core:1.6.2=benchmarkTestedApks,debugTestedApks
androidx.camera:camera-lifecycle:1.6.2=benchmarkTestedApks,debugTestedApks
androidx.camera:camera-video:1.6.2=benchmarkTestedApks,debugTestedApks
androidx.camera:camera-view:1.6.2=benchmarkTestedApks,debugTestedApks
androidx.collection:collection-jvm:1.6.0=benchmarkTestedApks,debugTestedApks
androidx.collection:collection:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.collection:collection:1.6.0=benchmarkTestedApks,debugTestedApks
@@ -64,9 +72,11 @@ androidx.compose.ui:ui-util-jvmstubs:1.12.1=benchmarkTestedApks,debugTestedApks
androidx.compose.ui:ui-util:1.12.1=benchmarkTestedApks,debugTestedApks
androidx.compose.ui:ui:1.12.1=benchmarkTestedApks,debugTestedApks
androidx.compose:compose-bom:2026.09.00=benchmarkTestedApks,debugTestedApks
androidx.concurrent:concurrent-futures-ktx:1.1.0=benchmarkTestedApks,debugTestedApks
androidx.concurrent:concurrent-futures-ktx:1.2.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.concurrent:concurrent-futures:1.1.0=benchmarkTestedApks,debugTestedApks
androidx.concurrent:concurrent-futures:1.2.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.core:core-backported-fixes:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.core:core-ktx:1.18.0=benchmarkTestedApks,debugTestedApks
androidx.core:core-viewtree:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.core:core:1.18.0=benchmarkTestedApks,debugTestedApks
@@ -74,6 +84,7 @@ androidx.core:core:1.9.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,
androidx.cursoradapter:cursoradapter:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.customview:customview:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.drawerlayout:drawerlayout:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.exifinterface:exifinterface:1.4.2=benchmarkTestedApks,debugTestedApks
androidx.fragment:fragment:1.9.1=benchmarkTestedApks,debugTestedApks
androidx.interpolator:interpolator:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.lifecycle:lifecycle-common-jvm:2.11.0=benchmarkTestedApks,debugTestedApks
@@ -97,6 +108,9 @@ androidx.lifecycle:lifecycle-viewmodel-savedstate-desktop:2.11.0=benchmarkTested
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=benchmarkTestedApks,debugTestedApks
androidx.lifecycle:lifecycle-viewmodel:2.11.0=benchmarkTestedApks,debugTestedApks
androidx.loader:loader:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.media3:media3-common:1.9.0=benchmarkTestedApks,debugTestedApks
androidx.media3:media3-container:1.9.0=benchmarkTestedApks,debugTestedApks
androidx.media3:media3-muxer:1.9.0=benchmarkTestedApks,debugTestedApks
androidx.navigation3:navigation3-runtime-desktop:1.2.0=benchmarkTestedApks,debugTestedApks
androidx.navigation3:navigation3-runtime:1.2.0=benchmarkTestedApks,debugTestedApks
androidx.navigation3:navigation3-ui-jvmstubs:1.2.0=benchmarkTestedApks,debugTestedApks
@@ -125,6 +139,7 @@ androidx.test:rules:1.5.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath
androidx.test:runner:1.7.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.tracing:tracing-desktop:2.0.0=benchmarkTestedApks,debugTestedApks
androidx.tracing:tracing-ktx:1.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.tracing:tracing-ktx:2.0.0=benchmarkTestedApks,debugTestedApks
androidx.tracing:tracing-perfetto-binary:1.0.1=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.tracing:tracing-perfetto-handshake:1.0.1=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.tracing:tracing-perfetto:1.0.1=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
@@ -169,18 +184,21 @@ com.android.tools:sdk-common:32.4.1=androidLintTool
com.android.tools:sdklib:32.4.1=androidLintTool
com.github.ajalt.clikt:clikt-jvm:5.0.3=ktlint
com.github.ajalt.clikt:clikt:5.0.3=ktlint
com.google.auto.value:auto-value-annotations:1.6.3=benchmarkTestedApks,debugTestedApks
com.google.code.findbugs:jsr305:3.0.2=androidLintTool,benchmarkTestedApks,debugTestedApks
com.google.code.gson:gson:2.11.0=androidLintTool
com.google.dagger:dagger-lint-aar:2.60.1=benchmarkTestedApks,debugTestedApks
com.google.dagger:dagger:2.60.1=benchmarkTestedApks,debugTestedApks
com.google.dagger:hilt-android:2.60.1=benchmarkTestedApks,debugTestedApks
com.google.dagger:hilt-core:2.60.1=benchmarkTestedApks,debugTestedApks
com.google.errorprone:error_prone_annotations:2.28.0=benchmarkTestedApks,debugTestedApks
com.google.errorprone:error_prone_annotations:2.36.0=androidLintTool
com.google.guava:failureaccess:1.0.2=androidLintTool
com.google.guava:failureaccess:1.0.2=androidLintTool,benchmarkTestedApks,debugTestedApks
com.google.guava:guava:33.3.1-android=benchmarkTestedApks,debugTestedApks
com.google.guava:guava:33.4.0-jre=androidLintTool
com.google.guava:listenablefuture:1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,benchmarkTestedApks,debugLintChecksClasspath,debugRuntimeClasspath,debugTestedApks
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool
com.google.guava:listenablefuture:1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool,benchmarkTestedApks,debugTestedApks
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,benchmarkTestedApks,debugTestedApks
com.google.jimfs:jimfs:1.1=androidLintTool
com.google.protobuf:protobuf-java:3.25.5=androidLintTool,unified-test-platform-gradle-work-action
com.pinterest.ktlint:ktlint-cli-reporter-baseline:1.7.1=ktlint,ktlintBaselineReporter
@@ -233,11 +251,12 @@ org.apache.httpcomponents:httpmime:4.5.6=androidLintTool
org.bouncycastle:bcpkix-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcprov-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcutil-jdk18on:1.80.2=androidLintTool
org.checkerframework:checker-qual:3.43.0=androidLintTool
org.checkerframework:checker-qual:3.43.0=androidLintTool,benchmarkTestedApks,debugTestedApks
org.ec4j.core:ec4j-core:1.1.1=ktlint,ktlintBaselineReporter,ktlintRuleset
org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
org.jetbrains.kotlin:kotlin-bom:1.8.0=benchmarkTestedApks,debugTestedApks
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
@@ -270,6 +289,8 @@ org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,benchmarkTestedApks,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugTestedApks,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:atomicfu-jvm:0.28.0=benchmarkTestedApks,debugTestedApks
org.jetbrains.kotlinx:atomicfu:0.28.0=benchmarkTestedApks,debugTestedApks
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=benchmarkTestedApks,debugTestedApks
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=benchmarkTestedApks,debugTestedApks
+3
View File
@@ -40,6 +40,9 @@ kotlin {
dependencies {
implementation(project(":core"))
implementation(libs.camera.camera2)
implementation(libs.camera.lifecycle)
implementation(libs.camera.view)
implementation(libs.biometric)
implementation(libs.fragment)
testImplementation(libs.junit)
+34 -7
View File
@@ -14,13 +14,23 @@ androidx.appcompat:appcompat:1.2.0=debugAndroidTestLintChecksClasspath,debugAndr
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.biometric:biometric:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera.featurecombinationquery:featurecombinationquery:1.6.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera.viewfinder:viewfinder-core:1.5.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-camera2-pipe:1.6.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-camera2:1.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-core:1.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-lifecycle:1.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-video:1.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-view:1.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.collection:collection-jvm:1.4.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.collection:collection-jvm:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.collection:collection:1.4.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.collection:collection:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.concurrent:concurrent-futures:1.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.core:core-backported-fixes:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-ktx:1.13.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-ktx:1.9.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.core:core-viewtree:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -29,6 +39,7 @@ androidx.core:core:1.9.0=debugAndroidTestCompileClasspath,debugCompileClasspath,
androidx.cursoradapter:cursoradapter:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.drawerlayout:drawerlayout:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.exifinterface:exifinterface:1.4.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.fragment:fragment:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.interpolator:interpolator:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-common-jvm:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -46,6 +57,9 @@ androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0=debugAndroidTes
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel:2.10.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.media3:media3-common:1.9.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.media3:media3-container:1.9.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.media3:media3-muxer:1.9.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,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-ktx:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -56,6 +70,7 @@ androidx.test:core:1.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintCh
androidx.test:monitor:1.8.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.test:runner:1.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.tracing:tracing-android:2.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.tracing:tracing-ktx:2.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.tracing:tracing:1.1.0=debugAndroidTestCompileClasspath
androidx.tracing:tracing:2.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.vectordrawable:vectordrawable-animated:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -95,14 +110,21 @@ com.android.tools:sdk-common:32.4.1=androidLintTool
com.android.tools:sdklib:32.4.1=androidLintTool
com.github.ajalt.clikt:clikt-jvm:5.0.3=ktlint
com.github.ajalt.clikt:clikt:5.0.3=ktlint
com.google.code.findbugs:jsr305:3.0.2=androidLintTool
com.google.auto.value:auto-value-annotations:1.6.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.code.findbugs:jsr305:3.0.2=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.code.gson:gson:2.11.0=androidLintTool
com.google.dagger:dagger:2.59=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.18.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
com.google.errorprone:error_prone_annotations:2.28.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.36.0=androidLintTool
com.google.guava:failureaccess:1.0.2=androidLintTool
com.google.guava:failureaccess:1.0.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
com.google.guava:failureaccess:1.0.2=androidLintTool,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.guava:guava:32.0.1-android=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
com.google.guava:guava:33.3.1-android=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.guava:guava:33.4.0-jre=androidLintTool
com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.j2objc:j2objc-annotations:2.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.jimfs:jimfs:1.1=androidLintTool
com.google.protobuf:protobuf-java:3.25.5=androidLintTool,unified-test-platform-gradle-work-action
com.pinterest.ktlint:ktlint-cli-reporter-baseline:1.7.1=ktlint,ktlintBaselineReporter
@@ -132,8 +154,9 @@ io.github.detekt.sarif4k:sarif4k:0.6.0=ktlint,ktlintReporter
io.github.oshai:kotlin-logging-jvm:7.0.7=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
io.github.oshai:kotlin-logging:5.1.0=ktlint,ktlintBaselineReporter,ktlintReporter
jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool
jakarta.inject:jakarta.inject-api:2.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=androidLintTool
javax.inject:javax.inject:1=androidLintTool,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.19.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -149,11 +172,13 @@ org.apache.httpcomponents:httpmime:4.5.6=androidLintTool
org.bouncycastle:bcpkix-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcprov-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcutil-jdk18on:1.80.2=androidLintTool
org.checkerframework:checker-qual:3.43.0=androidLintTool
org.checkerframework:checker-qual:3.33.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
org.checkerframework:checker-qual:3.43.0=androidLintTool,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.ec4j.core:ec4j-core:1.1.1=ktlint,ktlintBaselineReporter,ktlintRuleset
org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.jetbrains.kotlin:kotlin-bom:1.8.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
@@ -186,6 +211,8 @@ org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:atomicfu-jvm:0.28.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:atomicfu:0.28.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<application>
<activity android:name="de.rfc1437.ironstorage.platform.KeystoreTestActivity" android:exported="false" android:theme="@android:style/Theme.Material.Light.NoActionBar" />
</application>
@@ -0,0 +1,50 @@
package de.rfc1437.ironstorage.platform
import android.Manifest
import android.content.pm.PackageManager
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class AndroidQrCameraTest {
@Test fun livePreviewDeliversBoundedPlanesAndCanBeClosedAndReopened() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals(
PackageManager.PERMISSION_GRANTED,
context.checkSelfPermission(Manifest.permission.CAMERA)
)
ActivityScenario.launch(KeystoreTestActivity::class.java).use { activity ->
repeat(2) {
val received = CountDownLatch(1)
val failed = CountDownLatch(1)
var camera: AndroidQrCamera? = null
activity.onActivity { host ->
camera = AndroidQrCamera(host, { frame ->
try {
assertTrue(frame.width in 1u..2048u)
assertTrue(frame.height in 1u..2048u)
assertTrue(frame.pixels.size <= 4 * 1024 * 1024)
assertTrue(frame.rotationDegrees in listOf(0u, 90u, 180u, 270u))
received.countDown()
} finally {
frame.pixels.fill(0)
}
}, { failed.countDown() })
host.setContentView(requireNotNull(camera).view)
assertTrue(requireNotNull(camera).view.clipToOutline)
}
assertTrue(
"The emulator camera did not deliver a frame",
received.await(15, TimeUnit.SECONDS)
)
assertFalse("CameraX failed to bind", failed.count == 0L)
activity.onActivity { camera?.close() }
}
}
}
}
@@ -0,0 +1,113 @@
package de.rfc1437.ironstorage.platform
import android.util.Size
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import de.rfc1437.ironstorage.core.CameraFrame
import de.rfc1437.ironstorage.core.CameraPreview
import java.util.concurrent.Executors
internal class AndroidQrCamera(
private val activity: FragmentActivity,
private val frame: (CameraFrame) -> Unit,
private val failure: () -> Unit
) : CameraPreview {
override val view = PreviewView(activity).apply {
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
outlineProvider = android.view.ViewOutlineProvider.BOUNDS
clipToOutline = true
scaleType = PreviewView.ScaleType.FIT_CENTER
importantForAccessibility = android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
private val executor = Executors.newSingleThreadExecutor()
private val preview = Preview.Builder().build()
private val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setResolutionSelector(
ResolutionSelector.Builder().setResolutionStrategy(
ResolutionStrategy(Size(1280, 720), ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER)
).build()
).build()
private var provider: ProcessCameraProvider? = null
@Volatile private var active = true
init {
analysis.setAnalyzer(executor) { image ->
var bytes: ByteArray? = null
try {
if (active && image.width in 1..2048 && image.height in 1..2048) {
val plane = image.planes[0]
val source = plane.buffer.duplicate()
if (source.remaining() <= 4 * 1024 * 1024) {
val pixels = ByteArray(source.remaining())
bytes = pixels
source.get(pixels)
if (active) {
frame(
CameraFrame(
pixels,
image.width.toUInt(),
image.height.toUInt(),
plane.rowStride.toUInt(),
plane.pixelStride.toUInt(),
image.imageInfo.rotationDegrees.toUInt()
)
)
// The receiver clears transferred bytes after the native call.
bytes = null
}
} else {
failure()
}
}
} finally {
bytes?.fill(0)
image.close()
}
}
val pending = ProcessCameraProvider.getInstance(activity)
pending.addListener({
if (active) {
try {
provider = pending.get()
preview.setSurfaceProvider(view.surfaceProvider)
provider!!.bindToLifecycle(
activity,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis
)
} catch (_: java.util.concurrent.ExecutionException) {
close()
failure()
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
close()
failure()
} catch (_: IllegalArgumentException) {
close()
failure()
} catch (_: SecurityException) {
close()
failure()
}
}
}, ContextCompat.getMainExecutor(activity))
}
override fun close() {
active = false
analysis.clearAnalyzer()
provider?.unbind(preview, analysis)
provider = null
executor.shutdown()
}
}
@@ -40,6 +40,13 @@ class AndroidSettingsPlatform(
host.clear()
}
}
override fun cameraPreview(
frame: (de.rfc1437.ironstorage.core.CameraFrame) -> Unit,
failure: () -> Unit
): de.rfc1437.ironstorage.core.CameraPreview? {
val activity = host.get() ?: return null
return AndroidQrCamera(activity, frame, failure)
}
override fun chooseQrImages(): Boolean {
val picker = picker ?: return false
return try {
@@ -101,6 +101,24 @@ impl MobileAndroidOnboarding {
.import(SecretBytes::new(armor.into_bytes()), secret(passphrase))?
.into())
}
pub fn import_scanned(
&self,
transfer: Arc<crate::MobileKeyTransferImport>,
passphrase: Option<String>,
) -> Result<MobileAndroidSetupStatus, crate::MobileKeyTransferFfiError> {
let importer =
transfer
.importer
.lock()
.map_err(|_| crate::MobileKeyTransferFfiError::Failed {
kind: crate::MobileKeyTransferErrorKind::Failed,
message: "the key-transfer session is unavailable".to_owned(),
})?;
Ok(self
.setup
.import_scanned(&importer, secret(passphrase))?
.into())
}
pub fn setup_local(
&self,
fingerprint: String,
+29
View File
@@ -1581,6 +1581,10 @@ pub struct MobileKeyTransfer {
#[uniffi::export]
impl MobileKeyTransfer {
pub fn export_visible(&self) -> bool {
self.service.export_visible()
}
pub fn keys(&self) -> Vec<MobileKeyTransferKey> {
self.service.keys().iter().map(Into::into).collect()
}
@@ -1635,6 +1639,31 @@ pub struct MobileKeyTransferImport {
#[uniffi::export]
impl MobileKeyTransferImport {
pub fn add_camera(
&self,
frame: platform::MobileCameraFrame,
rotation_degrees: u32,
) -> Result<Option<MobileKeyTransferProgress>, MobileKeyTransferFfiError> {
self.importer
.lock()
.map_err(|_| MobileKeyTransferFfiError::Failed {
kind: MobileKeyTransferErrorKind::Failed,
message: "the key-transfer session is unavailable".to_owned(),
})?
.add_camera(
ironstorage::mobile_platform::MobileCameraFrame {
luminance: ironstorage::repository::SecretBytes::new(frame.luminance),
width: frame.width,
height: frame.height,
row_stride: frame.row_stride,
pixel_stride: frame.pixel_stride,
},
rotation_degrees,
)
.map(|progress| progress.map(Into::into))
.map_err(Into::into)
}
pub fn add_image(
&self,
image: Vec<u8>,
+178 -25
View File
@@ -1,6 +1,9 @@
//! Storage-owned OpenPGP key transfer for native mobile QR adapters.
use std::{collections::BTreeMap, error::Error, fmt, fs, io::Write as _, path::Path};
use std::{
collections::BTreeMap, error::Error, fmt, fs, io::Write as _, path::Path, sync::Mutex,
time::Instant,
};
use crate::atomic_file::TempFile;
use cap_std::{ambient_authority, fs::Dir};
@@ -17,6 +20,9 @@ use crate::{
const FRAME_PREFIX: &str = "ISKT1";
const FRAME_CHUNK_BYTES: usize = 1_600;
const MAX_TRANSFER_FRAMES: usize = 11_000;
const MAX_TRANSFER_BYTES: usize = MAX_TRANSFER_FRAMES * FRAME_CHUNK_BYTES;
// Bound eager QR matrices to under 8 MiB on lower-memory mobile clients.
const MAX_EXPORT_FRAMES: usize = 256;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MobileKeyTransferKind {
@@ -140,13 +146,18 @@ impl MobileKeyTransferOutcome {
pub struct MobileKeyTransferService {
config: Config,
keys: KeyStore,
export_deadline: Mutex<Option<Instant>>,
}
impl MobileKeyTransferService {
pub fn load() -> Result<Self, MobileKeyTransferError> {
let config = Config::load(None)?;
let keys = load_keys_or_empty(config.key_material())?;
Ok(Self { config, keys })
Ok(Self {
config,
keys,
export_deadline: Mutex::new(None),
})
}
pub fn keys(&self) -> Vec<MobileKeyTransferKey> {
@@ -169,6 +180,11 @@ impl MobileKeyTransferService {
.find(|key| key.fingerprint().as_str() == fingerprint)
.ok_or(MobileKeyTransferError::KeyUnavailable)?;
let frames = encode_frames(armor)?;
*self
.export_deadline
.lock()
.map_err(|_| MobileKeyTransferError::KeyUnavailable)? =
Some(Instant::now() + self.config.authentication_timeout().duration());
Ok(MobileKeyTransferExport {
key: transfer_key_with_kind(&info, kind),
frames,
@@ -178,13 +194,20 @@ impl MobileKeyTransferService {
pub fn importer(&self) -> MobileKeyTransferImport {
MobileKeyTransferImport::new(self.config.clone())
}
pub fn export_visible(&self) -> bool {
self.export_deadline
.lock()
.ok()
.is_some_and(|deadline| deadline.is_some_and(|deadline| Instant::now() < deadline))
}
}
pub struct MobileKeyTransferImport {
destination: MobileKeyTransferDestination,
digest: Option<String>,
total: Option<usize>,
chunks: BTreeMap<usize, Vec<u8>>,
chunks: BTreeMap<usize, SecretBytes>,
complete: Option<SecretBytes>,
key: Option<MobileKeyTransferKey>,
}
@@ -195,6 +218,31 @@ enum MobileKeyTransferDestination {
}
impl MobileKeyTransferImport {
pub fn add_camera(
&mut self,
frame: crate::mobile_platform::MobileCameraFrame,
rotation_degrees: u32,
) -> Result<Option<MobileKeyTransferProgress>, MobileKeyTransferError> {
if frame.width > 2048
|| frame.height > 2048
|| frame.luminance.expose().len() > 4 * 1024 * 1024
{
return Err(MobileKeyTransferError::InvalidFrame);
}
match frame.decode_rotated(rotation_degrees) {
Ok(payload) => self.add_frame(payload).map(Some),
Err(QrError::NotFound) => Ok(None),
Err(_) => Err(MobileKeyTransferError::InvalidFrame),
}
}
pub(crate) fn completed_armor(&self) -> Result<SecretBytes, MobileKeyTransferError> {
self.complete
.as_ref()
.map(|armor| SecretBytes::new(armor.expose().to_vec()))
.ok_or(MobileKeyTransferError::IncompleteTransfer)
}
fn new(config: Config) -> Self {
Self {
destination: MobileKeyTransferDestination::Configured(Box::new(config)),
@@ -230,6 +278,9 @@ impl MobileKeyTransferImport {
&mut self,
payload: SecretBytes,
) -> Result<MobileKeyTransferProgress, MobileKeyTransferError> {
if payload.expose().len() > MAX_TRANSFER_BYTES {
return Err(MobileKeyTransferError::InvalidFrame);
}
if self.complete.is_some() {
return Err(MobileKeyTransferError::AlreadyComplete);
}
@@ -251,52 +302,58 @@ impl MobileKeyTransferImport {
if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(MobileKeyTransferError::InvalidFrame);
}
let digest = digest.to_ascii_lowercase();
let sequence = parse_positive(fields.next())?;
let total = parse_positive(fields.next())?;
if sequence > total || total > MAX_TRANSFER_FRAMES {
return Err(MobileKeyTransferError::InvalidFrame);
}
let chunk = BASE64URL_NOPAD
.decode(
fields
.next()
.ok_or(MobileKeyTransferError::InvalidFrame)?
.as_bytes(),
)
.map_err(|_| MobileKeyTransferError::InvalidFrame)?;
if chunk.is_empty() || chunk.len() > FRAME_CHUNK_BYTES {
let encoded = fields.next().ok_or(MobileKeyTransferError::InvalidFrame)?;
if encoded.len() > FRAME_CHUNK_BYTES.div_ceil(3) * 4 {
return Err(MobileKeyTransferError::InvalidFrame);
}
let chunk = SecretBytes::new(
BASE64URL_NOPAD
.decode(encoded.as_bytes())
.map_err(|_| MobileKeyTransferError::InvalidFrame)?,
);
if chunk.expose().is_empty() || chunk.expose().len() > FRAME_CHUNK_BYTES {
return Err(MobileKeyTransferError::InvalidFrame);
}
if self.digest.as_deref().is_some_and(|value| value != digest)
if self
.digest
.as_deref()
.is_some_and(|value| value != digest.as_str())
|| self.total.is_some_and(|value| value != total)
{
return Err(MobileKeyTransferError::MixedTransfers);
}
self.digest
.get_or_insert_with(|| digest.to_ascii_lowercase());
self.digest.get_or_insert(digest);
self.total.get_or_insert(total);
if let Some(existing) = self.chunks.get(&sequence) {
if existing == &chunk {
if existing.expose() == chunk.expose() {
return Ok(self.progress(true));
}
return Err(MobileKeyTransferError::ConflictingFrame);
}
self.chunks.insert(sequence, chunk);
if self.chunks.len() == total {
let mut armor = Vec::new();
let mut armor = zeroize::Zeroizing::new(Vec::new());
for sequence in 1..=total {
armor.extend(
armor.extend_from_slice(
self.chunks
.get(&sequence)
.ok_or(MobileKeyTransferError::IncompleteTransfer)?,
.ok_or(MobileKeyTransferError::IncompleteTransfer)?
.expose(),
);
}
let actual = HEXLOWER.encode(&Sha256::digest(&armor));
let actual = HEXLOWER.encode(&Sha256::digest(armor.as_slice()));
if self.digest.as_deref() != Some(actual.as_str()) {
return Err(MobileKeyTransferError::ChecksumMismatch);
}
self.finish(SecretBytes::new(armor))?;
self.finish(SecretBytes::new(std::mem::take(&mut *armor)))?;
self.chunks.clear();
}
Ok(self.progress(false))
}
@@ -435,7 +492,11 @@ impl MobileKeyTransferImport {
fn progress(&self, duplicate: bool) -> MobileKeyTransferProgress {
MobileKeyTransferProgress {
received: self.chunks.len().max(usize::from(self.complete.is_some())),
received: if self.complete.is_some() {
self.total.unwrap_or(1)
} else {
self.chunks.len()
},
total: self.total.unwrap_or(1),
duplicate,
key: self.key.clone(),
@@ -576,7 +637,7 @@ fn encode_multipart(
}
let digest = HEXLOWER.encode(&Sha256::digest(armor.expose()));
let total = armor.expose().len().div_ceil(chunk_bytes);
if total == 0 || total > MAX_TRANSFER_FRAMES {
if total == 0 || total > MAX_EXPORT_FRAMES {
return Err(MobileKeyTransferError::QrPayload);
}
let mut frames = Vec::with_capacity(total);
@@ -706,6 +767,7 @@ mod tests {
MobileKeyTransferService, encode_multipart, load_keys_or_empty,
};
use crate::{config::Config, crypto::KeyStore, repository::SecretBytes};
use std::{sync::Mutex, time::Instant};
use tempfile::tempdir;
const ALICE_PUBLIC: &[u8] =
@@ -714,6 +776,89 @@ mod tests {
include_bytes!("../tests/fixtures/compatibility/keys/alice-secret.asc");
const ALICE_FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
#[test]
fn export_rejects_oversized_matrix_sets_before_rendering() {
assert!(matches!(
encode_multipart(SecretBytes::new(vec![0; 257]), 1),
Err(MobileKeyTransferError::QrPayload)
));
}
#[test]
fn export_visibility_is_bounded_and_does_not_renew_while_reading() {
let fixture = configured_secret_key().unwrap();
let service = MobileKeyTransferService {
keys: load_keys_or_empty(fixture.config.key_material()).unwrap(),
config: fixture.config,
export_deadline: Mutex::new(None),
};
assert!(!service.export_visible());
service
.export(ALICE_FINGERPRINT, MobileKeyTransferKind::Public, None)
.unwrap();
assert!(service.export_visible());
*service.export_deadline.lock().unwrap() = Some(Instant::now());
assert!(!service.export_visible());
assert!(!service.export_visible());
}
#[test]
fn camera_multipart_is_exact_duplicate_safe_and_transactional()
-> Result<(), Box<dyn std::error::Error>> {
let fixture = configured_public_key()?;
let mut importer = MobileKeyTransferImport::new(fixture.config.clone());
let mut frames = encode_multipart(SecretBytes::new(ALICE_SECRET.to_vec()), 128)?;
frames.reverse();
for (index, frame) in frames.iter().enumerate() {
let matrix = frame.matrix();
let width = (matrix.width() + 8) * 4;
let height = width + 8;
let row = width * 2 + 13;
let mut pixels = vec![255; height * row];
for y in 0..height {
for x in 0..width {
let dark = x / 4 >= 4
&& y / 4 >= 4
&& matrix.is_dark(x / 4 - 4, y / 4 - 4).unwrap_or(false);
pixels[y * row + x * 2] = if dark { 0 } else { 255 };
}
}
let camera = || crate::mobile_platform::MobileCameraFrame {
width: width as u32,
height: height as u32,
row_stride: row as u32,
pixel_stride: 2,
luminance: SecretBytes::new(pixels.clone()),
};
let progress = importer
.add_camera(camera(), [0, 90, 180, 270][index % 4])?
.unwrap();
assert_eq!(progress.received(), index + 1);
if index + 1 < frames.len() {
assert!(importer.add_camera(camera(), 0)?.unwrap().duplicate());
assert_eq!(
importer.import(None, true, false),
Err(MobileKeyTransferError::IncompleteTransfer)
);
}
}
assert_eq!(importer.completed_armor()?.expose(), ALICE_SECRET);
assert_eq!(
importer.import(Some(SecretBytes::new(b"wrong".to_vec())), true, false),
Err(MobileKeyTransferError::IncorrectPassphrase)
);
let before = std::fs::read(fixture.key_path.join("alice-public.asc"))?;
importer.import(
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
true,
false,
)?;
assert_eq!(before, ALICE_PUBLIC);
let keys = KeyStore::load(fixture.config.key_material())?;
assert!(keys.infos().any(|key| key.has_secret()));
Ok(())
}
#[test]
fn qr_images_reconstruct_the_same_key_and_reject_invalid_images()
-> Result<(), Box<dyn std::error::Error>> {
@@ -750,8 +895,14 @@ mod tests {
let mut importer = MobileKeyTransferImport::new(fixture.config);
let mut frames = encode_multipart(SecretBytes::new(ALICE_SECRET.to_vec()), 128)?;
frames.reverse();
for frame in frames {
importer.add_frame(SecretBytes::new(frame.payload().to_vec()))?;
for (index, frame) in frames.into_iter().enumerate() {
let mut payload = frame.payload().to_vec();
if index % 2 == 0 {
for byte in &mut payload[6..70] {
byte.make_ascii_uppercase();
}
}
importer.add_frame(SecretBytes::new(payload))?;
}
assert_eq!(
importer.complete.as_ref().map(SecretBytes::expose),
@@ -769,6 +920,7 @@ mod tests {
-> Result<(), Box<dyn std::error::Error>> {
let fixture = configured_secret_key()?;
let service = MobileKeyTransferService {
export_deadline: Mutex::new(None),
config: fixture.config.clone(),
keys: KeyStore::load(&fixture.key_path)?,
};
@@ -953,6 +1105,7 @@ mod tests {
let fixture = configured_key("placeholder", b"")?;
std::fs::remove_file(fixture.key_path.join("placeholder"))?;
let service = MobileKeyTransferService {
export_deadline: Mutex::new(None),
keys: load_keys_or_empty(&fixture.key_path)?,
config: fixture.config.clone(),
};
@@ -163,6 +163,15 @@ impl AndroidOnboarding {
self.status_unlocked()
}
pub fn import_scanned(
&self,
transfer: &crate::mobile_key_transfer::MobileKeyTransferImport,
passphrase: Option<SecretBytes>,
) -> Result<AndroidOnboardingStatus, crate::mobile_key_transfer::MobileKeyTransferError> {
self.import(transfer.completed_armor()?, passphrase)
.map_err(crate::mobile_key_transfer::MobileKeyTransferError::LocalSetup)
}
fn stage_key(
&self,
fingerprint: &str,
@@ -1052,6 +1061,40 @@ pub(crate) mod tests {
Ok(true)
}
}
#[test]
fn scanned_import_stages_exact_key_without_creating_a_store() {
use crate::mobile_key_transfer::{MobileKeyTransferError, MobileKeyTransferImport};
let temp = tempfile::tempdir().unwrap();
let private = storage(temp.path());
let backend = Arc::new(Backend::default());
let setup = AndroidOnboarding::new(&private, backend).unwrap();
let mut transfer = MobileKeyTransferImport::local_setup();
assert!(matches!(
setup.import_scanned(&transfer, password()),
Err(MobileKeyTransferError::IncompleteTransfer)
));
transfer
.add_frame(SecretBytes::new(
include_bytes!("../../tests/fixtures/compatibility/keys/alice-secret.asc").to_vec(),
))
.unwrap();
assert!(
setup
.import_scanned(&transfer, Some(SecretBytes::new(b"wrong".to_vec())))
.is_err()
);
assert!(setup.status().unwrap().keys.is_empty());
let status = setup.import_scanned(&transfer, password()).unwrap();
assert_eq!(status.keys[0].fingerprint().as_str(), ALICE);
assert!(!status.configured);
assert!(setup.configuration().unwrap().is_none());
assert!(matches!(
setup.import_scanned(&transfer, password()),
Err(MobileKeyTransferError::LocalSetup(_))
));
assert_eq!(setup.status().unwrap().keys.len(), 1);
}
#[test]
fn imported_key_restarts_then_creates_a_pass_compatible_local_store() {
let temp = tempfile::tempdir().unwrap();
+74 -1
View File
@@ -90,6 +90,10 @@ pub struct MobileCameraFrame {
impl MobileCameraFrame {
pub fn decode(&self) -> Result<SecretBytes, QrError> {
self.decode_rotated(0)
}
pub fn decode_rotated(&self, rotation_degrees: u32) -> Result<SecretBytes, QrError> {
let width = self.width as usize;
let height = self.height as usize;
let row = self.row_stride as usize;
@@ -99,6 +103,7 @@ impl MobileCameraFrame {
|| !(1..=4096).contains(&height)
|| !(1..=4).contains(&pixel)
|| bytes.len() > 32 * 1024 * 1024
|| !matches!(rotation_degrees, 0 | 90 | 180 | 270)
{
return Err(QrError::InvalidImage);
}
@@ -112,7 +117,20 @@ impl MobileCameraFrame {
if row <= last_column || last >= bytes.len() {
return Err(QrError::InvalidImage);
}
QrMatrix::decode_luminance(width, height, |x, y| bytes[y * row + x * pixel])
let (output_width, output_height) = if matches!(rotation_degrees, 90 | 270) {
(height, width)
} else {
(width, height)
};
QrMatrix::decode_luminance(output_width, output_height, |x, y| {
let (x, y) = match rotation_degrees {
90 => (y, height - 1 - x),
180 => (width - 1 - x, height - 1 - y),
270 => (width - 1 - y, x),
_ => (x, y),
};
bytes[y * row + x * pixel]
})
}
}
@@ -223,6 +241,61 @@ impl fmt::Debug for MobileCredentialRequest {
mod tests {
use super::*;
#[test]
fn camera_planes_decode_padded_pixels_at_every_orientation() {
let payload = SecretBytes::new(b"camera-qr-byte-exact".to_vec());
let matrix = QrMatrix::encode(&payload).unwrap();
let size = (matrix.width() + 8) * 5;
let row = size * 2 + 7;
let mut pixels = vec![255; size * row];
for y in 0..size {
for x in 0..size {
let dark = x / 5 >= 4
&& y / 5 >= 4
&& matrix.is_dark(x / 5 - 4, y / 5 - 4).unwrap_or(false);
pixels[y * row + x * 2] = if dark { 0 } else { 255 };
}
}
for rotation in [0, 90, 180, 270] {
let decoded = MobileCameraFrame {
luminance: SecretBytes::new(pixels.clone()),
width: size as u32,
height: size as u32,
row_stride: row as u32,
pixel_stride: 2,
}
.decode_rotated(rotation)
.unwrap();
assert_eq!(decoded.expose(), payload.expose());
}
}
#[test]
fn camera_rejects_truncated_oversized_and_invalid_planes() {
for (width, height, row, step, rotation, length) in [
(0, 1, 1, 1, 0, 1),
(4097, 1, 4097, 1, 0, 4097),
(2, 2, 2, 1, 0, 3),
(2, 2, 1, 1, 0, 4),
(2, 2, 2, 0, 0, 4),
(2, 2, 2, 1, 45, 4),
(2, 2, u32::MAX, 1, 0, 4),
(1, 1, 1, 1, 0, 32 * 1024 * 1024 + 1),
] {
assert!(matches!(
MobileCameraFrame {
luminance: SecretBytes::new(vec![255; length]),
width,
height,
row_stride: row,
pixel_stride: step,
}
.decode_rotated(rotation),
Err(QrError::InvalidImage)
));
}
}
#[test]
fn roots_and_caller_facts_are_validated_without_creating_data_or_grants() {
let paths =
+96
View File
@@ -0,0 +1,96 @@
# Android GPG QR transfer
Issue [#106](https://git.rfc1437.de/hugo/IronStorage/issues/106) uses the shared
ASCII-armored OpenPGP and `ISKT1` multipart format. `crates/storage` owns frame
bounds/orientation, QR decoding, checksum/reassembly, identity, passphrase
validation, duplicate handling and transactional key installation. Kotlin only
supplies camera facts and renders storage-provided QR matrices.
## Import and export
Open Preferences → Import or export GPG QR codes. Scanning requests camera
permission, binds CameraX Preview and ImageAnalysis to the Activity lifecycle,
and uses only the latest Y plane. Pause closes the camera while retaining the
in-memory multipart session; resume opens a fresh camera. Frames can arrive in
any order. Duplicate frames leave the received count unchanged. The complete
key's Rust-derived identity and fingerprint appear before import. Private import
requires a separate fingerprint/security confirmation. Default-key selection
and replacement are explicit controls; incorrect passphrases, incomplete,
conflicting or mismatched transfers cannot replace valid keys/configuration.
Onboarding → Import existing GPG key → Scan GPG QR codes uses the same scanner
and review, then stages the key through Android's existing onboarding transaction.
It does not create a store or bypass the later local/HTTPS setup choice. Public
keys cannot become onboarding/default decryption keys; Rust rejects them.
Export selects a storage-provided key. Private export requires a GPG passphrase
and a separate fingerprint/security confirmation. Every displayed export remains
labeled with the identity that produced it, even if selection changes. Previous
matrices are erased before a new export. Multipart exports support previous/next
and optional automatic frame advancement. Brightness and keep-screen-on apply
only while QR display is composed and are restored on disposal. Frame ticks do
not renew the storage-owned visibility deadline (the configured authentication
timeout).
## Privacy and interruption
The Activity and transfer dialog use `FLAG_SECURE`. Lock/background/focus loss,
process recreation and dismissal remove transfer state, clear Kotlin matrices
and inputs, and drop the Rust importer/exporter. Rust multipart chunks and
reassembly buffers zeroize on drop. No camera frame, armor or QR payload is
written to Android persistence, photos, clipboard, saved instance state or logs.
Only an explicitly confirmed successful import persists GPG-compatible keys.
Camera permission prompts, an external image picker, device rotation and app
backgrounding follow the existing fail-closed lifecycle policy: an interrupted
transfer is discarded; reopen and rescan rather than retaining private bytes
across an OS interruption. A camera pause within the active dialog retains
progress. Denial (including permanent denial) presents a settings link and image/
manual alternatives. Missing/busy camera reports an actionable recovery message.
Camera decoding accepts at most 2048 × 2048 pixels and a 4 MiB Y plane, validates
row/pixel strides and the four quarter-turn rotations before accessing pixels,
and allows one native decode in flight. Kotlin clears the input plane before
publishing a completed key to state observers, with a final cleanup on every
error/cancellation path. CameraX drops obsolete frames rather
than queueing them. Imported `ISKT1` envelopes retain the existing 11,000-frame/
17.6 MB armor bound and 1,600-byte chunk bound. Export rejects more than 256
frames before allocating matrices, keeping the matrix set below 8 MiB.
## Verification
Rust tests use upstream-compatible Alice public/private armor to prove byte-exact
single/multipart reconstruction, rotated/padded camera planes, out-of-order and
duplicate delivery, incomplete/corrupt/mismatched rejection, wrong-passphrase
retention, import rollback, export size bounds and deadline polling without renewal.
Native Android settings tests also exercise camera planes through generated UniFFI
and verify Kotlin frame bytes and QR matrices are cleared. Compose tests check
private import/export confirmation and hidden-input saved-state exclusions.
`AndroidQrCameraTest` exercises actual emulator CameraX frame delivery and repeated
close/reopen; the preview is clipped in both Android View and Compose layout.
Build gates include both ARM ABIs, debug/release/AAB, JVM tests, lint/ktlint,
architecture/source checks, exact dependency locks/checksums, the license/SBOM
gate, every native ELF/APK 16 KB alignment check, and matching FULL release symbols.
CameraX 1.6.2 and its exact transitives are reviewed against official Maven POM
licenses; camera-core includes Apache-2.0 and BSD-3-Clause portions. The merged
manifest removes Media3's unused ACCESS_NETWORK_STATE permission; only CAMERA
is added for this feature.
Validation is emulator-only under the milestone's ARM support policy. The
Keystore enrollment/invalidation tests require separate interactive device setup;
their setup failures are not camera test results and do not establish biometric
coverage for this change.
Recorded for #106 on 2026-10-12:
| Environment | Evidence |
| --- | --- |
| API 26 ARM, 4 KB | Native settings/viewer fixture including private export and relock erasure; 14 bridge tests; app runner OK (32 reported, including four opt-in/biometric methods not enabled); final onboarding/settings regression group 4 passed; CameraX close/reopen passed |
| API 37 ARM, strict 16 KB | Native fixture including private export and relock erasure; 14 bridge tests; CameraX close/reopen passed; original app run included both passing QR settings tests |
| API 37 ARM, 4 KB, computer use | Update retained existing store/preferences; camera permission interruption discarded transfer; live CameraX preview and clipped scrolling controls inspected; private export stayed disabled until fingerprint confirmation |
| Rust 1.97.1 and Android packages | Required workspace gates, androidVerification and release lint, license/SBOM, debug/release manifests/backup exclusions, ARM native alignment and release FULL-symbol checks passed |
Private export from the retained simulator profile awaits optional user input.
Successful private export/import and erasure evidence above uses isolated,
encrypted compatibility fixtures rather than a user's passphrase.
+3 -3
View File
@@ -46,9 +46,9 @@ shared core's recipient/signing rules, pass/pass-otp format, HOTP counter safety
Git failure semantics and secret-redaction requirements remain binding wherever
an Android workflow invokes them; platform presentation may not replace them.
Current iPhone camera adapters pass OS-decoded QR bytes. Android #106/#91 must
also supply bounded image/frame facts for Rust decoding as required by the
Android contract; that additional adapter is pending, not an existing API.
Current iPhone camera adapters pass OS-decoded QR bytes. Android #106 supplies bounded Y-plane/stride/orientation facts through the
shared native contract; storage decodes and reassembles the transfer. See
[key-transfer.md](key-transfer.md) for interruption/security behavior and evidence.
The external-credential matching/grant API required by #109 must be implemented
in storage, rather than copying frontend selection assumptions from AutoFill.
+9 -1
View File
@@ -60,6 +60,7 @@ fn manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
"android.permission.USE_FINGERPRINT",
"de.rfc1437.ironstorage.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION",
"android.permission.INTERNET",
"android.permission.CAMERA",
];
for node in root.children().filter(|node| node.is_element()) {
if node.tag_name().name().starts_with("uses-permission")
@@ -448,6 +449,13 @@ mod tests {
))
.is_ok()
);
assert!(
manifest_text(&source.replace(
"<application",
"<uses-permission android:name=\"android.permission.CAMERA\"/><application"
))
.is_ok()
);
for bad in [
source.replace("allowBackup=\"false\"", "allowBackup=\"true\""),
source.replace(
@@ -456,7 +464,7 @@ mod tests {
),
source.replace(
"<application",
"<uses-permission android:name=\"android.permission.CAMERA\"/><application",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/><application",
),
] {
assert!(manifest_text(&bad).is_err());
+2
View File
@@ -191,6 +191,8 @@ fn audit_package_abis(path: &Path, abis: &[&str]) -> Result<(), Box<dyn Error>>
if ![
"libironstorage_android.so",
"libjnidispatch.so",
"libimage_processing_util_jni.so",
"libsurface_util_jni.so",
"libandroidx.graphics.path.so",
]
.contains(&library)