mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-29 07:16:08 +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-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-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.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-android:1.11.4=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-android:1.11.4=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-core:1.11.4=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.animation:animation:1.11.4=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-android:1.11.4=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-android:1.11.4=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-layout:1.11.4=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.foundation:foundation:1.11.4=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-android:1.4.0=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:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,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.material:material-icons-core-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,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-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-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-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-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-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-android:1.11.4=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.material:material-ripple:1.11.4=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-android:1.11.4=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-android:1.11.4=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-annotation:1.11.4=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-android:1.11.4=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-retain:1.11.4=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-android:1.11.4=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-saveable:1.11.4=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.runtime:runtime:1.11.4=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-android:1.11.4=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-android:1.11.4=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-geometry:1.11.4=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-android:1.11.4=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-graphics:1.11.4=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-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||||
androidx.compose.ui:ui-test-junit4-android:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
androidx.compose.ui:ui-test-junit4-android:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||||
androidx.compose.ui:ui-test-junit4:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
androidx.compose.ui:ui-test-junit4:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||||
androidx.compose.ui:ui-test-manifest:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
androidx.compose.ui:ui-test-manifest:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||||
androidx.compose.ui:ui-test:1.12.0-beta01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
androidx.compose.ui:ui-test:1.11.4=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-android:1.11.4=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-text:1.11.4=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-android:1.11.4=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-android:1.11.4=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-data:1.11.4=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-android:1.11.4=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-preview:1.11.4=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-tooling:1.11.4=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-android:1.11.4=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-unit:1.11.4=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-android:1.11.4=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-util:1.11.4=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.ui:ui:1.11.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
androidx.compose:compose-bom:2026.06.01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
androidx.compose: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.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
androidx.concurrent:concurrent-futures-ktx:1.2.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
|
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.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-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.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.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-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.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.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.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-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.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-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-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-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-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-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-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
androidx.lifecycle:lifecycle-runtime-compose-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-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-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.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-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-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
androidx.navigation:navigation-compose-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-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-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.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.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.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
androidx.room:room-common:2.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
androidx.room:room-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.0.0=debugAndroidTestCompileClasspath
|
||||||
androidx.tracing:tracing:1.1.0=debugUnitTestCompileClasspath
|
androidx.tracing:tracing:1.1.0=debugUnitTestCompileClasspath
|
||||||
androidx.tracing:tracing:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
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-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.vectordrawable:vectordrawable:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
androidx.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.testparameterinjector:test-parameter-injector:1.18=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
|
||||||
com.google.zxing:core:3.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
com.google.zxing:core:3.5.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
com.ibm.icu:icu4j:77.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
|
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-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.okhttp3:okhttp:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||||
com.squareup.okio:okio-jvm:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
com.squareup.okio:okio-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.
|
* Centralized OkHttp provider to ensure all network traffic honors Tor settings.
|
||||||
*/
|
*/
|
||||||
object OkHttpProvider {
|
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)
|
private val wsClientRef = AtomicReference<OkHttpClient?>(null)
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
@ -18,20 +28,30 @@ object OkHttpProvider {
|
|||||||
wsClientRef.set(null)
|
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 }
|
httpClientRef.get()?.let { return it }
|
||||||
val client = baseBuilderForCurrentProxy()
|
val (builder, route) = baseBuilderForCurrentProxy()
|
||||||
|
val client = builder
|
||||||
.callTimeout(15, TimeUnit.SECONDS)
|
.callTimeout(15, TimeUnit.SECONDS)
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
.readTimeout(15, TimeUnit.SECONDS)
|
.readTimeout(15, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
httpClientRef.set(client)
|
val routedClient = RoutedClient(client, route)
|
||||||
return client
|
httpClientRef.set(routedClient)
|
||||||
|
return routedClient
|
||||||
}
|
}
|
||||||
|
|
||||||
fun webSocketClient(): OkHttpClient {
|
fun webSocketClient(): OkHttpClient {
|
||||||
wsClientRef.get()?.let { return it }
|
wsClientRef.get()?.let { return it }
|
||||||
val client = baseBuilderForCurrentProxy()
|
val client = baseBuilderForCurrentProxy().first
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
.readTimeout(0, TimeUnit.SECONDS)
|
.readTimeout(0, TimeUnit.SECONDS)
|
||||||
.writeTimeout(10, TimeUnit.SECONDS)
|
.writeTimeout(10, TimeUnit.SECONDS)
|
||||||
@ -40,7 +60,7 @@ object OkHttpProvider {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun baseBuilderForCurrentProxy(): OkHttpClient.Builder {
|
private fun baseBuilderForCurrentProxy(): Pair<OkHttpClient.Builder, Route> {
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
val torProvider = ArtiTorManager.getInstance()
|
val torProvider = ArtiTorManager.getInstance()
|
||||||
val socks: InetSocketAddress? = torProvider.currentSocksAddress()
|
val socks: InetSocketAddress? = torProvider.currentSocksAddress()
|
||||||
@ -50,6 +70,6 @@ object OkHttpProvider {
|
|||||||
val proxy = Proxy(Proxy.Type.SOCKS, socks)
|
val proxy = Proxy(Proxy.Type.SOCKS, socks)
|
||||||
builder.proxy(proxy)
|
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 apkViewModel: ApkDownloadViewModel = viewModel()
|
||||||
val apkUiState by apkViewModel.state.collectAsStateWithLifecycle()
|
val apkUiState by apkViewModel.state.collectAsStateWithLifecycle()
|
||||||
val apkStatus = apkUiState.apkStatus
|
val apkStatus = apkUiState.apkStatus
|
||||||
|
val releaseStatus = apkUiState.releaseStatus
|
||||||
val downloadProgress = apkUiState.downloadProgress
|
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)
|
// Handle one-shot effects (navigation, toasts, share intents)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@ -659,7 +670,11 @@ fun AboutSheet(
|
|||||||
// does, so the row can never look tappable and do
|
// does, so the row can never look tappable and do
|
||||||
// nothing.
|
// nothing.
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = prepareRowTapAction(apkStatus) != null
|
enabled = prepareRowTapAction(
|
||||||
|
apkStatus,
|
||||||
|
releaseStatus,
|
||||||
|
apkUiState.downloadRetryAtMillis
|
||||||
|
) != null
|
||||||
) {
|
) {
|
||||||
apkViewModel.onEvent(ApkUiEvent.PrepareRowClicked)
|
apkViewModel.onEvent(ApkUiEvent.PrepareRowClicked)
|
||||||
}
|
}
|
||||||
@ -667,7 +682,7 @@ fun AboutSheet(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (apkStatus is ApkPreparationStatus.Ready) {
|
imageVector = if (shareableApk != null) {
|
||||||
Icons.Default.Share
|
Icons.Default.Share
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.CloudDownload
|
Icons.Default.CloudDownload
|
||||||
@ -683,16 +698,53 @@ fun AboutSheet(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
text = if (apkStatus is ApkPreparationStatus.Ready) {
|
Text(
|
||||||
stringResource(R.string.prepare_apk_ready_title)
|
text = if (shareableApk != null) {
|
||||||
} else {
|
stringResource(R.string.prepare_apk_ready_title)
|
||||||
stringResource(R.string.prepare_apk_title)
|
} else {
|
||||||
},
|
stringResource(R.string.prepare_apk_title)
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
},
|
||||||
fontWeight = FontWeight.Medium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = colorScheme.onSurface
|
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(
|
||||||
text = when (val status = apkStatus) {
|
text = when (val status = apkStatus) {
|
||||||
is ApkPreparationStatus.Loading -> stringResource(R.string.checking)
|
is ApkPreparationStatus.Loading -> stringResource(R.string.checking)
|
||||||
@ -764,7 +816,10 @@ fun AboutSheet(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
is ApkPreparationStatus.Ready -> {
|
is ApkPreparationStatus.Ready -> {
|
||||||
if (apkStatus.variant == ShareableApkVariant.ARM64) {
|
if (
|
||||||
|
apkStatus.source ==
|
||||||
|
UniversalApkManager.ApkSource.INSTALLED
|
||||||
|
) {
|
||||||
ApkPrepareRowIconButton(
|
ApkPrepareRowIconButton(
|
||||||
icon = Icons.Default.CloudDownload,
|
icon = Icons.Default.CloudDownload,
|
||||||
description = stringResource(
|
description = stringResource(
|
||||||
@ -775,6 +830,7 @@ fun AboutSheet(
|
|||||||
ApkUiEvent.DownloadUniversalClicked
|
ApkUiEvent.DownloadUniversalClicked
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
enabled = !downloadRetryBlocked,
|
||||||
tint = colorScheme.primary
|
tint = colorScheme.primary
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
@ -807,6 +863,11 @@ fun AboutSheet(
|
|||||||
ApkUiEvent.PrepareRowClicked
|
ApkUiEvent.PrepareRowClicked
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
enabled = prepareRowTapAction(
|
||||||
|
apkStatus,
|
||||||
|
releaseStatus,
|
||||||
|
apkUiState.downloadRetryAtMillis
|
||||||
|
) != null,
|
||||||
tint = colorScheme.primary
|
tint = colorScheme.primary
|
||||||
)
|
)
|
||||||
else -> {}
|
else -> {}
|
||||||
@ -820,16 +881,28 @@ fun AboutSheet(
|
|||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
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
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = if (availableUpdate != null) {
|
||||||
R.string.prepare_apk_dialog_message_unknown_size
|
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
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -875,7 +948,11 @@ fun AboutSheet(
|
|||||||
containerColor = colorScheme.error
|
containerColor = colorScheme.error
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Delete")
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.prepare_apk_button_delete
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
@ -887,8 +964,9 @@ fun AboutSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show sharing rows only when APK is ready
|
// A GitHub update is optional. Keep sharing visible while the
|
||||||
val canShareAPK = apkStatus is ApkPreparationStatus.Ready
|
// replacement downloads or while metadata refreshes.
|
||||||
|
val canShareAPK = shareableApk != null
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = canShareAPK,
|
visible = canShareAPK,
|
||||||
|
|||||||
@ -7,11 +7,16 @@ import androidx.lifecycle.AndroidViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import com.bitchat.android.util.ApkDownloader
|
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.ShareableApkVariant
|
||||||
import com.bitchat.android.util.UniversalApkManager
|
import com.bitchat.android.util.UniversalApkManager
|
||||||
import com.bitchat.android.util.WorkManagerApkDownloader
|
import com.bitchat.android.util.WorkManagerApkDownloader
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@ -33,14 +38,32 @@ sealed class ApkPreparationStatus {
|
|||||||
) : ApkPreparationStatus()
|
) : ApkPreparationStatus()
|
||||||
/** [phase] is what the operation is actually doing; only a transfer has a real percentage. */
|
/** [phase] is what the operation is actually doing; only a transfer has a real percentage. */
|
||||||
data class Downloading(
|
data class Downloading(
|
||||||
val phase: ApkDownloader.DownloadPhase = ApkDownloader.DownloadPhase.SelectingSource
|
val phase: ApkDownloader.DownloadPhase = ApkDownloader.DownloadPhase.SelectingSource,
|
||||||
|
val shareableFallback: Ready? = null
|
||||||
) : ApkPreparationStatus()
|
) : ApkPreparationStatus()
|
||||||
data class Resumable(val progressPercent: Int, val message: String) : ApkPreparationStatus()
|
data class Resumable(
|
||||||
data class Error(val message: String) : ApkPreparationStatus()
|
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(
|
data class ApkUiState(
|
||||||
val apkStatus: ApkPreparationStatus = ApkPreparationStatus.Loading,
|
val apkStatus: ApkPreparationStatus = ApkPreparationStatus.Loading,
|
||||||
|
val releaseStatus: ApkReleaseStatus = ApkReleaseStatus.Unknown,
|
||||||
|
val downloadRetryAtMillis: Long? = null,
|
||||||
val downloadProgress: Int = 0,
|
val downloadProgress: Int = 0,
|
||||||
val showPrepareDialog: Boolean = false,
|
val showPrepareDialog: Boolean = false,
|
||||||
val showDeleteDialog: 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
|
* 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.
|
* 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
|
status is ApkPreparationStatus.NotDownloaded -> PrepareRowTapAction.OpenPrepareDialog
|
||||||
// Consent was already given for these; resuming straight away avoids a redundant prompt.
|
// Consent was already given for these; resuming straight away avoids a redundant prompt.
|
||||||
status is ApkPreparationStatus.Resumable -> PrepareRowTapAction.StartDownload
|
status is ApkPreparationStatus.Resumable &&
|
||||||
status is ApkPreparationStatus.Error -> PrepareRowTapAction.StartDownload
|
(status.retryAtMillis == null || status.retryAtMillis <= nowMillis) ->
|
||||||
status is ApkPreparationStatus.Ready && status.variant == ShareableApkVariant.ARM64 ->
|
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
|
PrepareRowTapAction.OpenPrepareDialog
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -102,21 +137,33 @@ sealed class ApkUiEffect {
|
|||||||
* ViewModel for APK download/status/share logic following MVI pattern.
|
* ViewModel for APK download/status/share logic following MVI pattern.
|
||||||
* UI sends [ApkUiEvent], observes [ApkUiState], and collects [ApkUiEffect].
|
* 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 {
|
companion object {
|
||||||
private const val TAG = "ApkDownloadVM"
|
private const val TAG = "ApkDownloadVM"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val apkManager = UniversalApkManager(application)
|
|
||||||
private val downloader: ApkDownloader = WorkManagerApkDownloader(application)
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ApkUiState())
|
private val _state = MutableStateFlow(ApkUiState())
|
||||||
val state: StateFlow<ApkUiState> = _state.asStateFlow()
|
val state: StateFlow<ApkUiState> = _state.asStateFlow()
|
||||||
|
|
||||||
private val _effect = Channel<ApkUiEffect>(Channel.BUFFERED)
|
private val _effect = Channel<ApkUiEffect>(Channel.BUFFERED)
|
||||||
val effect = _effect.receiveAsFlow()
|
val effect = _effect.receiveAsFlow()
|
||||||
|
|
||||||
|
private var metadataRefreshJob: Job? = null
|
||||||
|
private var retryUnlockJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeDownloader()
|
observeDownloader()
|
||||||
}
|
}
|
||||||
@ -140,7 +187,13 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onPrepareRowClicked() {
|
private fun onPrepareRowClicked() {
|
||||||
when (prepareRowTapAction(_state.value.apkStatus)) {
|
when (
|
||||||
|
prepareRowTapAction(
|
||||||
|
_state.value.apkStatus,
|
||||||
|
_state.value.releaseStatus,
|
||||||
|
_state.value.downloadRetryAtMillis
|
||||||
|
)
|
||||||
|
) {
|
||||||
PrepareRowTapAction.OpenPrepareDialog ->
|
PrepareRowTapAction.OpenPrepareDialog ->
|
||||||
_state.update { it.copy(showPrepareDialog = true) }
|
_state.update { it.copy(showPrepareDialog = true) }
|
||||||
PrepareRowTapAction.StartDownload -> startDownload()
|
PrepareRowTapAction.StartDownload -> startDownload()
|
||||||
@ -154,9 +207,14 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDownloadUniversalClicked() {
|
private fun onDownloadUniversalClicked() {
|
||||||
|
if (_state.value.downloadRetryAtMillis?.let { it > System.currentTimeMillis() } == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val status = _state.value.apkStatus
|
val status = _state.value.apkStatus
|
||||||
|
val hasUpdate = (_state.value.releaseStatus as? ApkReleaseStatus.Known)
|
||||||
|
?.isNewerThanSharedApk == true
|
||||||
if (status is ApkPreparationStatus.Ready &&
|
if (status is ApkPreparationStatus.Ready &&
|
||||||
status.variant == ShareableApkVariant.ARM64
|
(status.source == UniversalApkManager.ApkSource.INSTALLED || hasUpdate)
|
||||||
) {
|
) {
|
||||||
_state.update { it.copy(showPrepareDialog = true) }
|
_state.update { it.copy(showPrepareDialog = true) }
|
||||||
}
|
}
|
||||||
@ -212,20 +270,39 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
private fun onCancelDownload() {
|
private fun onCancelDownload() {
|
||||||
downloader.cancelDownload()
|
downloader.cancelDownload()
|
||||||
|
|
||||||
// Leave Downloading immediately. The cancelled job maps to Idle, which
|
val fallback = (_state.value.apkStatus as? ApkPreparationStatus.Downloading)
|
||||||
// the observer intentionally ignores because local status is resolved below.
|
?.shareableFallback
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(apkStatus = ApkPreparationStatus.Loading, downloadProgress = 0)
|
it.copy(
|
||||||
|
apkStatus = fallback ?: ApkPreparationStatus.Loading,
|
||||||
|
downloadProgress = 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
if (fallback == null) checkStatus()
|
||||||
checkStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDownload() {
|
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()
|
val partial = apkManager.getPartialDownloadProgress()
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
apkStatus = ApkPreparationStatus.Downloading(),
|
apkStatus = ApkPreparationStatus.Downloading(
|
||||||
|
shareableFallback = fallback
|
||||||
|
),
|
||||||
|
downloadRetryAtMillis = null,
|
||||||
downloadProgress = partial ?: 0
|
downloadProgress = partial ?: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -251,6 +328,36 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
current.copy(apkStatus = resolvedStatus, downloadProgress = 0)
|
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 ->
|
downloader.downloadState.collect { downloadState ->
|
||||||
when (downloadState) {
|
when (downloadState) {
|
||||||
is ApkDownloader.DownloadState.Idle -> {
|
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 -> {
|
is ApkDownloader.DownloadState.Downloading -> {
|
||||||
_state.update {
|
_state.update {
|
||||||
|
val fallback = (it.apkStatus as? ApkPreparationStatus.Downloading)
|
||||||
|
?.shareableFallback
|
||||||
|
?: (it.apkStatus as? ApkPreparationStatus.Ready)
|
||||||
it.copy(
|
it.copy(
|
||||||
apkStatus = ApkPreparationStatus.Downloading(downloadState.phase),
|
apkStatus = ApkPreparationStatus.Downloading(
|
||||||
|
phase = downloadState.phase,
|
||||||
|
shareableFallback = fallback
|
||||||
|
),
|
||||||
downloadProgress = downloadState.progressPercent
|
downloadProgress = downloadState.progressPercent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -272,31 +395,33 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
is ApkDownloader.DownloadState.Success -> {
|
is ApkDownloader.DownloadState.Success -> {
|
||||||
val info = apkManager.getCachedApkInfo()
|
val info = apkManager.getCachedApkInfo()
|
||||||
_state.update {
|
_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(
|
it.copy(
|
||||||
apkStatus = ApkPreparationStatus.Ready(
|
apkStatus = ready,
|
||||||
version = info?.version ?: downloadState.version,
|
releaseStatus = releaseStatusFor(ready, it.releaseStatus),
|
||||||
sizeMB = info?.let { cached ->
|
downloadRetryAtMillis = null,
|
||||||
(cached.size / 1024 / 1024).toInt()
|
|
||||||
} ?: downloadState.sizeMB,
|
|
||||||
source = info?.source ?: UniversalApkManager.ApkSource.DOWNLOADED,
|
|
||||||
variant = info?.variant ?: ShareableApkVariant.UNIVERSAL
|
|
||||||
),
|
|
||||||
downloadProgress = 100
|
downloadProgress = 100
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ApkDownloader.DownloadState.Failed -> {
|
is ApkDownloader.DownloadState.Failed -> {
|
||||||
val localArm64 = apkManager.getCachedApkInfo()
|
val fallback = (_state.value.apkStatus as? ApkPreparationStatus.Downloading)
|
||||||
?.takeIf { it.variant == ShareableApkVariant.ARM64 }
|
?.shareableFallback
|
||||||
if (localArm64 != null) {
|
?: apkManager.getCachedApkInfo()?.toReady()
|
||||||
|
if (fallback != null) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
apkStatus = ApkPreparationStatus.Ready(
|
apkStatus = fallback,
|
||||||
version = localArm64.version,
|
releaseStatus = releaseStatusFor(fallback, it.releaseStatus),
|
||||||
sizeMB = (localArm64.size / 1024 / 1024).toInt(),
|
downloadRetryAtMillis = downloadState.retryAtMillis,
|
||||||
source = localArm64.source,
|
downloadProgress = 0
|
||||||
variant = localArm64.variant
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_effect.send(ApkUiEffect.ShowToast(failureMessage(downloadState)))
|
_effect.send(ApkUiEffect.ShowToast(failureMessage(downloadState)))
|
||||||
@ -307,17 +432,24 @@ class ApkDownloadViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
it.copy(
|
it.copy(
|
||||||
apkStatus = ApkPreparationStatus.Resumable(
|
apkStatus = ApkPreparationStatus.Resumable(
|
||||||
progressPercent = downloadState.resumablePercent,
|
progressPercent = downloadState.resumablePercent,
|
||||||
message = message
|
message = message,
|
||||||
|
retryAtMillis = downloadState.retryAtMillis
|
||||||
),
|
),
|
||||||
|
downloadRetryAtMillis = downloadState.retryAtMillis,
|
||||||
downloadProgress = downloadState.resumablePercent
|
downloadProgress = downloadState.resumablePercent
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
it.copy(
|
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.
|
* this resolves it, so the message follows the device locale rather than the worker's.
|
||||||
*/
|
*/
|
||||||
private fun failureMessage(state: ApkDownloader.DownloadState.Failed): String =
|
private fun failureMessage(state: ApkDownloader.DownloadState.Failed): String =
|
||||||
getApplication<Application>().getString(
|
runCatching {
|
||||||
state.reason.messageRes,
|
getApplication<Application>().getString(
|
||||||
*state.messageArgs.toTypedArray()
|
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) {
|
private suspend fun resolveApkStatus(): ApkPreparationStatus = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val info = apkManager.prepareLocalApkInfo()
|
val info = apkManager.prepareLocalApkInfo()
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
ApkPreparationStatus.Ready(
|
info.toReady()
|
||||||
version = info.version,
|
|
||||||
sizeMB = (info.size / 1024 / 1024).toInt(),
|
|
||||||
source = info.source,
|
|
||||||
variant = info.variant
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
val partial = apkManager.getPartialDownloadProgress()
|
val partial = apkManager.getPartialDownloadProgress()
|
||||||
if (partial != null) {
|
if (partial != null) {
|
||||||
|
|||||||
@ -4,10 +4,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearWavyProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.Text
|
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
|
* 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.
|
* 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
|
* Determinate transfer/resume progress and indeterminate non-transfer phases use the stable
|
||||||
* flat line at the fraction it reached rather than a bar indistinguishable from a live one.
|
* Material 3 progress API. The expressive wavy variant can be introduced independently later.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ApkDownloadProgressBar(
|
internal fun ApkDownloadProgressBar(
|
||||||
status: ApkPreparationStatus,
|
status: ApkPreparationStatus,
|
||||||
@ -46,19 +44,17 @@ internal fun ApkDownloadProgressBar(
|
|||||||
status is ApkPreparationStatus.Downloading &&
|
status is ApkPreparationStatus.Downloading &&
|
||||||
status.phase.hasMeasurableProgress &&
|
status.phase.hasMeasurableProgress &&
|
||||||
progressPercent > 0 ->
|
progressPercent > 0 ->
|
||||||
LinearWavyProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { progressPercent.asProgressFraction() },
|
progress = { progressPercent.asProgressFraction() },
|
||||||
modifier = barModifier
|
modifier = barModifier
|
||||||
)
|
)
|
||||||
|
|
||||||
status is ApkPreparationStatus.Downloading ->
|
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 ->
|
status is ApkPreparationStatus.Resumable ->
|
||||||
LinearWavyProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { status.progressPercent.asProgressFraction() },
|
progress = { status.progressPercent.asProgressFraction() },
|
||||||
amplitude = { 0f },
|
|
||||||
modifier = barModifier
|
modifier = barModifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -84,22 +80,24 @@ internal fun ApkPrepareRowIconButton(
|
|||||||
description: String,
|
description: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
tint: Color = MaterialTheme.colorScheme.onSurfaceVariant
|
tint: Color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
) {
|
) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
tooltip = { PlainTooltip { Text(description) } },
|
tooltip = { PlainTooltip { Text(description) } },
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier.size(48.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = description,
|
contentDescription = description,
|
||||||
tint = tint,
|
tint = if (enabled) tint else tint.copy(alpha = 0.38f),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package com.bitchat.android.util
|
|||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
@ -244,6 +246,14 @@ internal data class ContentRange(
|
|||||||
val total: Long?
|
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? {
|
internal fun parseContentRange(value: String?): ContentRange? {
|
||||||
if (value == null) return null
|
if (value == null) return null
|
||||||
val match = Regex("""bytes\s+(\d+)-(\d+)/(\d+|\*)""", RegexOption.IGNORE_CASE)
|
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_REASON = "error_reason"
|
||||||
const val KEY_ERROR_ARGS = "error_args"
|
const val KEY_ERROR_ARGS = "error_args"
|
||||||
const val KEY_RESUMABLE_PERCENT = "resumable_percent"
|
const val KEY_RESUMABLE_PERCENT = "resumable_percent"
|
||||||
|
const val KEY_RETRY_AT = "retry_at"
|
||||||
|
|
||||||
private const val CHANNEL_ID = "apk_download"
|
private const val CHANNEL_ID = "apk_download"
|
||||||
private const val NOTIFICATION_ID = 4201
|
private const val NOTIFICATION_ID = 4201
|
||||||
@ -120,6 +121,7 @@ class ApkDownloadWorker(
|
|||||||
failure?.messageArgs.orEmpty().toTypedArray()
|
failure?.messageArgs.orEmpty().toTypedArray()
|
||||||
)
|
)
|
||||||
.putInt(KEY_RESUMABLE_PERCENT, partial ?: -1)
|
.putInt(KEY_RESUMABLE_PERCENT, partial ?: -1)
|
||||||
|
.apply { failure?.retryAtMillis?.let { putLong(KEY_RETRY_AT, it) } }
|
||||||
.build()
|
.build()
|
||||||
Result.failure(outputData)
|
Result.failure(outputData)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,8 @@ interface ApkDownloader {
|
|||||||
data class Failed(
|
data class Failed(
|
||||||
val reason: ApkDownloadFailureReason,
|
val reason: ApkDownloadFailureReason,
|
||||||
val messageArgs: List<String>,
|
val messageArgs: List<String>,
|
||||||
val resumablePercent: Int?
|
val resumablePercent: Int?,
|
||||||
|
val retryAtMillis: Long? = null
|
||||||
) : DownloadState()
|
) : 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 metadataFile: File get() = File(cacheDir, METADATA_FILE_NAME)
|
||||||
private val progressFile: File get() = File(cacheDir, PROGRESS_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
|
// Download client: inherits the current client's actual route but has no call timeout for
|
||||||
// for large file downloads that can take minutes
|
// large files that can take minutes. Keep the route attached for route-specific cooldowns.
|
||||||
private val downloadClient
|
private fun downloadClient(): OkHttpProvider.RoutedClient {
|
||||||
get() = OkHttpProvider.httpClient().newBuilder()
|
val routed = OkHttpProvider.routedHttpClient()
|
||||||
.callTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
|
return routed.copy(
|
||||||
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
|
client = routed.client.newBuilder()
|
||||||
.build()
|
.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.
|
* 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 safeVersion = version.replace(Regex("[^A-Za-z0-9._-]"), "_")
|
||||||
val finalFileName = "$APK_FILE_PREFIX$safeVersion.apk"
|
val finalFileName = "$APK_FILE_PREFIX$safeVersion.apk"
|
||||||
val finalFile = File(cacheDir, finalFileName)
|
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)
|
replaceFileSafely(tempFile, finalFile)
|
||||||
progressFile.delete()
|
progressFile.delete()
|
||||||
|
|
||||||
@ -360,6 +368,13 @@ class UniversalApkManager(
|
|||||||
resume: ResumeInfo?,
|
resume: ResumeInfo?,
|
||||||
progressCallback: ((Int) -> Unit)?
|
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()
|
val request = Request.Builder()
|
||||||
// Always start from the configured source endpoint. If a release changed,
|
// Always start from the configured source endpoint. If a release changed,
|
||||||
// If-Range makes the server return 200 and we overwrite the partial.
|
// If-Range makes the server return 200 and we overwrite the partial.
|
||||||
@ -374,8 +389,10 @@ class UniversalApkManager(
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
downloadToTempFile(
|
downloadToTempFile(
|
||||||
call = downloadClient.newCall(request),
|
call = routedClient.client.newCall(request),
|
||||||
source = source,
|
source = source,
|
||||||
|
rateLimitScope = rateLimitScope,
|
||||||
|
route = routedClient.route,
|
||||||
endpointUrl = endpointUrl,
|
endpointUrl = endpointUrl,
|
||||||
tempFile = tempFile,
|
tempFile = tempFile,
|
||||||
existingBytes = existingBytes,
|
existingBytes = existingBytes,
|
||||||
@ -391,6 +408,8 @@ class UniversalApkManager(
|
|||||||
private suspend fun downloadToTempFile(
|
private suspend fun downloadToTempFile(
|
||||||
call: Call,
|
call: Call,
|
||||||
source: ApkDownloadSource,
|
source: ApkDownloadSource,
|
||||||
|
rateLimitScope: String,
|
||||||
|
route: OkHttpProvider.Route,
|
||||||
endpointUrl: String,
|
endpointUrl: String,
|
||||||
tempFile: File,
|
tempFile: File,
|
||||||
existingBytes: Long,
|
existingBytes: Long,
|
||||||
@ -454,7 +473,7 @@ class UniversalApkManager(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw ApkDownloadHttpErrors.fromResponse(
|
val failure = ApkDownloadHttpErrors.fromResponse(
|
||||||
source = source,
|
source = source,
|
||||||
code = response.code,
|
code = response.code,
|
||||||
responseMessage = response.message,
|
responseMessage = response.message,
|
||||||
@ -463,8 +482,23 @@ class UniversalApkManager(
|
|||||||
rateLimitResetEpochSeconds =
|
rateLimitResetEpochSeconds =
|
||||||
response.header("X-RateLimit-Reset")
|
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 body = response.body
|
||||||
val range = if (response.code == 206) {
|
val range = if (response.code == 206) {
|
||||||
parseContentRange(response.header("Content-Range"))
|
parseContentRange(response.header("Content-Range"))
|
||||||
@ -491,6 +525,12 @@ class UniversalApkManager(
|
|||||||
val validator = response.header("ETag")
|
val validator = response.header("ETag")
|
||||||
?: response.header("Last-Modified")
|
?: response.header("Last-Modified")
|
||||||
?: previousResume?.validator?.takeIf { append }
|
?: 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) {
|
if (validator != null) {
|
||||||
saveResumeInfo(
|
saveResumeInfo(
|
||||||
ResumeInfo(
|
ResumeInfo(
|
||||||
@ -511,7 +551,9 @@ class UniversalApkManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.byteStream().use { input ->
|
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)
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
var bytesRead: Int
|
var bytesRead: Int
|
||||||
var totalBytesRead = resumedBytes
|
var totalBytesRead = resumedBytes
|
||||||
|
|||||||
@ -92,7 +92,11 @@ class WorkManagerApkDownloader(context: Context) : ApkDownloader {
|
|||||||
ApkDownloader.DownloadState.Failed(
|
ApkDownloader.DownloadState.Failed(
|
||||||
reason = reason,
|
reason = reason,
|
||||||
messageArgs = args,
|
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 -> {
|
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">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_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_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. -->
|
<!-- 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_get_universal">Download universal APK</string>
|
||||||
<string name="prepare_apk_retry">Retry download</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_button_delete">Delete</string>
|
||||||
<string name="prepare_apk_info">Version %1$s • %2$d MB</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_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_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_dialog_confirm">Download</string>
|
||||||
<string name="prepare_apk_downloading_title">Downloading Universal APK</string>
|
<string name="prepare_apk_downloading_title">Downloading Universal APK</string>
|
||||||
<string name="prepare_apk_downloading_message">Downloading %1$d MB…</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
|
@Test
|
||||||
fun `nothing left to fetch means the row is inert`() {
|
fun `a standalone installed universal apk can optionally be replaced from github`() {
|
||||||
assertNull(prepareRowTapAction(ready(ShareableApkVariant.UNIVERSAL)))
|
assertEquals(
|
||||||
|
PrepareRowTapAction.OpenPrepareDialog,
|
||||||
|
prepareRowTapAction(ready(ShareableApkVariant.UNIVERSAL))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a current downloaded universal apk leaves the row inert`() {
|
||||||
assertNull(
|
assertNull(
|
||||||
prepareRowTapAction(
|
prepareRowTapAction(
|
||||||
ready(ShareableApkVariant.UNIVERSAL, UniversalApkManager.ApkSource.DOWNLOADED)
|
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
|
@Test
|
||||||
fun `a missing apk asks before spending the bytes`() {
|
fun `a missing apk asks before spending the bytes`() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@ -72,4 +95,34 @@ class PrepareRowTapActionTest {
|
|||||||
}
|
}
|
||||||
assertNull(prepareRowTapAction(ApkPreparationStatus.Loading))
|
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.assertFalse
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class ApkDownloadSourceTest {
|
class ApkDownloadSourceTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
private val source = ApkDownloadSource(
|
private val source = ApkDownloadSource(
|
||||||
id = "mirror-one",
|
id = "mirror-one",
|
||||||
displayName = "Mirror One",
|
displayName = "Mirror One",
|
||||||
@ -120,6 +125,28 @@ class ApkDownloadSourceTest {
|
|||||||
assertNull(parseContentRange("bytes 90-100/100"))
|
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
|
@Test
|
||||||
fun `version comparison is host independent`() {
|
fun `version comparison is host independent`() {
|
||||||
assertTrue(AppVersion.isNewer("1.7.4", "1.7.5"))
|
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
|
||||||
compose-bom = "2026.06.01"
|
compose-bom = "2026.06.01"
|
||||||
compose-icons-extended = "1.7.8"
|
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
|
||||||
navigation-compose = "2.9.8"
|
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-graphics = { module = "androidx.compose.ui:ui-graphics" }
|
||||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
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" }
|
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose-icons-extended" }
|
||||||
|
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
@ -124,6 +121,7 @@ nordic-ble = { module = "no.nordicsemi.android:ble", version.ref = "nordic-ble"
|
|||||||
|
|
||||||
# WebSocket
|
# WebSocket
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
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-android-binary = { module = "org.torproject:tor-android-binary", version.ref = "tor-android-binary" }
|
||||||
|
|
||||||
# Tor (embed) intentionally not pinned yet; add once repo is chosen
|
# Tor (embed) intentionally not pinned yet; add once repo is chosen
|
||||||
@ -195,7 +193,8 @@ testing = [
|
|||||||
"mockito-kotlin",
|
"mockito-kotlin",
|
||||||
"mockito-core",
|
"mockito-core",
|
||||||
"roboelectric",
|
"roboelectric",
|
||||||
"kotlinx-coroutines-test"
|
"kotlinx-coroutines-test",
|
||||||
|
"okhttp-mockwebserver"
|
||||||
]
|
]
|
||||||
|
|
||||||
compose-testing = [
|
compose-testing = [
|
||||||
|
|||||||
@ -3658,6 +3658,14 @@
|
|||||||
<sha256 value="e1abd7f1116cf5e0c59947693e2189208ec94296b2a3394c959e3511d399a7b0" origin="Generated by Gradle"/>
|
<sha256 value="e1abd7f1116cf5e0c59947693e2189208ec94296b2a3394c959e3511d399a7b0" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="com.squareup.okhttp3" name="okhttp" version="5.4.0">
|
||||||
<artifact name="okhttp-5.4.0.module">
|
<artifact name="okhttp-5.4.0.module">
|
||||||
<sha256 value="63973ba755ba6c77de1e82f5efa4195569800be00c93c0130ce4238a9b56c93c" origin="Generated by Gradle"/>
|
<sha256 value="63973ba755ba6c77de1e82f5efa4195569800be00c93c0130ce4238a9b56c93c" origin="Generated by Gradle"/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user