Skip to content

Commit f209ee9

Browse files
committed
Allow try again or restart when no backups found
1 parent 441a3a3 commit f209ee9

File tree

7 files changed

+156
-47
lines changed

7 files changed

+156
-47
lines changed

app/src/main/java/com/stevesoltys/seedvault/backend/BackendManager.kt

+15
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ class BackendManager(
101101
// TODO not critical, but nice to have: clear also local snapshot cache
102102
}
103103

104+
/**
105+
* Clears the storage plugins and current [BackendProperties].
106+
*
107+
* IMPORTANT: Do no call this while current plugins are being used,
108+
* e.g. while backup/restore operation is still running.
109+
*/
110+
@WorkerThread
111+
@Synchronized
112+
fun removePlugins() {
113+
settingsManager.clearStorageBackend()
114+
mBackend = null
115+
mBackendProperties = null
116+
blobCache.clearLocalCache()
117+
}
118+
104119
/**
105120
* Check if we are able to do backups now by examining possible pre-conditions
106121
* such as plugged-in flash drive or internet access.

app/src/main/java/com/stevesoltys/seedvault/crypto/KeyManager.kt

+9
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ interface KeyManager : org.calyxos.seedvault.core.crypto.KeyManager {
5757
* because the key can not leave the [KeyStore]'s hardware security module.
5858
*/
5959
fun getBackupKey(): SecretKey
60+
61+
/**
62+
* Removes the backup key from the [KeyStore]. Use with care!
63+
*/
64+
fun removeBackupKey()
6065
}
6166

6267
internal class KeyManagerImpl(
@@ -86,6 +91,10 @@ internal class KeyManagerImpl(
8691
return ksEntry.secretKey
8792
}
8893

94+
override fun removeBackupKey() {
95+
keyStore.deleteEntry(KEY_ALIAS_BACKUP)
96+
}
97+
8998
override fun getMainKey(): SecretKey {
9099
val ksEntry = keyStore.getEntry(KEY_ALIAS_MAIN, null) as SecretKeyEntry
91100
return ksEntry.secretKey

app/src/main/java/com/stevesoltys/seedvault/restore/RestoreSetFragment.kt

+36
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@
66
package com.stevesoltys.seedvault.restore
77

88
import android.os.Bundle
9+
import android.text.method.ScrollingMovementMethod
910
import android.view.LayoutInflater
1011
import android.view.View
1112
import android.view.View.INVISIBLE
1213
import android.view.View.VISIBLE
1314
import android.view.ViewGroup
15+
import android.widget.Button
1416
import android.widget.ProgressBar
1517
import android.widget.TextView
1618
import androidx.fragment.app.Fragment
19+
import androidx.lifecycle.lifecycleScope
1720
import androidx.recyclerview.widget.RecyclerView
21+
import androidx.transition.TransitionManager.beginDelayedTransition
1822
import com.stevesoltys.seedvault.R
1923
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
24+
import kotlinx.coroutines.launch
2025
import org.koin.androidx.viewmodel.ext.android.activityViewModel
2126

2227
class RestoreSetFragment : Fragment() {
@@ -26,6 +31,8 @@ class RestoreSetFragment : Fragment() {
2631
private lateinit var listView: RecyclerView
2732
private lateinit var progressBar: ProgressBar
2833
private lateinit var errorView: TextView
34+
private lateinit var tryAgainButton: Button
35+
private lateinit var restartButton: Button
2936
private lateinit var skipView: TextView
3037

3138
override fun onCreateView(
@@ -38,6 +45,8 @@ class RestoreSetFragment : Fragment() {
3845
listView = v.requireViewById(R.id.listView)
3946
progressBar = v.requireViewById(R.id.progressBar)
4047
errorView = v.requireViewById(R.id.errorView)
48+
tryAgainButton = v.requireViewById(R.id.tryAgainButton)
49+
restartButton = v.requireViewById(R.id.restartButton)
4150
skipView = v.requireViewById(R.id.skipView)
4251

4352
return v
@@ -65,12 +74,19 @@ class RestoreSetFragment : Fragment() {
6574
private fun onRestoreResultsLoaded(results: RestoreSetResult) {
6675
if (results.hasError()) {
6776
errorView.visibility = VISIBLE
77+
tryAgainButton.visibility = VISIBLE
78+
if (viewModel.isSetupWizard) restartButton.visibility = VISIBLE
6879
listView.visibility = INVISIBLE
6980
progressBar.visibility = INVISIBLE
7081

7182
errorView.text = results.errorMsg
83+
errorView.movementMethod = ScrollingMovementMethod()
84+
tryAgainButton.setOnClickListener { tryAgain() }
85+
restartButton.setOnClickListener { restart() }
7286
} else {
7387
errorView.visibility = INVISIBLE
88+
tryAgainButton.visibility = INVISIBLE
89+
restartButton.visibility = INVISIBLE
7490
listView.visibility = VISIBLE
7591
progressBar.visibility = INVISIBLE
7692

@@ -81,6 +97,26 @@ class RestoreSetFragment : Fragment() {
8197
}
8298
}
8399

100+
private fun tryAgain() {
101+
beginDelayedTransition(view as ViewGroup)
102+
103+
progressBar.visibility = VISIBLE
104+
listView.visibility = VISIBLE
105+
106+
errorView.visibility = INVISIBLE
107+
tryAgainButton.visibility = INVISIBLE
108+
restartButton.visibility = INVISIBLE
109+
110+
viewModel.loadRestoreSets()
111+
}
112+
113+
private fun restart() {
114+
lifecycleScope.launch {
115+
viewModel.restartRestore()
116+
requireActivity().recreate()
117+
}
118+
}
119+
84120
}
85121

86122
internal interface RestorableBackupClickListener {

app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
4848
import kotlinx.coroutines.Dispatchers
4949
import kotlinx.coroutines.GlobalScope
5050
import kotlinx.coroutines.launch
51+
import kotlinx.coroutines.withContext
5152
import org.calyxos.backup.storage.api.SnapshotItem
5253
import org.calyxos.backup.storage.api.StorageBackup
5354
import org.calyxos.backup.storage.api.StoredSnapshot
@@ -116,6 +117,8 @@ internal class RestoreViewModel(
116117
is ErrorResult -> {
117118
val msg = if (backups.e == null) {
118119
app.getString(R.string.restore_set_empty_result)
120+
} else if (backups.e.message?.contains("BAD_DECRYPT") == true) {
121+
app.getString(R.string.restore_set_bad_decrypt)
119122
} else {
120123
app.getString(R.string.restore_set_error) + "\n\n${backups.e}"
121124
}
@@ -180,6 +183,11 @@ internal class RestoreViewModel(
180183
GlobalScope.launch(ioDispatcher) { iconManager.removeIcons() }
181184
}
182185

186+
internal suspend fun restartRestore() = withContext(ioDispatcher) {
187+
keyManager.removeBackupKey()
188+
backendManager.removePlugins()
189+
}
190+
183191
@UiThread
184192
internal fun onFinishClickedAfterRestoringAppData() {
185193
val backup = chosenRestorableBackup.value

app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt

+48-41
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ package com.stevesoltys.seedvault.settings
88
import android.content.Context
99
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
1010
import android.hardware.usb.UsbDevice
11-
import android.net.Uri
1211
import androidx.annotation.UiThread
12+
import androidx.core.content.edit
13+
import androidx.core.net.toUri
1314
import androidx.lifecycle.LiveData
1415
import androidx.lifecycle.MutableLiveData
1516
import androidx.preference.PreferenceManager
@@ -81,13 +82,13 @@ class SettingsManager(private val context: Context) {
8182
var token: Long? = null
8283
private set(newToken) {
8384
if (newToken == null) {
84-
prefs.edit()
85-
.remove(PREF_KEY_TOKEN)
86-
.apply()
85+
prefs.edit {
86+
remove(PREF_KEY_TOKEN)
87+
}
8788
} else {
88-
prefs.edit()
89-
.putLong(PREF_KEY_TOKEN, newToken)
90-
.apply()
89+
prefs.edit {
90+
putLong(PREF_KEY_TOKEN, newToken)
91+
}
9192
}
9293
field = newToken
9394
}
@@ -105,9 +106,9 @@ class SettingsManager(private val context: Context) {
105106
// check if this is an existing user that needs to be migrated
106107
// this check could be removed after a reasonable migration time (added 2024)
107108
if (prefs.getString(PREF_KEY_STORAGE_URI, null) != null) {
108-
prefs.edit()
109-
.putString(PREF_KEY_STORAGE_PLUGIN, StoragePluginType.SAF.name)
110-
.apply()
109+
prefs.edit {
110+
putString(PREF_KEY_STORAGE_PLUGIN, StoragePluginType.SAF.name)
111+
}
111112
StoragePluginType.SAF
112113
} else null
113114
} else savedType.let {
@@ -130,7 +131,7 @@ class SettingsManager(private val context: Context) {
130131
fun onSuccessfulBackupCompleted(token: Long) {
131132
this.token = token
132133
val now = System.currentTimeMillis()
133-
prefs.edit().putLong(PREF_KEY_LAST_BACKUP, now).apply()
134+
prefs.edit { putLong(PREF_KEY_LAST_BACKUP, now) }
134135
mLastBackupTime.postValue(now)
135136
}
136137

@@ -140,24 +141,30 @@ class SettingsManager(private val context: Context) {
140141
BackendId.WEBDAV -> StoragePluginType.WEB_DAV
141142
else -> error("Unsupported plugin: ${plugin::class.java.simpleName}")
142143
}.name
143-
prefs.edit()
144-
.putString(PREF_KEY_STORAGE_PLUGIN, value)
145-
.apply()
144+
prefs.edit {
145+
putString(PREF_KEY_STORAGE_PLUGIN, value)
146+
}
147+
}
148+
149+
fun clearStorageBackend() {
150+
prefs.edit {
151+
putString(PREF_KEY_STORAGE_PLUGIN, null)
152+
}
146153
}
147154

148155
fun setSafProperties(safProperties: SafProperties) {
149-
prefs.edit()
150-
.putString(PREF_KEY_STORAGE_URI, safProperties.uri.toString())
151-
.putString(PREF_KEY_STORAGE_ROOT_ID, safProperties.rootId)
152-
.putString(PREF_KEY_STORAGE_NAME, safProperties.name)
153-
.putBoolean(PREF_KEY_STORAGE_IS_USB, safProperties.isUsb)
154-
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safProperties.requiresNetwork)
155-
.apply()
156+
prefs.edit {
157+
putString(PREF_KEY_STORAGE_URI, safProperties.uri.toString())
158+
putString(PREF_KEY_STORAGE_ROOT_ID, safProperties.rootId)
159+
putString(PREF_KEY_STORAGE_NAME, safProperties.name)
160+
putBoolean(PREF_KEY_STORAGE_IS_USB, safProperties.isUsb)
161+
putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safProperties.requiresNetwork)
162+
}
156163
}
157164

158165
fun getSafProperties(): SafProperties? {
159166
val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null
160-
val uri = Uri.parse(uriStr)
167+
val uri = uriStr.toUri()
161168
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null)
162169
?: throw IllegalStateException("no storage name")
163170
val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
@@ -168,19 +175,19 @@ class SettingsManager(private val context: Context) {
168175

169176
fun setFlashDrive(usb: FlashDrive?) {
170177
if (usb == null) {
171-
prefs.edit()
172-
.remove(PREF_KEY_FLASH_DRIVE_NAME)
173-
.remove(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER)
174-
.remove(PREF_KEY_FLASH_DRIVE_VENDOR_ID)
175-
.remove(PREF_KEY_FLASH_DRIVE_PRODUCT_ID)
176-
.apply()
178+
prefs.edit {
179+
remove(PREF_KEY_FLASH_DRIVE_NAME)
180+
remove(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER)
181+
remove(PREF_KEY_FLASH_DRIVE_VENDOR_ID)
182+
remove(PREF_KEY_FLASH_DRIVE_PRODUCT_ID)
183+
}
177184
} else {
178-
prefs.edit()
179-
.putString(PREF_KEY_FLASH_DRIVE_NAME, usb.name)
180-
.putString(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER, usb.serialNumber)
181-
.putInt(PREF_KEY_FLASH_DRIVE_VENDOR_ID, usb.vendorId)
182-
.putInt(PREF_KEY_FLASH_DRIVE_PRODUCT_ID, usb.productId)
183-
.apply()
185+
prefs.edit {
186+
putString(PREF_KEY_FLASH_DRIVE_NAME, usb.name)
187+
putString(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER, usb.serialNumber)
188+
putInt(PREF_KEY_FLASH_DRIVE_VENDOR_ID, usb.vendorId)
189+
putInt(PREF_KEY_FLASH_DRIVE_PRODUCT_ID, usb.productId)
190+
}
184191
}
185192
}
186193

@@ -203,11 +210,11 @@ class SettingsManager(private val context: Context) {
203210
}
204211

205212
fun saveWebDavConfig(config: WebDavConfig) {
206-
prefs.edit()
207-
.putString(PREF_KEY_WEBDAV_URL, config.url)
208-
.putString(PREF_KEY_WEBDAV_USER, config.username)
209-
.putString(PREF_KEY_WEBDAV_PASS, config.password)
210-
.apply()
213+
prefs.edit {
214+
putString(PREF_KEY_WEBDAV_URL, config.url)
215+
putString(PREF_KEY_WEBDAV_USER, config.username)
216+
putString(PREF_KEY_WEBDAV_PASS, config.password)
217+
}
211218
}
212219

213220
fun backupApks(): Boolean {
@@ -231,7 +238,7 @@ class SettingsManager(private val context: Context) {
231238
*/
232239
fun disableBackup(packageName: String) {
233240
if (blacklistedApps.add(packageName)) {
234-
prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply()
241+
prefs.edit { putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps) }
235242
}
236243
}
237244

@@ -241,7 +248,7 @@ class SettingsManager(private val context: Context) {
241248
fun onAppBackupStatusChanged(status: AppStatus) {
242249
if (status.enabled) blacklistedApps.remove(status.packageName)
243250
else blacklistedApps.add(status.packageName)
244-
prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply()
251+
prefs.edit { putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps) }
245252
}
246253

247254
val quota: Long = 3L * 1024 * 1024 * 1024 // 3 GiB for now

app/src/main/res/layout/fragment_restore_set.xml

+36-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
2-
<!--
1+
<?xml version="1.0" encoding="utf-8"?><!--
32
SPDX-FileCopyrightText: 2020 The Calyx Institute
43
SPDX-License-Identifier: Apache-2.0
54
-->
@@ -57,23 +56,55 @@
5756
style="@style/SudContent"
5857
android:layout_width="0dp"
5958
android:layout_height="wrap_content"
59+
android:scrollbars="vertical"
6060
android:textColor="?android:colorError"
6161
android:textSize="18sp"
6262
android:visibility="invisible"
63-
app:layout_constraintBottom_toTopOf="@+id/skipView"
63+
app:layout_constrainedHeight="true"
64+
app:layout_constraintBottom_toTopOf="@+id/tryAgainButton"
6465
app:layout_constraintEnd_toEndOf="parent"
6566
app:layout_constraintStart_toStartOf="parent"
6667
app:layout_constraintTop_toBottomOf="@+id/titleView"
67-
tools:text="@string/restore_set_empty_result"
68+
app:layout_constraintVertical_bias="0.25"
69+
tools:text="wlsjdfsdfljsdflkjsdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf\nsdf;lmsdf;lmsd;flmsdf;msdf"
70+
tools:visibility="visible" />
71+
72+
<Button
73+
android:id="@+id/tryAgainButton"
74+
style="@style/Widget.Material3.Button.OutlinedButton"
75+
android:layout_width="wrap_content"
76+
android:layout_height="wrap_content"
77+
android:layout_marginTop="16dp"
78+
android:text="@string/restore_set_button_try_again"
79+
android:visibility="invisible"
80+
app:layout_constraintEnd_toEndOf="@+id/errorView"
81+
app:layout_constraintStart_toStartOf="@+id/errorView"
82+
app:layout_constraintTop_toBottomOf="@+id/errorView"
83+
tools:visibility="visible" />
84+
85+
<Button
86+
android:id="@+id/restartButton"
87+
style="@style/Widget.Material3.Button.OutlinedButton"
88+
android:layout_width="wrap_content"
89+
android:layout_height="wrap_content"
90+
android:layout_marginTop="16dp"
91+
android:layout_marginBottom="16dp"
92+
android:text="@string/restore_set_button_restart"
93+
android:visibility="invisible"
94+
app:layout_constraintBottom_toTopOf="@+id/skipView"
95+
app:layout_constraintEnd_toEndOf="@+id/errorView"
96+
app:layout_constraintStart_toStartOf="@+id/errorView"
97+
app:layout_constraintTop_toBottomOf="@+id/tryAgainButton"
98+
app:layout_constraintVertical_bias="0"
6899
tools:visibility="visible" />
69100

70101
<TextView
71102
android:id="@+id/skipView"
72103
style="@style/SudSecondaryButton"
73104
android:layout_width="wrap_content"
74105
android:layout_height="wrap_content"
75-
android:background="?android:selectableItemBackground"
76106
android:layout_marginStart="40dp"
107+
android:background="?android:selectableItemBackground"
77108
android:text="@string/restore_skip_apps"
78109
app:layout_constraintBottom_toBottomOf="parent"
79110
app:layout_constraintStart_toStartOf="parent"

0 commit comments

Comments
 (0)