Add expert settings with an option for unlimited quota

Change-Id: Iebaea41ce4e69912f7cb723bd92e94e4396aa657
This commit is contained in:
Torsten Grote 2021-07-15 15:04:10 +02:00 committed by Chirayu Desai
parent d2a748c34a
commit a5a3a85c6c
12 changed files with 104 additions and 11 deletions

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@ val backupModule = module {
single {
KVBackup(
plugin = get<BackupPlugin>().kvBackupPlugin,
settingsManager = get(),
inputFactory = get(),
headerWriter = get(),
crypto = get(),
@ -30,6 +31,7 @@ val backupModule = module {
single {
FullBackup(
plugin = get<BackupPlugin>().fullBackupPlugin,
settingsManager = get(),
inputFactory = get(),
headerWriter = get(),
crypto = get()

View file

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

View file

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

View file

@ -17,6 +17,11 @@
app:showAsAction="never"
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"

View file

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

View file

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

View file

@ -65,10 +65,22 @@ internal class CoordinatorIntegrationTest : TransportTest() {
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 @@ 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(

View file

@ -22,7 +22,7 @@ import kotlin.random.Random
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,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

View file

@ -36,7 +36,14 @@ internal class KVBackupTest : BackupTest() {
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))