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