From a5a3a85c6c49712e45d35ef3763593ea9a8ab498 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 15 Jul 2021 15:04:10 +0200 Subject: [PATCH] Add expert settings with an option for unlimited quota Change-Id: Iebaea41ce4e69912f7cb723bd92e94e4396aa657 --- .../settings/ExpertSettingsFragment.kt | 19 ++++++++++++++++++ .../seedvault/settings/SettingsFragment.kt | 7 +++++++ .../seedvault/settings/SettingsManager.kt | 6 ++++-- .../transport/backup/BackupModule.kt | 2 ++ .../seedvault/transport/backup/FullBackup.kt | 8 ++++++-- .../seedvault/transport/backup/KVBackup.kt | 8 ++++++-- app/src/main/res/menu/settings_menu.xml | 5 +++++ app/src/main/res/values/strings.xml | 4 ++++ app/src/main/res/xml/settings_expert.xml | 8 ++++++++ .../transport/CoordinatorIntegrationTest.kt | 19 +++++++++++++++--- .../transport/backup/FullBackupTest.kt | 20 ++++++++++++++++++- .../transport/backup/KVBackupTest.kt | 9 ++++++++- 12 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt create mode 100644 app/src/main/res/xml/settings_expert.xml 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 00000000..05164d78 --- /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 a339bc51..0057d9c1 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 @@ class SettingsFragment : PreferenceFragmentCompat() { 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 bb28b149..6d59857f 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_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId" 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 @@ class SettingsManager(private val context: Context) { 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 @@ class SettingsManager(private val context: Context) { 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 f2a3378c..85ee1962 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 @@ val backupModule = module { single { KVBackup( plugin = get().kvBackupPlugin, + settingsManager = get(), inputFactory = get(), headerWriter = get(), crypto = get(), @@ -30,6 +31,7 @@ val backupModule = module { single { FullBackup( plugin = get().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 85e2591c..b7ba16e0 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 android.util.Log 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 @@ private val TAG = FullBackup::class.java.simpleName @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 @@ internal class FullBackup( 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 @@ internal class FullBackup( // 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 153b0989..27455aea 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.crypto.Crypto 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 @@ private val TAG = KVBackup::class.java.simpleName @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 @@ internal class KVBackup( 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 @@ internal class KVBackup( 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 c5e8b2c9..e9978f91 100644 --- a/app/src/main/res/menu/settings_menu.xml +++ b/app/src/main/res/menu/settings_menu.xml @@ -17,6 +17,11 @@ app:showAsAction="never" tools:visible="true" /> + + To enable storage backup, you need to first verify your recovery code or generate a new one. Verify code + Expert settings + Unlimited app quota + 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. + Choose where to store backups Where to find your backups? 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 00000000..a257d898 --- /dev/null +++ b/app/src/main/res/xml/settings_expert.xml @@ -0,0 +1,8 @@ + + + + \ 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 b6c28f53..e7e07b4a 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 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val backupPlugin = mockk() private val kvBackupPlugin = mockk() - 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() - 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() private val packageService: PackageService = mockk() private val backup = BackupCoordinator( @@ -277,6 +289,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { 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 1328741a..c5c49d3f 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 @@ import kotlin.random.Random internal class FullBackupTest : BackupTest() { private val plugin = mockk() - 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,11 +35,19 @@ internal class FullBackupTest : BackupTest() { @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 @@ internal class FullBackupTest : BackupTest() { @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 @@ internal class FullBackupTest : BackupTest() { @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 @@ internal class FullBackupTest : BackupTest() { @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 @@ internal class FullBackupTest : BackupTest() { @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 @@ internal class FullBackupTest : BackupTest() { 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 @@ internal class FullBackupTest : BackupTest() { 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 @@ internal class FullBackupTest : BackupTest() { 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 @@ internal class FullBackupTest : BackupTest() { 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 @@ internal class FullBackupTest : BackupTest() { @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 @@ internal class FullBackupTest : BackupTest() { @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 1a7f8d80..f7670825 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 @@ internal class KVBackupTest : BackupTest() { private val dataInput = mockk() private val notificationManager = mockk() - 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))