Merge pull request #321 from chirayudesai/android11-2.2-merge
Merge master into android11 (11-2.2)
This commit is contained in:
commit
1a48d339d5
255 changed files with 11579 additions and 475 deletions
|
@ -1,4 +1,24 @@
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
indent_size=4
|
indent_size = 4
|
||||||
insert_final_newline=true
|
max_line_length = 100
|
||||||
max_line_length=100
|
|
||||||
|
[app/src/main/res/values-*/strings.xml]
|
||||||
|
insert_final_newline = unset
|
||||||
|
trim_trailing_whitespace = unset
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[gradlew.bat]
|
||||||
|
charset = latin1
|
||||||
|
end_of_line = crlf
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
|
[.editorconfig]
|
||||||
|
ij_editorconfig_spaces_around_assignment_operators = true
|
||||||
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -6,11 +6,12 @@ hs_err_pid*
|
||||||
|
|
||||||
## Intellij
|
## Intellij
|
||||||
out/
|
out/
|
||||||
lib/
|
/lib/
|
||||||
.idea/*
|
.idea/*
|
||||||
!.idea/runConfigurations*
|
!.idea/runConfigurations*
|
||||||
!.idea/inspectionProfiles*
|
!.idea/inspectionProfiles*
|
||||||
!.idea/codeStyles*
|
!.idea/codeStyles*
|
||||||
|
!.idea/dictionaries*
|
||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -33,7 +34,8 @@ local.properties
|
||||||
|
|
||||||
## NetBeans
|
## NetBeans
|
||||||
**/nbproject/private/
|
**/nbproject/private/
|
||||||
build/
|
/build/
|
||||||
|
/app/build/
|
||||||
nbbuild/
|
nbbuild/
|
||||||
dist/
|
dist/
|
||||||
nbdist/
|
nbdist/
|
||||||
|
@ -50,6 +52,3 @@ gradle-app.setting
|
||||||
|
|
||||||
## Android
|
## Android
|
||||||
gen/
|
gen/
|
||||||
|
|
||||||
## Prebuilt
|
|
||||||
Backup.apk
|
|
||||||
|
|
|
@ -4,15 +4,6 @@
|
||||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
<value />
|
<value />
|
||||||
</option>
|
</option>
|
||||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
|
||||||
<value>
|
|
||||||
<package name="" alias="false" withSubpackages="true" />
|
|
||||||
<package name="java" alias="false" withSubpackages="true" />
|
|
||||||
<package name="javax" alias="false" withSubpackages="true" />
|
|
||||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
|
||||||
<package name="" alias="true" withSubpackages="true" />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
|
14
.idea/dictionaries/user.xml
Normal file
14
.idea/dictionaries/user.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="user">
|
||||||
|
<words>
|
||||||
|
<w>apk</w>
|
||||||
|
<w>chunker</w>
|
||||||
|
<w>ejectable</w>
|
||||||
|
<w>hasher</w>
|
||||||
|
<w>hkdf</w>
|
||||||
|
<w>restorable</w>
|
||||||
|
<w>seedvault</w>
|
||||||
|
<w>snowden</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
|
@ -3,6 +3,7 @@
|
||||||
<component name="RunConfigurationProducerService">
|
<component name="RunConfigurationProducerService">
|
||||||
<option name="ignoredProducers">
|
<option name="ignoredProducers">
|
||||||
<set>
|
<set>
|
||||||
|
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||||
|
|
8
.idea/runConfigurations/All_unit_tests.xml
Normal file
8
.idea/runConfigurations/All_unit_tests.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="All unit tests" type="CompoundRunConfigurationType">
|
||||||
|
<toRun name="Unit tests: storage/lib" type="AndroidJUnit" />
|
||||||
|
<toRun name="Unit tests: app" type="AndroidJUnit" />
|
||||||
|
<toRun name="Unit tests: contactsbackup" type="AndroidJUnit" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
|
@ -1,5 +1,5 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Instrumentation Tests" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests" singleton="true">
|
<configuration default="false" name="Instrumentation tests: app" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests" singleton="true">
|
||||||
<module name="seedvault.app" />
|
<module name="seedvault.app" />
|
||||||
<option name="TESTING_TYPE" value="0" />
|
<option name="TESTING_TYPE" value="0" />
|
||||||
<option name="METHOD_NAME" value="" />
|
<option name="METHOD_NAME" value="" />
|
|
@ -1,5 +1,5 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Unit Tests" type="AndroidJUnit" factoryName="Android JUnit">
|
<configuration default="false" name="Unit tests: app" type="AndroidJUnit" factoryName="Android JUnit">
|
||||||
<module name="seedvault.app" />
|
<module name="seedvault.app" />
|
||||||
<useClassPathOnly />
|
<useClassPathOnly />
|
||||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
15
.idea/runConfigurations/Unit_tests__contactsbackup.xml
Normal file
15
.idea/runConfigurations/Unit_tests__contactsbackup.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Unit tests: contactsbackup" type="AndroidJUnit" factoryName="Android JUnit">
|
||||||
|
<module name="seedvault.contactsbackup" />
|
||||||
|
<useClassPathOnly />
|
||||||
|
<option name="MAIN_CLASS_NAME" value="" />
|
||||||
|
<option name="METHOD_NAME" value="" />
|
||||||
|
<option name="TEST_OBJECT" value="directory" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
|
||||||
|
<dir value="$PROJECT_DIR$/contactsbackup/src/test/java" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
15
.idea/runConfigurations/Unit_tests__storage_lib.xml
Normal file
15
.idea/runConfigurations/Unit_tests__storage_lib.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Unit tests: storage/lib" type="AndroidJUnit" factoryName="Android JUnit">
|
||||||
|
<module name="seedvault.storage.lib" />
|
||||||
|
<useClassPathOnly />
|
||||||
|
<option name="MAIN_CLASS_NAME" value="" />
|
||||||
|
<option name="METHOD_NAME" value="" />
|
||||||
|
<option name="TEST_OBJECT" value="directory" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
|
||||||
|
<dir value="$PROJECT_DIR$/storage/lib/src/test/java" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="seedvault:storage:lib [assembleRelease]" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/storage/lib" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value="assembleRelease" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" value="" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
|
@ -31,10 +31,17 @@ android_app {
|
||||||
"androidx.lifecycle_lifecycle-livedata-ktx",
|
"androidx.lifecycle_lifecycle-livedata-ktx",
|
||||||
"androidx-constraintlayout_constraintlayout",
|
"androidx-constraintlayout_constraintlayout",
|
||||||
"com.google.android.material_material",
|
"com.google.android.material_material",
|
||||||
|
// storage
|
||||||
|
"seedvault-lib-storage", // did not manage to add this as transitive dependency
|
||||||
|
"seedvault-lib-tink-android",
|
||||||
|
"androidx.room_room-runtime",
|
||||||
|
"libprotobuf-java-lite",
|
||||||
|
// koin
|
||||||
"seedvault-lib-koin-core", // did not manage to add this as transitive dependency
|
"seedvault-lib-koin-core", // did not manage to add this as transitive dependency
|
||||||
"seedvault-lib-koin-android",
|
"seedvault-lib-koin-android",
|
||||||
"seedvault-lib-koin-androidx-viewmodel",
|
"seedvault-lib-koin-androidx-viewmodel",
|
||||||
"seedvault-lib-novacrypto-bip39",
|
// bip39
|
||||||
|
"seedvault-lib-kotlin-bip39",
|
||||||
],
|
],
|
||||||
manifest: "app/src/main/AndroidManifest.xml",
|
manifest: "app/src/main/AndroidManifest.xml",
|
||||||
|
|
||||||
|
|
64
CHANGELOG.md
64
CHANGELOG.md
|
@ -1,6 +1,66 @@
|
||||||
|
## [11-2.2] - 2021-09-29
|
||||||
|
### User-facing changes
|
||||||
|
* Don't backup on metered networks
|
||||||
|
* Disable spell-checker on recovery code input
|
||||||
|
* Disable Nextcloud restore when not installed and no store available
|
||||||
|
* Ask for system authentication before storing a new recovery code
|
||||||
|
* Prevent screenshots of recovery code
|
||||||
|
* Add expert settings with an option for unlimited quota
|
||||||
|
* Allow launching restore through a dialer code
|
||||||
|
* Restrict exported components
|
||||||
|
|
||||||
|
### Others
|
||||||
|
* Improve .editorconfig setup
|
||||||
|
* Move LocalContactsBackup to product partition
|
||||||
|
* Link FAQ in Readme to make it more discoverable
|
||||||
|
* Compares kotlin-bip39 library with bitcoinj library
|
||||||
|
* Provide an overview over key derivations
|
||||||
|
* document potential information leakage through the long-lived SQL caches
|
||||||
|
* Add warning for third-party tools to README
|
||||||
|
|
||||||
|
## [11-2.1] - 2021-07-06
|
||||||
|
### Updated
|
||||||
|
* Switch to a different BIP39 library due to licensing
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
* Not tagged as a stable release
|
||||||
|
|
||||||
|
## [11-2.0] - 2021-07-05
|
||||||
|
### Added
|
||||||
|
* Storage backup!
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
* Not tagged as a stable release
|
||||||
|
|
||||||
|
## [11-1.2] - 2021-07-05
|
||||||
|
### Fixed
|
||||||
|
* Fix local contacts backup on LineageOS.
|
||||||
|
* Minor string fixes.
|
||||||
|
* Make recovery code fit on smaller screens / larger densities
|
||||||
|
* Sync app colors with system Settings theme for consistency
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
* Translations update, both existing languages and new.
|
||||||
|
* Switch all text references to github.com/seedvault-app
|
||||||
|
|
||||||
|
## [11-1.1] - 2021-04-16
|
||||||
|
### Fixed
|
||||||
|
* Don't crash when storage app gets uninstalled
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Allow verifying and re-generating the 12 word recovery code
|
||||||
|
* Prepare for storage backup
|
||||||
|
* gradle: Use AOSP platform key for signing
|
||||||
|
|
||||||
|
## [11-1.0] - 2021-04-16
|
||||||
|
### Notes
|
||||||
|
* Change versioning scheme to include Android version
|
||||||
|
* 11-1.0 is the first release for Android 11
|
||||||
|
* Incomplete changelog entry, lots of changes done
|
||||||
|
|
||||||
## [1.0.0] - 2020-03-07
|
## [1.0.0] - 2020-03-07
|
||||||
|
|
||||||
## Added
|
### Added
|
||||||
- APK backup and restore support with the option to toggle them off.
|
- APK backup and restore support with the option to toggle them off.
|
||||||
- Note to auto-restore setting in case removable storage is used.
|
- Note to auto-restore setting in case removable storage is used.
|
||||||
- UX for showing which packages were restored and which failed.
|
- UX for showing which packages were restored and which failed.
|
||||||
|
@ -8,7 +68,7 @@
|
||||||
- Show list of apps and their backup status.
|
- Show list of apps and their backup status.
|
||||||
- Support for excluding apps from backups.
|
- Support for excluding apps from backups.
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Device initialization and generation of new backup tokens.
|
- Device initialization and generation of new backup tokens.
|
||||||
|
|
||||||
## [1.0.0-alpha1] - 2019-12-14
|
## [1.0.0-alpha1] - 2019-12-14
|
||||||
|
|
15
README.md
15
README.md
|
@ -3,6 +3,14 @@
|
||||||
|
|
||||||
A backup application for the [Android Open Source Project](https://source.android.com/).
|
A backup application for the [Android Open Source Project](https://source.android.com/).
|
||||||
|
|
||||||
|
If you are having an issue/question, please look at our [FAQ](../../wiki/FAQ).
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
* [Local Contacts Backup](contactsbackup) - an app that backs up local on-device contacts
|
||||||
|
* [Storage library](storage) - a library handling efficient backup of files
|
||||||
|
* [Seedvault app](app) - the main app where all functionality comes together
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Backup application data to a flash drive.
|
- Backup application data to a flash drive.
|
||||||
- Restore application data from a flash drive.
|
- Restore application data from a flash drive.
|
||||||
|
@ -30,7 +38,11 @@ It uses the same internal APIs as `adb backup` which is deprecated and thus need
|
||||||
* `android.permission.WRITE_SECURE_SETTINGS` to change system backup settings and enable call log backup.
|
* `android.permission.WRITE_SECURE_SETTINGS` to change system backup settings and enable call log backup.
|
||||||
* `android.permission.QUERY_ALL_PACKAGES` to get information about all installed apps for backup.
|
* `android.permission.QUERY_ALL_PACKAGES` to get information about all installed apps for backup.
|
||||||
* `android.permission.INSTALL_PACKAGES` to re-install apps when restoring from backup.
|
* `android.permission.INSTALL_PACKAGES` to re-install apps when restoring from backup.
|
||||||
|
* `android.permission.MANAGE_EXTERNAL_STORAGE` to backup and restore files from device storage.
|
||||||
|
* `android.permission.ACCESS_MEDIA_LOCATION` to backup original media files e.g. without stripped EXIF metadata.
|
||||||
|
* `android.permission.FOREGROUND_SERVICE` to do periodic storage backups without interruption.
|
||||||
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots (optional) for better UX.
|
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots (optional) for better UX.
|
||||||
|
* `android.permission.USE_BIOMETRIC` to authenticate saving a new recovery code
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Bug reports and pull requests are welcome on GitHub at https://github.com/seedvault-app/seedvault.
|
Bug reports and pull requests are welcome on GitHub at https://github.com/seedvault-app/seedvault.
|
||||||
|
@ -39,6 +51,9 @@ This project aims to adhere to the [official Kotlin coding style](https://develo
|
||||||
|
|
||||||
## Third-party tools
|
## Third-party tools
|
||||||
|
|
||||||
|
> **⚠ WARNING**: the Seedvault developers make no guarantees about external software projects.
|
||||||
|
> Please be aware that disclosing your secret recovery key to other software has security risks.
|
||||||
|
|
||||||
The [Seedvault backup parser](https://github.com/tlambertz/seedvault_backup_parser)
|
The [Seedvault backup parser](https://github.com/tlambertz/seedvault_backup_parser)
|
||||||
allows you to decrypt and inspect your backups.
|
allows you to decrypt and inspect your backups.
|
||||||
It can also re-encrypt them.
|
It can also re-encrypt them.
|
||||||
|
|
|
@ -16,13 +16,12 @@ def gitDescribe = { ->
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
compileSdkVersion 30
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
buildToolsVersion '30.0.2'
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 29 // leave at 29 for robolectric tests
|
minSdkVersion 29 // leave at 29 for robolectric tests
|
||||||
targetSdkVersion 30
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionNameSuffix "-$gitDescribe"
|
versionNameSuffix "-$gitDescribe"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
testInstrumentationRunnerArguments disableAnalytics: 'true'
|
testInstrumentationRunnerArguments disableAnalytics: 'true'
|
||||||
|
@ -44,6 +43,7 @@ android {
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
languageVersion = "1.3"
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.all {
|
unitTests.all {
|
||||||
|
@ -81,19 +81,77 @@ android {
|
||||||
buildTypes.debug.signingConfig = signingConfigs.aosp
|
buildTypes.debug.signingConfig = signingConfigs.aosp
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../gradle/dependencies.gradle'
|
dependencies {
|
||||||
|
compileOnly rootProject.ext.aosp_libs
|
||||||
|
|
||||||
ktlint {
|
/**
|
||||||
version = "0.36.0" // https://github.com/pinterest/ktlint/issues/764
|
* Dependencies in AOSP
|
||||||
android = true
|
*
|
||||||
enableExperimentalRules = false
|
* We try to keep the dependencies in sync with what AOSP ships as Seedvault is meant to be built
|
||||||
verbose = true
|
* with the AOSP build system and gradle builds are just for more pleasant development.
|
||||||
disabledRules = [
|
* Using the AOSP versions in gradle builds allows us to spot issues early on.
|
||||||
"import-ordering",
|
*/
|
||||||
"no-blank-line-before-rbrace",
|
implementation rootProject.ext.kotlin_libs.std
|
||||||
]
|
// These coroutine libraries get upgraded otherwise to versions incompatible with kotlin version
|
||||||
|
implementation rootProject.ext.kotlin_libs.coroutines
|
||||||
|
|
||||||
|
implementation rootProject.ext.std_libs.androidx_core
|
||||||
|
// A newer version gets pulled in with AOSP via core, so we include fragment here explicitly
|
||||||
|
implementation rootProject.ext.std_libs.androidx_fragment
|
||||||
|
implementation rootProject.ext.std_libs.androidx_preference
|
||||||
|
implementation rootProject.ext.std_libs.androidx_lifecycle_viewmodel_ktx
|
||||||
|
implementation rootProject.ext.std_libs.androidx_lifecycle_livedata_ktx
|
||||||
|
implementation rootProject.ext.std_libs.androidx_constraintlayout
|
||||||
|
implementation rootProject.ext.std_libs.androidx_documentfile
|
||||||
|
implementation rootProject.ext.std_libs.com_google_android_material
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage Dependencies
|
||||||
|
*/
|
||||||
|
implementation project(':storage:lib')
|
||||||
|
// implementation fileTree(include: ['storage.aar'], dir: "${rootProject.rootDir}/storage/lib/libs")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External Dependencies
|
||||||
|
*
|
||||||
|
* If the dependencies below are updated,
|
||||||
|
* please make sure to update the prebuilt libraries and the Android.bp files
|
||||||
|
* in the top-level `libs` folder to reflect that.
|
||||||
|
* You can copy these libraries from ~/.gradle/caches/modules-2
|
||||||
|
*/
|
||||||
|
// later versions than 2.1.1 require newer kotlin version
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: "${rootProject.rootDir}/libs/koin-android")
|
||||||
|
implementation fileTree(include: ['*.aar'], dir: "${rootProject.rootDir}/libs/koin-android")
|
||||||
|
|
||||||
|
implementation fileTree(include: ['kotlin-bip39-1.0.2.jar'], dir: "${rootProject.rootDir}/libs")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Dependencies (do not concern the AOSP build)
|
||||||
|
*/
|
||||||
|
lintChecks rootProject.ext.lint_libs.exceptions
|
||||||
|
|
||||||
|
// anything less than 'implementation' fails tests run with gradlew
|
||||||
|
testImplementation rootProject.ext.aosp_libs
|
||||||
|
testImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
|
testImplementation('org.robolectric:robolectric:4.3.1') { // 4.4 has issue with non-idle Looper
|
||||||
|
// https://github.com/robolectric/robolectric/issues/5245
|
||||||
|
exclude group: "com.google.auto.service", module: "auto-service"
|
||||||
|
}
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5_version"
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5_version"
|
||||||
|
testImplementation "io.mockk:mockk:$mockk_version"
|
||||||
|
testImplementation 'org.bitcoinj:bitcoinj-core:0.15.10'
|
||||||
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
|
||||||
|
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit5_version"
|
||||||
|
|
||||||
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
|
androidTestImplementation "io.mockk:mockk-android:$mockk_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
|
||||||
|
|
||||||
gradle.projectsEvaluated {
|
gradle.projectsEvaluated {
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
|
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.stevesoltys.seedvault"
|
package="com.stevesoltys.seedvault"
|
||||||
android:versionCode="30000021"
|
android:versionCode="30000221"
|
||||||
android:versionName="11-1.2">
|
android:versionName="11-2.2">
|
||||||
<!--
|
<!--
|
||||||
The version code is the targeted SDK_VERSION plus 6 digits for our own version code.
|
The version code is the targeted SDK_VERSION plus 6 digits for our own version code.
|
||||||
The version name is the targeted Android version followed by - and our own version name.
|
The version name is the targeted Android version followed by - and our own version name.
|
||||||
|
@ -47,6 +47,19 @@
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
|
<!-- Used to authenticate saving a new recovery code -->
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
|
||||||
|
<!-- Permission used to open settings -->
|
||||||
|
<permission
|
||||||
|
android:name="com.stevesoltys.seedvault.OPEN_SETTINGS"
|
||||||
|
android:protectionLevel="system|signature" />
|
||||||
|
|
||||||
|
<!-- Permission used to open backup gui -->
|
||||||
|
<permission
|
||||||
|
android:name="com.stevesoltys.seedvault.RESTORE_BACKUP"
|
||||||
|
android:protectionLevel="system|signature" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
@ -59,6 +72,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
|
android:permission="com.stevesoltys.seedvault.OPEN_SETTINGS"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
@ -77,6 +91,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".restore.RestoreActivity"
|
android:name=".restore.RestoreActivity"
|
||||||
|
android:permission="com.stevesoltys.seedvault.RESTORE_BACKUP"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/restore_title"
|
android:label="@string/restore_title"
|
||||||
android:theme="@style/AppTheme.NoActionBar">
|
android:theme="@style/AppTheme.NoActionBar">
|
||||||
|
@ -113,5 +128,33 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".SecretCodeReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.telephony.action.SECRET_CODE" />
|
||||||
|
<!-- *#*#RESTORE#*#* -->
|
||||||
|
<data android:scheme="android_secret_code" android:host="7378673" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- Used to start actual BackupService depending on scheduling criteria -->
|
||||||
|
<service
|
||||||
|
android:name=".storage.StorageBackupJobService"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="BackupJobService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
<!-- Does the actual backup work as a foreground service -->
|
||||||
|
<service
|
||||||
|
android:name=".storage.StorageBackupService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:label="BackupService" />
|
||||||
|
<!-- Does restore as a foreground service -->
|
||||||
|
<service
|
||||||
|
android:name=".storage.StorageRestoreService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:label="RestoreService" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -17,8 +17,10 @@ import com.stevesoltys.seedvault.restore.install.installModule
|
||||||
import com.stevesoltys.seedvault.settings.AppListRetriever
|
import com.stevesoltys.seedvault.settings.AppListRetriever
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
||||||
|
import com.stevesoltys.seedvault.storage.storageModule
|
||||||
import com.stevesoltys.seedvault.transport.backup.backupModule
|
import com.stevesoltys.seedvault.transport.backup.backupModule
|
||||||
import com.stevesoltys.seedvault.transport.restore.restoreModule
|
import com.stevesoltys.seedvault.transport.restore.restoreModule
|
||||||
|
import com.stevesoltys.seedvault.ui.files.FileSelectionViewModel
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
|
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
|
||||||
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
||||||
|
@ -43,11 +45,12 @@ open class App : Application() {
|
||||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||||
factory { AppListRetriever(this@App, get(), get(), get()) }
|
factory { AppListRetriever(this@App, get(), get(), get()) }
|
||||||
|
|
||||||
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get()) }
|
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get()) }
|
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get()) }
|
||||||
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
|
viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) }
|
||||||
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
||||||
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get()) }
|
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
|
viewModel { FileSelectionViewModel(this@App, get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
@ -85,6 +88,7 @@ open class App : Application() {
|
||||||
backupModule,
|
backupModule,
|
||||||
restoreModule,
|
restoreModule,
|
||||||
installModule,
|
installModule,
|
||||||
|
storageModule,
|
||||||
appModule
|
appModule
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
import android.util.Log
|
||||||
|
import com.stevesoltys.seedvault.restore.RestoreActivity
|
||||||
|
|
||||||
|
private val TAG = BroadcastReceiver::class.java.simpleName
|
||||||
|
private val RESTORE_SECRET_CODE = "7378673"
|
||||||
|
|
||||||
|
class SecretCodeReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val host = intent.data.host
|
||||||
|
if (!RESTORE_SECRET_CODE.equals(host)) return
|
||||||
|
Log.d(TAG, "Restore secret code received.")
|
||||||
|
val i = Intent(context, RestoreActivity::class.java).apply {
|
||||||
|
flags = FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
context.startActivity(i)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,9 +14,12 @@ import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat.startForegroundService
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.settings.FlashDrive
|
import com.stevesoltys.seedvault.settings.FlashDrive
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import com.stevesoltys.seedvault.storage.StorageBackupService
|
||||||
|
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
||||||
import com.stevesoltys.seedvault.transport.requestBackup
|
import com.stevesoltys.seedvault.transport.requestBackup
|
||||||
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
|
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
|
||||||
import org.koin.core.context.KoinContextHandler.get
|
import org.koin.core.context.KoinContextHandler.get
|
||||||
|
@ -54,10 +57,17 @@ class UsbIntentReceiver : UsbMonitor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
|
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
|
||||||
|
if (settingsManager.isStorageBackupEnabled()) {
|
||||||
|
val i = Intent(context, StorageBackupService::class.java)
|
||||||
|
// this starts an app backup afterwards
|
||||||
|
i.putExtra(EXTRA_START_APP_BACKUP, true)
|
||||||
|
startForegroundService(context, i)
|
||||||
|
} else {
|
||||||
Thread {
|
Thread {
|
||||||
requestBackup(context)
|
requestBackup(context)
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,11 +44,6 @@ internal class DocumentsProviderBackupPlugin(
|
||||||
storage.currentFullBackupDir ?: throw IOException()
|
storage.currentFullBackupDir ?: throw IOException()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun deleteAllBackups() {
|
|
||||||
storage.rootBackupDir?.deleteContents(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override suspend fun getMetadataOutputStream(): OutputStream {
|
override suspend fun getMetadataOutputStream(): OutputStream {
|
||||||
val setDir = storage.getSetDir() ?: throw IOException()
|
val setDir = storage.getSetDir() ?: throw IOException()
|
||||||
|
|
|
@ -24,6 +24,9 @@ internal class DocumentsProviderRestorePlugin(
|
||||||
override val fullRestorePlugin: FullRestorePlugin
|
override val fullRestorePlugin: FullRestorePlugin
|
||||||
) : RestorePlugin {
|
) : RestorePlugin {
|
||||||
|
|
||||||
|
private val tokenRegex = Regex("([0-9]{13})") // good until the year 2286
|
||||||
|
private val chunkFolderRegex = Regex("[a-f0-9]{2}")
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override suspend fun hasBackup(uri: Uri): Boolean {
|
override suspend fun hasBackup(uri: Uri): Boolean {
|
||||||
val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
|
val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
|
||||||
|
@ -59,18 +62,21 @@ internal class DocumentsProviderRestorePlugin(
|
||||||
return backupSets
|
return backupSets
|
||||||
}
|
}
|
||||||
for (set in files) {
|
for (set in files) {
|
||||||
|
// retrieve name only once as this causes a DB query
|
||||||
|
val name = set.name
|
||||||
|
|
||||||
// get current token from set or continue to next file/set
|
// get current token from set or continue to next file/set
|
||||||
val token = set.getTokenOrNull() ?: continue
|
val token = set.getTokenOrNull(name) ?: continue
|
||||||
|
|
||||||
// block until children of set are available
|
// block until children of set are available
|
||||||
val metadata = try {
|
val metadata = try {
|
||||||
set.findFileBlocking(context, FILE_BACKUP_METADATA)
|
set.findFileBlocking(context, FILE_BACKUP_METADATA)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error reading metadata file in backup set folder: ${set.name}", e)
|
Log.e(TAG, "Error reading metadata file in backup set folder: $name", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
if (metadata == null) {
|
if (metadata == null) {
|
||||||
Log.w(TAG, "Missing metadata file in backup set folder: ${set.name}")
|
Log.w(TAG, "Missing metadata file in backup set folder: $name")
|
||||||
} else {
|
} else {
|
||||||
backupSets.add(BackupSet(token, metadata))
|
backupSets.add(BackupSet(token, metadata))
|
||||||
}
|
}
|
||||||
|
@ -78,21 +84,29 @@ internal class DocumentsProviderRestorePlugin(
|
||||||
return backupSets
|
return backupSets
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DocumentFile.getTokenOrNull(): Long? {
|
private fun DocumentFile.getTokenOrNull(name: String?): Long? {
|
||||||
if (!isDirectory || name == null) {
|
val looksLikeToken = name != null && tokenRegex.matches(name)
|
||||||
if (name != FILE_NO_MEDIA) {
|
// check for isDirectory only if we already have a valid token (causes DB query)
|
||||||
|
if (!looksLikeToken || !isDirectory) {
|
||||||
|
// only log unexpected output
|
||||||
|
if (name != null && isUnexpectedFile(name)) {
|
||||||
Log.w(TAG, "Found invalid backup set folder: $name")
|
Log.w(TAG, "Found invalid backup set folder: $name")
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
name!!.toLong()
|
name?.toLong()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
Log.w(TAG, "Found invalid backup set folder: $name")
|
throw AssertionError(e)
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isUnexpectedFile(name: String): Boolean {
|
||||||
|
return name != FILE_NO_MEDIA &&
|
||||||
|
!chunkFolderRegex.matches(name) &&
|
||||||
|
!name.endsWith(".SeedSnap")
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override suspend fun getApkInputStream(
|
override suspend fun getApkInputStream(
|
||||||
token: Long,
|
token: Long,
|
||||||
|
|
|
@ -5,6 +5,8 @@ import androidx.annotation.CallSuper
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||||
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
||||||
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
|
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
|
||||||
import com.stevesoltys.seedvault.ui.LiveEventHandler
|
import com.stevesoltys.seedvault.ui.LiveEventHandler
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
||||||
|
@ -28,6 +30,8 @@ class RestoreActivity : RequireProvisioningActivity() {
|
||||||
when (fragment) {
|
when (fragment) {
|
||||||
RESTORE_APPS -> showFragment(InstallProgressFragment())
|
RESTORE_APPS -> showFragment(InstallProgressFragment())
|
||||||
RESTORE_BACKUP -> showFragment(RestoreProgressFragment())
|
RESTORE_BACKUP -> showFragment(RestoreProgressFragment())
|
||||||
|
RESTORE_FILES -> showFragment(RestoreFilesFragment())
|
||||||
|
RESTORE_FILES_STARTED -> showFragment(RestoreFilesStartedFragment())
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.stevesoltys.seedvault.restore
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewStub
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import org.calyxos.backup.storage.api.SnapshotItem
|
||||||
|
import org.calyxos.backup.storage.ui.restore.SnapshotFragment
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
|
internal class RestoreFilesFragment : SnapshotFragment() {
|
||||||
|
override val viewModel: RestoreViewModel by sharedViewModel()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
val topStub: ViewStub = v.findViewById(R.id.topStub)
|
||||||
|
topStub.layoutResource = R.layout.header_snapshots
|
||||||
|
topStub.inflate()
|
||||||
|
|
||||||
|
val bottomStub: ViewStub = v.findViewById(R.id.bottomStub)
|
||||||
|
bottomStub.layoutResource = R.layout.footer_snapshots
|
||||||
|
val footer = bottomStub.inflate()
|
||||||
|
val skipView: TextView = footer.findViewById(R.id.skipView)
|
||||||
|
skipView.setOnClickListener {
|
||||||
|
requireActivity().apply {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSnapshotClicked(item: SnapshotItem) {
|
||||||
|
viewModel.startFilesRestore(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class RestoreFilesStartedFragment : Fragment() {
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val v: View = inflater.inflate(R.layout.fragment_restore_files_started, container, false)
|
||||||
|
|
||||||
|
val button: Button = v.findViewById(R.id.button)
|
||||||
|
button.setOnClickListener {
|
||||||
|
requireActivity().apply {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.restore
|
||||||
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -38,7 +37,7 @@ class RestoreProgressFragment : Fragment() {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
val v: View = inflater.inflate(R.layout.fragment_restore_progress, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_restore_progress, container, false)
|
||||||
|
|
||||||
progressBar = v.findViewById(R.id.progressBar)
|
progressBar = v.findViewById(R.id.progressBar)
|
||||||
|
@ -61,8 +60,7 @@ class RestoreProgressFragment : Fragment() {
|
||||||
|
|
||||||
button.setText(R.string.restore_finished_button)
|
button.setText(R.string.restore_finished_button)
|
||||||
button.setOnClickListener {
|
button.setOnClickListener {
|
||||||
requireActivity().setResult(RESULT_OK)
|
viewModel.onFinishClickedAfterRestoringAppData()
|
||||||
requireActivity().finishAfterTransition()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// decryption will fail when the device is locked, so keep the screen on to prevent locking
|
// decryption will fail when the device is locked, so keep the screen on to prevent locking
|
||||||
|
|
|
@ -22,19 +22,19 @@ class RestoreSetFragment : Fragment() {
|
||||||
private lateinit var listView: RecyclerView
|
private lateinit var listView: RecyclerView
|
||||||
private lateinit var progressBar: ProgressBar
|
private lateinit var progressBar: ProgressBar
|
||||||
private lateinit var errorView: TextView
|
private lateinit var errorView: TextView
|
||||||
private lateinit var backView: TextView
|
private lateinit var skipView: TextView
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
val v: View = inflater.inflate(R.layout.fragment_restore_set, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_restore_set, container, false)
|
||||||
|
|
||||||
listView = v.findViewById(R.id.listView)
|
listView = v.findViewById(R.id.listView)
|
||||||
progressBar = v.findViewById(R.id.progressBar)
|
progressBar = v.findViewById(R.id.progressBar)
|
||||||
errorView = v.findViewById(R.id.errorView)
|
errorView = v.findViewById(R.id.errorView)
|
||||||
backView = v.findViewById(R.id.backView)
|
skipView = v.findViewById(R.id.skipView)
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,9 @@ class RestoreSetFragment : Fragment() {
|
||||||
onRestoreResultsLoaded(result)
|
onRestoreResultsLoaded(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
backView.setOnClickListener { requireActivity().finishAfterTransition() }
|
skipView.setOnClickListener {
|
||||||
|
viewModel.onFinishClickedAfterRestoringAppData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
@ -5,9 +5,11 @@ import android.app.backup.IBackupManager
|
||||||
import android.app.backup.IRestoreObserver
|
import android.app.backup.IRestoreObserver
|
||||||
import android.app.backup.IRestoreSession
|
import android.app.backup.IRestoreSession
|
||||||
import android.app.backup.RestoreSet
|
import android.app.backup.RestoreSet
|
||||||
|
import android.content.Intent
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
@ -27,11 +29,14 @@ import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||||
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
||||||
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkRestore
|
import com.stevesoltys.seedvault.restore.install.ApkRestore
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
|
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallResult
|
import com.stevesoltys.seedvault.restore.install.InstallResult
|
||||||
import com.stevesoltys.seedvault.restore.install.isInstalled
|
import com.stevesoltys.seedvault.restore.install.isInstalled
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import com.stevesoltys.seedvault.storage.StorageRestoreService
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||||
|
@ -54,6 +59,11 @@ import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.calyxos.backup.storage.api.SnapshotItem
|
||||||
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTAMP_START
|
||||||
|
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
|
||||||
|
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
@ -68,8 +78,10 @@ internal class RestoreViewModel(
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val restoreCoordinator: RestoreCoordinator,
|
private val restoreCoordinator: RestoreCoordinator,
|
||||||
private val apkRestore: ApkRestore,
|
private val apkRestore: ApkRestore,
|
||||||
|
storageBackup: StorageBackup,
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager), RestorableBackupClickListener {
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager),
|
||||||
|
RestorableBackupClickListener, SnapshotViewModel {
|
||||||
|
|
||||||
override val isRestoreOperation = true
|
override val isRestoreOperation = true
|
||||||
|
|
||||||
|
@ -110,6 +122,8 @@ internal class RestoreViewModel(
|
||||||
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
|
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
|
||||||
internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
|
internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
|
||||||
|
|
||||||
|
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
|
||||||
|
|
||||||
@Throws(RemoteException::class)
|
@Throws(RemoteException::class)
|
||||||
private fun getOrStartSession(): IRestoreSession {
|
private fun getOrStartSession(): IRestoreSession {
|
||||||
val session = this.session
|
val session = this.session
|
||||||
|
@ -168,7 +182,7 @@ internal class RestoreViewModel(
|
||||||
.asLiveData(ioDispatcher)
|
.asLiveData(ioDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun onNextClicked() {
|
internal fun onNextClickedAfterInstallingApps() {
|
||||||
mDisplayFragment.postEvent(RESTORE_BACKUP)
|
mDisplayFragment.postEvent(RESTORE_BACKUP)
|
||||||
val token = mChosenRestorableBackup.value?.token ?: throw AssertionError()
|
val token = mChosenRestorableBackup.value?.token ?: throw AssertionError()
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
@ -371,6 +385,20 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
internal fun onFinishClickedAfterRestoringAppData() {
|
||||||
|
mDisplayFragment.setEvent(RESTORE_FILES)
|
||||||
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
internal fun startFilesRestore(item: SnapshotItem) {
|
||||||
|
val i = Intent(app, StorageRestoreService::class.java)
|
||||||
|
i.putExtra(EXTRA_USER_ID, item.storedSnapshot.userId)
|
||||||
|
i.putExtra(EXTRA_TIMESTAMP_START, item.time)
|
||||||
|
app.startForegroundService(i)
|
||||||
|
mDisplayFragment.setEvent(RESTORE_FILES_STARTED)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class RestoreSetResult(
|
internal class RestoreSetResult(
|
||||||
|
@ -389,4 +417,6 @@ internal class RestoreBackupResult(val errorMsg: String? = null) {
|
||||||
internal fun hasError(): Boolean = errorMsg != null
|
internal fun hasError(): Boolean = errorMsg != null
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum class DisplayFragment { RESTORE_APPS, RESTORE_BACKUP }
|
internal enum class DisplayFragment {
|
||||||
|
RESTORE_APPS, RESTORE_BACKUP, RESTORE_FILES, RESTORE_FILES_STARTED
|
||||||
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ class InstallProgressFragment : Fragment(), InstallItemListener {
|
||||||
addItemDecoration(DividerItemDecoration(context, VERTICAL))
|
addItemDecoration(DividerItemDecoration(context, VERTICAL))
|
||||||
}
|
}
|
||||||
button.setText(R.string.restore_next)
|
button.setText(R.string.restore_next)
|
||||||
button.setOnClickListener { viewModel.onNextClicked() }
|
button.setOnClickListener { viewModel.onNextClickedAfterInstallingApps() }
|
||||||
|
|
||||||
viewModel.chosenRestorableBackup.observe(viewLifecycleOwner, Observer { restorableBackup ->
|
viewModel.chosenRestorableBackup.observe(viewLifecycleOwner, Observer { restorableBackup ->
|
||||||
backupNameView.text = restorableBackup.name
|
backupNameView.text = restorableBackup.name
|
||||||
|
@ -76,7 +76,7 @@ class InstallProgressFragment : Fragment(), InstallItemListener {
|
||||||
|
|
||||||
private fun onInstallResult(installResult: InstallResult) {
|
private fun onInstallResult(installResult: InstallResult) {
|
||||||
// skip this screen, if there are no apps to install
|
// skip this screen, if there are no apps to install
|
||||||
if (installResult.isEmpty) viewModel.onNextClicked()
|
if (installResult.isEmpty) viewModel.onNextClickedAfterInstallingApps()
|
||||||
|
|
||||||
// if finished, treat all still queued apps as failed and resort/redisplay adapter items
|
// if finished, treat all still queued apps as failed and resort/redisplay adapter items
|
||||||
if (installResult.isFinished) {
|
if (installResult.isFinished) {
|
||||||
|
|
|
@ -36,7 +36,7 @@ class AppStatusFragment : Fragment(), AppStatusToggleListener {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
val v: View = inflater.inflate(R.layout.fragment_app_status, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_app_status, container, false)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
|
|
||||||
|
class ExpertSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
permitDiskReads {
|
||||||
|
setPreferencesFromResource(R.xml.settings_expert, rootKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
activity?.setTitle(R.string.settings_expert_title)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context.BACKUP_SERVICE // ktlint-disable no-unused-imports
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE // ktlint-disable no-unused-imports
|
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
@ -38,6 +37,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
private lateinit var apkBackup: TwoStatePreference
|
private lateinit var apkBackup: TwoStatePreference
|
||||||
private lateinit var backupLocation: Preference
|
private lateinit var backupLocation: Preference
|
||||||
private lateinit var backupStatus: Preference
|
private lateinit var backupStatus: Preference
|
||||||
|
private lateinit var backupStorage: TwoStatePreference
|
||||||
|
private lateinit var backupRecoveryCode: Preference
|
||||||
|
|
||||||
private var menuBackupNow: MenuItem? = null
|
private var menuBackupNow: MenuItem? = null
|
||||||
private var menuRestore: MenuItem? = null
|
private var menuRestore: MenuItem? = null
|
||||||
|
@ -102,6 +103,19 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
return@OnPreferenceChangeListener false
|
return@OnPreferenceChangeListener false
|
||||||
}
|
}
|
||||||
backupStatus = findPreference("backup_status")!!
|
backupStatus = findPreference("backup_status")!!
|
||||||
|
|
||||||
|
backupStorage = findPreference("backup_storage")!!
|
||||||
|
backupStorage.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||||
|
val disable = !(newValue as Boolean)
|
||||||
|
if (disable) {
|
||||||
|
viewModel.disableStorageBackup()
|
||||||
|
return@OnPreferenceChangeListener true
|
||||||
|
}
|
||||||
|
onEnablingStorageBackup()
|
||||||
|
return@OnPreferenceChangeListener false
|
||||||
|
}
|
||||||
|
|
||||||
|
backupRecoveryCode = findPreference("backup_recovery_code")!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -110,6 +124,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
viewModel.lastBackupTime.observe(viewLifecycleOwner, Observer { time ->
|
viewModel.lastBackupTime.observe(viewLifecycleOwner, Observer { time ->
|
||||||
setAppBackupStatusSummary(time)
|
setAppBackupStatusSummary(time)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
val backupFiles: Preference = findPreference("backup_files")!!
|
||||||
|
viewModel.filesSummary.observe(viewLifecycleOwner, Observer { summary ->
|
||||||
|
backupFiles.summary = summary
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -147,6 +166,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
startActivity(Intent(requireContext(), RestoreActivity::class.java))
|
startActivity(Intent(requireContext(), RestoreActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_settings_expert -> {
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragment, ExpertSettingsFragment())
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.action_about -> {
|
R.id.action_about -> {
|
||||||
AboutDialogFragment().show(parentFragmentManager, AboutDialogFragment.TAG)
|
AboutDialogFragment().show(parentFragmentManager, AboutDialogFragment.TAG)
|
||||||
true
|
true
|
||||||
|
@ -190,4 +216,40 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
backupStatus.summary = getString(R.string.settings_backup_status_summary, lastBackup)
|
backupStatus.summary = getString(R.string.settings_backup_status_summary, lastBackup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onEnablingStorageBackup() {
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setIcon(R.drawable.ic_warning)
|
||||||
|
.setTitle(R.string.settings_backup_storage_dialog_title)
|
||||||
|
.setMessage(R.string.settings_backup_storage_dialog_message)
|
||||||
|
.setPositiveButton(R.string.settings_backup_storage_dialog_ok) { dialog, _ ->
|
||||||
|
if (viewModel.hasMainKey()) {
|
||||||
|
viewModel.enableStorageBackup()
|
||||||
|
backupStorage.isChecked = true
|
||||||
|
} else {
|
||||||
|
showCodeVerificationNeededDialog()
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.settings_backup_apk_dialog_cancel) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCodeVerificationNeededDialog() {
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setIcon(R.drawable.ic_vpn_key)
|
||||||
|
.setTitle(R.string.settings_backup_storage_code_dialog_title)
|
||||||
|
.setMessage(R.string.settings_backup_storage_code_dialog_message)
|
||||||
|
.setPositiveButton(R.string.settings_backup_storage_code_dialog_ok) { dialog, _ ->
|
||||||
|
val callback = (requireActivity() as OnPreferenceStartFragmentCallback)
|
||||||
|
callback.onPreferenceStartFragment(this, backupRecoveryCode)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.settings_backup_apk_dialog_cancel) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,9 @@ private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
|
||||||
|
|
||||||
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
|
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
|
||||||
|
|
||||||
|
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
||||||
|
private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
||||||
|
|
||||||
class SettingsManager(private val context: Context) {
|
class SettingsManager(private val context: Context) {
|
||||||
|
|
||||||
private val prefs = permitDiskReads {
|
private val prefs = permitDiskReads {
|
||||||
|
@ -48,10 +51,10 @@ class SettingsManager(private val context: Context) {
|
||||||
ConcurrentSkipListSet(prefs.getStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, emptySet()))
|
ConcurrentSkipListSet(prefs.getStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, emptySet()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToken(): Long? = token ?: {
|
fun getToken(): Long? = token ?: run {
|
||||||
val value = prefs.getLong(PREF_KEY_TOKEN, 0L)
|
val value = prefs.getLong(PREF_KEY_TOKEN, 0L)
|
||||||
if (value == 0L) null else value
|
if (value == 0L) null else value
|
||||||
}()
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a new RestoreSet token.
|
* Sets a new RestoreSet token.
|
||||||
|
@ -138,6 +141,8 @@ class SettingsManager(private val context: Context) {
|
||||||
|
|
||||||
fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName)
|
fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName)
|
||||||
|
|
||||||
|
fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false)
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
fun onAppBackupStatusChanged(status: AppStatus) {
|
fun onAppBackupStatusChanged(status: AppStatus) {
|
||||||
if (status.enabled) blacklistedApps.remove(status.packageName)
|
if (status.enabled) blacklistedApps.remove(status.packageName)
|
||||||
|
@ -145,6 +150,7 @@ class SettingsManager(private val context: Context) {
|
||||||
prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply()
|
prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isQuotaUnlimited() = prefs.getBoolean(PREF_KEY_UNLIMITED_QUOTA, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Storage(
|
data class Storage(
|
||||||
|
@ -171,13 +177,14 @@ data class Storage(
|
||||||
* but it isn't available right now.
|
* but it isn't available right now.
|
||||||
*/
|
*/
|
||||||
fun isUnavailableNetwork(context: Context): Boolean {
|
fun isUnavailableNetwork(context: Context): Boolean {
|
||||||
return requiresNetwork && !hasInternet(context)
|
return requiresNetwork && !hasUnmeteredInternet(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasInternet(context: Context): Boolean {
|
private fun hasUnmeteredInternet(context: Context): Boolean {
|
||||||
val cm = context.getSystemService(ConnectivityManager::class.java)
|
val cm = context.getSystemService(ConnectivityManager::class.java)
|
||||||
|
val isMetered = cm.isActiveNetworkMetered()
|
||||||
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && !isMetered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.job.JobInfo.NETWORK_TYPE_NONE
|
||||||
|
import android.app.job.JobInfo.NETWORK_TYPE_UNMETERED
|
||||||
|
import android.content.Intent
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
|
@ -12,6 +15,7 @@ import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.Toast.LENGTH_LONG
|
import android.widget.Toast.LENGTH_LONG
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
|
import androidx.core.content.ContextCompat.startForegroundService
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations.switchMap
|
import androidx.lifecycle.Transformations.switchMap
|
||||||
|
@ -22,11 +26,17 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
|
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||||
|
import com.stevesoltys.seedvault.storage.StorageBackupService
|
||||||
|
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
||||||
import com.stevesoltys.seedvault.transport.requestBackup
|
import com.stevesoltys.seedvault.transport.requestBackup
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
import org.calyxos.backup.storage.backup.BackupJobService
|
||||||
|
import java.util.concurrent.TimeUnit.HOURS
|
||||||
|
|
||||||
private const val TAG = "SettingsViewModel"
|
private const val TAG = "SettingsViewModel"
|
||||||
private const val USER_FULL_DATA_BACKUP_AWARE = "user_full_data_backup_aware"
|
private const val USER_FULL_DATA_BACKUP_AWARE = "user_full_data_backup_aware"
|
||||||
|
@ -37,7 +47,8 @@ internal class SettingsViewModel(
|
||||||
keyManager: KeyManager,
|
keyManager: KeyManager,
|
||||||
private val notificationManager: BackupNotificationManager,
|
private val notificationManager: BackupNotificationManager,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataManager: MetadataManager,
|
||||||
private val appListRetriever: AppListRetriever
|
private val appListRetriever: AppListRetriever,
|
||||||
|
private val storageBackup: StorageBackup
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
|
||||||
|
|
||||||
private val contentResolver = app.contentResolver
|
private val contentResolver = app.contentResolver
|
||||||
|
@ -59,6 +70,9 @@ internal class SettingsViewModel(
|
||||||
private val mAppEditMode = MutableLiveData<Boolean>()
|
private val mAppEditMode = MutableLiveData<Boolean>()
|
||||||
internal val appEditMode: LiveData<Boolean> = mAppEditMode
|
internal val appEditMode: LiveData<Boolean> = mAppEditMode
|
||||||
|
|
||||||
|
private val _filesSummary = MutableLiveData<String>()
|
||||||
|
internal val filesSummary: LiveData<String> = _filesSummary
|
||||||
|
|
||||||
private val storageObserver = object : ContentObserver(null) {
|
private val storageObserver = object : ContentObserver(null) {
|
||||||
override fun onChange(selfChange: Boolean, uris: MutableCollection<Uri>, flags: Int) {
|
override fun onChange(selfChange: Boolean, uris: MutableCollection<Uri>, flags: Int) {
|
||||||
onStorageLocationChanged()
|
onStorageLocationChanged()
|
||||||
|
@ -89,6 +103,7 @@ internal class SettingsViewModel(
|
||||||
metadataManager.getLastBackupTime()
|
metadataManager.getLastBackupTime()
|
||||||
}
|
}
|
||||||
onStorageLocationChanged()
|
onStorageLocationChanged()
|
||||||
|
loadFilesSummary()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStorageLocationChanged() {
|
override fun onStorageLocationChanged() {
|
||||||
|
@ -116,6 +131,14 @@ internal class SettingsViewModel(
|
||||||
networkCallback.registered = true
|
networkCallback.registered = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsManager.isStorageBackupEnabled()) {
|
||||||
|
// disable storage backup if new storage is on USB
|
||||||
|
if (storage.isUsb) disableStorageBackup()
|
||||||
|
// enable it, just in case the previous storage was on USB,
|
||||||
|
// also to update the network requirement of the new storage
|
||||||
|
else enableStorageBackup()
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val canDo = settingsManager.canDoBackupNow()
|
val canDo = settingsManager.canDoBackupNow()
|
||||||
mBackupPossible.postValue(canDo)
|
mBackupPossible.postValue(canDo)
|
||||||
|
@ -134,8 +157,15 @@ internal class SettingsViewModel(
|
||||||
// maybe replace the check below with one that checks if our transport service is running
|
// maybe replace the check below with one that checks if our transport service is running
|
||||||
if (notificationManager.hasActiveBackupNotifications()) {
|
if (notificationManager.hasActiveBackupNotifications()) {
|
||||||
Toast.makeText(app, R.string.notification_backup_already_running, LENGTH_LONG).show()
|
Toast.makeText(app, R.string.notification_backup_already_running, LENGTH_LONG).show()
|
||||||
|
} else viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (settingsManager.isStorageBackupEnabled()) {
|
||||||
|
val i = Intent(app, StorageBackupService::class.java)
|
||||||
|
// this starts an app backup afterwards
|
||||||
|
i.putExtra(EXTRA_START_APP_BACKUP, true)
|
||||||
|
startForegroundService(app, i)
|
||||||
} else {
|
} else {
|
||||||
Thread { requestBackup(app) }.start()
|
requestBackup(app)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +186,14 @@ internal class SettingsViewModel(
|
||||||
settingsManager.onAppBackupStatusChanged(status)
|
settingsManager.onAppBackupStatusChanged(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
fun loadFilesSummary() = viewModelScope.launch {
|
||||||
|
val uriSummary = storageBackup.getUriSummaryString()
|
||||||
|
_filesSummary.value = if (uriSummary.isEmpty()) {
|
||||||
|
app.getString(R.string.settings_backup_files_summary)
|
||||||
|
} else uriSummary
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures that the call log will be included in backups.
|
* Ensures that the call log will be included in backups.
|
||||||
*
|
*
|
||||||
|
@ -170,4 +208,25 @@ internal class SettingsViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasMainKey(): Boolean {
|
||||||
|
return keyManager.hasMainKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableStorageBackup() {
|
||||||
|
val storage = settingsManager.getStorage() ?: error("no storage available")
|
||||||
|
if (!storage.isUsb) BackupJobService.scheduleJob(
|
||||||
|
context = app,
|
||||||
|
jobServiceClass = StorageBackupJobService::class.java,
|
||||||
|
periodMillis = HOURS.toMillis(24),
|
||||||
|
networkType = if (storage.requiresNetwork) NETWORK_TYPE_UNMETERED
|
||||||
|
else NETWORK_TYPE_NONE,
|
||||||
|
deviceIdle = false,
|
||||||
|
charging = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableStorageBackup() {
|
||||||
|
BackupJobService.cancelJob(app)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.stevesoltys.seedvault.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
||||||
|
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
internal class SeedvaultStoragePlugin(
|
||||||
|
context: Context,
|
||||||
|
private val storage: DocumentsStorage,
|
||||||
|
private val keyManager: KeyManager
|
||||||
|
) : SafStoragePlugin(context) {
|
||||||
|
override val root: DocumentFile
|
||||||
|
get() = storage.rootBackupDir ?: error("No storage set")
|
||||||
|
|
||||||
|
override fun getMasterKey(): SecretKey = keyManager.getMainKey()
|
||||||
|
override fun hasMasterKey(): Boolean = keyManager.hasMainKey()
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.stevesoltys.seedvault.storage
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import com.stevesoltys.seedvault.transport.requestBackup
|
||||||
|
import org.calyxos.backup.storage.api.BackupObserver
|
||||||
|
import org.calyxos.backup.storage.api.RestoreObserver
|
||||||
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
import org.calyxos.backup.storage.backup.BackupJobService
|
||||||
|
import org.calyxos.backup.storage.backup.BackupService
|
||||||
|
import org.calyxos.backup.storage.backup.NotificationBackupObserver
|
||||||
|
import org.calyxos.backup.storage.restore.NotificationRestoreObserver
|
||||||
|
import org.calyxos.backup.storage.restore.RestoreService
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
|
/*
|
||||||
|
test and debug with
|
||||||
|
|
||||||
|
adb shell dumpsys jobscheduler |
|
||||||
|
grep -A 23 -B 4 "Service: com.stevesoltys.seedvault/.storage.StorageBackupJobService"
|
||||||
|
|
||||||
|
force running with:
|
||||||
|
|
||||||
|
adb shell cmd jobscheduler run -f com.stevesoltys.seedvault 0
|
||||||
|
|
||||||
|
*/
|
||||||
|
internal class StorageBackupJobService : BackupJobService(StorageBackupService::class.java)
|
||||||
|
|
||||||
|
internal class StorageBackupService : BackupService() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal const val EXTRA_START_APP_BACKUP = "startAppBackup"
|
||||||
|
}
|
||||||
|
|
||||||
|
override val storageBackup: StorageBackup by inject()
|
||||||
|
|
||||||
|
// use lazy delegate because context isn't available during construction time
|
||||||
|
override val backupObserver: BackupObserver by lazy {
|
||||||
|
NotificationBackupObserver(applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackupFinished(intent: Intent, success: Boolean) {
|
||||||
|
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
|
||||||
|
requestBackup(applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class StorageRestoreService : RestoreService() {
|
||||||
|
override val storageBackup: StorageBackup by inject()
|
||||||
|
|
||||||
|
// use lazy delegate because context isn't available during construction time
|
||||||
|
override val restoreObserver: RestoreObserver by lazy {
|
||||||
|
NotificationRestoreObserver(applicationContext)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.stevesoltys.seedvault.storage
|
||||||
|
|
||||||
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
import org.calyxos.backup.storage.api.StoragePlugin
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val storageModule = module {
|
||||||
|
single<StoragePlugin> { SeedvaultStoragePlugin(get(), get(), get()) }
|
||||||
|
single { StorageBackup(get(), get()) }
|
||||||
|
}
|
|
@ -2,10 +2,8 @@ package com.stevesoltys.seedvault.transport
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP // ktlint-disable no-unused-imports
|
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.BACKUP_SERVICE // ktlint-disable no-unused-imports
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
|
@ -55,13 +53,15 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun requestBackup(context: Context) {
|
fun requestBackup(context: Context) {
|
||||||
|
val backupManager: IBackupManager = get().get()
|
||||||
|
if (backupManager.isBackupEnabled) {
|
||||||
val packageService: PackageService = get().get()
|
val packageService: PackageService = get().get()
|
||||||
val packages = packageService.eligiblePackages
|
val packages = packageService.eligiblePackages
|
||||||
val appTotals = packageService.expectedAppTotals
|
val appTotals = packageService.expectedAppTotals
|
||||||
|
|
||||||
val observer = NotificationBackupObserver(context, packages.size, appTotals)
|
|
||||||
val result = try {
|
val result = try {
|
||||||
val backupManager: IBackupManager = get().get()
|
Log.d(TAG, "Backup is enabled, request backup...")
|
||||||
|
val observer = NotificationBackupObserver(context, packages.size, appTotals)
|
||||||
backupManager.requestBackup(packages, observer, BackupMonitor(), 0)
|
backupManager.requestBackup(packages, observer, BackupMonitor(), 0)
|
||||||
} catch (e: RemoteException) {
|
} catch (e: RemoteException) {
|
||||||
Log.e(TAG, "Error during backup: ", e)
|
Log.e(TAG, "Error during backup: ", e)
|
||||||
|
@ -73,4 +73,7 @@ fun requestBackup(context: Context) {
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Backup failed: $result")
|
Log.e(TAG, "Backup failed: $result")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Backup is not enabled")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -321,7 +321,7 @@ internal class BackupCoordinator(
|
||||||
?: throw AssertionError("Cancelling full backup, but no current package")
|
?: throw AssertionError("Cancelling full backup, but no current package")
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG, "Cancel full backup of ${packageInfo.packageName}" +
|
TAG, "Cancel full backup of ${packageInfo.packageName}" +
|
||||||
" because of $state.cancelReason"
|
" because of ${state.cancelReason}"
|
||||||
)
|
)
|
||||||
onPackageBackupError(packageInfo)
|
onPackageBackupError(packageInfo)
|
||||||
full.cancelFullBackup()
|
full.cancelFullBackup()
|
||||||
|
|
|
@ -21,6 +21,7 @@ val backupModule = module {
|
||||||
single {
|
single {
|
||||||
KVBackup(
|
KVBackup(
|
||||||
plugin = get<BackupPlugin>().kvBackupPlugin,
|
plugin = get<BackupPlugin>().kvBackupPlugin,
|
||||||
|
settingsManager = get(),
|
||||||
inputFactory = get(),
|
inputFactory = get(),
|
||||||
headerWriter = get(),
|
headerWriter = get(),
|
||||||
crypto = get(),
|
crypto = get(),
|
||||||
|
@ -30,6 +31,7 @@ val backupModule = module {
|
||||||
single {
|
single {
|
||||||
FullBackup(
|
FullBackup(
|
||||||
plugin = get<BackupPlugin>().fullBackupPlugin,
|
plugin = get<BackupPlugin>().fullBackupPlugin,
|
||||||
|
settingsManager = get(),
|
||||||
inputFactory = get(),
|
inputFactory = get(),
|
||||||
headerWriter = get(),
|
headerWriter = get(),
|
||||||
crypto = get()
|
crypto = get()
|
||||||
|
|
|
@ -25,12 +25,6 @@ interface BackupPlugin {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun initializeDevice()
|
suspend fun initializeDevice()
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all existing [RestoreSet]s from the storage medium.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun deleteAllBackups()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an [OutputStream] for writing backup metadata.
|
* Returns an [OutputStream] for writing backup metadata.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.util.Log
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.header.HeaderWriter
|
import com.stevesoltys.seedvault.header.HeaderWriter
|
||||||
import com.stevesoltys.seedvault.header.VersionHeader
|
import com.stevesoltys.seedvault.header.VersionHeader
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -35,6 +36,7 @@ private val TAG = FullBackup::class.java.simpleName
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class FullBackup(
|
internal class FullBackup(
|
||||||
private val plugin: FullBackupPlugin,
|
private val plugin: FullBackupPlugin,
|
||||||
|
private val settingsManager: SettingsManager,
|
||||||
private val inputFactory: InputFactory,
|
private val inputFactory: InputFactory,
|
||||||
private val headerWriter: HeaderWriter,
|
private val headerWriter: HeaderWriter,
|
||||||
private val crypto: Crypto
|
private val crypto: Crypto
|
||||||
|
@ -46,7 +48,9 @@ internal class FullBackup(
|
||||||
|
|
||||||
fun getCurrentPackage() = state?.packageInfo
|
fun getCurrentPackage() = state?.packageInfo
|
||||||
|
|
||||||
fun getQuota(): Long = plugin.getQuota()
|
fun getQuota(): Long {
|
||||||
|
return if (settingsManager.isQuotaUnlimited()) Long.MAX_VALUE else plugin.getQuota()
|
||||||
|
}
|
||||||
|
|
||||||
fun checkFullBackupSize(size: Long): Int {
|
fun checkFullBackupSize(size: Long): Int {
|
||||||
Log.i(TAG, "Check full backup size of $size bytes.")
|
Log.i(TAG, "Check full backup size of $size bytes.")
|
||||||
|
@ -134,7 +138,7 @@ internal class FullBackup(
|
||||||
|
|
||||||
// check if size fits quota
|
// check if size fits quota
|
||||||
state.size += numBytes
|
state.size += numBytes
|
||||||
val quota = plugin.getQuota()
|
val quota = getQuota()
|
||||||
if (state.size > quota) {
|
if (state.size > quota) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
import com.stevesoltys.seedvault.encodeBase64
|
||||||
import com.stevesoltys.seedvault.header.HeaderWriter
|
import com.stevesoltys.seedvault.header.HeaderWriter
|
||||||
import com.stevesoltys.seedvault.header.VersionHeader
|
import com.stevesoltys.seedvault.header.VersionHeader
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -27,6 +28,7 @@ private val TAG = KVBackup::class.java.simpleName
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class KVBackup(
|
internal class KVBackup(
|
||||||
private val plugin: KVBackupPlugin,
|
private val plugin: KVBackupPlugin,
|
||||||
|
private val settingsManager: SettingsManager,
|
||||||
private val inputFactory: InputFactory,
|
private val inputFactory: InputFactory,
|
||||||
private val headerWriter: HeaderWriter,
|
private val headerWriter: HeaderWriter,
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
|
@ -39,7 +41,9 @@ internal class KVBackup(
|
||||||
|
|
||||||
fun getCurrentPackage() = state?.packageInfo
|
fun getCurrentPackage() = state?.packageInfo
|
||||||
|
|
||||||
fun getQuota(): Long = plugin.getQuota()
|
fun getQuota(): Long {
|
||||||
|
return if (settingsManager.isQuotaUnlimited()) Long.MAX_VALUE else plugin.getQuota()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun performBackup(
|
suspend fun performBackup(
|
||||||
packageInfo: PackageInfo,
|
packageInfo: PackageInfo,
|
||||||
|
@ -94,7 +98,7 @@ internal class KVBackup(
|
||||||
return backupError(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)
|
return backupError(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check if package is over-quota
|
// TODO check if package is over-quota and respect unlimited setting
|
||||||
|
|
||||||
if (isNonIncremental && hasDataForPackage) {
|
if (isNonIncremental && hasDataForPackage) {
|
||||||
Log.w(TAG, "Requested non-incremental, deleting existing data.")
|
Log.w(TAG, "Requested non-incremental, deleting existing data.")
|
||||||
|
|
|
@ -9,7 +9,7 @@ import com.stevesoltys.seedvault.ui.storage.StorageViewModel
|
||||||
abstract class RequireProvisioningViewModel(
|
abstract class RequireProvisioningViewModel(
|
||||||
protected val app: Application,
|
protected val app: Application,
|
||||||
protected val settingsManager: SettingsManager,
|
protected val settingsManager: SettingsManager,
|
||||||
private val keyManager: KeyManager
|
protected val keyManager: KeyManager
|
||||||
) : AndroidViewModel(app) {
|
) : AndroidViewModel(app) {
|
||||||
|
|
||||||
abstract val isRestoreOperation: Boolean
|
abstract val isRestoreOperation: Boolean
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.stevesoltys.seedvault.ui.files
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
||||||
|
import org.calyxos.backup.storage.ui.backup.BackupContentFragment
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
|
class FileSelectionFragment() : BackupContentFragment() {
|
||||||
|
|
||||||
|
override val viewModel by viewModel<FileSelectionViewModel>()
|
||||||
|
private val settingsViewModel by sharedViewModel<SettingsViewModel>()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
requireActivity().setTitle(R.string.settings_backup_files_title)
|
||||||
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
// reload files summary when we navigate away (it might have changed)
|
||||||
|
settingsViewModel.loadFilesSummary()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.stevesoltys.seedvault.ui.files
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
import org.calyxos.backup.storage.ui.backup.BackupContentViewModel
|
||||||
|
|
||||||
|
class FileSelectionViewModel(
|
||||||
|
app: Application,
|
||||||
|
override val storageBackup: StorageBackup
|
||||||
|
) : BackupContentViewModel(app) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch { loadContent() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.ui.recoverycode
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.ui.BackupActivity
|
import com.stevesoltys.seedvault.ui.BackupActivity
|
||||||
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
|
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
|
||||||
|
@ -17,6 +18,7 @@ class RecoveryCodeActivity : BackupActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (isSetupWizard()) hideSystemUiNavigation()
|
if (isSetupWizard()) hideSystemUiNavigation()
|
||||||
|
window.addFlags(FLAG_SECURE)
|
||||||
|
|
||||||
setContentView(R.layout.activity_recovery_code)
|
setContentView(R.layout.activity_recovery_code)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
|
||||||
class RecoveryCodeAdapter(private val items: List<CharSequence>) :
|
class RecoveryCodeAdapter(private val items: List<CharArray>) :
|
||||||
Adapter<RecoveryCodeViewHolder>() {
|
Adapter<RecoveryCodeViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecoveryCodeViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecoveryCodeViewHolder {
|
||||||
|
@ -30,9 +30,9 @@ class RecoveryCodeViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||||
private val num = v.findViewById<TextView>(R.id.num)
|
private val num = v.findViewById<TextView>(R.id.num)
|
||||||
private val word = v.findViewById<TextView>(R.id.word)
|
private val word = v.findViewById<TextView>(R.id.word)
|
||||||
|
|
||||||
internal fun bind(number: Int, item: CharSequence) {
|
internal fun bind(number: Int, item: CharArray) {
|
||||||
num.text = number.toString()
|
num.text = number.toString()
|
||||||
word.text = item
|
word.text = String(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
package com.stevesoltys.seedvault.ui.recoverycode
|
package com.stevesoltys.seedvault.ui.recoverycode
|
||||||
|
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.KeyguardManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||||
|
import android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import android.hardware.biometrics.BiometricPrompt
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.CancellationSignal
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
|
@ -16,20 +22,23 @@ import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.Toast.LENGTH_LONG
|
import android.widget.Toast.LENGTH_LONG
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat.getMainExecutor
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics.ChecksumException
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics.InvalidWordException
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.isDebugBuild
|
import com.stevesoltys.seedvault.isDebugBuild
|
||||||
import com.stevesoltys.seedvault.ui.LiveEventHandler
|
import com.stevesoltys.seedvault.ui.LiveEventHandler
|
||||||
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
|
|
||||||
import io.github.novacrypto.bip39.Validation.WordNotFoundException
|
|
||||||
import io.github.novacrypto.bip39.wordlists.English
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
internal const val ARG_FOR_NEW_CODE = "forVerifyingNewCode"
|
internal const val ARG_FOR_NEW_CODE = "forStoringNewCode"
|
||||||
|
|
||||||
class RecoveryCodeInputFragment : Fragment() {
|
class RecoveryCodeInputFragment : Fragment() {
|
||||||
|
|
||||||
|
@ -56,13 +65,13 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
/**
|
/**
|
||||||
* True if this is for verifying a new recovery code, false for verifying an existing one.
|
* True if this is for verifying a new recovery code, false for verifying an existing one.
|
||||||
*/
|
*/
|
||||||
private var forVerifyingNewCode: Boolean = true
|
private var forStoringNewCode: Boolean = true
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
val v: View = inflater.inflate(R.layout.fragment_recovery_code_input, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_recovery_code_input, container, false)
|
||||||
|
|
||||||
introText = v.findViewById(R.id.introText)
|
introText = v.findViewById(R.id.introText)
|
||||||
|
@ -84,7 +93,7 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
wordList = v.findViewById(R.id.wordList)
|
wordList = v.findViewById(R.id.wordList)
|
||||||
|
|
||||||
arguments?.getBoolean(ARG_FOR_NEW_CODE, true)?.let {
|
arguments?.getBoolean(ARG_FOR_NEW_CODE, true)?.let {
|
||||||
forVerifyingNewCode = it
|
forStoringNewCode = it
|
||||||
}
|
}
|
||||||
|
|
||||||
return v
|
return v
|
||||||
|
@ -93,13 +102,18 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
activity?.setTitle(R.string.recovery_code_title)
|
||||||
|
|
||||||
if (viewModel.isRestore) {
|
if (viewModel.isRestore) {
|
||||||
introText.setText(R.string.recovery_code_input_intro)
|
introText.setText(R.string.recovery_code_input_intro)
|
||||||
backView.visibility = VISIBLE
|
backView.visibility = VISIBLE
|
||||||
backView.setOnClickListener { requireActivity().finishAfterTransition() }
|
backView.setOnClickListener { requireActivity().finishAfterTransition() }
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = getAdapter()
|
val adapterLayout = android.R.layout.simple_list_item_1
|
||||||
|
val adapter = ArrayAdapter<String>(requireContext(), adapterLayout).apply {
|
||||||
|
addAll(Mnemonics.getCachedWords(Locale.ENGLISH.language))
|
||||||
|
}
|
||||||
|
|
||||||
for (i in 0 until WORD_NUM) {
|
for (i in 0 until WORD_NUM) {
|
||||||
val wordLayout = getWordLayout(i)
|
val wordLayout = getWordLayout(i)
|
||||||
|
@ -110,22 +124,14 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
editText.setAdapter(adapter)
|
editText.setAdapter(adapter)
|
||||||
}
|
}
|
||||||
doneButton.setOnClickListener { done() }
|
doneButton.setOnClickListener { done() }
|
||||||
newCodeButton.visibility = if (forVerifyingNewCode) GONE else VISIBLE
|
newCodeButton.visibility = if (forStoringNewCode) GONE else VISIBLE
|
||||||
newCodeButton.setOnClickListener { generateNewCode() }
|
newCodeButton.setOnClickListener { generateNewCode() }
|
||||||
|
|
||||||
viewModel.existingCodeChecked.observeEvent(viewLifecycleOwner,
|
viewModel.existingCodeChecked.observeEvent(viewLifecycleOwner,
|
||||||
LiveEventHandler { verified -> onExistingCodeChecked(verified) }
|
LiveEventHandler { verified -> onExistingCodeChecked(verified) }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (forVerifyingNewCode && isDebugBuild() && !viewModel.isRestore) debugPreFill()
|
if (forStoringNewCode && isDebugBuild() && !viewModel.isRestore) debugPreFill()
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAdapter(): ArrayAdapter<String> {
|
|
||||||
val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_list_item_1)
|
|
||||||
for (i in 0 until WORD_LIST_SIZE) {
|
|
||||||
adapter.add(English.INSTANCE.getWord(i))
|
|
||||||
}
|
|
||||||
return adapter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getInput(): List<CharSequence> = ArrayList<String>(WORD_NUM).apply {
|
private fun getInput(): List<CharSequence> = ArrayList<String>(WORD_NUM).apply {
|
||||||
|
@ -136,12 +142,43 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
val input = getInput()
|
val input = getInput()
|
||||||
if (!allFilledOut(input)) return
|
if (!allFilledOut(input)) return
|
||||||
try {
|
try {
|
||||||
viewModel.validateAndContinue(input, forVerifyingNewCode)
|
viewModel.validateCode(input)
|
||||||
} catch (e: InvalidChecksumException) {
|
} catch (e: ChecksumException) {
|
||||||
Toast.makeText(context, R.string.recovery_code_error_checksum_word, LENGTH_LONG).show()
|
Toast.makeText(context, R.string.recovery_code_error_checksum_word, LENGTH_LONG).show()
|
||||||
} catch (e: WordNotFoundException) {
|
return
|
||||||
showWrongWordError(input, e)
|
} catch (e: InvalidWordException) {
|
||||||
|
showWrongWordError(input)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if (forStoringNewCode) {
|
||||||
|
val keyguardManager = requireContext().getSystemService(KeyguardManager::class.java)
|
||||||
|
if (SDK_INT >= 30 && keyguardManager.isDeviceSecure) {
|
||||||
|
// if we have a lock-screen secret, we can ask for it before storing the code
|
||||||
|
storeNewCodeAfterAuth(input)
|
||||||
|
} else {
|
||||||
|
// user doesn't seem to care about security, store key without auth
|
||||||
|
viewModel.storeNewCode(input)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.verifyExistingCode(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(30)
|
||||||
|
private fun storeNewCodeAfterAuth(input: List<CharSequence>) {
|
||||||
|
val biometricPrompt = BiometricPrompt.Builder(context)
|
||||||
|
.setConfirmationRequired(true)
|
||||||
|
.setTitle(getString(R.string.recovery_code_auth_title))
|
||||||
|
.setDescription(getString(R.string.recovery_code_auth_description))
|
||||||
|
// BIOMETRIC_STRONG could be made optional in the future, setting guarded by credentials
|
||||||
|
.setAllowedAuthenticators(DEVICE_CREDENTIAL or BIOMETRIC_STRONG)
|
||||||
|
.build()
|
||||||
|
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
|
||||||
|
viewModel.storeNewCode(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
biometricPrompt.authenticate(CancellationSignal(), getMainExecutor(context), callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun allFilledOut(input: List<CharSequence>): Boolean {
|
private fun allFilledOut(input: List<CharSequence>): Boolean {
|
||||||
|
@ -153,10 +190,11 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showWrongWordError(input: List<CharSequence>, e: WordNotFoundException) {
|
private fun showWrongWordError(input: List<CharSequence>) {
|
||||||
val i = input.indexOf(e.word)
|
val words = Mnemonics.getCachedWords(Locale.ENGLISH.language)
|
||||||
|
val i = input.indexOfFirst { it !in words }
|
||||||
if (i == -1) throw AssertionError()
|
if (i == -1) throw AssertionError()
|
||||||
val str = getString(R.string.recovery_code_error_invalid_word, e.suggestion1, e.suggestion2)
|
val str = getString(R.string.recovery_code_error_invalid_word)
|
||||||
showError(i, str)
|
showError(i, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +228,7 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
|
|
||||||
private val regenRequest = registerForActivityResult(StartActivityForResult()) {
|
private val regenRequest = registerForActivityResult(StartActivityForResult()) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
viewModel.deleteAllBackup()
|
viewModel.reinitializeBackupLocation()
|
||||||
parentFragmentManager.popBackStack()
|
parentFragmentManager.popBackStack()
|
||||||
Snackbar.make(requireView(), R.string.recovery_code_recreated, Snackbar.LENGTH_LONG)
|
Snackbar.make(requireView(), R.string.recovery_code_recreated, Snackbar.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
|
@ -233,7 +271,7 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
private fun debugPreFill() {
|
private fun debugPreFill() {
|
||||||
val words = viewModel.wordList
|
val words = viewModel.wordList
|
||||||
for (i in words.indices) {
|
for (i in words.indices) {
|
||||||
getWordLayout(i).editText!!.setText(words[i])
|
getWordLayout(i).editText!!.setText(String(words[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class RecoveryCodeOutputFragment : Fragment() {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
val v: View = inflater.inflate(R.layout.fragment_recovery_code_output, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_recovery_code_output, container, false)
|
||||||
|
|
||||||
wordList = v.findViewById(R.id.wordList)
|
wordList = v.findViewById(R.id.wordList)
|
||||||
|
|
|
@ -1,48 +1,47 @@
|
||||||
package com.stevesoltys.seedvault.ui.recoverycode
|
package com.stevesoltys.seedvault.ui.recoverycode
|
||||||
|
|
||||||
|
import android.app.backup.IBackupManager
|
||||||
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics.ChecksumException
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics.InvalidWordException
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics.WordCountException
|
||||||
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import com.stevesoltys.seedvault.App
|
import com.stevesoltys.seedvault.App
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||||
import io.github.novacrypto.bip39.JavaxPBKDF2WithHmacSHA512
|
|
||||||
import io.github.novacrypto.bip39.MnemonicGenerator
|
|
||||||
import io.github.novacrypto.bip39.MnemonicValidator
|
|
||||||
import io.github.novacrypto.bip39.SeedCalculator
|
|
||||||
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
|
|
||||||
import io.github.novacrypto.bip39.Validation.InvalidWordCountException
|
|
||||||
import io.github.novacrypto.bip39.Validation.UnexpectedWhiteSpaceException
|
|
||||||
import io.github.novacrypto.bip39.Validation.WordNotFoundException
|
|
||||||
import io.github.novacrypto.bip39.Words
|
|
||||||
import io.github.novacrypto.bip39.wordlists.English
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.ArrayList
|
|
||||||
|
|
||||||
internal const val WORD_NUM = 12
|
internal const val WORD_NUM = 12
|
||||||
internal const val WORD_LIST_SIZE = 2048
|
|
||||||
|
|
||||||
class RecoveryCodeViewModel(
|
private val TAG = RecoveryCodeViewModel::class.java.simpleName
|
||||||
|
|
||||||
|
internal class RecoveryCodeViewModel(
|
||||||
app: App,
|
app: App,
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
private val keyManager: KeyManager,
|
private val keyManager: KeyManager,
|
||||||
private val backupPlugin: BackupPlugin
|
private val backupManager: IBackupManager,
|
||||||
|
private val backupCoordinator: BackupCoordinator,
|
||||||
|
private val storageBackup: StorageBackup
|
||||||
) : AndroidViewModel(app) {
|
) : AndroidViewModel(app) {
|
||||||
|
|
||||||
internal val wordList: List<CharSequence> by lazy {
|
internal val wordList: List<CharArray> by lazy {
|
||||||
val items: ArrayList<CharSequence> = ArrayList(WORD_NUM)
|
// we use our own entropy to not having to trust the library to use SecureRandom
|
||||||
val entropy = ByteArray(Words.TWELVE.byteLength())
|
val entropy = ByteArray(Mnemonics.WordCount.COUNT_12.bitLength / 8)
|
||||||
SecureRandom().nextBytes(entropy)
|
SecureRandom().nextBytes(entropy)
|
||||||
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) {
|
// create the words from the entropy
|
||||||
if (it != " ") items.add(it)
|
Mnemonics.MnemonicCode(entropy).words
|
||||||
}
|
|
||||||
items
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mConfirmButtonClicked = MutableLiveEvent<Boolean>()
|
private val mConfirmButtonClicked = MutableLiveEvent<Boolean>()
|
||||||
|
@ -57,36 +56,72 @@ class RecoveryCodeViewModel(
|
||||||
|
|
||||||
internal var isRestore: Boolean = false
|
internal var isRestore: Boolean = false
|
||||||
|
|
||||||
@Throws(WordNotFoundException::class, InvalidChecksumException::class)
|
@Throws(InvalidWordException::class, ChecksumException::class)
|
||||||
fun validateAndContinue(input: List<CharSequence>, forVerifyingNewCode: Boolean) {
|
fun validateCode(input: List<CharSequence>): Mnemonics.MnemonicCode {
|
||||||
|
val code = Mnemonics.MnemonicCode(input.toMnemonicChars())
|
||||||
try {
|
try {
|
||||||
MnemonicValidator.ofWordList(English.INSTANCE).validate(input)
|
code.validate()
|
||||||
} catch (e: UnexpectedWhiteSpaceException) {
|
} catch (e: WordCountException) {
|
||||||
throw AssertionError(e)
|
|
||||||
} catch (e: InvalidWordCountException) {
|
|
||||||
throw AssertionError(e)
|
throw AssertionError(e)
|
||||||
}
|
}
|
||||||
val mnemonic = input.joinToString(" ")
|
return code
|
||||||
val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "")
|
}
|
||||||
if (forVerifyingNewCode) {
|
|
||||||
keyManager.storeBackupKey(seed)
|
/**
|
||||||
keyManager.storeMainKey(seed)
|
* Verifies existing recovery code and returns result via [existingCodeChecked].
|
||||||
mRecoveryCodeSaved.setEvent(true)
|
*/
|
||||||
} else {
|
fun verifyExistingCode(input: List<CharSequence>) {
|
||||||
|
// we validate the code again, just in case
|
||||||
|
val seed = validateCode(input).toSeed()
|
||||||
val verified = crypto.verifyBackupKey(seed)
|
val verified = crypto.verifyBackupKey(seed)
|
||||||
|
// store main key at this opportunity if it is still missing
|
||||||
if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
|
if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
|
||||||
mExistingCodeChecked.setEvent(verified)
|
mExistingCodeChecked.setEvent(verified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a new recovery code and returns result via [recoveryCodeSaved].
|
||||||
|
*/
|
||||||
|
fun storeNewCode(input: List<CharSequence>) {
|
||||||
|
// we validate the code again, just in case
|
||||||
|
val seed = validateCode(input).toSeed()
|
||||||
|
keyManager.storeBackupKey(seed)
|
||||||
|
keyManager.storeMainKey(seed)
|
||||||
|
mRecoveryCodeSaved.setEvent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllBackup() {
|
/**
|
||||||
|
* Deletes all storage backups for current user and clears the storage backup cache.
|
||||||
|
* Also starts a new app data restore set and initializes it.
|
||||||
|
*
|
||||||
|
* The reason is that old backups won't be readable anymore with the new key.
|
||||||
|
* We can't delete other backups safely, because we can't be sure
|
||||||
|
* that they don't belong to a different device or user.
|
||||||
|
*/
|
||||||
|
fun reinitializeBackupLocation() {
|
||||||
|
Log.d(TAG, "Re-initializing backup location...")
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
// remove old storage snapshots and clear cache
|
||||||
|
storageBackup.deleteAllSnapshots()
|
||||||
|
storageBackup.clearCache()
|
||||||
try {
|
try {
|
||||||
backupPlugin.deleteAllBackups()
|
// will also generate a new backup token for the new restore set
|
||||||
|
backupCoordinator.startNewRestoreSet()
|
||||||
|
|
||||||
|
// initialize the new location
|
||||||
|
backupManager.initializeTransportsForUser(
|
||||||
|
UserHandle.myUserId(),
|
||||||
|
arrayOf(TRANSPORT_ID),
|
||||||
|
null
|
||||||
|
)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e("RecoveryCodeViewModel", "Error deleting backups", e)
|
Log.e(TAG, "Error starting new RestoreSet", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun List<CharSequence>.toMnemonicChars(): CharArray {
|
||||||
|
return joinToString(" ").toCharArray()
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import com.stevesoltys.seedvault.transport.requestBackup
|
import com.stevesoltys.seedvault.transport.requestBackup
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
private val TAG = BackupStorageViewModel::class.java.simpleName
|
private val TAG = BackupStorageViewModel::class.java.simpleName
|
||||||
|
@ -24,6 +25,7 @@ internal class BackupStorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val backupCoordinator: BackupCoordinator,
|
private val backupCoordinator: BackupCoordinator,
|
||||||
|
private val storageBackup: StorageBackup,
|
||||||
settingsManager: SettingsManager
|
settingsManager: SettingsManager
|
||||||
) : StorageViewModel(app, settingsManager) {
|
) : StorageViewModel(app, settingsManager) {
|
||||||
|
|
||||||
|
@ -32,6 +34,9 @@ internal class BackupStorageViewModel(
|
||||||
override fun onLocationSet(uri: Uri) {
|
override fun onLocationSet(uri: Uri) {
|
||||||
val isUsb = saveStorage(uri)
|
val isUsb = saveStorage(uri)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
// remove old storage snapshots and clear cache
|
||||||
|
storageBackup.deleteAllSnapshots()
|
||||||
|
storageBackup.clearCache()
|
||||||
try {
|
try {
|
||||||
// will also generate a new backup token for the new restore set
|
// will also generate a new backup token for the new restore set
|
||||||
backupCoordinator.startNewRestoreSet()
|
backupCoordinator.startNewRestoreSet()
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.ui.storage
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
import android.view.View.INVISIBLE
|
import android.view.View.INVISIBLE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -37,7 +38,7 @@ class StorageCheckFragment : Fragment() {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
val v: View = inflater.inflate(R.layout.fragment_storage_check, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_storage_check, container, false)
|
||||||
|
|
||||||
titleView = v.findViewById(R.id.titleView)
|
titleView = v.findViewById(R.id.titleView)
|
||||||
|
@ -55,6 +56,7 @@ class StorageCheckFragment : Fragment() {
|
||||||
|
|
||||||
val errorMsg = requireArguments().getString(ERROR_MSG)
|
val errorMsg = requireArguments().getString(ERROR_MSG)
|
||||||
if (errorMsg != null) {
|
if (errorMsg != null) {
|
||||||
|
view.findViewById<View>(R.id.patienceView).visibility = GONE
|
||||||
progressBar.visibility = INVISIBLE
|
progressBar.visibility = INVISIBLE
|
||||||
errorView.text = errorMsg
|
errorView.text = errorMsg
|
||||||
errorView.visibility = VISIBLE
|
errorView.visibility = VISIBLE
|
||||||
|
|
|
@ -156,11 +156,19 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
data = Uri.parse("nc://login/server:")
|
data = Uri.parse("nc://login/server:")
|
||||||
putExtra("onlyAdd", true)
|
putExtra("onlyAdd", true)
|
||||||
}
|
}
|
||||||
|
val marketIntent =
|
||||||
|
Intent(ACTION_VIEW, Uri.parse("market://details?id=$NEXTCLOUD_PACKAGE")).apply {
|
||||||
|
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
val isInstalled = packageManager.resolveActivity(intent, 0) != null
|
val isInstalled = packageManager.resolveActivity(intent, 0) != null
|
||||||
|
val canInstall = packageManager.resolveActivity(marketIntent, 0) != null
|
||||||
val summaryRes = if (isInstalled) {
|
val summaryRes = if (isInstalled) {
|
||||||
if (isRestore) R.string.storage_fake_nextcloud_summary_installed
|
if (isRestore) R.string.storage_fake_nextcloud_summary_installed
|
||||||
else R.string.storage_fake_nextcloud_summary_unavailable
|
else R.string.storage_fake_nextcloud_summary_unavailable
|
||||||
} else R.string.storage_fake_nextcloud_summary
|
} else {
|
||||||
|
if (canInstall) R.string.storage_fake_nextcloud_summary
|
||||||
|
else R.string.storage_fake_nextcloud_summary_unavailable_market
|
||||||
|
}
|
||||||
val root = StorageRoot(
|
val root = StorageRoot(
|
||||||
authority = AUTHORITY_NEXTCLOUD,
|
authority = AUTHORITY_NEXTCLOUD,
|
||||||
rootId = "fake",
|
rootId = "fake",
|
||||||
|
@ -171,15 +179,10 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
availableBytes = null,
|
availableBytes = null,
|
||||||
isUsb = false,
|
isUsb = false,
|
||||||
requiresNetwork = true,
|
requiresNetwork = true,
|
||||||
enabled = !isInstalled || isRestore,
|
enabled = isInstalled || canInstall,
|
||||||
overrideClickListener = {
|
overrideClickListener = {
|
||||||
if (isInstalled) context.startActivity(intent)
|
if (isInstalled) context.startActivity(intent)
|
||||||
else {
|
else if (canInstall) context.startActivity(marketIntent)
|
||||||
val uri = Uri.parse("market://details?id=$NEXTCLOUD_PACKAGE")
|
|
||||||
val i = Intent(ACTION_VIEW, uri)
|
|
||||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
context.startActivity(i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
roots.add(root)
|
roots.add(root)
|
||||||
|
|
|
@ -83,6 +83,9 @@ internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
||||||
backView.setOnClickListener { requireActivity().finishAfterTransition() }
|
backView.setOnClickListener { requireActivity().finishAfterTransition() }
|
||||||
} else {
|
} else {
|
||||||
warningIcon.visibility = VISIBLE
|
warningIcon.visibility = VISIBLE
|
||||||
|
if (viewModel.hasStorageSet) {
|
||||||
|
warningText.setText(R.string.storage_fragment_warning_delete)
|
||||||
|
}
|
||||||
warningText.visibility = VISIBLE
|
warningText.visibility = VISIBLE
|
||||||
divider.visibility = VISIBLE
|
divider.visibility = VISIBLE
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,8 @@ internal abstract class StorageViewModel(
|
||||||
private var storageRoot: StorageRoot? = null
|
private var storageRoot: StorageRoot? = null
|
||||||
|
|
||||||
internal var isSetupWizard: Boolean = false
|
internal var isSetupWizard: Boolean = false
|
||||||
|
internal val hasStorageSet: Boolean
|
||||||
|
get() = settingsManager.getStorage() != null
|
||||||
abstract val isRestoreOperation: Boolean
|
abstract val isRestoreOperation: Boolean
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
10
app/src/main/res/drawable/ic_library_add.xml
Normal file
10
app/src/main/res/drawable/ic_library_add.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM19,11h-4v4h-2v-4L9,11L9,9h4L13,5h2v4h4v2z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_save_alt.xml
Normal file
10
app/src/main/res/drawable/ic_save_alt.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,12v7L5,19v-7L3,12v7c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2zM13,12.67l2.59,-2.58L17,11.5l-5,5 -5,-5 1.41,-1.41L11,12.67L11,3h2z" />
|
||||||
|
</vector>
|
|
@ -1,7 +1,7 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:tint="?attr/colorControlNormal"
|
android:tint="?android:attr/colorControlNormal"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
|
|
19
app/src/main/res/layout/footer_snapshots.xml
Normal file
19
app/src/main/res/layout/footer_snapshots.xml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/skipView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/restore_storage_skip"
|
||||||
|
android:textColor="?android:colorAccent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -75,7 +75,8 @@
|
||||||
android:id="@+id/backView"
|
android:id="@+id/backView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
android:text="@string/restore_back"
|
android:text="@string/restore_back"
|
||||||
android:textColor="?android:colorAccent"
|
android:textColor="?android:colorAccent"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
|
56
app/src/main/res/layout/fragment_restore_files_started.xml
Normal file
56
app/src/main/res/layout/fragment_restore_files_started.xml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:tint="?android:colorAccent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_cloud_restore"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/restore_storage_in_progress_title"
|
||||||
|
android:textSize="24sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/infoView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:text="@string/restore_storage_in_progress_info"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/titleView" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button"
|
||||||
|
style="@style/Widget.AppCompat.Button.Colored"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:text="@string/restore_storage_got_it"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="1.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/infoView"
|
||||||
|
app:layout_constraintVertical_bias="1.0" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -36,7 +36,7 @@
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/backView"
|
app:layout_constraintBottom_toTopOf="@+id/skipView"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
style="?android:progressBarStyleLarge"
|
style="?android:progressBarStyleLarge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/backView"
|
app:layout_constraintBottom_toTopOf="@+id/skipView"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/titleView" />
|
app:layout_constraintTop_toBottomOf="@+id/titleView" />
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
android:textColor="?android:colorError"
|
android:textColor="?android:colorError"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/backView"
|
app:layout_constraintBottom_toTopOf="@+id/skipView"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
||||||
|
@ -68,11 +68,12 @@
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/backView"
|
android:id="@+id/skipView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:background="?android:selectableItemBackground"
|
||||||
android:text="@string/restore_back"
|
android:padding="16dp"
|
||||||
|
android:text="@string/restore_skip"
|
||||||
android:textColor="?android:colorAccent"
|
android:textColor="?android:colorAccent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -29,6 +29,17 @@
|
||||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||||
tools:text="@string/storage_check_fragment_backup_title" />
|
tools:text="@string/storage_check_fragment_backup_title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/patienceView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/storage_check_fragment_patience"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/titleView" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
style="?android:progressBarStyleLarge"
|
style="?android:progressBarStyleLarge"
|
||||||
|
@ -37,10 +48,10 @@
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="32dp"
|
android:layout_marginTop="32dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toTopOf="@+id/backButton"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
app:layout_constraintTop_toBottomOf="@+id/patienceView"
|
||||||
app:layout_constraintVertical_bias="0.0" />
|
app:layout_constraintVertical_bias="0.0" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -56,7 +67,7 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
app:layout_constraintTop_toBottomOf="@+id/patienceView"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0"
|
||||||
tools:text="@string/storage_check_fragment_backup_error"
|
tools:text="@string/storage_check_fragment_backup_error"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
|
@ -22,24 +22,24 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
|
android:gravity="center"
|
||||||
android:text="@string/storage_fragment_backup_title"
|
android:text="@string/storage_fragment_backup_title"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:gravity="center"
|
|
||||||
tools:text="Choose where to store backup (is a short title, but it can be longer)"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||||
|
tools:text="Choose where to store backup (is a short title, but it can be longer)" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/warningIcon"
|
android:id="@+id/warningIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:src="@drawable/ic_warning"
|
android:src="@drawable/ic_warning"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/warningText"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
app:layout_constraintTop_toTopOf="@+id/warningText"
|
||||||
tools:ignore="ContentDescription"
|
tools:ignore="ContentDescription"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
@ -96,7 +96,8 @@
|
||||||
android:id="@+id/backView"
|
android:id="@+id/backView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
android:text="@string/restore_back"
|
android:text="@string/restore_back"
|
||||||
android:textColor="?android:colorAccent"
|
android:textColor="?android:colorAccent"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
|
33
app/src/main/res/layout/header_snapshots.xml
Normal file
33
app/src/main/res/layout/header_snapshots.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:tint="?android:colorAccent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_cloud_download"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/restore_storage_choose_snapshot"
|
||||||
|
android:textSize="24sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -25,7 +25,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput2" />
|
android:nextFocusForward="@+id/wordInput2" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput4" />
|
android:nextFocusForward="@+id/wordInput4" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput6" />
|
android:nextFocusForward="@+id/wordInput6" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput8" />
|
android:nextFocusForward="@+id/wordInput8" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput10" />
|
android:nextFocusForward="@+id/wordInput10" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput12" />
|
android:nextFocusForward="@+id/wordInput12" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -158,7 +158,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput3" />
|
android:nextFocusForward="@+id/wordInput3" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -180,7 +180,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput5" />
|
android:nextFocusForward="@+id/wordInput5" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -202,7 +202,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput7" />
|
android:nextFocusForward="@+id/wordInput7" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -224,7 +224,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput9" />
|
android:nextFocusForward="@+id/wordInput9" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -246,7 +246,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete"
|
android:inputType="textAutoComplete|textNoSuggestions"
|
||||||
android:nextFocusForward="@+id/wordInput11" />
|
android:nextFocusForward="@+id/wordInput11" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
@ -268,7 +268,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:completionThreshold="1"
|
android:completionThreshold="1"
|
||||||
android:imeOptions="actionDone|flagNoPersonalizedLearning"
|
android:imeOptions="actionDone|flagNoPersonalizedLearning"
|
||||||
android:inputType="textAutoComplete" />
|
android:inputType="textAutoComplete|textNoSuggestions" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
tools:visible="true" />
|
tools:visible="true" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings_expert"
|
||||||
|
android:title="@string/settings_expert_title"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_about"
|
android:id="@+id/action_about"
|
||||||
android:title="@string/about_title"
|
android:title="@string/about_title"
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
<string name="restore_backup_button">Restore backup</string>
|
<string name="restore_backup_button">Restore backup</string>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="settings_backup">Backup my data</string>
|
<string name="settings_category_apps">App backup</string>
|
||||||
|
<string name="settings_backup">Backup my apps</string>
|
||||||
<string name="settings_backup_location">Backup location</string>
|
<string name="settings_backup_location">Backup location</string>
|
||||||
<string name="settings_backup_location_none">None</string>
|
<string name="settings_backup_location_none">None</string>
|
||||||
<string name="settings_backup_location_internal">Internal storage</string>
|
<string name="settings_backup_location_internal">Internal storage</string>
|
||||||
|
@ -28,13 +29,28 @@
|
||||||
<string name="settings_backup_status_summary">Last backup: %1$s</string>
|
<string name="settings_backup_status_summary">Last backup: %1$s</string>
|
||||||
<string name="settings_backup_exclude_apps">Exclude apps</string>
|
<string name="settings_backup_exclude_apps">Exclude apps</string>
|
||||||
<string name="settings_backup_now">Backup now</string>
|
<string name="settings_backup_now">Backup now</string>
|
||||||
|
<string name="settings_category_storage">Storage backup (experimental)</string>
|
||||||
|
<string name="settings_backup_storage_title">Backup my files</string>
|
||||||
|
<string name="settings_backup_files_title">Included files and folders</string>
|
||||||
|
<string name="settings_backup_files_summary">None</string>
|
||||||
<string name="settings_backup_recovery_code">Recovery code</string>
|
<string name="settings_backup_recovery_code">Recovery code</string>
|
||||||
<string name="settings_backup_recovery_code_summary">Verify existing code or generate a new one</string>
|
<string name="settings_backup_recovery_code_summary">Verify existing code or generate a new one</string>
|
||||||
|
<string name="settings_backup_storage_dialog_title">Experimental feature</string>
|
||||||
|
<string name="settings_backup_storage_dialog_message">Backing up files is still experimental and might not work. Do not rely on it for important data.</string>
|
||||||
|
<string name="settings_backup_storage_dialog_ok">Enable anyway</string>
|
||||||
|
<string name="settings_backup_storage_code_dialog_title">Recovery code verification required</string>
|
||||||
|
<string name="settings_backup_storage_code_dialog_message">To enable storage backup, you need to first verify your recovery code or generate a new one.</string>
|
||||||
|
<string name="settings_backup_storage_code_dialog_ok">Verify code</string>
|
||||||
|
|
||||||
<!-- Storage -->
|
<string name="settings_expert_title">Expert settings</string>
|
||||||
|
<string name="settings_expert_quota_title">Unlimited app quota</string>
|
||||||
|
<string name="settings_expert_quota_summary">Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps.</string>
|
||||||
|
|
||||||
|
<!-- Storage Location -->
|
||||||
<string name="storage_fragment_backup_title">Choose where to store backups</string>
|
<string name="storage_fragment_backup_title">Choose where to store backups</string>
|
||||||
<string name="storage_fragment_restore_title">Where to find your backups?</string>
|
<string name="storage_fragment_restore_title">Where to find your backups?</string>
|
||||||
<string name="storage_fragment_warning">People with access to your storage location can learn which apps you use, but do not get access to the apps\' data.</string>
|
<string name="storage_fragment_warning">People with access to your storage location can learn which apps you use, but do not get access to the apps\' data.</string>
|
||||||
|
<string name="storage_fragment_warning_delete">Existing backups in this location will be deleted.</string>
|
||||||
<string name="storage_fake_drive_title">USB flash drive</string>
|
<string name="storage_fake_drive_title">USB flash drive</string>
|
||||||
<string name="storage_fake_drive_summary">Needs to be plugged in</string>
|
<string name="storage_fake_drive_summary">Needs to be plugged in</string>
|
||||||
<string name="storage_available_bytes"><xliff:g example="1 GB" id="size">%1$s</xliff:g> free</string>
|
<string name="storage_available_bytes"><xliff:g example="1 GB" id="size">%1$s</xliff:g> free</string>
|
||||||
|
@ -42,7 +58,9 @@
|
||||||
<string name="storage_fake_nextcloud_summary">Tap to install</string>
|
<string name="storage_fake_nextcloud_summary">Tap to install</string>
|
||||||
<string name="storage_fake_nextcloud_summary_installed">Tap to set up account</string>
|
<string name="storage_fake_nextcloud_summary_installed">Tap to set up account</string>
|
||||||
<string name="storage_fake_nextcloud_summary_unavailable">Account not available. Set one up (or disable passcode).</string>
|
<string name="storage_fake_nextcloud_summary_unavailable">Account not available. Set one up (or disable passcode).</string>
|
||||||
|
<string name="storage_fake_nextcloud_summary_unavailable_market">Not installed</string>
|
||||||
<string name="storage_check_fragment_backup_title">Initializing backup location…</string>
|
<string name="storage_check_fragment_backup_title">Initializing backup location…</string>
|
||||||
|
<string name="storage_check_fragment_patience">This may take some time…</string>
|
||||||
<string name="storage_check_fragment_restore_title">Looking for backups…</string>
|
<string name="storage_check_fragment_restore_title">Looking for backups…</string>
|
||||||
<string name="storage_check_fragment_backup_error">An error occurred while accessing the backup location.</string>
|
<string name="storage_check_fragment_backup_error">An error occurred while accessing the backup location.</string>
|
||||||
<string name="storage_check_fragment_permission_error">Unable to get the permission to write to the backup location.</string>
|
<string name="storage_check_fragment_permission_error">Unable to get the permission to write to the backup location.</string>
|
||||||
|
@ -69,8 +87,8 @@
|
||||||
<string name="recovery_code_input_hint_11">Word 11</string>
|
<string name="recovery_code_input_hint_11">Word 11</string>
|
||||||
<string name="recovery_code_input_hint_12">Word 12</string>
|
<string name="recovery_code_input_hint_12">Word 12</string>
|
||||||
<string name="recovery_code_error_empty_word">You forgot to enter this word.</string>
|
<string name="recovery_code_error_empty_word">You forgot to enter this word.</string>
|
||||||
<string name="recovery_code_error_invalid_word">Wrong word. Did you mean %1$s or %2$s?</string>
|
<string name="recovery_code_error_invalid_word">Wrong word.</string>
|
||||||
<string name="recovery_code_error_checksum_word">Your code is invalid. Please check all words and try again!</string>
|
<string name="recovery_code_error_checksum_word">Your code is invalid. Please check all words as well as their position and try again!</string>
|
||||||
<string name="recovery_code_verification_ok_title">Recovery code verified</string>
|
<string name="recovery_code_verification_ok_title">Recovery code verified</string>
|
||||||
<string name="recovery_code_verification_ok_message">Your code is correct and will work for restoring your backup.</string>
|
<string name="recovery_code_verification_ok_message">Your code is correct and will work for restoring your backup.</string>
|
||||||
<string name="recovery_code_verification_error_title">Incorrect recovery code</string>
|
<string name="recovery_code_verification_error_title">Incorrect recovery code</string>
|
||||||
|
@ -80,6 +98,8 @@
|
||||||
<string name="recovery_code_verification_new_dialog_title">Wait one second…</string>
|
<string name="recovery_code_verification_new_dialog_title">Wait one second…</string>
|
||||||
<string name="recovery_code_verification_new_dialog_message">Generating a new code will make your existing backups inaccessible. We\'ll try to delete them if possible.\n\nAre you sure you want to do this?</string>
|
<string name="recovery_code_verification_new_dialog_message">Generating a new code will make your existing backups inaccessible. We\'ll try to delete them if possible.\n\nAre you sure you want to do this?</string>
|
||||||
<string name="recovery_code_recreated">New recovery code has been created successfully</string>
|
<string name="recovery_code_recreated">New recovery code has been created successfully</string>
|
||||||
|
<string name="recovery_code_auth_title">Re-enter your screen lock</string>
|
||||||
|
<string name="recovery_code_auth_description">Enter your device credentials to continue</string>
|
||||||
|
|
||||||
<!-- Notification -->
|
<!-- Notification -->
|
||||||
<string name="notification_channel_title">Backup notification</string>
|
<string name="notification_channel_title">Backup notification</string>
|
||||||
|
@ -125,6 +145,7 @@
|
||||||
<string name="restore_choose_restore_set">Choose a backup to restore</string>
|
<string name="restore_choose_restore_set">Choose a backup to restore</string>
|
||||||
<string name="restore_restore_set_times">Last backup %1$s · First %2$s.</string>
|
<string name="restore_restore_set_times">Last backup %1$s · First %2$s.</string>
|
||||||
<string name="restore_back">Don\'t restore</string>
|
<string name="restore_back">Don\'t restore</string>
|
||||||
|
<string name="restore_skip">Skip restoring apps</string>
|
||||||
<string name="restore_invalid_location_title">No backups found</string>
|
<string name="restore_invalid_location_title">No backups found</string>
|
||||||
<string name="restore_invalid_location_message">We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder.</string>
|
<string name="restore_invalid_location_message">We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder.</string>
|
||||||
<string name="restore_set_error">An error occurred while loading the backups.</string>
|
<string name="restore_set_error">An error occurred while loading the backups.</string>
|
||||||
|
@ -141,6 +162,13 @@
|
||||||
<string name="restore_finished_success">Restore complete</string>
|
<string name="restore_finished_success">Restore complete</string>
|
||||||
<string name="restore_finished_error">An error occurred while restoring the backup.</string>
|
<string name="restore_finished_error">An error occurred while restoring the backup.</string>
|
||||||
<string name="restore_finished_button">Finish</string>
|
<string name="restore_finished_button">Finish</string>
|
||||||
|
|
||||||
|
<string name="restore_storage_skip">Skip restoring files</string>
|
||||||
|
<string name="restore_storage_choose_snapshot">Choose a storage backup to restore (experimental)</string>
|
||||||
|
<string name="restore_storage_in_progress_title">Files are being restored…</string>
|
||||||
|
<string name="restore_storage_in_progress_info">Your files are being restored in the background. You can start using your phone while this is running.\n\nSome apps (e.g. Signal or WhatsApp) might require files to be fully restored to import a backup. Try to avoid starting those apps before file restore is not complete.</string>
|
||||||
|
<string name="restore_storage_got_it">Got it</string>
|
||||||
|
|
||||||
<string name="storage_internal_warning_title">Warning</string>
|
<string name="storage_internal_warning_title">Warning</string>
|
||||||
<string name="storage_internal_warning_message">You have chosen internal storage for your backup. This will not be available when your phone is lost or broken.</string>
|
<string name="storage_internal_warning_message">You have chosen internal storage for your backup. This will not be available when your phone is lost or broken.</string>
|
||||||
<string name="storage_internal_warning_choose_other">Choose other</string>
|
<string name="storage_internal_warning_choose_other">Choose other</string>
|
||||||
|
|
|
@ -1,33 +1,43 @@
|
||||||
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:icon="@drawable/ic_save_alt"
|
||||||
|
app:key="backup_location"
|
||||||
|
app:summary="@string/settings_backup_location_none"
|
||||||
|
app:title="@string/settings_backup_location" />
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:fragment="com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeInputFragment"
|
||||||
|
app:icon="@drawable/ic_vpn_key"
|
||||||
|
app:key="backup_recovery_code"
|
||||||
|
app:summary="@string/settings_backup_recovery_code_summary"
|
||||||
|
app:title="@string/settings_backup_recovery_code" />
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/settings_category_apps">
|
||||||
|
|
||||||
<androidx.preference.SwitchPreferenceCompat
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
|
app:allowDividerBelow="true"
|
||||||
app:icon="@drawable/ic_cloud_upload"
|
app:icon="@drawable/ic_cloud_upload"
|
||||||
app:key="backup"
|
app:key="backup"
|
||||||
app:persistent="false"
|
app:persistent="false"
|
||||||
app:title="@string/settings_backup" />
|
app:title="@string/settings_backup" />
|
||||||
|
|
||||||
<androidx.preference.Preference
|
<androidx.preference.Preference
|
||||||
app:allowDividerAbove="true"
|
|
||||||
app:fragment="com.stevesoltys.seedvault.settings.AppStatusFragment"
|
app:fragment="com.stevesoltys.seedvault.settings.AppStatusFragment"
|
||||||
app:icon="@drawable/ic_apps"
|
app:icon="@drawable/ic_apps"
|
||||||
app:key="backup_status"
|
app:key="backup_status"
|
||||||
app:title="@string/settings_backup_status_title"
|
app:title="@string/settings_backup_status_title"
|
||||||
tools:summary="Last backup: Never" />
|
tools:summary="Last backup: Never" />
|
||||||
|
|
||||||
<androidx.preference.Preference
|
|
||||||
app:dependency="backup"
|
|
||||||
app:icon="@drawable/ic_storage"
|
|
||||||
app:key="backup_location"
|
|
||||||
app:summary="@string/settings_backup_location_none"
|
|
||||||
app:title="@string/settings_backup_location" />
|
|
||||||
|
|
||||||
<androidx.preference.SwitchPreferenceCompat
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
app:dependency="backup"
|
app:dependency="backup"
|
||||||
app:key="auto_restore"
|
app:key="auto_restore"
|
||||||
app:persistent="false"
|
app:persistent="false"
|
||||||
app:summary="@string/settings_auto_restore_summary"
|
app:summary="@string/settings_auto_restore_summary"
|
||||||
app:title="@string/settings_auto_restore_title" />
|
app:title="@string/settings_auto_restore_title"
|
||||||
|
tools:defaultValue="true" />
|
||||||
|
|
||||||
<androidx.preference.SwitchPreferenceCompat
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
app:defaultValue="true"
|
app:defaultValue="true"
|
||||||
|
@ -36,18 +46,30 @@
|
||||||
app:summary="@string/settings_backup_apk_summary"
|
app:summary="@string/settings_backup_apk_summary"
|
||||||
app:title="@string/settings_backup_apk_title" />
|
app:title="@string/settings_backup_apk_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/settings_category_storage">
|
||||||
|
|
||||||
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
|
app:defaultValue="false"
|
||||||
|
app:icon="@drawable/ic_storage"
|
||||||
|
app:key="backup_storage"
|
||||||
|
app:title="@string/settings_backup_storage_title" />
|
||||||
|
|
||||||
<androidx.preference.Preference
|
<androidx.preference.Preference
|
||||||
app:dependency="backup"
|
android:dependency="backup_storage"
|
||||||
app:fragment="com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeInputFragment"
|
app:dependency="backup_storage"
|
||||||
app:icon="@drawable/ic_vpn_key"
|
app:fragment="com.stevesoltys.seedvault.ui.files.FileSelectionFragment"
|
||||||
app:key="backup_recovery_code"
|
app:icon="@drawable/ic_library_add"
|
||||||
app:summary="@string/settings_backup_recovery_code_summary"
|
app:key="backup_files"
|
||||||
app:title="@string/settings_backup_recovery_code" />
|
app:summary="@string/settings_backup_files_summary"
|
||||||
|
app:title="@string/settings_backup_files_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<androidx.preference.Preference
|
<androidx.preference.Preference
|
||||||
app:allowDividerAbove="true"
|
app:allowDividerAbove="true"
|
||||||
app:allowDividerBelow="false"
|
app:allowDividerBelow="false"
|
||||||
app:dependency="backup"
|
|
||||||
app:icon="@drawable/ic_info_outline"
|
app:icon="@drawable/ic_info_outline"
|
||||||
app:selectable="false"
|
app:selectable="false"
|
||||||
app:summary="@string/settings_info" />
|
app:summary="@string/settings_info" />
|
||||||
|
|
8
app/src/main/res/xml/settings_expert.xml
Normal file
8
app/src/main/res/xml/settings_expert.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="unlimited_quota"
|
||||||
|
android:summary="@string/settings_expert_quota_summary"
|
||||||
|
android:title="@string/settings_expert_quota_title" />
|
||||||
|
</PreferenceScreen>
|
|
@ -44,7 +44,7 @@ fun ByteArray.toHexString(spacer: String = " "): String {
|
||||||
for (b in this) {
|
for (b in this) {
|
||||||
str += String.format("%02X$spacer", b)
|
str += String.format("%02X$spacer", b)
|
||||||
}
|
}
|
||||||
return str
|
return str.trimEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ByteArray.toIntString(): String {
|
fun ByteArray.toIntString(): String {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.stevesoltys.seedvault.crypto
|
||||||
|
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
|
import com.stevesoltys.seedvault.ui.recoverycode.toMnemonicChars
|
||||||
|
import org.bitcoinj.crypto.MnemonicCode
|
||||||
|
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares kotlin-bip39 library with bitcoinj library
|
||||||
|
* to ensure that kotlin-bip39 is not malicious and can be upgraded safely.
|
||||||
|
*/
|
||||||
|
class Bip39ComparisonTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ITERATIONS = 128
|
||||||
|
private val SEED_SIZE = Mnemonics.WordCount.COUNT_12.bitLength / 8
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Suppress("unused")
|
||||||
|
private fun provideEntropy() = ArrayList<Arguments>(ITERATIONS).apply {
|
||||||
|
for (i in 0 until ITERATIONS) {
|
||||||
|
add(Arguments.of(Random.nextBytes(SEED_SIZE)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideEntropy")
|
||||||
|
fun compareLibs(entropy: ByteArray) {
|
||||||
|
val actualCodeFromEntropy = Mnemonics.MnemonicCode(entropy)
|
||||||
|
val actualWordsFromEntropy = actualCodeFromEntropy.words.map { it.joinToString("") }
|
||||||
|
val expectedWordsFromEntropy = MnemonicCode.INSTANCE.toMnemonic(entropy)
|
||||||
|
// check that entropy produces the same words
|
||||||
|
assertEquals(expectedWordsFromEntropy, actualWordsFromEntropy)
|
||||||
|
|
||||||
|
val actualCodeFromWords =
|
||||||
|
Mnemonics.MnemonicCode(expectedWordsFromEntropy.toMnemonicChars())
|
||||||
|
// check that both codes are valid
|
||||||
|
MnemonicCode.INSTANCE.check(expectedWordsFromEntropy)
|
||||||
|
actualCodeFromEntropy.validate()
|
||||||
|
|
||||||
|
// check that both codes produce same seed
|
||||||
|
assertArrayEquals(
|
||||||
|
MnemonicCode.toSeed(expectedWordsFromEntropy, ""),
|
||||||
|
actualCodeFromWords.toSeed()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
package com.stevesoltys.seedvault.crypto
|
package com.stevesoltys.seedvault.crypto
|
||||||
|
|
||||||
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
import cash.z.ecc.android.bip39.WordList
|
||||||
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import com.stevesoltys.seedvault.toHexString
|
import com.stevesoltys.seedvault.toHexString
|
||||||
import io.github.novacrypto.bip39.JavaxPBKDF2WithHmacSHA512
|
import com.stevesoltys.seedvault.ui.recoverycode.toMnemonicChars
|
||||||
import io.github.novacrypto.bip39.MnemonicGenerator
|
|
||||||
import io.github.novacrypto.bip39.SeedCalculator
|
|
||||||
import io.github.novacrypto.bip39.Words
|
|
||||||
import io.github.novacrypto.bip39.wordlists.English
|
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
@ -2067,62 +2066,197 @@ class WordListTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `word list of library did not change`() {
|
fun `word list of library did not change`() {
|
||||||
|
val libWords = WordList().words
|
||||||
for (i in words.indices) {
|
for (i in words.indices) {
|
||||||
assertEquals(words[i], English.INSTANCE.getWord(i))
|
assertEquals(words[i], libWords[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test createMnemonic`() {
|
fun `test creating MnemonicCode from entropy`() {
|
||||||
val entropy = ByteArray(Words.TWELVE.byteLength())
|
val entropy = ByteArray(Mnemonics.WordCount.COUNT_12.bitLength / 8)
|
||||||
Random.nextBytes(entropy)
|
Random.nextBytes(entropy)
|
||||||
val list = ArrayList<String>(12)
|
val code = Mnemonics.MnemonicCode(entropy)
|
||||||
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) {
|
assertEquals(12, code.words.size)
|
||||||
if (it != " ") list.add(it.toString())
|
for (word in code) {
|
||||||
}
|
assertTrue(word in words, "$word unknown")
|
||||||
assertEquals(12, list.size)
|
|
||||||
for (word in list) {
|
|
||||||
assertTrue(word in words)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
fun `12 not validating words generate seed that novacrypt generated`() {
|
||||||
fun `12 words generate expected seed`() {
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"64AA8C388EC0F3A13C7E51653BC766E30668D30952AB34381C4B174BF3278774" +
|
"64AA8C388EC0F3A13C7E51653BC766E30668D30952AB34381C4B174BF3278774" +
|
||||||
"B4EE43D0BA08BCBCE0D0B806DEB7AA364A83525C34847078B2A8002A3E116066",
|
"B4EE43D0BA08BCBCE0D0B806DEB7AA364A83525C34847078B2A8002A3E116066",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
Mnemonics.MnemonicCode(
|
||||||
"write wrong yard year yellow you young youth zebra zero zone zoo", ""
|
"write wrong yard year yellow you young youth zebra zero zone zoo"
|
||||||
).toHexString("")
|
).toSeed(validate = false).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"E911FAA42F389AA9F6D5A40B2ECB876B06D6D1FFBD5885C54720398EB11918CA" +
|
"E911FAA42F389AA9F6D5A40B2ECB876B06D6D1FFBD5885C54720398EB11918CA" +
|
||||||
"B8F7BAD70FD5BE39BEB4EB065610700D1CFF1D4BFAA26F998357E15E79002779",
|
"B8F7BAD70FD5BE39BEB4EB065610700D1CFF1D4BFAA26F998357E15E79002779",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
Mnemonics.MnemonicCode(
|
||||||
"matrix lava they brand negative spray floor gym purity picture ritual disorder", ""
|
"matrix lava they brand negative spray floor gym purity picture ritual disorder"
|
||||||
).toHexString("")
|
).toSeed(validate = false).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"DDB26091680CF30D0DC615546E4612327DB287B6B2B8B8947A3E12580315D38C" +
|
"DDB26091680CF30D0DC615546E4612327DB287B6B2B8B8947A3E12580315D38C" +
|
||||||
"3BF7DD0EB4E9E50B10A41925332E0C8ED43C80DBA29281EF331A1DFA858BF1C9",
|
"3BF7DD0EB4E9E50B10A41925332E0C8ED43C80DBA29281EF331A1DFA858BF1C9",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
Mnemonics.MnemonicCode(
|
||||||
"middle rack south alert ribbon tube hope involve defy oxygen gloom rabbit", ""
|
"middle rack south alert ribbon tube hope involve defy oxygen gloom rabbit"
|
||||||
).toHexString("")
|
).toSeed(validate = false).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"4815B580D0DCDA08334C92B3CB9A8436CD581C55841FB2794FB1E3D6E389F447" +
|
"4815B580D0DCDA08334C92B3CB9A8436CD581C55841FB2794FB1E3D6E389F447" +
|
||||||
"C8C6520B2FE567720950F5B39BE7EC42C0BC98D3C63F8FEF642B5BD3EE4CDD7B",
|
"C8C6520B2FE567720950F5B39BE7EC42C0BC98D3C63F8FEF642B5BD3EE4CDD7B",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
Mnemonics.MnemonicCode(
|
||||||
"interest mask trial hold foot segment fade page monitor apple garden shuffle", ""
|
"interest mask trial hold foot segment fade page monitor apple garden shuffle"
|
||||||
).toHexString("")
|
).toSeed(validate = false).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"FF462543D8FB9DAE6C17FA7BA047238664207FCC797D6688E10DD1B3CFD183D4" +
|
"FF462543D8FB9DAE6C17FA7BA047238664207FCC797D6688E10DD1B3CFD183D4" +
|
||||||
"928AD088E8287B69BABCAEB0F87A2DFF2ADD49A7FDB7EB2554D7344F09C41A76",
|
"928AD088E8287B69BABCAEB0F87A2DFF2ADD49A7FDB7EB2554D7344F09C41A76",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
Mnemonics.MnemonicCode(
|
||||||
"palace glory gospel garment obscure person edge total hunt fix setup uphold\n", ""
|
"palace glory gospel garment obscure person edge total hunt fix setup uphold\n"
|
||||||
).toHexString("")
|
).toSeed(validate = false).toHexString("")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `12 valid words generate seed that novacrypt generated`() {
|
||||||
|
assertEquals(
|
||||||
|
"C6F9762718449C9D0794FEC140D2C8D4E23FF8E3701D64C03DDD13C69BC73E48" +
|
||||||
|
"6AB89AB2C7C9BEA43F4AF839F2078595851D5D48FEC6A9FC6C25F399DBB909F9",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"script vault basic album cotton car entire jaguar correct anger select flower"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"13C5188428B1DF8A5333E60BA7EC47F7E75585315C73BD19812D3591C5F4C52B" +
|
||||||
|
"B2FC1FF40B1942E2A1EF9F34F586114ED37D46A5A3907A43B317E937C1D9D2CD",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"drastic toy fatal goose treat saddle chalk fame dismiss employ super behind"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"40B41BB22AC3A507F26A78E027A3B3C5C8F45FF0F5593D82762C74AE69FA548B" +
|
||||||
|
"A72C0CED31DED6211884B412E7B80F932F9830FA7A67CDB5B28604213DE6599C",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"nation infant heart virus argue two vivid slam lend decorate turn wish"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"B6D755172B6E9353A25EB3559336C17A8619F3EBE55E8A9A74A44E1AB88EF5E2" +
|
||||||
|
"C6E12FE132E42A55CC3F8F9224E6A0ABC9C3FF4EB9523A4E9750CDAAEFBA6282",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"elbow boy powder robot eagle rival neutral pigeon oil shrimp demand health"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"3EDB1292B4D124426201AC523FCC2572184E0B63667DA7DF105AD8FCCD16C074" +
|
||||||
|
"C6DAF9C7D644B4B48AF75185D21B9E7D778FFE55F836C539581DEBB98C331526",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"build setup screen solution prepare spice organ ten loud seek ask attract"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"65986351CD054822B40E417855AC2B5651C5F87892F17ED2A984F6B59DD5FB4E" +
|
||||||
|
"6A4568ABF7E06D93CBCC69BB68F2625E3E8AF2751106380922D49C0D0D0B456B",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"unhappy welcome pizza inflict inherit village minimum orient cheap swear grunt giraffe" // ktlint-disable max-line-length
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"639034B381740A9FA5B8A84715CF18B21EBD343DD91F7B6124A0EFC32A636619" +
|
||||||
|
"49B02A7810B1A99D8E8CC4CD7D046CE59EAAADBB52DDC0B5036EFED007E1CFF6",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"rather suit pluck afford avocado diary swap library earn song rival fiber"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"43E1417221BFB40851DE286B543B51DEE9C01D239B2C2E8A355D45B3DF95DFAA" +
|
||||||
|
"C8DBCAEF1D864D91759A07057DBDB891900D583CAB09BD0655493912108AE65A",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"toss note family morning silk edge high error appear tilt almost myth"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"14084AAF9CFCAC386D4CE5B9140BEADBF727B1B09786A67A574B668A1A4AE0A3" +
|
||||||
|
"21B8D4E7BC005980B088A160B6EC08A1CB892C2090C58D95A7C6AAD16C14EE1E",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"parrot burden release bronze section fantasy ridge blood direct physical spoil asthma" // ktlint-disable max-line-length
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"E11E8737327EE6A640761B3888C349D829A60FEAEFB7914D2AE1616F0AC45B9A" +
|
||||||
|
"322F41D0030C89E209300FA25615FE6B5BDEF73F3E5CE21167685E8A27EE0790",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"version deliver worry sick flee submit pledge adapt night swear glare adult"
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"02652896F67695C03F379A354685A8A0B92D0F303F77461476E80BB594EAD84B" +
|
||||||
|
"D00B2943C2229ED843C65F6C53A376005871FF74F834E6B6E3B57FFD83D3FB12",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"exercise curtain initial model travel client twist neutral peace unfold start shell" // ktlint-disable max-line-length
|
||||||
|
).toSeed().toHexString("")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `entropy generates same words that novacrypt generated`() {
|
||||||
|
assertEquals(
|
||||||
|
"B8 3F 0D 49 7F 2A F4 69 13 71 47 D5 54 9D 17 0B",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"return wear false wrestle quantum cruel evidence cigar stem pilot easy blood"
|
||||||
|
).toEntropy().toHexString()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"AA 8E 3F 1C EF 60 20 D7 D2 BB FD A0 AA AD 69 09",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"pride impose shrimp tell acoustic hip enough leisure pass fever fog basket"
|
||||||
|
).toEntropy().toHexString()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"CE E8 17 7E AB 9E 49 8A 0B 32 3C 97 94 07 0C 32",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"solve doll text fire tonight shallow coast elegant nurse parent seek grass"
|
||||||
|
).toEntropy().toHexString()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"AA 27 4B D8 7B 0D 18 EB 5D 08 BD 11 73 36 C1 06",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"pretty demise voyage voyage spice interest injury bless badge often raccoon artefact" // ktlint-disable max-line-length
|
||||||
|
).toEntropy().toHexString()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"16 5F A7 40 26 E9 51 70 1B 7A 5C D2 AB CD 73 7E",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
"bind wood source evidence never retreat hospital entire sport fury fresh woman"
|
||||||
|
).toEntropy().toHexString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test create MnemonicCode from List of CharSequence`() {
|
||||||
|
assertEquals(
|
||||||
|
"B8 3F 0D 49 7F 2A F4 69 13 71 47 D5 54 9D 17 0B",
|
||||||
|
Mnemonics.MnemonicCode(
|
||||||
|
listOf<CharSequence>(
|
||||||
|
"return",
|
||||||
|
"wear",
|
||||||
|
"false",
|
||||||
|
"wrestle",
|
||||||
|
"quantum",
|
||||||
|
"cruel",
|
||||||
|
"evidence",
|
||||||
|
"cigar",
|
||||||
|
"stem",
|
||||||
|
"pilot",
|
||||||
|
"easy",
|
||||||
|
"blood"
|
||||||
|
).toMnemonicChars()
|
||||||
|
).toEntropy().toHexString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.stevesoltys.seedvault.TestApp
|
import com.stevesoltys.seedvault.TestApp
|
||||||
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -27,6 +29,14 @@ internal class DocumentFileTest {
|
||||||
"content://com.android.externalstorage.documents/tree/" +
|
"content://com.android.externalstorage.documents/tree/" +
|
||||||
"primary%3A/document/primary%3A.SeedVaultAndroidBackup"
|
"primary%3A/document/primary%3A.SeedVaultAndroidBackup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// needed since 'androidx.documentfile:documentfile:1.0.1'
|
||||||
|
val pm: PackageManager = mockk()
|
||||||
|
every { context.packageManager } returns pm
|
||||||
|
every { pm.queryIntentContentProviders(any(), 0) } returns emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
private val parentFile: DocumentFile = DocumentFile.fromTreeUri(context, parentUri)!!
|
private val parentFile: DocumentFile = DocumentFile.fromTreeUri(context, parentUri)!!
|
||||||
private val uri: Uri = Uri.parse(
|
private val uri: Uri = Uri.parse(
|
||||||
"content://com.android.externalstorage.documents/tree/" +
|
"content://com.android.externalstorage.documents/tree/" +
|
||||||
|
|
|
@ -65,10 +65,22 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
|
|
||||||
private val backupPlugin = mockk<BackupPlugin>()
|
private val backupPlugin = mockk<BackupPlugin>()
|
||||||
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
||||||
private val kvBackup =
|
private val kvBackup = KVBackup(
|
||||||
KVBackup(kvBackupPlugin, inputFactory, headerWriter, cryptoImpl, notificationManager)
|
plugin = kvBackupPlugin,
|
||||||
|
settingsManager = settingsManager,
|
||||||
|
inputFactory = inputFactory,
|
||||||
|
headerWriter = headerWriter,
|
||||||
|
crypto = cryptoImpl,
|
||||||
|
nm = notificationManager
|
||||||
|
)
|
||||||
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
||||||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
private val fullBackup = FullBackup(
|
||||||
|
plugin = fullBackupPlugin,
|
||||||
|
settingsManager = settingsManager,
|
||||||
|
inputFactory = inputFactory,
|
||||||
|
headerWriter = headerWriter,
|
||||||
|
crypto = cryptoImpl
|
||||||
|
)
|
||||||
private val apkBackup = mockk<ApkBackup>()
|
private val apkBackup = mockk<ApkBackup>()
|
||||||
private val packageService: PackageService = mockk()
|
private val packageService: PackageService = mockk()
|
||||||
private val backup = BackupCoordinator(
|
private val backup = BackupCoordinator(
|
||||||
|
@ -277,6 +289,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
val bInputStream = ByteArrayInputStream(appData)
|
val bInputStream = ByteArrayInputStream(appData)
|
||||||
coEvery { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
|
coEvery { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
|
||||||
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
|
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
||||||
coEvery {
|
coEvery {
|
||||||
apkBackup.backupApkIfNecessary(
|
apkBackup.backupApkIfNecessary(
|
||||||
|
|
|
@ -22,7 +22,7 @@ import kotlin.random.Random
|
||||||
internal class FullBackupTest : BackupTest() {
|
internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
private val plugin = mockk<FullBackupPlugin>()
|
private val plugin = mockk<FullBackupPlugin>()
|
||||||
private val backup = FullBackup(plugin, inputFactory, headerWriter, crypto)
|
private val backup = FullBackup(plugin, settingsManager, inputFactory, headerWriter, crypto)
|
||||||
|
|
||||||
private val bytes = ByteArray(23).apply { Random.nextBytes(this) }
|
private val bytes = ByteArray(23).apply { Random.nextBytes(this) }
|
||||||
private val closeBytes = ByteArray(42).apply { Random.nextBytes(this) }
|
private val closeBytes = ByteArray(42).apply { Random.nextBytes(this) }
|
||||||
|
@ -35,11 +35,19 @@ internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `checkFullBackupSize exceeds quota`() {
|
fun `checkFullBackupSize exceeds quota`() {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { plugin.getQuota() } returns quota
|
every { plugin.getQuota() } returns quota
|
||||||
|
|
||||||
assertEquals(TRANSPORT_QUOTA_EXCEEDED, backup.checkFullBackupSize(quota + 1))
|
assertEquals(TRANSPORT_QUOTA_EXCEEDED, backup.checkFullBackupSize(quota + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `checkFullBackupSize does not exceed quota when unlimited`() {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns true
|
||||||
|
|
||||||
|
assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(quota + 1))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `checkFullBackupSize for no data`() {
|
fun `checkFullBackupSize for no data`() {
|
||||||
assertEquals(TRANSPORT_PACKAGE_REJECTED, backup.checkFullBackupSize(0))
|
assertEquals(TRANSPORT_PACKAGE_REJECTED, backup.checkFullBackupSize(0))
|
||||||
|
@ -52,6 +60,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `checkFullBackupSize accepts min data`() {
|
fun `checkFullBackupSize accepts min data`() {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { plugin.getQuota() } returns quota
|
every { plugin.getQuota() } returns quota
|
||||||
|
|
||||||
assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(1))
|
assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(1))
|
||||||
|
@ -59,6 +68,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `checkFullBackupSize accepts max data`() {
|
fun `checkFullBackupSize accepts max data`() {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { plugin.getQuota() } returns quota
|
every { plugin.getQuota() } returns quota
|
||||||
|
|
||||||
assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(quota))
|
assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(quota))
|
||||||
|
@ -77,6 +87,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sendBackupData first call over quota`() = runBlocking {
|
fun `sendBackupData first call over quota`() = runBlocking {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
expectInitializeOutputStream()
|
expectInitializeOutputStream()
|
||||||
val numBytes = (quota + 1).toInt()
|
val numBytes = (quota + 1).toInt()
|
||||||
|
@ -93,6 +104,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sendBackupData second call over quota`() = runBlocking {
|
fun `sendBackupData second call over quota`() = runBlocking {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
expectInitializeOutputStream()
|
expectInitializeOutputStream()
|
||||||
val numBytes1 = quota.toInt()
|
val numBytes1 = quota.toInt()
|
||||||
|
@ -115,6 +127,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
fun `sendBackupData throws exception when reading from InputStream`() = runBlocking {
|
fun `sendBackupData throws exception when reading from InputStream`() = runBlocking {
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
expectInitializeOutputStream()
|
expectInitializeOutputStream()
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { plugin.getQuota() } returns quota
|
every { plugin.getQuota() } returns quota
|
||||||
every { inputStream.read(any(), any(), bytes.size) } throws IOException()
|
every { inputStream.read(any(), any(), bytes.size) } throws IOException()
|
||||||
expectClearState()
|
expectClearState()
|
||||||
|
@ -131,6 +144,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
fun `sendBackupData throws exception when getting outputStream`() = runBlocking {
|
fun `sendBackupData throws exception when getting outputStream`() = runBlocking {
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
|
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { plugin.getQuota() } returns quota
|
every { plugin.getQuota() } returns quota
|
||||||
coEvery { plugin.getOutputStream(packageInfo) } throws IOException()
|
coEvery { plugin.getOutputStream(packageInfo) } throws IOException()
|
||||||
expectClearState()
|
expectClearState()
|
||||||
|
@ -147,6 +161,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
fun `sendBackupData throws exception when writing header`() = runBlocking {
|
fun `sendBackupData throws exception when writing header`() = runBlocking {
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
|
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { plugin.getQuota() } returns quota
|
every { plugin.getQuota() } returns quota
|
||||||
coEvery { plugin.getOutputStream(packageInfo) } returns outputStream
|
coEvery { plugin.getOutputStream(packageInfo) } returns outputStream
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
|
@ -166,6 +181,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
expectInitializeOutputStream()
|
expectInitializeOutputStream()
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { plugin.getQuota() } returns quota
|
every { plugin.getQuota() } returns quota
|
||||||
every { inputStream.read(any(), any(), bytes.size) } returns bytes.size
|
every { inputStream.read(any(), any(), bytes.size) } returns bytes.size
|
||||||
every { crypto.encryptSegment(outputStream, any()) } throws IOException()
|
every { crypto.encryptSegment(outputStream, any()) } throws IOException()
|
||||||
|
@ -181,6 +197,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sendBackupData runs ok`() = runBlocking {
|
fun `sendBackupData runs ok`() = runBlocking {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
expectInitializeOutputStream()
|
expectInitializeOutputStream()
|
||||||
val numBytes1 = (quota / 2).toInt()
|
val numBytes1 = (quota / 2).toInt()
|
||||||
|
@ -234,6 +251,7 @@ internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clearState throws exception when flushing OutputStream`() = runBlocking {
|
fun `clearState throws exception when flushing OutputStream`() = runBlocking {
|
||||||
|
every { settingsManager.isQuotaUnlimited() } returns false
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
expectInitializeOutputStream()
|
expectInitializeOutputStream()
|
||||||
val numBytes = 42
|
val numBytes = 42
|
||||||
|
|
|
@ -36,7 +36,14 @@ internal class KVBackupTest : BackupTest() {
|
||||||
private val dataInput = mockk<BackupDataInput>()
|
private val dataInput = mockk<BackupDataInput>()
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
private val notificationManager = mockk<BackupNotificationManager>()
|
||||||
|
|
||||||
private val backup = KVBackup(plugin, inputFactory, headerWriter, crypto, notificationManager)
|
private val backup = KVBackup(
|
||||||
|
plugin = plugin,
|
||||||
|
settingsManager = settingsManager,
|
||||||
|
inputFactory = inputFactory,
|
||||||
|
headerWriter = headerWriter,
|
||||||
|
crypto = crypto,
|
||||||
|
nm = notificationManager
|
||||||
|
)
|
||||||
|
|
||||||
private val key = getRandomString(MAX_KEY_LENGTH_SIZE)
|
private val key = getRandomString(MAX_KEY_LENGTH_SIZE)
|
||||||
private val key64 = Base64.getEncoder().encodeToString(key.toByteArray(Utf8))
|
private val key64 = Base64.getEncoder().encodeToString(key.toByteArray(Utf8))
|
||||||
|
|
17
build.gradle
17
build.gradle
|
@ -1,22 +1,33 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
|
|
||||||
// 1.3.21 Android 10
|
// 1.3.21 Android 10
|
||||||
// 1.3.61 Android 11
|
// 1.3.61 Android 11
|
||||||
// Check:
|
// Check:
|
||||||
// https://android.googlesource.com/platform/external/kotlinc/+/refs/tags/android-11.0.0_r3/build.txt
|
// https://android.googlesource.com/platform/external/kotlinc/+/refs/tags/android-11.0.0_r3/build.txt
|
||||||
ext.kotlin_version = '1.3.61'
|
ext.aosp_kotlin_version = '1.3.61'
|
||||||
|
ext.kotlin_version = '1.4.31'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
jcenter()
|
jcenter()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection DifferentKotlinGradleVersion
|
//noinspection DifferentKotlinGradleVersion
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.14"
|
||||||
|
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
buildToolsVersion = '30.0.2'
|
||||||
|
compileSdkVersion = 30
|
||||||
|
minSdkVersion = 29
|
||||||
|
targetSdkVersion = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'gradle/dependencies.gradle'
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
|
@ -13,11 +13,13 @@ android_app {
|
||||||
required: [
|
required: [
|
||||||
"default-permissions_org.calyxos.backup.contacts",
|
"default-permissions_org.calyxos.backup.contacts",
|
||||||
],
|
],
|
||||||
|
product_specific: true,
|
||||||
sdk_version: "current",
|
sdk_version: "current",
|
||||||
}
|
}
|
||||||
|
|
||||||
prebuilt_etc {
|
prebuilt_etc {
|
||||||
name: "default-permissions_org.calyxos.backup.contacts",
|
name: "default-permissions_org.calyxos.backup.contacts",
|
||||||
|
product_specific: true,
|
||||||
sub_dir: "default-permissions",
|
sub_dir: "default-permissions",
|
||||||
src: "default-permissions_org.calyxos.backup.contacts.xml",
|
src: "default-permissions_org.calyxos.backup.contacts.xml",
|
||||||
filename_from_src: true,
|
filename_from_src: true,
|
||||||
|
|
|
@ -50,16 +50,12 @@ def aospDeps = fileTree(include: [
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation aospDeps
|
implementation aospDeps
|
||||||
|
|
||||||
//noinspection GradleDependency
|
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
testImplementation 'junit:junit:4.13.1'
|
testImplementation "junit:junit:$junit4_version"
|
||||||
def mockk_version = "1.10.2"
|
|
||||||
testImplementation "io.mockk:mockk:$mockk_version"
|
testImplementation "io.mockk:mockk:$mockk_version"
|
||||||
|
|
||||||
//noinspection GradleDependency
|
|
||||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
def espresso_version = "3.3.0"
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
|
||||||
androidTestImplementation "io.mockk:mockk-android:$mockk_version"
|
androidTestImplementation "io.mockk:mockk-android:$mockk_version"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.calyxos.backup.contacts"
|
package="org.calyxos.backup.contacts"
|
||||||
android:versionCode="30000021"
|
android:versionCode="30000221"
|
||||||
android:versionName="11-1.2">
|
android:versionName="11-2.2">
|
||||||
<!--
|
<!--
|
||||||
The version code is the targeted SDK_VERSION plus 6 digits for our own version code.
|
The version code is the targeted SDK_VERSION plus 6 digits for our own version code.
|
||||||
The version name is the targeted Android version followed by - and our own version name.
|
The version name is the targeted Android version followed by - and our own version name.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
org.gradle.jvmargs=-Xmx1g
|
org.gradle.jvmargs=-Xmx1g
|
||||||
|
org.gradle.configureondemand=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=false
|
android.enableJetifier=false
|
||||||
|
kotlin.code.style=official
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
|
ext {
|
||||||
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#2713
|
||||||
|
ext.room_version = "2.3.0-alpha02"
|
||||||
|
// http://aosp.opersys.com/xref/android-11.0.0_r27/xref/external/protobuf/java/pom.xml#7
|
||||||
|
ext.protobuf_version = "3.9.1"
|
||||||
|
junit4_version = "4.13.2"
|
||||||
|
junit5_version = "5.5.2" // careful, upgrading this can change a Cipher's IV size in tests!?
|
||||||
|
mockk_version = "1.10.2" // higher versions get us a kotlin version conflict
|
||||||
|
espresso_version = "3.3.0"
|
||||||
|
}
|
||||||
|
|
||||||
// To produce these binaries, in latest AOSP source tree, run
|
// To produce these binaries, in latest AOSP source tree, run
|
||||||
// $ m
|
// $ m
|
||||||
def aospDeps = fileTree(include: [
|
ext.aosp_libs = fileTree(include: [
|
||||||
// For more information about this module:
|
// For more information about this module:
|
||||||
// https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-11.0.0_r3/Android.bp#507
|
// https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-11.0.0_r3/Android.bp#507
|
||||||
// framework_intermediates/classes-header.jar works for gradle build as well,
|
// framework_intermediates/classes-header.jar works for gradle build as well,
|
||||||
|
@ -9,122 +20,82 @@ def aospDeps = fileTree(include: [
|
||||||
// out/target/common/obj/JAVA_LIBRARIES/framework-minus-apex_intermediates/classes.jar
|
// out/target/common/obj/JAVA_LIBRARIES/framework-minus-apex_intermediates/classes.jar
|
||||||
'android.jar',
|
'android.jar',
|
||||||
// out/target/common/obj/JAVA_LIBRARIES/core-libart.com.android.art.release_intermediates/classes.jar
|
// out/target/common/obj/JAVA_LIBRARIES/core-libart.com.android.art.release_intermediates/classes.jar
|
||||||
'libcore.jar'
|
'libcore.jar',
|
||||||
], dir: 'libs')
|
], dir: "$projectDir/app/libs")
|
||||||
|
|
||||||
dependencies {
|
ext.kotlin_libs = [
|
||||||
compileOnly aospDeps
|
std: [
|
||||||
|
dependencies.create('org.jetbrains.kotlin:kotlin-stdlib') {
|
||||||
/**
|
version { strictly "$aosp_kotlin_version" }
|
||||||
* Dependencies in AOSP
|
},
|
||||||
*
|
dependencies.create('org.jetbrains.kotlin:kotlin-stdlib-jdk8') {
|
||||||
* We try to keep the dependencies in sync with what AOSP ships as Seedvault is meant to be built
|
version { strictly "$aosp_kotlin_version" }
|
||||||
* with the AOSP build system and gradle builds are just for more pleasant development.
|
},
|
||||||
* Using the AOSP versions in gradle builds allows us to spot issues early on.
|
dependencies.create('org.jetbrains.kotlin:kotlin-stdlib-common') {
|
||||||
*/
|
version { strictly "$aosp_kotlin_version" }
|
||||||
|
},
|
||||||
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8') {
|
],
|
||||||
version { strictly "$kotlin_version" }
|
coroutines: [
|
||||||
}
|
dependencies.create('org.jetbrains.kotlinx:kotlinx-coroutines-core') {
|
||||||
implementation('org.jetbrains.kotlin:kotlin-stdlib-common') {
|
|
||||||
version { strictly "$kotlin_version" }
|
|
||||||
}
|
|
||||||
implementation('org.jetbrains.kotlin:kotlin-stdlib') {
|
|
||||||
version { strictly "$kotlin_version" }
|
|
||||||
}
|
|
||||||
|
|
||||||
// These coroutine libraries get upgraded otherwise to versions incompatible with kotlin version
|
|
||||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-core') {
|
|
||||||
// https://android.googlesource.com/platform/prebuilts/tools/+/refs/tags/android-11.0.0_r3/common/m2/Android.bp#326
|
// https://android.googlesource.com/platform/prebuilts/tools/+/refs/tags/android-11.0.0_r3/common/m2/Android.bp#326
|
||||||
version { strictly '1.3.0' }
|
version { strictly '1.3.0' }
|
||||||
}
|
},
|
||||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-android') {
|
dependencies.create('org.jetbrains.kotlinx:kotlinx-coroutines-android') {
|
||||||
// https://android.googlesource.com/platform/prebuilts/tools/+/refs/tags/android-11.0.0_r3/common/m2/Android.bp#340
|
// https://android.googlesource.com/platform/prebuilts/tools/+/refs/tags/android-11.0.0_r3/common/m2/Android.bp#340
|
||||||
version { strictly '1.3.0' }
|
version { strictly '1.3.0' }
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
implementation('androidx.core:core-ktx') {
|
ext.std_libs = [
|
||||||
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#610
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#610
|
||||||
|
androidx_core: dependencies.create('androidx.core:core-ktx') {
|
||||||
version { strictly '1.5.0-alpha01' }
|
version { strictly '1.5.0-alpha01' }
|
||||||
}
|
},
|
||||||
|
|
||||||
// A newer version gets pulled in with AOSP via core, so we include this here explicitly
|
|
||||||
implementation('androidx.fragment:fragment-ktx') {
|
|
||||||
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#930
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#930
|
||||||
|
androidx_fragment: dependencies.create('androidx.fragment:fragment-ktx') {
|
||||||
version { strictly '1.3.0-alpha07' }
|
version { strictly '1.3.0-alpha07' }
|
||||||
}
|
},
|
||||||
|
|
||||||
implementation('androidx.preference:preference') {
|
|
||||||
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#2412
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#2412
|
||||||
|
androidx_preference: dependencies.create('androidx.preference:preference') {
|
||||||
version { strictly '1.1.1' } // should be 1.2.0-alpha01, but that is not even released, yet
|
version { strictly '1.1.1' } // should be 1.2.0-alpha01, but that is not even released, yet
|
||||||
}
|
},
|
||||||
|
|
||||||
implementation('androidx.lifecycle:lifecycle-viewmodel-ktx') {
|
|
||||||
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#1553
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#1553
|
||||||
|
androidx_lifecycle_viewmodel_ktx: dependencies.create('androidx.lifecycle:lifecycle-viewmodel-ktx') {
|
||||||
version { strictly '2.3.0-alpha05' }
|
version { strictly '2.3.0-alpha05' }
|
||||||
}
|
},
|
||||||
|
|
||||||
implementation('androidx.lifecycle:lifecycle-livedata-ktx') {
|
|
||||||
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#1353
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#1353
|
||||||
|
androidx_lifecycle_livedata_ktx: dependencies.create('androidx.lifecycle:lifecycle-livedata-ktx') {
|
||||||
version { strictly '2.3.0-alpha05' }
|
version { strictly '2.3.0-alpha05' }
|
||||||
}
|
},
|
||||||
|
|
||||||
implementation('androidx.constraintlayout:constraintlayout') {
|
|
||||||
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/extras/constraint-layout-x/Android.bp#30
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/extras/constraint-layout-x/Android.bp#30
|
||||||
|
androidx_constraintlayout: dependencies.create('androidx.constraintlayout:constraintlayout') {
|
||||||
version { strictly '2.0.0-beta7' }
|
version { strictly '2.0.0-beta7' }
|
||||||
}
|
},
|
||||||
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#708
|
||||||
implementation('com.google.android.material:material') {
|
androidx_documentfile: dependencies.create('androidx.documentfile:documentfile') {
|
||||||
|
version { strictly '1.0.1' } // should be 1.1.0-alpha01, but that is not even released, yet
|
||||||
|
},
|
||||||
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/extras/material-design-x/Android.bp#6
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/extras/material-design-x/Android.bp#6
|
||||||
|
com_google_android_material: dependencies.create('com.google.android.material:material') {
|
||||||
version { strictly '1.1.0-alpha05' }
|
version { strictly '1.1.0-alpha05' }
|
||||||
}
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
ext.lint_libs = [
|
||||||
|
exceptions: 'com.github.thirdegg:lint-rules:0.0.6-beta'
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
ext.storage_libs = [
|
||||||
* External Dependencies
|
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-11.0.0_r3/current/androidx/Android.bp#2711
|
||||||
*
|
androidx_room_runtime: dependencies.create('androidx.room:room-runtime') {
|
||||||
* If the dependencies below are updated,
|
version { strictly "$room_version" }
|
||||||
* please make sure to update the prebuilt libraries and the Android.bp files
|
},
|
||||||
* in the top-level `libs` folder to reflect that.
|
// http://aosp.opersys.com/xref/android-11.0.0_r27/xref/external/protobuf/java/pom.xml#7
|
||||||
* You can copy these libraries from ~/.gradle/caches/modules-2
|
com_google_protobuf_javalite: dependencies.create('com.google.protobuf:protobuf-javalite') {
|
||||||
*/
|
version { strictly "$protobuf_version" }
|
||||||
|
},
|
||||||
def koin_version = '2.1.1' // later versions require newer kotlin version
|
com_google_crypto_tink_android: dependencies.create('com.google.crypto.tink:tink-android') {
|
||||||
//noinspection GradleDependency
|
version { strictly '1.5.0' }
|
||||||
implementation("org.koin:koin-android:$koin_version") {
|
},
|
||||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
|
]
|
||||||
}
|
|
||||||
//noinspection GradleDependency
|
|
||||||
implementation("org.koin:koin-androidx-viewmodel:$koin_version") {
|
|
||||||
exclude group: 'org.koin', module: 'koin-androidx-scope'
|
|
||||||
exclude group: 'androidx.lifecycle'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation('io.github.novacrypto:BIP39:2019.01.27') {
|
|
||||||
exclude group: 'com.madgag.spongycastle'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Dependencies (do not concern the AOSP build)
|
|
||||||
*/
|
|
||||||
|
|
||||||
lintChecks 'com.github.thirdegg:lint-rules:0.0.5-alpha'
|
|
||||||
|
|
||||||
def junit_version = "5.5.2" // careful, upgrading this can change a Cipher's IV size in tests!?
|
|
||||||
def mockk_version = "1.10.0"
|
|
||||||
testImplementation aospDeps // anything less than 'implementation' fails tests run with gradlew
|
|
||||||
testImplementation 'androidx.test.ext:junit:1.1.2'
|
|
||||||
testImplementation('org.robolectric:robolectric:4.3.1') { // 4.4 has issue with non-idle Looper
|
|
||||||
// https://github.com/robolectric/robolectric/issues/5245
|
|
||||||
exclude group: "com.google.auto.service", module: "auto-service"
|
|
||||||
}
|
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version"
|
|
||||||
testImplementation "io.mockk:mockk:$mockk_version"
|
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_version"
|
|
||||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit_version"
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
|
||||||
androidTestImplementation "io.mockk:mockk-android:$mockk_version"
|
|
||||||
}
|
|
||||||
|
|
11
gradle/ktlint.gradle
Normal file
11
gradle/ktlint.gradle
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
ktlint {
|
||||||
|
version = "0.40.0"
|
||||||
|
android = true
|
||||||
|
enableExperimentalRules = false
|
||||||
|
verbose = true
|
||||||
|
disabledRules = [
|
||||||
|
"import-ordering",
|
||||||
|
"no-blank-line-before-rbrace",
|
||||||
|
"indent", // remove in 0.41 https://github.com/pinterest/ktlint/issues/764
|
||||||
|
]
|
||||||
|
}
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||||
distributionSha256Sum=143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920
|
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
|
||||||
|
|
5
libs/Android.bp
Normal file
5
libs/Android.bp
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
java_import {
|
||||||
|
name: "seedvault-lib-kotlin-bip39",
|
||||||
|
jars: ["kotlin-bip39-1.0.2.jar"],
|
||||||
|
sdk_version: "current",
|
||||||
|
}
|
BIN
libs/kotlin-bip39-1.0.2.jar
Normal file
BIN
libs/kotlin-bip39-1.0.2.jar
Normal file
Binary file not shown.
|
@ -1,27 +0,0 @@
|
||||||
java_import {
|
|
||||||
name: "seedvault-lib-novacrypto-bip39-nodeps",
|
|
||||||
jars: ["BIP39-2019.01.27.jar"],
|
|
||||||
sdk_version: "current",
|
|
||||||
}
|
|
||||||
|
|
||||||
java_library_static {
|
|
||||||
name: "seedvault-lib-novacrypto-bip39",
|
|
||||||
static_libs: [
|
|
||||||
"seedvault-lib-novacrypto-bip39-nodeps",
|
|
||||||
"seedvault-lib-novacrypto-sha256",
|
|
||||||
"seedvault-lib-novacrypto-toruntime",
|
|
||||||
],
|
|
||||||
sdk_version: "current",
|
|
||||||
}
|
|
||||||
|
|
||||||
java_import {
|
|
||||||
name: "seedvault-lib-novacrypto-sha256",
|
|
||||||
jars: ["SHA256-2019.01.27.jar"],
|
|
||||||
sdk_version: "current",
|
|
||||||
}
|
|
||||||
|
|
||||||
java_import {
|
|
||||||
name: "seedvault-lib-novacrypto-toruntime",
|
|
||||||
jars: ["ToRuntime-0.9.0.jar"],
|
|
||||||
sdk_version: "current",
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -5,5 +5,6 @@
|
||||||
<permission name="android.permission.MANAGE_USB"/>
|
<permission name="android.permission.MANAGE_USB"/>
|
||||||
<permission name="android.permission.INSTALL_PACKAGES"/>
|
<permission name="android.permission.INSTALL_PACKAGES"/>
|
||||||
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
|
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
|
||||||
|
<permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||||
</privapp-permissions>
|
</privapp-permissions>
|
||||||
</permissions>
|
</permissions>
|
|
@ -1,2 +1,4 @@
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':contactsbackup'
|
include ':contactsbackup'
|
||||||
|
include ':storage:lib'
|
||||||
|
include ':storage:demo'
|
1
storage/.gitignore
vendored
Normal file
1
storage/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
release.sh
|
15
storage/README.md
Normal file
15
storage/README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Seedvault Storage
|
||||||
|
|
||||||
|
This is a library for Seedvault storage backup.
|
||||||
|
It can also be used by other apps wanting to provide storage backup feature.
|
||||||
|
|
||||||
|
Please see the [design document](doc/design.md) for more information.
|
||||||
|
|
||||||
|
There is also a [demo app](demo) that illustrates the working of the library
|
||||||
|
and does not need to be a system app with elevated permissions.
|
||||||
|
It can be built and installed as a regular app requesting permissions at runtime.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
The design document mentions several limitations of this initial implementation.
|
||||||
|
One of them is that you cannot backup more than one device to the same storage location.
|
2
storage/demo/.gitignore
vendored
Normal file
2
storage/demo/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/build
|
||||||
|
/debug
|
57
storage/demo/build.gradle
Normal file
57
storage/demo/build.gradle
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'com.google.protobuf'
|
||||||
|
id 'kotlin-android'
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "de.grobox.storagebackuptester"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 20
|
||||||
|
versionName "0.9.7"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
testInstrumentationRunnerArguments disableAnalytics: 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/*.kotlin_module'
|
||||||
|
exclude 'META-INF/androidx.*.version'
|
||||||
|
exclude 'META-INF/services/kotlin*'
|
||||||
|
exclude 'kotlin/internal/internal.kotlin_builtins'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':storage:lib')
|
||||||
|
|
||||||
|
implementation rootProject.ext.std_libs.androidx_core
|
||||||
|
// A newer version gets pulled in with AOSP via core, so we include fragment here explicitly
|
||||||
|
implementation rootProject.ext.std_libs.androidx_fragment
|
||||||
|
implementation rootProject.ext.std_libs.androidx_lifecycle_viewmodel_ktx
|
||||||
|
implementation rootProject.ext.std_libs.androidx_lifecycle_livedata_ktx
|
||||||
|
implementation rootProject.ext.std_libs.androidx_constraintlayout
|
||||||
|
implementation rootProject.ext.std_libs.com_google_android_material
|
||||||
|
|
||||||
|
implementation rootProject.ext.storage_libs.com_google_protobuf_javalite
|
||||||
|
}
|
23
storage/demo/proguard-rules.pro
vendored
Normal file
23
storage/demo/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-dontobfuscate
|
43
storage/demo/src/main/AndroidManifest.xml
Normal file
43
storage/demo/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="de.grobox.storagebackuptester">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".App"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.StorageBackupTester">
|
||||||
|
|
||||||
|
<activity android:name=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- Used to start actual BackupService depending on scheduling criteria -->
|
||||||
|
<service
|
||||||
|
android:name=".DemoBackupJobService"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="BackupJobService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
<!-- Does the actual backup work as a foreground service -->
|
||||||
|
<service
|
||||||
|
android:name=".DemoBackupService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:label="BackupService" />
|
||||||
|
<!-- Does restore as a foreground service -->
|
||||||
|
<service
|
||||||
|
android:name=".DemoRestoreService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:label="RestoreService" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
BIN
storage/demo/src/main/assets/test.jpg
Normal file
BIN
storage/demo/src/main/assets/test.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
storage/demo/src/main/ic_launcher-playstore.png
Normal file
BIN
storage/demo/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue