UI for checking app backups

This commit is contained in:
Torsten Grote 2024-10-15 15:52:40 -03:00
parent f67c1d5544
commit 060bb425da
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
9 changed files with 268 additions and 0 deletions

View file

@ -90,6 +90,7 @@ open class App : Application() {
storageBackup = get(), storageBackup = get(),
backupManager = get(), backupManager = get(),
backupStateManager = get(), backupStateManager = get(),
checker = get(),
) )
} }
viewModel { viewModel {

View 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() }
}
}

View file

@ -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()) }
} }

View file

@ -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
}
}
}

View file

@ -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()

View 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>

View 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>

View file

@ -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>

View file

@ -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">