fix(apk): preserve local sharing during update checks

This commit is contained in:
Moe Hamade 2026-08-02 17:44:20 +03:00
parent b60b5121aa
commit 2c19d00d17
19 changed files with 1181 additions and 160 deletions

View File

@ -24,57 +24,55 @@ androidx.camera:camera-lifecycle:1.6.1=debugAndroidTestCompileClasspath,debugAnd
androidx.collection:collection-jvm:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.collection:collection-ktx:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.collection:collection:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation-core-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation-core:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation-layout-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation-layout:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material3:material3-android:1.5.0-alpha25=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material3:material3-ripple-android:1.5.0-alpha25=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material3:material3-ripple:1.5.0-alpha25=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material3:material3:1.5.0-alpha25=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation-core-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation-core:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.animation:animation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation-layout-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation-layout:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.foundation:foundation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material3:material3-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material3:material3:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material:material-icons-core-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
androidx.compose.material:material-icons-core-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath
androidx.compose.material:material-icons-core:1.7.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material:material-icons-extended-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
androidx.compose.material:material-icons-extended-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath
androidx.compose.material:material-icons-extended:1.7.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material:material-ripple-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material:material-ripple:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-annotation-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-annotation:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-retain-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-retain:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-saveable-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-saveable:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-geometry-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-geometry:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-graphics-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-graphics:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-test-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test-junit4-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test-junit4:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test-manifest:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-text-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-text:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-tooling-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-tooling-data-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-tooling-data:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-tooling-preview-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-tooling-preview:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-tooling:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-unit-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-unit:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-util-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-util:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material:material-ripple-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.material:material-ripple:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-annotation-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-annotation:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-retain-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-retain:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-saveable-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime-saveable:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.runtime:runtime:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-geometry-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-geometry:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-graphics-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-graphics:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-test-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test-junit4-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test-junit4:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test-manifest:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-test:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.compose.ui:ui-text-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-text:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-tooling-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-tooling-data-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-tooling-data:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-tooling-preview-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-tooling-preview:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-tooling:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui-unit-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-unit:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-util-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui-util:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose.ui:ui:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.compose:compose-bom:2026.06.01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.2.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
@ -87,22 +85,22 @@ androidx.core:core:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintC
androidx.cursoradapter:cursoradapter:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.customview:customview-poolingcontainer:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.documentfile:documentfile:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.drawerlayout:drawerlayout:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.dynamicanimation:dynamicanimation:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.emoji2:emoji2-views-helper:1.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.exifinterface:exifinterface:1.4.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.fragment:fragment:1.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.graphics:graphics-path:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.graphics:graphics-shapes-android:1.0.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
androidx.graphics:graphics-shapes-desktop:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath
androidx.graphics:graphics-shapes:1.0.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.interpolator:interpolator:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.legacy:legacy-support-core-utils:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-common-java8:2.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-common-jvm:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-common:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata-core:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-process:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-runtime-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@ -118,7 +116,8 @@ androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestCompileClasspa
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigation:navigation-common-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigation:navigation-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@ -129,6 +128,7 @@ androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileCl
androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.room:room-common:2.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@ -164,6 +164,7 @@ androidx.tracing:tracing-ktx:1.3.0=debugAndroidTestLintChecksClasspath,debugLint
androidx.tracing:tracing:1.0.0=debugAndroidTestCompileClasspath
androidx.tracing:tracing:1.1.0=debugUnitTestCompileClasspath
androidx.tracing:tracing:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.transition:transition:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.vectordrawable:vectordrawable-animated:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.vectordrawable:vectordrawable:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@ -282,6 +283,7 @@ com.google.testing.platform:launcher:0.0.9-alpha04=unified-test-platform-gradle-
com.google.testparameterinjector:test-parameter-injector:1.18=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
com.google.zxing:core:3.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.ibm.icu:icu4j:77.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
com.squareup.okhttp3:mockwebserver3:5.4.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
com.squareup.okhttp3:okhttp-android:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.squareup.okhttp3:okhttp:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.squareup.okio:okio-jvm:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath

View File

@ -10,7 +10,17 @@ import java.util.concurrent.atomic.AtomicReference
* Centralized OkHttp provider to ensure all network traffic honors Tor settings.
*/
object OkHttpProvider {
private val httpClientRef = AtomicReference<OkHttpClient?>(null)
enum class Route {
DIRECT,
TOR
}
data class RoutedClient(
val client: OkHttpClient,
val route: Route
)
private val httpClientRef = AtomicReference<RoutedClient?>(null)
private val wsClientRef = AtomicReference<OkHttpClient?>(null)
fun reset() {
@ -18,20 +28,30 @@ object OkHttpProvider {
wsClientRef.set(null)
}
fun httpClient(): OkHttpClient {
fun httpClient(): OkHttpClient = routedHttpClient().client
/**
* Returns the client and the route it was actually built with as one snapshot.
*
* The selected Tor mode can change while an existing client is still cached. Consumers that
* key cooldowns by network identity must use this value rather than re-reading the preference.
*/
fun routedHttpClient(): RoutedClient {
httpClientRef.get()?.let { return it }
val client = baseBuilderForCurrentProxy()
val (builder, route) = baseBuilderForCurrentProxy()
val client = builder
.callTimeout(15, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()
httpClientRef.set(client)
return client
val routedClient = RoutedClient(client, route)
httpClientRef.set(routedClient)
return routedClient
}
fun webSocketClient(): OkHttpClient {
wsClientRef.get()?.let { return it }
val client = baseBuilderForCurrentProxy()
val client = baseBuilderForCurrentProxy().first
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
@ -40,7 +60,7 @@ object OkHttpProvider {
return client
}
private fun baseBuilderForCurrentProxy(): OkHttpClient.Builder {
private fun baseBuilderForCurrentProxy(): Pair<OkHttpClient.Builder, Route> {
val builder = OkHttpClient.Builder()
val torProvider = ArtiTorManager.getInstance()
val socks: InetSocketAddress? = torProvider.currentSocksAddress()
@ -50,6 +70,6 @@ object OkHttpProvider {
val proxy = Proxy(Proxy.Type.SOCKS, socks)
builder.proxy(proxy)
}
return builder
return builder to if (socks == null) Route.DIRECT else Route.TOR
}
}

View File

@ -620,7 +620,18 @@ fun AboutSheet(
val apkViewModel: ApkDownloadViewModel = viewModel()
val apkUiState by apkViewModel.state.collectAsStateWithLifecycle()
val apkStatus = apkUiState.apkStatus
val releaseStatus = apkUiState.releaseStatus
val downloadProgress = apkUiState.downloadProgress
val shareableApk = when (apkStatus) {
is ApkPreparationStatus.Ready -> apkStatus
is ApkPreparationStatus.Downloading ->
apkStatus.shareableFallback
else -> null
}
val availableUpdate = (releaseStatus as? ApkReleaseStatus.Known)
?.takeIf { it.isNewerThanSharedApk }
val downloadRetryBlocked = apkUiState.downloadRetryAtMillis
?.let { it > System.currentTimeMillis() } == true
// Handle one-shot effects (navigation, toasts, share intents)
LaunchedEffect(Unit) {
@ -659,7 +670,11 @@ fun AboutSheet(
// does, so the row can never look tappable and do
// nothing.
.clickable(
enabled = prepareRowTapAction(apkStatus) != null
enabled = prepareRowTapAction(
apkStatus,
releaseStatus,
apkUiState.downloadRetryAtMillis
) != null
) {
apkViewModel.onEvent(ApkUiEvent.PrepareRowClicked)
}
@ -667,7 +682,7 @@ fun AboutSheet(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (apkStatus is ApkPreparationStatus.Ready) {
imageVector = if (shareableApk != null) {
Icons.Default.Share
} else {
Icons.Default.CloudDownload
@ -683,16 +698,53 @@ fun AboutSheet(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = if (apkStatus is ApkPreparationStatus.Ready) {
stringResource(R.string.prepare_apk_ready_title)
} else {
stringResource(R.string.prepare_apk_title)
},
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = if (shareableApk != null) {
stringResource(R.string.prepare_apk_ready_title)
} else {
stringResource(R.string.prepare_apk_title)
},
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface
)
if (availableUpdate != null) {
TooltipBox(
positionProvider = TooltipDefaults
.rememberTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(
stringResource(
R.string.prepare_apk_update_available,
availableUpdate.version
)
)
}
},
state = rememberTooltipState()
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = stringResource(
R.string.prepare_apk_update_warning
),
tint = colorScheme.tertiary,
modifier = Modifier
.padding(start = 6.dp)
.size(18.dp)
.clickable(
enabled = !downloadRetryBlocked
) {
apkViewModel.onEvent(
ApkUiEvent.DownloadUniversalClicked
)
}
)
}
}
}
Text(
text = when (val status = apkStatus) {
is ApkPreparationStatus.Loading -> stringResource(R.string.checking)
@ -764,7 +816,10 @@ fun AboutSheet(
}
)
is ApkPreparationStatus.Ready -> {
if (apkStatus.variant == ShareableApkVariant.ARM64) {
if (
apkStatus.source ==
UniversalApkManager.ApkSource.INSTALLED
) {
ApkPrepareRowIconButton(
icon = Icons.Default.CloudDownload,
description = stringResource(
@ -775,6 +830,7 @@ fun AboutSheet(
ApkUiEvent.DownloadUniversalClicked
)
},
enabled = !downloadRetryBlocked,
tint = colorScheme.primary
)
} else if (
@ -807,6 +863,11 @@ fun AboutSheet(
ApkUiEvent.PrepareRowClicked
)
},
enabled = prepareRowTapAction(
apkStatus,
releaseStatus,
apkUiState.downloadRetryAtMillis
) != null,
tint = colorScheme.primary
)
else -> {}
@ -820,16 +881,28 @@ fun AboutSheet(
title = {
Text(
text = stringResource(
R.string.prepare_apk_dialog_title
if (availableUpdate != null) {
R.string.prepare_apk_update_dialog_title
} else {
R.string.prepare_apk_dialog_title
}
),
style = MaterialTheme.typography.titleLarge
)
},
text = {
Text(
text = stringResource(
R.string.prepare_apk_dialog_message_unknown_size
),
text = if (availableUpdate != null) {
stringResource(
R.string.prepare_apk_update_dialog_message,
availableUpdate.version,
availableUpdate.sizeMB
)
} else {
stringResource(
R.string.prepare_apk_dialog_message_unknown_size
)
},
style = MaterialTheme.typography.bodyMedium
)
},
@ -875,7 +948,11 @@ fun AboutSheet(
containerColor = colorScheme.error
)
) {
Text("Delete")
Text(
stringResource(
R.string.prepare_apk_button_delete
)
)
}
},
dismissButton = {
@ -887,8 +964,9 @@ fun AboutSheet(
)
}
// Show sharing rows only when APK is ready
val canShareAPK = apkStatus is ApkPreparationStatus.Ready
// A GitHub update is optional. Keep sharing visible while the
// replacement downloads or while metadata refreshes.
val canShareAPK = shareableApk != null
AnimatedVisibility(
visible = canShareAPK,

View File

@ -7,11 +7,16 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.bitchat.android.R
import com.bitchat.android.util.ApkDownloader
import com.bitchat.android.util.AppVersion
import com.bitchat.android.util.GitHubReleaseClient
import com.bitchat.android.util.LatestReleaseProvider
import com.bitchat.android.util.ShareableApkVariant
import com.bitchat.android.util.UniversalApkManager
import com.bitchat.android.util.WorkManagerApkDownloader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -33,14 +38,32 @@ sealed class ApkPreparationStatus {
) : ApkPreparationStatus()
/** [phase] is what the operation is actually doing; only a transfer has a real percentage. */
data class Downloading(
val phase: ApkDownloader.DownloadPhase = ApkDownloader.DownloadPhase.SelectingSource
val phase: ApkDownloader.DownloadPhase = ApkDownloader.DownloadPhase.SelectingSource,
val shareableFallback: Ready? = null
) : ApkPreparationStatus()
data class Resumable(val progressPercent: Int, val message: String) : ApkPreparationStatus()
data class Error(val message: String) : ApkPreparationStatus()
data class Resumable(
val progressPercent: Int,
val message: String,
val retryAtMillis: Long? = null
) : ApkPreparationStatus()
data class Error(val message: String, val retryAtMillis: Long? = null) : ApkPreparationStatus()
}
sealed class ApkReleaseStatus {
object Unknown : ApkReleaseStatus()
object Checking : ApkReleaseStatus()
data class Known(
val version: String,
val sizeMB: Int,
val isNewerThanSharedApk: Boolean,
val fromStaleCache: Boolean
) : ApkReleaseStatus()
}
data class ApkUiState(
val apkStatus: ApkPreparationStatus = ApkPreparationStatus.Loading,
val releaseStatus: ApkReleaseStatus = ApkReleaseStatus.Unknown,
val downloadRetryAtMillis: Long? = null,
val downloadProgress: Int = 0,
val showPrepareDialog: Boolean = false,
val showDeleteDialog: Boolean = false,
@ -80,12 +103,24 @@ internal enum class PrepareRowTapAction {
* and has to stay in step with them. Deriving both the tap handler and the row's `enabled` flag
* from this one function keeps the row from looking clickable while doing nothing.
*/
internal fun prepareRowTapAction(status: ApkPreparationStatus): PrepareRowTapAction? = when {
internal fun prepareRowTapAction(
status: ApkPreparationStatus,
releaseStatus: ApkReleaseStatus = ApkReleaseStatus.Unknown,
downloadRetryAtMillis: Long? = null,
nowMillis: Long = System.currentTimeMillis()
): PrepareRowTapAction? = when {
downloadRetryAtMillis != null && downloadRetryAtMillis > nowMillis -> null
status is ApkPreparationStatus.NotDownloaded -> PrepareRowTapAction.OpenPrepareDialog
// Consent was already given for these; resuming straight away avoids a redundant prompt.
status is ApkPreparationStatus.Resumable -> PrepareRowTapAction.StartDownload
status is ApkPreparationStatus.Error -> PrepareRowTapAction.StartDownload
status is ApkPreparationStatus.Ready && status.variant == ShareableApkVariant.ARM64 ->
status is ApkPreparationStatus.Resumable &&
(status.retryAtMillis == null || status.retryAtMillis <= nowMillis) ->
PrepareRowTapAction.StartDownload
status is ApkPreparationStatus.Error &&
(status.retryAtMillis == null || status.retryAtMillis <= nowMillis) ->
PrepareRowTapAction.StartDownload
status is ApkPreparationStatus.Ready &&
(status.source == UniversalApkManager.ApkSource.INSTALLED ||
(releaseStatus as? ApkReleaseStatus.Known)?.isNewerThanSharedApk == true) ->
PrepareRowTapAction.OpenPrepareDialog
else -> null
}
@ -102,21 +137,33 @@ sealed class ApkUiEffect {
* ViewModel for APK download/status/share logic following MVI pattern.
* UI sends [ApkUiEvent], observes [ApkUiState], and collects [ApkUiEffect].
*/
class ApkDownloadViewModel(application: Application) : AndroidViewModel(application) {
class ApkDownloadViewModel internal constructor(
application: Application,
private val apkManager: UniversalApkManager,
private val downloader: ApkDownloader,
private val latestReleaseProvider: LatestReleaseProvider
) : AndroidViewModel(application) {
constructor(application: Application) : this(
application = application,
apkManager = UniversalApkManager(application),
downloader = WorkManagerApkDownloader(application),
latestReleaseProvider = GitHubReleaseClient(application)
)
companion object {
private const val TAG = "ApkDownloadVM"
}
private val apkManager = UniversalApkManager(application)
private val downloader: ApkDownloader = WorkManagerApkDownloader(application)
private val _state = MutableStateFlow(ApkUiState())
val state: StateFlow<ApkUiState> = _state.asStateFlow()
private val _effect = Channel<ApkUiEffect>(Channel.BUFFERED)
val effect = _effect.receiveAsFlow()
private var metadataRefreshJob: Job? = null
private var retryUnlockJob: Job? = null
init {
observeDownloader()
}
@ -140,7 +187,13 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
}
private fun onPrepareRowClicked() {
when (prepareRowTapAction(_state.value.apkStatus)) {
when (
prepareRowTapAction(
_state.value.apkStatus,
_state.value.releaseStatus,
_state.value.downloadRetryAtMillis
)
) {
PrepareRowTapAction.OpenPrepareDialog ->
_state.update { it.copy(showPrepareDialog = true) }
PrepareRowTapAction.StartDownload -> startDownload()
@ -154,9 +207,14 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
}
private fun onDownloadUniversalClicked() {
if (_state.value.downloadRetryAtMillis?.let { it > System.currentTimeMillis() } == true) {
return
}
val status = _state.value.apkStatus
val hasUpdate = (_state.value.releaseStatus as? ApkReleaseStatus.Known)
?.isNewerThanSharedApk == true
if (status is ApkPreparationStatus.Ready &&
status.variant == ShareableApkVariant.ARM64
(status.source == UniversalApkManager.ApkSource.INSTALLED || hasUpdate)
) {
_state.update { it.copy(showPrepareDialog = true) }
}
@ -212,20 +270,39 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
private fun onCancelDownload() {
downloader.cancelDownload()
// Leave Downloading immediately. The cancelled job maps to Idle, which
// the observer intentionally ignores because local status is resolved below.
val fallback = (_state.value.apkStatus as? ApkPreparationStatus.Downloading)
?.shareableFallback
_state.update {
it.copy(apkStatus = ApkPreparationStatus.Loading, downloadProgress = 0)
it.copy(
apkStatus = fallback ?: ApkPreparationStatus.Loading,
downloadProgress = 0
)
}
checkStatus()
if (fallback == null) checkStatus()
}
private fun startDownload() {
val current = _state.value.apkStatus
val stateRetryAt = _state.value.downloadRetryAtMillis
val retryAt = when (current) {
is ApkPreparationStatus.Resumable -> current.retryAtMillis
is ApkPreparationStatus.Error -> current.retryAtMillis
else -> null
}
if ((stateRetryAt ?: retryAt)?.let { it > System.currentTimeMillis() } == true) return
val fallback = when (current) {
is ApkPreparationStatus.Ready -> current
is ApkPreparationStatus.Downloading -> current.shareableFallback
else -> null
}
val partial = apkManager.getPartialDownloadProgress()
_state.update {
it.copy(
apkStatus = ApkPreparationStatus.Downloading(),
apkStatus = ApkPreparationStatus.Downloading(
shareableFallback = fallback
),
downloadRetryAtMillis = null,
downloadProgress = partial ?: 0
)
}
@ -251,6 +328,36 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
current.copy(apkStatus = resolvedStatus, downloadProgress = 0)
}
}
// Local availability is resolved and published before this independent network task
// starts. Metadata can add a freshness warning, but can never hide sharing.
refreshReleaseMetadata()
}
}
private fun refreshReleaseMetadata() {
if (metadataRefreshJob?.isActive == true) return
metadataRefreshJob = viewModelScope.launch {
_state.update { it.copy(releaseStatus = ApkReleaseStatus.Checking) }
latestReleaseProvider.latestRelease()
.onSuccess { snapshot ->
val shared = shareableReady(_state.value.apkStatus)
_state.update {
it.copy(
releaseStatus = ApkReleaseStatus.Known(
version = snapshot.release.versionName,
sizeMB = (snapshot.release.universalApkSize / 1024 / 1024).toInt(),
isNewerThanSharedApk = shared?.let { ready ->
AppVersion.isNewer(ready.version, snapshot.release.versionName)
} ?: false,
fromStaleCache = snapshot.isStale
)
)
}
}
.onFailure {
// Metadata is an optional enhancement. Keep the locally resolved APK state.
_state.update { it.copy(releaseStatus = ApkReleaseStatus.Unknown) }
}
}
}
@ -259,12 +366,28 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
downloader.downloadState.collect { downloadState ->
when (downloadState) {
is ApkDownloader.DownloadState.Idle -> {
// Don't overwrite — status set by checkStatus()
val downloading = _state.value.apkStatus as? ApkPreparationStatus.Downloading
if (downloading != null) {
val fallback = downloading.shareableFallback
_state.update {
it.copy(
apkStatus = fallback ?: ApkPreparationStatus.Loading,
downloadProgress = 0
)
}
if (fallback == null) checkStatus()
}
}
is ApkDownloader.DownloadState.Downloading -> {
_state.update {
val fallback = (it.apkStatus as? ApkPreparationStatus.Downloading)
?.shareableFallback
?: (it.apkStatus as? ApkPreparationStatus.Ready)
it.copy(
apkStatus = ApkPreparationStatus.Downloading(downloadState.phase),
apkStatus = ApkPreparationStatus.Downloading(
phase = downloadState.phase,
shareableFallback = fallback
),
downloadProgress = downloadState.progressPercent
)
}
@ -272,31 +395,33 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
is ApkDownloader.DownloadState.Success -> {
val info = apkManager.getCachedApkInfo()
_state.update {
val ready = ApkPreparationStatus.Ready(
version = info?.version ?: downloadState.version,
sizeMB = info?.let { cached ->
(cached.size / 1024 / 1024).toInt()
} ?: downloadState.sizeMB,
source = info?.source ?: UniversalApkManager.ApkSource.DOWNLOADED,
variant = info?.variant ?: ShareableApkVariant.UNIVERSAL
)
it.copy(
apkStatus = ApkPreparationStatus.Ready(
version = info?.version ?: downloadState.version,
sizeMB = info?.let { cached ->
(cached.size / 1024 / 1024).toInt()
} ?: downloadState.sizeMB,
source = info?.source ?: UniversalApkManager.ApkSource.DOWNLOADED,
variant = info?.variant ?: ShareableApkVariant.UNIVERSAL
),
apkStatus = ready,
releaseStatus = releaseStatusFor(ready, it.releaseStatus),
downloadRetryAtMillis = null,
downloadProgress = 100
)
}
}
is ApkDownloader.DownloadState.Failed -> {
val localArm64 = apkManager.getCachedApkInfo()
?.takeIf { it.variant == ShareableApkVariant.ARM64 }
if (localArm64 != null) {
val fallback = (_state.value.apkStatus as? ApkPreparationStatus.Downloading)
?.shareableFallback
?: apkManager.getCachedApkInfo()?.toReady()
if (fallback != null) {
_state.update {
it.copy(
apkStatus = ApkPreparationStatus.Ready(
version = localArm64.version,
sizeMB = (localArm64.size / 1024 / 1024).toInt(),
source = localArm64.source,
variant = localArm64.variant
)
apkStatus = fallback,
releaseStatus = releaseStatusFor(fallback, it.releaseStatus),
downloadRetryAtMillis = downloadState.retryAtMillis,
downloadProgress = 0
)
}
_effect.send(ApkUiEffect.ShowToast(failureMessage(downloadState)))
@ -307,17 +432,24 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
it.copy(
apkStatus = ApkPreparationStatus.Resumable(
progressPercent = downloadState.resumablePercent,
message = message
message = message,
retryAtMillis = downloadState.retryAtMillis
),
downloadRetryAtMillis = downloadState.retryAtMillis,
downloadProgress = downloadState.resumablePercent
)
} else {
it.copy(
apkStatus = ApkPreparationStatus.Error(message)
apkStatus = ApkPreparationStatus.Error(
message,
downloadState.retryAtMillis
),
downloadRetryAtMillis = downloadState.retryAtMillis
)
}
}
}
scheduleRetryUnlock(downloadState.retryAtMillis)
}
}
}
@ -339,21 +471,57 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
* this resolves it, so the message follows the device locale rather than the worker's.
*/
private fun failureMessage(state: ApkDownloader.DownloadState.Failed): String =
getApplication<Application>().getString(
state.reason.messageRes,
*state.messageArgs.toTypedArray()
)
runCatching {
getApplication<Application>().getString(
state.reason.messageRes,
*state.messageArgs.toTypedArray()
)
}.getOrElse {
getString(R.string.prepare_apk_error_generic)
}
private fun scheduleRetryUnlock(retryAtMillis: Long?) {
if (retryAtMillis == null) return
retryUnlockJob?.cancel()
retryUnlockJob = viewModelScope.launch {
delay((retryAtMillis - System.currentTimeMillis()).coerceAtLeast(0L))
_state.update {
val unlocked = when (val status = it.apkStatus) {
is ApkPreparationStatus.Resumable -> status.copy(retryAtMillis = null)
is ApkPreparationStatus.Error -> status.copy(retryAtMillis = null)
else -> status
}
it.copy(apkStatus = unlocked, downloadRetryAtMillis = null)
}
}
}
private fun shareableReady(status: ApkPreparationStatus): ApkPreparationStatus.Ready? =
when (status) {
is ApkPreparationStatus.Ready -> status
is ApkPreparationStatus.Downloading -> status.shareableFallback
else -> null
}
private fun releaseStatusFor(
ready: ApkPreparationStatus.Ready,
releaseStatus: ApkReleaseStatus
): ApkReleaseStatus = (releaseStatus as? ApkReleaseStatus.Known)?.let {
it.copy(isNewerThanSharedApk = AppVersion.isNewer(ready.version, it.version))
} ?: releaseStatus
private fun UniversalApkManager.ApkInfo.toReady() = ApkPreparationStatus.Ready(
version = version,
sizeMB = (size / 1024 / 1024).toInt(),
source = source,
variant = variant
)
private suspend fun resolveApkStatus(): ApkPreparationStatus = withContext(Dispatchers.IO) {
try {
val info = apkManager.prepareLocalApkInfo()
if (info != null) {
ApkPreparationStatus.Ready(
version = info.version,
sizeMB = (info.size / 1024 / 1024).toInt(),
source = info.source,
variant = info.variant
)
info.toReady()
} else {
val partial = apkManager.getPartialDownloadProgress()
if (partial != null) {

View File

@ -4,10 +4,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
@ -26,10 +25,9 @@ import androidx.compose.ui.unit.dp
* This sits under the row's subtitle so the trailing slot is free to hold a single control. Which
* of the three renderings applies is decided entirely by [status]; the caller does not choose.
*
* The wave is not decoration. A moving wave means bytes are moving, so a stalled download draws a
* flat line at the fraction it reached rather than a bar indistinguishable from a live one.
* Determinate transfer/resume progress and indeterminate non-transfer phases use the stable
* Material 3 progress API. The expressive wavy variant can be introduced independently later.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal fun ApkDownloadProgressBar(
status: ApkPreparationStatus,
@ -46,19 +44,17 @@ internal fun ApkDownloadProgressBar(
status is ApkPreparationStatus.Downloading &&
status.phase.hasMeasurableProgress &&
progressPercent > 0 ->
LinearWavyProgressIndicator(
LinearProgressIndicator(
progress = { progressPercent.asProgressFraction() },
modifier = barModifier
)
status is ApkPreparationStatus.Downloading ->
LinearWavyProgressIndicator(modifier = barModifier)
LinearProgressIndicator(modifier = barModifier)
// Flat: how far it got, and that it is not getting further on its own.
status is ApkPreparationStatus.Resumable ->
LinearWavyProgressIndicator(
LinearProgressIndicator(
progress = { status.progressPercent.asProgressFraction() },
amplitude = { 0f },
modifier = barModifier
)
}
@ -84,22 +80,24 @@ internal fun ApkPrepareRowIconButton(
description: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
tint: Color = MaterialTheme.colorScheme.onSurfaceVariant
) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = { PlainTooltip { Text(description) } },
state = rememberTooltipState(),
modifier = modifier
) {
IconButton(
onClick = onClick,
enabled = enabled,
modifier = Modifier.size(48.dp)
) {
Icon(
imageVector = icon,
contentDescription = description,
tint = tint,
tint = if (enabled) tint else tint.copy(alpha = 0.38f),
modifier = Modifier.size(20.dp)
)
}

View File

@ -2,6 +2,8 @@ package com.bitchat.android.util
import androidx.annotation.StringRes
import com.bitchat.android.R
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.time.Instant
import java.time.ZonedDateTime
@ -244,6 +246,14 @@ internal data class ContentRange(
val total: Long?
)
/**
* Makes a response body safe to append after resume metadata is updated.
* A full 200 replacement must discard bytes from the release that supplied the Range request.
*/
internal fun prepareApkTempFileForResponse(tempFile: File, appendResponse: Boolean) {
if (!appendResponse) FileOutputStream(tempFile, false).use { }
}
internal fun parseContentRange(value: String?): ContentRange? {
if (value == null) return null
val match = Regex("""bytes\s+(\d+)-(\d+)/(\d+|\*)""", RegexOption.IGNORE_CASE)

View File

@ -39,6 +39,7 @@ class ApkDownloadWorker(
const val KEY_ERROR_REASON = "error_reason"
const val KEY_ERROR_ARGS = "error_args"
const val KEY_RESUMABLE_PERCENT = "resumable_percent"
const val KEY_RETRY_AT = "retry_at"
private const val CHANNEL_ID = "apk_download"
private const val NOTIFICATION_ID = 4201
@ -120,6 +121,7 @@ class ApkDownloadWorker(
failure?.messageArgs.orEmpty().toTypedArray()
)
.putInt(KEY_RESUMABLE_PERCENT, partial ?: -1)
.apply { failure?.retryAtMillis?.let { putLong(KEY_RETRY_AT, it) } }
.build()
Result.failure(outputData)
}

View File

@ -42,7 +42,8 @@ interface ApkDownloader {
data class Failed(
val reason: ApkDownloadFailureReason,
val messageArgs: List<String>,
val resumablePercent: Int?
val resumablePercent: Int?,
val retryAtMillis: Long? = null
) : DownloadState()
}

View File

@ -0,0 +1,73 @@
package com.bitchat.android.util
import android.content.Context
import androidx.core.content.edit
import com.bitchat.android.net.OkHttpProvider
import kotlin.math.ceil
/** Persistent, route-specific cooldowns for APK-related network requests. */
internal class ApkRateLimitStore(context: Context) {
companion object {
private const val PREFS_NAME = "apk_network_cooldowns"
private const val FALLBACK_COOLDOWN_MILLIS = 60_000L
private const val MAX_COOLDOWN_MILLIS = 60 * 60_000L
}
private val preferences = context.applicationContext.getSharedPreferences(
PREFS_NAME,
Context.MODE_PRIVATE
)
fun retryAtMillis(
scope: String,
route: OkHttpProvider.Route,
nowMillis: Long = System.currentTimeMillis()
): Long? {
val key = key(scope, route)
val deadline = preferences.getLong(key, 0L)
if (deadline <= nowMillis) {
if (deadline != 0L) preferences.edit { remove(key) }
return null
}
return deadline
}
fun recordRateLimit(
scope: String,
route: OkHttpProvider.Route,
serverRetryAtMillis: Long?,
nowMillis: Long = System.currentTimeMillis()
): Long {
val fallback = nowMillis + FALLBACK_COOLDOWN_MILLIS
val maximum = nowMillis + MAX_COOLDOWN_MILLIS
val deadline = (serverRetryAtMillis ?: fallback).coerceIn(nowMillis + 1_000L, maximum)
// Persist before reporting the failure so a process restart cannot bypass the cooldown.
preferences.edit(commit = true) { putLong(key(scope, route), deadline) }
return deadline
}
fun clear(scope: String, route: OkHttpProvider.Route) {
preferences.edit { remove(key(scope, route)) }
}
fun blockedException(
source: ApkDownloadSource,
retryAtMillis: Long,
nowMillis: Long = System.currentTimeMillis()
): ApkDownloadException {
val minutes = ceil(
(retryAtMillis - nowMillis).coerceAtLeast(1L) / 60_000.0
).toLong()
return ApkDownloadException(
message = "${source.id} is in a persisted rate-limit cooldown until $retryAtMillis",
reason = ApkDownloadFailureReason.RateLimitedWithWait,
messageArgs = listOf(source.displayName, minutes.toString()),
retryable = false,
sourceId = source.id,
retryAtMillis = retryAtMillis
)
}
private fun key(scope: String, route: OkHttpProvider.Route): String =
"${scope}_${route.name.lowercase()}"
}

View File

@ -0,0 +1,256 @@
package com.bitchat.android.util
import android.content.Context
import android.util.Log
import androidx.core.content.edit
import com.bitchat.android.net.ArtiTorManager
import com.bitchat.android.net.OkHttpProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
internal interface LatestReleaseProvider {
suspend fun latestRelease(): Result<GitHubReleaseClient.ReleaseSnapshot>
}
/** Fetches GitHub release metadata without participating in APK availability. */
internal class GitHubReleaseClient(
context: Context,
private val apiUrl: String = GITHUB_API_URL,
private val nowMillis: () -> Long = System::currentTimeMillis,
private val routedClient: () -> OkHttpProvider.RoutedClient = OkHttpProvider::routedHttpClient,
private val awaitRoute: suspend () -> Boolean = {
ArtiTorManager.getInstance().awaitSelectedRoute(ROUTE_READY_TIMEOUT_MILLIS)
},
private val rateLimits: ApkRateLimitStore = ApkRateLimitStore(context)
) : LatestReleaseProvider {
companion object {
private const val TAG = "GitHubRelease"
private const val GITHUB_API_URL =
"https://api.github.com/repos/permissionlesstech/bitchat-android/releases/latest"
private const val ROUTE_READY_TIMEOUT_MILLIS = 60_000L
private const val CACHE_TTL_MILLIS = 30 * 60_000L
private const val PREFS_NAME = "apk_release_metadata"
private const val RATE_LIMIT_SCOPE = "github_release_metadata"
private const val USER_AGENT = "BitChat-Android"
private val SOURCE = ApkDownloadSource(
id = DefaultApkDownloadSources.GITHUB_ID,
displayName = "GitHub Releases",
latestApkUrl = "https://github.com/permissionlesstech/bitchat-android/releases/latest/" +
"download/bitchat-android-universal.apk"
)
internal fun parseRelease(jsonString: String): Release? = runCatching {
val json = JSONObject(jsonString)
val tagName = json.optString("tag_name")
val versionName = tagName.removePrefix("v").trim()
if (versionName.isBlank()) return null
val assets = json.optJSONArray("assets") ?: return null
for (index in 0 until assets.length()) {
val asset = assets.getJSONObject(index)
val name = asset.optString("name")
val url = asset.optString("browser_download_url")
if (name.contains("universal", ignoreCase = true) &&
name.endsWith(".apk", ignoreCase = true) &&
url.startsWith("https://")
) {
return Release(
versionName = versionName,
universalApkSize = asset.optLong("size", 0L),
universalApkUrl = url,
universalApkName = name
)
}
}
null
}.getOrNull()
}
private val appContext = context.applicationContext
private val preferences = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val mutex = Mutex()
override suspend fun latestRelease(): Result<ReleaseSnapshot> = withContext(Dispatchers.IO) {
mutex.withLock {
val cached = readCache()
val now = nowMillis()
val cacheAge = cached?.let { now - it.fetchedAtMillis }
if (cached != null && cacheAge != null && cacheAge in 0 until CACHE_TTL_MILLIS) {
return@withLock Result.success(ReleaseSnapshot(cached.release, isStale = false))
}
if (!awaitRoute()) return@withLock cached.orRouteFailure()
val routeSnapshot = routedClient()
rateLimits.retryAtMillis(RATE_LIMIT_SCOPE, routeSnapshot.route, now)?.let { deadline ->
return@withLock cached.orFailure(
rateLimits.blockedException(SOURCE, deadline, now)
)
}
val request = Request.Builder()
.url(apiUrl)
.addHeader("User-Agent", USER_AGENT)
.addHeader("Accept", "application/vnd.github+json")
.addHeader("X-GitHub-Api-Version", "2022-11-28")
.apply { cached?.etag?.let { addHeader("If-None-Match", it) } }
.build()
val client = routeSnapshot.client.newBuilder()
.callTimeout(45, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
try {
client.newCall(request).awaitResponse().use { response ->
if (apiUrl.startsWith("https://") && !response.request.url.isHttps) {
return@withLock cached.orFailure(
IOException("GitHub redirected release metadata to an insecure URL")
)
}
if (response.code == 304 && cached != null) {
val refreshed = cached.copy(fetchedAtMillis = now)
writeCache(refreshed)
rateLimits.clear(RATE_LIMIT_SCOPE, routeSnapshot.route)
return@withLock Result.success(
ReleaseSnapshot(refreshed.release, isStale = false)
)
}
if (!response.isSuccessful) {
val failure = ApkDownloadHttpErrors.fromResponse(
source = SOURCE,
code = response.code,
responseMessage = response.message,
retryAfter = response.header("Retry-After"),
rateLimitRemaining = response.header("X-RateLimit-Remaining"),
rateLimitResetEpochSeconds = response.header("X-RateLimit-Reset"),
nowMillis = now
)
val persistedFailure = if (
failure.reason == ApkDownloadFailureReason.RateLimited ||
failure.reason == ApkDownloadFailureReason.RateLimitedWithWait
) {
val deadline = rateLimits.recordRateLimit(
RATE_LIMIT_SCOPE,
routeSnapshot.route,
failure.retryAtMillis,
now
)
rateLimits.blockedException(SOURCE, deadline, now)
} else {
failure
}
return@withLock cached.orFailure(persistedFailure)
}
val rawBody = response.body.string()
val release = parseRelease(rawBody)
?: return@withLock cached.orFailure(
IOException("GitHub's latest release has no universal APK asset")
)
val entry = CachedRelease(
release = release,
etag = response.header("ETag"),
fetchedAtMillis = now
)
writeCache(entry)
rateLimits.clear(RATE_LIMIT_SCOPE, routeSnapshot.route)
Result.success(ReleaseSnapshot(release, isStale = false))
}
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
Log.w(TAG, "Could not refresh release metadata; using cache when available", error)
cached.orFailure(error)
}
}
}
private fun CachedRelease?.orRouteFailure(): Result<ReleaseSnapshot> = orFailure(
IOException("The selected network route is not ready")
)
private fun CachedRelease?.orFailure(error: Throwable): Result<ReleaseSnapshot> =
if (this != null) {
Result.success(ReleaseSnapshot(release, isStale = true))
} else {
Result.failure(error)
}
private fun readCache(): CachedRelease? = runCatching {
val version = preferences.getString("version", null)?.takeIf { it.isNotBlank() } ?: return null
val url = preferences.getString("url", null)?.takeIf { it.startsWith("https://") } ?: return null
val name = preferences.getString("name", null)?.takeIf { it.isNotBlank() } ?: return null
CachedRelease(
release = Release(
versionName = version,
universalApkSize = preferences.getLong("size", 0L),
universalApkUrl = url,
universalApkName = name
),
etag = preferences.getString("etag", null),
fetchedAtMillis = preferences.getLong("fetched_at", 0L)
)
}.getOrNull()
private fun writeCache(entry: CachedRelease) {
preferences.edit(commit = true) {
putString("version", entry.release.versionName)
putLong("size", entry.release.universalApkSize)
putString("url", entry.release.universalApkUrl)
putString("name", entry.release.universalApkName)
putString("etag", entry.etag)
putLong("fetched_at", entry.fetchedAtMillis)
}
}
private suspend fun Call.awaitResponse(): Response =
suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { cancel() }
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
if (continuation.isActive) {
continuation.resumeWith(Result.failure(e))
}
}
override fun onResponse(call: Call, response: Response) {
if (continuation.isActive) {
continuation.resumeWith(Result.success(response))
} else {
response.close()
}
}
})
}
data class Release(
val versionName: String,
val universalApkSize: Long,
val universalApkUrl: String,
val universalApkName: String
)
data class ReleaseSnapshot(
val release: Release,
val isStale: Boolean
)
private data class CachedRelease(
val release: Release,
val etag: String?,
val fetchedAtMillis: Long
)
}

View File

@ -56,14 +56,19 @@ class UniversalApkManager(
private val metadataFile: File get() = File(cacheDir, METADATA_FILE_NAME)
private val progressFile: File get() = File(cacheDir, PROGRESS_FILE_NAME)
private val rateLimits = ApkRateLimitStore(context)
// Download client: inherits Tor proxy settings but with no call timeout
// for large file downloads that can take minutes
private val downloadClient
get() = OkHttpProvider.httpClient().newBuilder()
.callTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.build()
// Download client: inherits the current client's actual route but has no call timeout for
// large files that can take minutes. Keep the route attached for route-specific cooldowns.
private fun downloadClient(): OkHttpProvider.RoutedClient {
val routed = OkHttpProvider.routedHttpClient()
return routed.copy(
client = routed.client.newBuilder()
.callTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.build()
)
}
/**
* Get information about the cached sharing APK, if it exists.
@ -228,6 +233,9 @@ class UniversalApkManager(
val safeVersion = version.replace(Regex("[^A-Za-z0-9._-]"), "_")
val finalFileName = "$APK_FILE_PREFIX$safeVersion.apk"
val finalFile = File(cacheDir, finalFileName)
// Package parsing above is blocking. A cancellation arriving while it runs
// must not promote the verified temporary file into the shareable slot.
ensureActive()
replaceFileSafely(tempFile, finalFile)
progressFile.delete()
@ -360,6 +368,13 @@ class UniversalApkManager(
resume: ResumeInfo?,
progressCallback: ((Int) -> Unit)?
) {
val routedClient = downloadClient()
val rateLimitScope = "apk_asset_${source.id}"
val now = System.currentTimeMillis()
rateLimits.retryAtMillis(rateLimitScope, routedClient.route, now)?.let { deadline ->
throw rateLimits.blockedException(source, deadline, now)
}
val request = Request.Builder()
// Always start from the configured source endpoint. If a release changed,
// If-Range makes the server return 200 and we overwrite the partial.
@ -374,8 +389,10 @@ class UniversalApkManager(
.build()
downloadToTempFile(
call = downloadClient.newCall(request),
call = routedClient.client.newCall(request),
source = source,
rateLimitScope = rateLimitScope,
route = routedClient.route,
endpointUrl = endpointUrl,
tempFile = tempFile,
existingBytes = existingBytes,
@ -391,6 +408,8 @@ class UniversalApkManager(
private suspend fun downloadToTempFile(
call: Call,
source: ApkDownloadSource,
rateLimitScope: String,
route: OkHttpProvider.Route,
endpointUrl: String,
tempFile: File,
existingBytes: Long,
@ -454,7 +473,7 @@ class UniversalApkManager(
)
}
if (!response.isSuccessful) {
throw ApkDownloadHttpErrors.fromResponse(
val failure = ApkDownloadHttpErrors.fromResponse(
source = source,
code = response.code,
responseMessage = response.message,
@ -463,8 +482,23 @@ class UniversalApkManager(
rateLimitResetEpochSeconds =
response.header("X-RateLimit-Reset")
)
if (failure.reason == ApkDownloadFailureReason.RateLimited ||
failure.reason == ApkDownloadFailureReason.RateLimitedWithWait
) {
val now = System.currentTimeMillis()
val deadline = rateLimits.recordRateLimit(
rateLimitScope,
route,
failure.retryAtMillis,
now
)
throw rateLimits.blockedException(source, deadline, now)
}
throw failure
}
rateLimits.clear(rateLimitScope, route)
val body = response.body
val range = if (response.code == 206) {
parseContentRange(response.header("Content-Range"))
@ -491,6 +525,12 @@ class UniversalApkManager(
val validator = response.header("ETag")
?: response.header("Last-Modified")
?: previousResume?.validator?.takeIf { append }
// A server may ignore Range and return a replacement 200 response.
// Remove the old bytes before recording the new validator. If the
// process dies at any later point, old and new release bytes cannot be
// combined on the next resume.
prepareApkTempFileForResponse(tempFile, append)
if (validator != null) {
saveResumeInfo(
ResumeInfo(
@ -511,7 +551,9 @@ class UniversalApkManager(
}
body.byteStream().use { input ->
FileOutputStream(tempFile, append).use { output ->
// Non-resume responses were already truncated above; always append
// after resume metadata is safely committed.
FileOutputStream(tempFile, true).use { output ->
val buffer = ByteArray(BUFFER_SIZE)
var bytesRead: Int
var totalBytesRead = resumedBytes

View File

@ -92,7 +92,11 @@ class WorkManagerApkDownloader(context: Context) : ApkDownloader {
ApkDownloader.DownloadState.Failed(
reason = reason,
messageArgs = args,
resumablePercent = if (resumable >= 0) resumable else null
resumablePercent = if (resumable >= 0) resumable else null,
retryAtMillis = workInfo.outputData.getLong(
ApkDownloadWorker.KEY_RETRY_AT,
0L
).takeIf { it > 0L }
)
}
WorkInfo.State.CANCELLED -> {

View File

@ -246,6 +246,8 @@
<string name="prepare_apk_source_installed">Sharing source: this installed APK</string>
<string name="prepare_apk_source_installed_arm64">Sharing source: this installed APK • ARM64 devices only</string>
<string name="prepare_apk_source_downloaded">Sharing source: verified downloaded universal APK</string>
<string name="prepare_apk_update_available">Version %1$s is available. You can keep sharing this APK or download the update.</string>
<string name="prepare_apk_update_warning">New version available</string>
<!-- Now the accessibility label and tooltip for an icon-only button, not a visible label. -->
<string name="prepare_apk_get_universal">Download universal APK</string>
<string name="prepare_apk_retry">Retry download</string>
@ -262,7 +264,9 @@
<string name="prepare_apk_button_delete">Delete</string>
<string name="prepare_apk_info">Version %1$s • %2$d MB</string>
<string name="prepare_apk_dialog_title">Download Universal APK?</string>
<string name="prepare_apk_update_dialog_title">Download Newer Universal APK?</string>
<string name="prepare_apk_dialog_message_unknown_size">This will download a verified universal APK from a configured source. You only need to do this once.</string>
<string name="prepare_apk_update_dialog_message">Version %1$s is available from GitHub (%2$d MB). Your current APK remains shareable during the download.</string>
<string name="prepare_apk_dialog_confirm">Download</string>
<string name="prepare_apk_downloading_title">Downloading Universal APK</string>
<string name="prepare_apk_downloading_message">Downloading %1$d MB…</string>

View File

@ -0,0 +1,142 @@
package com.bitchat.android.ui
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import com.bitchat.android.util.ApkDownloader
import com.bitchat.android.util.GitHubReleaseClient
import com.bitchat.android.util.LatestReleaseProvider
import com.bitchat.android.util.ShareableApkVariant
import com.bitchat.android.util.UniversalApkManager
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import java.io.File
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class ApkDownloadViewModelTest {
private lateinit var application: Application
@Before
fun setUp() {
Dispatchers.setMain(UnconfinedTestDispatcher())
application = ApplicationProvider.getApplicationContext()
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `local apk is shareable before release metadata starts`() = runTest {
val manager = managerWithLocalApk()
val downloader = FakeDownloader()
lateinit var viewModel: ApkDownloadViewModel
val statusWhenMetadataStarted = CompletableDeferred<ApkPreparationStatus>()
val metadata = object : LatestReleaseProvider {
override suspend fun latestRelease(): Result<GitHubReleaseClient.ReleaseSnapshot> {
statusWhenMetadataStarted.complete(viewModel.state.value.apkStatus)
return Result.failure(IllegalStateException("synthetic offline response"))
}
}
viewModel = ApkDownloadViewModel(application, manager, downloader, metadata)
viewModel.onEvent(ApkUiEvent.CheckStatus)
val ready = awaitReady(viewModel)
assertEquals("1.7.5", ready.version)
assertTrue(statusWhenMetadataStarted.await() is ApkPreparationStatus.Ready)
}
@Test
fun `notification cancellation returning idle restores shareable fallback`() = runTest {
val manager = managerWithLocalApk()
val downloader = FakeDownloader()
val metadata = object : LatestReleaseProvider {
override suspend fun latestRelease(): Result<GitHubReleaseClient.ReleaseSnapshot> =
Result.failure(IllegalStateException("synthetic offline response"))
}
val viewModel = ApkDownloadViewModel(application, manager, downloader, metadata)
viewModel.onEvent(ApkUiEvent.CheckStatus)
val originalReady = awaitReady(viewModel)
viewModel.onEvent(ApkUiEvent.PrepareRowClicked)
viewModel.onEvent(ApkUiEvent.ConfirmDownload)
val downloading = viewModel.state.value.apkStatus as ApkPreparationStatus.Downloading
assertSame(originalReady, downloading.shareableFallback)
assertEquals(1, downloader.startCount)
downloader.emit(ApkDownloader.DownloadState.Idle)
val restored = awaitReady(viewModel)
assertEquals(originalReady, restored)
}
private suspend fun managerWithLocalApk(): UniversalApkManager {
val manager = mock<UniversalApkManager>()
whenever(manager.prepareLocalApkInfo()).thenReturn(
UniversalApkManager.ApkInfo(
version = "1.7.5",
downloadDate = 1_700_000_000_000L,
size = 12L * 1024 * 1024,
file = File(application.cacheDir, "synthetic-shareable.apk"),
source = UniversalApkManager.ApkSource.INSTALLED,
variant = ShareableApkVariant.UNIVERSAL,
downloadSourceId = null
)
)
whenever(manager.getPartialDownloadProgress()).thenReturn(null)
return manager
}
private suspend fun awaitReady(
viewModel: ApkDownloadViewModel
): ApkPreparationStatus.Ready = withTimeout(5_000L) {
while (true) {
(viewModel.state.value.apkStatus as? ApkPreparationStatus.Ready)?.let { return@withTimeout it }
delay(1L)
}
error("unreachable")
}
private class FakeDownloader : ApkDownloader {
private val mutableState = MutableStateFlow<ApkDownloader.DownloadState>(
ApkDownloader.DownloadState.Idle
)
override val downloadState = mutableState.asStateFlow()
var startCount = 0
override fun startDownload() {
startCount += 1
mutableState.value = ApkDownloader.DownloadState.Downloading(
progressPercent = 0,
phase = ApkDownloader.DownloadPhase.SelectingSource
)
}
override fun cancelDownload() = Unit
fun emit(state: ApkDownloader.DownloadState) {
mutableState.value = state
}
}
}

View File

@ -34,8 +34,15 @@ class PrepareRowTapActionTest {
}
@Test
fun `nothing left to fetch means the row is inert`() {
assertNull(prepareRowTapAction(ready(ShareableApkVariant.UNIVERSAL)))
fun `a standalone installed universal apk can optionally be replaced from github`() {
assertEquals(
PrepareRowTapAction.OpenPrepareDialog,
prepareRowTapAction(ready(ShareableApkVariant.UNIVERSAL))
)
}
@Test
fun `a current downloaded universal apk leaves the row inert`() {
assertNull(
prepareRowTapAction(
ready(ShareableApkVariant.UNIVERSAL, UniversalApkManager.ApkSource.DOWNLOADED)
@ -43,6 +50,22 @@ class PrepareRowTapActionTest {
)
}
@Test
fun `a stale downloaded apk opens the update dialog without blocking sharing`() {
assertEquals(
PrepareRowTapAction.OpenPrepareDialog,
prepareRowTapAction(
ready(ShareableApkVariant.UNIVERSAL, UniversalApkManager.ApkSource.DOWNLOADED),
ApkReleaseStatus.Known(
version = "1.7.6",
sizeMB = 24,
isNewerThanSharedApk = true,
fromStaleCache = false
)
)
)
}
@Test
fun `a missing apk asks before spending the bytes`() {
assertEquals(
@ -72,4 +95,34 @@ class PrepareRowTapActionTest {
}
assertNull(prepareRowTapAction(ApkPreparationStatus.Loading))
}
@Test
fun `a persisted rate limit disables manual retry until its deadline`() {
val retryAt = 50_000L
assertNull(
prepareRowTapAction(
ApkPreparationStatus.Error("Rate limited", retryAtMillis = retryAt),
nowMillis = retryAt - 1
)
)
assertEquals(
PrepareRowTapAction.StartDownload,
prepareRowTapAction(
ApkPreparationStatus.Error("Rate limited", retryAtMillis = retryAt),
nowMillis = retryAt
)
)
}
@Test
fun `rate limit also disables optional update while an apk remains shareable`() {
val retryAt = 50_000L
assertNull(
prepareRowTapAction(
ready(ShareableApkVariant.UNIVERSAL),
downloadRetryAtMillis = retryAt,
nowMillis = retryAt - 1
)
)
}
}

View File

@ -4,11 +4,16 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.IOException
class ApkDownloadSourceTest {
@get:Rule
val temporaryFolder = TemporaryFolder()
private val source = ApkDownloadSource(
id = "mirror-one",
displayName = "Mirror One",
@ -120,6 +125,28 @@ class ApkDownloadSourceTest {
assertNull(parseContentRange("bytes 90-100/100"))
}
@Test
fun `a full response discards bytes from the release that was being resumed`() {
val tempFile = temporaryFolder.newFile("download-temp.apk")
tempFile.writeBytes("old-release-prefix".toByteArray())
prepareApkTempFileForResponse(tempFile, appendResponse = false)
tempFile.appendBytes("new-release".toByteArray())
assertEquals("new-release", tempFile.readText())
}
@Test
fun `a valid partial response keeps resumable bytes`() {
val tempFile = temporaryFolder.newFile("download-temp.apk")
tempFile.writeBytes("first-".toByteArray())
prepareApkTempFileForResponse(tempFile, appendResponse = true)
tempFile.appendBytes("second".toByteArray())
assertEquals("first-second", tempFile.readText())
}
@Test
fun `version comparison is host independent`() {
assertTrue(AppVersion.isNewer("1.7.4", "1.7.5"))

View File

@ -0,0 +1,134 @@
package com.bitchat.android.util
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.bitchat.android.net.OkHttpProvider
import kotlinx.coroutines.test.runTest
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import okhttp3.OkHttpClient
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class GitHubReleaseClientTest {
private lateinit var context: Context
private lateinit var server: MockWebServer
private var nowMillis = 1_700_000_000_000L
private var route = OkHttpProvider.Route.DIRECT
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
context.getSharedPreferences("apk_release_metadata", Context.MODE_PRIVATE)
.edit().clear().commit()
context.getSharedPreferences("apk_network_cooldowns", Context.MODE_PRIVATE)
.edit().clear().commit()
server = MockWebServer()
server.start()
}
@After
fun tearDown() {
server.close()
}
@Test
fun `cached metadata is conditionally refreshed with its etag`() = runTest {
server.enqueue(successResponse(etag = "release-v1"))
val client = client()
val first = client.latestRelease().getOrThrow()
assertEquals("1.7.6", first.release.versionName)
assertFalse(first.isStale)
nowMillis += 31 * 60_000L
server.enqueue(
MockResponse.Builder()
.code(304)
.build()
)
val refreshed = client.latestRelease().getOrThrow()
assertFalse(refreshed.isStale)
server.takeRequest()
assertEquals("release-v1", server.takeRequest().headers["If-None-Match"])
}
@Test
fun `rate limit serves stale metadata and suppresses repeated requests`() = runTest {
server.enqueue(successResponse(etag = "release-v1"))
val client = client()
client.latestRelease().getOrThrow()
nowMillis += 31 * 60_000L
server.enqueue(
MockResponse.Builder()
.code(429)
.build()
)
val stale = client.latestRelease().getOrThrow()
val stillStale = client.latestRelease().getOrThrow()
assertTrue(stale.isStale)
assertTrue(stillStale.isStale)
assertEquals(2, server.requestCount)
}
@Test
fun `cooldown follows the actual client route`() = runTest {
server.enqueue(
MockResponse.Builder()
.code(429)
.addHeader("Retry-After", "120")
.build()
)
val client = client()
assertTrue(client.latestRelease().isFailure)
assertTrue(client.latestRelease().isFailure)
assertEquals(1, server.requestCount)
route = OkHttpProvider.Route.TOR
server.enqueue(successResponse(etag = "release-v1"))
assertTrue(client.latestRelease().isSuccess)
assertEquals(2, server.requestCount)
}
private fun client() = GitHubReleaseClient(
context = context,
apiUrl = server.url("/releases/latest").toString(),
nowMillis = { nowMillis },
routedClient = {
OkHttpProvider.RoutedClient(
client = OkHttpClient.Builder().build(),
route = route
)
},
awaitRoute = { true }
)
private fun successResponse(etag: String): MockResponse = MockResponse.Builder()
.code(200)
.addHeader("ETag", etag)
.body(
"""
{
"tag_name": "v1.7.6",
"assets": [
{
"name": "bitchat-android-universal.apk",
"browser_download_url": "https://downloads.example/bitchat-universal.apk",
"size": 25165824
}
]
}
""".trimIndent()
)
.build()
}

View File

@ -16,9 +16,6 @@ appcompat = "1.7.1"
# Compose
compose-bom = "2026.06.01"
compose-icons-extended = "1.7.8"
# Overrides the BOM's 1.4.0. The expressive wavy progress indicators are Compose-only in the
# 1.5.0 line, which has no stable release yet; drop this override once 1.5.0 ships.
compose-material3 = "1.5.0-alpha25"
# Navigation
navigation-compose = "2.9.8"
@ -92,7 +89,7 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose-icons-extended" }
# Lifecycle
@ -124,6 +121,7 @@ nordic-ble = { module = "no.nordicsemi.android:ble", version.ref = "nordic-ble"
# WebSocket
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver3", version.ref = "okhttp" }
tor-android-binary = { module = "org.torproject:tor-android-binary", version.ref = "tor-android-binary" }
# Tor (embed) intentionally not pinned yet; add once repo is chosen
@ -195,7 +193,8 @@ testing = [
"mockito-kotlin",
"mockito-core",
"roboelectric",
"kotlinx-coroutines-test"
"kotlinx-coroutines-test",
"okhttp-mockwebserver"
]
compose-testing = [

View File

@ -3658,6 +3658,14 @@
<sha256 value="e1abd7f1116cf5e0c59947693e2189208ec94296b2a3394c959e3511d399a7b0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okhttp3" name="mockwebserver3" version="5.4.0">
<artifact name="mockwebserver3-5.4.0.jar">
<sha256 value="e1e1d51a57567d6db834f93d98831ab10bb0220b91a67580894d9ab36cc9f065" origin="Generated by Gradle"/>
</artifact>
<artifact name="mockwebserver3-5.4.0.module">
<sha256 value="1161099d541033926bc0886a0bc2b4fb92a99a4fe9410180baf377159a996bbb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okhttp3" name="okhttp" version="5.4.0">
<artifact name="okhttp-5.4.0.module">
<sha256 value="63973ba755ba6c77de1e82f5efa4195569800be00c93c0130ce4238a9b56c93c" origin="Generated by Gradle"/>