Add expert settings with an option for unlimited quota
Change-Id: Iebaea41ce4e69912f7cb723bd92e94e4396aa657
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt
new file mode 100644
index 0000000..05164d7
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt
@@ -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)
+ }
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt
index a339bc5..0057d9c 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt
@@ -166,6 +166,13 @@
startActivity(Intent(requireContext(), RestoreActivity::class.java))
true
}
+ R.id.action_settings_expert -> {
+ parentFragmentManager.beginTransaction()
+ .replace(R.id.fragment, ExpertSettingsFragment())
+ .addToBackStack(null)
+ .commit()
+ true
+ }
R.id.action_about -> {
AboutDialogFragment().show(parentFragmentManager, AboutDialogFragment.TAG)
true
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
index bb28b14..6d59857 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
@@ -31,6 +31,7 @@
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) {
@@ -50,10 +51,10 @@
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)
if (value == 0L) null else value
- }()
+ }
/**
* Sets a new RestoreSet token.
@@ -149,6 +150,7 @@
prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply()
}
+ fun isQuotaUnlimited() = prefs.getBoolean(PREF_KEY_UNLIMITED_QUOTA, false)
}
data class Storage(
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
index f2a3378..85ee196 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
@@ -21,6 +21,7 @@
single {
KVBackup(
plugin = get<BackupPlugin>().kvBackupPlugin,
+ settingsManager = get(),
inputFactory = get(),
headerWriter = get(),
crypto = get(),
@@ -30,6 +31,7 @@
single {
FullBackup(
plugin = get<BackupPlugin>().fullBackupPlugin,
+ settingsManager = get(),
inputFactory = get(),
headerWriter = get(),
crypto = get()
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt
index 85e2591..b7ba16e 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt
@@ -11,6 +11,7 @@
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.HeaderWriter
import com.stevesoltys.seedvault.header.VersionHeader
+import com.stevesoltys.seedvault.settings.SettingsManager
import libcore.io.IoUtils.closeQuietly
import java.io.EOFException
import java.io.IOException
@@ -35,6 +36,7 @@
@Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup(
private val plugin: FullBackupPlugin,
+ private val settingsManager: SettingsManager,
private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter,
private val crypto: Crypto
@@ -46,7 +48,9 @@
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 {
Log.i(TAG, "Check full backup size of $size bytes.")
@@ -134,7 +138,7 @@
// check if size fits quota
state.size += numBytes
- val quota = plugin.getQuota()
+ val quota = getQuota()
if (state.size > quota) {
Log.w(
TAG,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
index 153b098..27455ae 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
@@ -14,6 +14,7 @@
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.header.HeaderWriter
import com.stevesoltys.seedvault.header.VersionHeader
+import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import libcore.io.IoUtils.closeQuietly
import java.io.IOException
@@ -27,6 +28,7 @@
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVBackup(
private val plugin: KVBackupPlugin,
+ private val settingsManager: SettingsManager,
private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter,
private val crypto: Crypto,
@@ -39,7 +41,9 @@
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(
packageInfo: PackageInfo,
@@ -94,7 +98,7 @@
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) {
Log.w(TAG, "Requested non-incremental, deleting existing data.")
diff --git a/app/src/main/res/menu/settings_menu.xml b/app/src/main/res/menu/settings_menu.xml
index c5e8b2c..e9978f9 100644
--- a/app/src/main/res/menu/settings_menu.xml
+++ b/app/src/main/res/menu/settings_menu.xml
@@ -18,6 +18,11 @@
tools:visible="true" />
<item
+ android:id="@+id/action_settings_expert"
+ android:title="@string/settings_expert_title"
+ app:showAsAction="never" />
+
+ <item
android:id="@+id/action_about"
android:title="@string/about_title"
app:showAsAction="never" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 87705e4..eaa3c7d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -42,6 +42,10 @@
<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>
+ <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_restore_title">Where to find your backups?</string>
diff --git a/app/src/main/res/xml/settings_expert.xml b/app/src/main/res/xml/settings_expert.xml
new file mode 100644
index 0000000..a257d89
--- /dev/null
+++ b/app/src/main/res/xml/settings_expert.xml
@@ -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>
\ No newline at end of file
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
index b6c28f5..e7e07b4 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
@@ -65,10 +65,22 @@
private val backupPlugin = mockk<BackupPlugin>()
private val kvBackupPlugin = mockk<KVBackupPlugin>()
- private val kvBackup =
- KVBackup(kvBackupPlugin, inputFactory, headerWriter, cryptoImpl, notificationManager)
+ private val kvBackup = KVBackup(
+ plugin = kvBackupPlugin,
+ settingsManager = settingsManager,
+ inputFactory = inputFactory,
+ headerWriter = headerWriter,
+ crypto = cryptoImpl,
+ nm = notificationManager
+ )
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 packageService: PackageService = mockk()
private val backup = BackupCoordinator(
@@ -277,6 +289,7 @@
val bInputStream = ByteArrayInputStream(appData)
coEvery { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
+ every { settingsManager.isQuotaUnlimited() } returns false
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
coEvery {
apkBackup.backupApkIfNecessary(
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt
index 1328741..c5c49d3 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt
@@ -22,7 +22,7 @@
internal class FullBackupTest : BackupTest() {
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 closeBytes = ByteArray(42).apply { Random.nextBytes(this) }
@@ -35,12 +35,20 @@
@Test
fun `checkFullBackupSize exceeds quota`() {
+ every { settingsManager.isQuotaUnlimited() } returns false
every { plugin.getQuota() } returns quota
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
fun `checkFullBackupSize for no data`() {
assertEquals(TRANSPORT_PACKAGE_REJECTED, backup.checkFullBackupSize(0))
}
@@ -52,6 +60,7 @@
@Test
fun `checkFullBackupSize accepts min data`() {
+ every { settingsManager.isQuotaUnlimited() } returns false
every { plugin.getQuota() } returns quota
assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(1))
@@ -59,6 +68,7 @@
@Test
fun `checkFullBackupSize accepts max data`() {
+ every { settingsManager.isQuotaUnlimited() } returns false
every { plugin.getQuota() } returns quota
assertEquals(TRANSPORT_OK, backup.checkFullBackupSize(quota))
@@ -77,6 +87,7 @@
@Test
fun `sendBackupData first call over quota`() = runBlocking {
+ every { settingsManager.isQuotaUnlimited() } returns false
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
val numBytes = (quota + 1).toInt()
@@ -93,6 +104,7 @@
@Test
fun `sendBackupData second call over quota`() = runBlocking {
+ every { settingsManager.isQuotaUnlimited() } returns false
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
val numBytes1 = quota.toInt()
@@ -115,6 +127,7 @@
fun `sendBackupData throws exception when reading from InputStream`() = runBlocking {
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
+ every { settingsManager.isQuotaUnlimited() } returns false
every { plugin.getQuota() } returns quota
every { inputStream.read(any(), any(), bytes.size) } throws IOException()
expectClearState()
@@ -131,6 +144,7 @@
fun `sendBackupData throws exception when getting outputStream`() = runBlocking {
every { inputFactory.getInputStream(data) } returns inputStream
+ every { settingsManager.isQuotaUnlimited() } returns false
every { plugin.getQuota() } returns quota
coEvery { plugin.getOutputStream(packageInfo) } throws IOException()
expectClearState()
@@ -147,6 +161,7 @@
fun `sendBackupData throws exception when writing header`() = runBlocking {
every { inputFactory.getInputStream(data) } returns inputStream
+ every { settingsManager.isQuotaUnlimited() } returns false
every { plugin.getQuota() } returns quota
coEvery { plugin.getOutputStream(packageInfo) } returns outputStream
every { inputFactory.getInputStream(data) } returns inputStream
@@ -166,6 +181,7 @@
runBlocking {
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
+ every { settingsManager.isQuotaUnlimited() } returns false
every { plugin.getQuota() } returns quota
every { inputStream.read(any(), any(), bytes.size) } returns bytes.size
every { crypto.encryptSegment(outputStream, any()) } throws IOException()
@@ -181,6 +197,7 @@
@Test
fun `sendBackupData runs ok`() = runBlocking {
+ every { settingsManager.isQuotaUnlimited() } returns false
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
val numBytes1 = (quota / 2).toInt()
@@ -234,6 +251,7 @@
@Test
fun `clearState throws exception when flushing OutputStream`() = runBlocking {
+ every { settingsManager.isQuotaUnlimited() } returns false
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
val numBytes = 42
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
index 1a7f8d8..f767082 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
@@ -36,7 +36,14 @@
private val dataInput = mockk<BackupDataInput>()
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 key64 = Base64.getEncoder().encodeToString(key.toByteArray(Utf8))