mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
fix(apk): preserve local sharing during update checks
This commit is contained in:
parent
b60b5121aa
commit
2c19d00d17
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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()}"
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 -> {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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 = [
|
||||
|
||||
@ -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"/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user