UI for checking app backups
This commit is contained in:
parent
f67c1d5544
commit
060bb425da
9 changed files with 268 additions and 0 deletions
|
@ -90,6 +90,7 @@ open class App : Application() {
|
||||||
storageBackup = get(),
|
storageBackup = get(),
|
||||||
backupManager = get(),
|
backupManager = get(),
|
||||||
backupStateManager = get(),
|
backupStateManager = get(),
|
||||||
|
checker = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModel {
|
viewModel {
|
||||||
|
|
39
app/src/main/java/com/stevesoltys/seedvault/repo/Checker.kt
Normal file
39
app/src/main/java/com/stevesoltys/seedvault/repo/Checker.kt
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.repo
|
||||||
|
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.stevesoltys.seedvault.backend.BackendManager
|
||||||
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
|
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||||
|
import org.calyxos.seedvault.core.backends.TopLevelFolder
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
internal class Checker(
|
||||||
|
private val crypto: Crypto,
|
||||||
|
private val backendManager: BackendManager,
|
||||||
|
private val snapshotManager: SnapshotManager,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun getBackupSize(): Long {
|
||||||
|
// get all snapshots
|
||||||
|
val folder = TopLevelFolder(crypto.repoId)
|
||||||
|
val handles = mutableListOf<AppBackupFileType.Snapshot>()
|
||||||
|
backendManager.backend.list(folder, AppBackupFileType.Snapshot::class) { fileInfo ->
|
||||||
|
handles.add(fileInfo.fileHandle as AppBackupFileType.Snapshot)
|
||||||
|
}
|
||||||
|
val snapshots = snapshotManager.onSnapshotsLoaded(handles)
|
||||||
|
|
||||||
|
// get total disk space used by snapshots
|
||||||
|
val sizeMap = mutableMapOf<String, Int>()
|
||||||
|
snapshots.forEach { snapshot ->
|
||||||
|
// add sizes to a map first, so we don't double count
|
||||||
|
snapshot.blobsMap.forEach { (chunkId, blob) -> sizeMap[chunkId] = blob.length }
|
||||||
|
}
|
||||||
|
return sizeMap.values.sumOf { it.toLong() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -21,4 +21,5 @@ val repoModule = module {
|
||||||
}
|
}
|
||||||
factory { SnapshotCreatorFactory(androidContext(), get(), get(), get()) }
|
factory { SnapshotCreatorFactory(androidContext(), get(), get(), get()) }
|
||||||
factory { Pruner(get(), get(), get()) }
|
factory { Pruner(get(), get(), get()) }
|
||||||
|
single { Checker(get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.slider.LabelFormatter.LABEL_VISIBLE
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.activityViewModel
|
||||||
|
|
||||||
|
private const val WARN_PERCENT = 25
|
||||||
|
private const val WARN_BYTES = 1024 * 1024 * 1024 // 1 GB
|
||||||
|
|
||||||
|
class AppCheckFragment : Fragment() {
|
||||||
|
|
||||||
|
private val viewModel: SettingsViewModel by activityViewModel()
|
||||||
|
private lateinit var sliderLabel: TextView
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
): View {
|
||||||
|
val v = inflater.inflate(R.layout.fragment_app_check, container, false) as ScrollView
|
||||||
|
|
||||||
|
val slider = v.requireViewById<Slider>(R.id.slider)
|
||||||
|
sliderLabel = v.requireViewById(R.id.sliderLabel)
|
||||||
|
|
||||||
|
// label not scrolling will be fixed in material-components 1.12.0 (next update)
|
||||||
|
slider.setLabelFormatter { value ->
|
||||||
|
viewModel.backupSize.value?.let {
|
||||||
|
formatShortFileSize(context, (it * value / 100).toLong())
|
||||||
|
} ?: "${value.toInt()}%"
|
||||||
|
}
|
||||||
|
slider.addOnChangeListener { _, value, _ ->
|
||||||
|
onSliderChanged(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.backupSize.observe(viewLifecycleOwner) {
|
||||||
|
slider.labelBehavior = LABEL_VISIBLE
|
||||||
|
slider.invalidate()
|
||||||
|
onSliderChanged(slider.value)
|
||||||
|
// we can stop observing as the loaded size won't change again
|
||||||
|
viewModel.backupSize.removeObservers(viewLifecycleOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.requireViewById<Button>(R.id.startButton).setOnClickListener {
|
||||||
|
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
activity?.setTitle(null)
|
||||||
|
viewModel.loadBackupSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSliderChanged(value: Float) {
|
||||||
|
val size = viewModel.backupSize.value
|
||||||
|
// when size is unknown, we show warning based on percent
|
||||||
|
val showWarning = if (size == null) {
|
||||||
|
value > WARN_PERCENT
|
||||||
|
} else {
|
||||||
|
size * value / 100 > WARN_BYTES
|
||||||
|
}
|
||||||
|
// only update label visibility when different from before
|
||||||
|
val newVisibility = if (showWarning) VISIBLE else GONE
|
||||||
|
if (sliderLabel.visibility != newVisibility) {
|
||||||
|
sliderLabel.visibility = newVisibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.backend.BackendManager
|
import com.stevesoltys.seedvault.backend.BackendManager
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
|
import com.stevesoltys.seedvault.repo.Checker
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||||
|
@ -69,6 +70,7 @@ internal class SettingsViewModel(
|
||||||
private val appListRetriever: AppListRetriever,
|
private val appListRetriever: AppListRetriever,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
|
private val checker: Checker,
|
||||||
backupStateManager: BackupStateManager,
|
backupStateManager: BackupStateManager,
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager) {
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager) {
|
||||||
|
|
||||||
|
@ -84,6 +86,9 @@ internal class SettingsViewModel(
|
||||||
private val mBackupPossible = MutableLiveData(false)
|
private val mBackupPossible = MutableLiveData(false)
|
||||||
val backupPossible: LiveData<Boolean> = mBackupPossible
|
val backupPossible: LiveData<Boolean> = mBackupPossible
|
||||||
|
|
||||||
|
private val mBackupSize = MutableLiveData<Long>()
|
||||||
|
val backupSize: LiveData<Long> = mBackupSize
|
||||||
|
|
||||||
internal val lastBackupTime = settingsManager.lastBackupTime
|
internal val lastBackupTime = settingsManager.lastBackupTime
|
||||||
internal val appBackupWorkInfo =
|
internal val appBackupWorkInfo =
|
||||||
workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map {
|
workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map {
|
||||||
|
@ -308,6 +313,12 @@ internal class SettingsViewModel(
|
||||||
BackupJobService.cancelJob(app)
|
BackupJobService.cancelJob(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadBackupSize() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
mBackupSize.postValue(checker.getBackupSize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onLogcatUriReceived(uri: Uri?) = viewModelScope.launch(Dispatchers.IO) {
|
fun onLogcatUriReceived(uri: Uri?) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
onLogcatError()
|
onLogcatError()
|
||||||
|
|
10
app/src/main/res/drawable/ic_cloud_search.xml
Normal file
10
app/src/main/res/drawable/ic_cloud_search.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/textColorSecondary"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M21.86 12.5C21.1 11.63 20.15 11.13 19 11C19 9.05 18.32 7.4 16.96 6.04C15.6 4.68 13.95 4 12 4C10.42 4 9 4.47 7.75 5.43S5.67 7.62 5.25 9.15C4 9.43 2.96 10.08 2.17 11.1S1 13.28 1 14.58C1 16.09 1.54 17.38 2.61 18.43C3.69 19.5 5 20 6.5 20H18.5C19.75 20 20.81 19.56 21.69 18.69C22.56 17.81 23 16.75 23 15.5C23 14.35 22.62 13.35 21.86 12.5M16.57 18L14 15.43C13.43 15.79 12.74 16 12 16C9.79 16 8 14.21 8 12S9.79 8 12 8 16 9.79 16 12C16 12.74 15.79 13.43 15.43 14L18 16.57L16.57 18M14 12C14 13.11 13.11 14 12 14S10 13.11 10 12 10.9 10 12 10 14 10.9 14 12Z" />
|
||||||
|
</vector>
|
104
app/src/main/res/layout/fragment_app_check.xml
Normal file
104
app/src/main/res/layout/fragment_app_check.xml
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
<ScrollView 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"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView"
|
||||||
|
style="@style/SudHeaderIcon"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_cloud_search"
|
||||||
|
app:tint="?android:colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleView"
|
||||||
|
style="@style/SudHeaderTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_app_check_title"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/descriptionView"
|
||||||
|
style="@style/SudContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_app_check_text"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/titleView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/introView"
|
||||||
|
style="@style/SudContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_app_check_text2"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/descriptionView" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slider"
|
||||||
|
style="@style/SudContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:stepSize="5.0"
|
||||||
|
android:value="10.0"
|
||||||
|
android:valueFrom="5.0"
|
||||||
|
android:valueTo="100.0"
|
||||||
|
app:labelBehavior="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/introView"
|
||||||
|
app:tickVisible="false"
|
||||||
|
tools:labelBehavior="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sliderLabel"
|
||||||
|
style="@style/SudContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/settings_app_check_warning"
|
||||||
|
android:textColor="?colorError"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/slider"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/startButton"
|
||||||
|
style="@style/SudPrimaryButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="40dp"
|
||||||
|
android:text="@string/settings_app_check_button"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/sliderLabel"
|
||||||
|
app:layout_constraintVertical_bias="1.0" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
|
@ -41,6 +41,8 @@
|
||||||
<string name="settings_backup_status_next_backup_past">once conditions are fulfilled</string>
|
<string name="settings_backup_status_next_backup_past">once conditions are fulfilled</string>
|
||||||
<string name="settings_backup_status_next_backup_usb">Backups will happen automatically when you plug in your USB drive</string>
|
<string name="settings_backup_status_next_backup_usb">Backups will happen automatically when you plug in your USB drive</string>
|
||||||
<string name="settings_backup_scheduling_title">Backup scheduling</string>
|
<string name="settings_backup_scheduling_title">Backup scheduling</string>
|
||||||
|
<string name="settings_backup_app_check_title">Check integrity</string>
|
||||||
|
<string name="settings_backup_app_check_summary">Ensure that backup is working for restore</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 (beta)</string>
|
<string name="settings_category_storage">Storage backup (beta)</string>
|
||||||
|
@ -66,6 +68,12 @@
|
||||||
<string name="settings_scheduling_metered_title">Back up when using mobile data</string>
|
<string name="settings_scheduling_metered_title">Back up when using mobile data</string>
|
||||||
<string name="settings_scheduling_charging_title">Back up only when charging</string>
|
<string name="settings_scheduling_charging_title">Back up only when charging</string>
|
||||||
|
|
||||||
|
<string name="settings_app_check_title">Verify app backup integrity</string>
|
||||||
|
<string name="settings_app_check_text">Each backup process will automatically check the backup integrity. However, to save time, actual app data will not be downloaded and verified.</string>
|
||||||
|
<string name="settings_app_check_text2">Here you can run a one-time check to verify your app backup. Select how much of the app data should be downloaded for the check. The more you select, the longer it will take, yet the more reliable the check will be. This will run in the background and show a notification once the check is done.</string>
|
||||||
|
<string name="settings_app_check_warning">Warning: Downloading large amounts of data can take a long time.</string>
|
||||||
|
<string name="settings_app_check_button">Check backup</string>
|
||||||
|
|
||||||
<string name="settings_expert_title">Expert settings</string>
|
<string name="settings_expert_title">Expert settings</string>
|
||||||
<string name="settings_expert_logcat_title">Save app log</string>
|
<string name="settings_expert_logcat_title">Save app log</string>
|
||||||
<string name="settings_expert_logcat_summary">Developers can diagnose bugs with these logs.\n\nWarning: The log file might contain personally identifiable information. Review before and delete after sharing!</string>
|
<string name="settings_expert_logcat_summary">Developers can diagnose bugs with these logs.\n\nWarning: The log file might contain personally identifiable information. Review before and delete after sharing!</string>
|
||||||
|
|
|
@ -50,6 +50,13 @@
|
||||||
app:title="@string/settings_backup_scheduling_title"
|
app:title="@string/settings_backup_scheduling_title"
|
||||||
tools:summary="Next backup: Never" />
|
tools:summary="Next backup: Never" />
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:fragment="com.stevesoltys.seedvault.settings.AppCheckFragment"
|
||||||
|
app:icon="@drawable/ic_cloud_search"
|
||||||
|
app:key="backup_scheduling"
|
||||||
|
app:summary="@string/settings_backup_app_check_summary"
|
||||||
|
app:title="@string/settings_backup_app_check_title" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_category_storage">
|
<PreferenceCategory android:title="@string/settings_category_storage">
|
||||||
|
|
Loading…
Reference in a new issue