Add expert settings with an option for unlimited quota
Change-Id: Iebaea41ce4e69912f7cb723bd92e94e4396aa657
This commit is contained in:
parent
d2a748c34a
commit
a5a3a85c6c
12 changed files with 104 additions and 11 deletions
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
8
app/src/main/res/xml/settings_expert.xml
Normal file
8
app/src/main/res/xml/settings_expert.xml
Normal 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>
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in a new issue