Merge pull request #605 from grote/backup-size
Store and show the size of app backups
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
index c8ad6aa..01a1026 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
@@ -74,6 +74,7 @@
internal var time: Long = 0L,
internal var state: PackageState = UNKNOWN_ERROR,
internal var backupType: BackupType? = null,
+ internal var size: Long? = null,
internal val system: Boolean = false,
internal val version: Long? = null,
internal val installer: String? = null,
@@ -97,6 +98,7 @@
internal const val JSON_PACKAGE_TIME = "time"
internal const val JSON_PACKAGE_BACKUP_TYPE = "backupType"
internal const val JSON_PACKAGE_STATE = "state"
+internal const val JSON_PACKAGE_SIZE = "size"
internal const val JSON_PACKAGE_SYSTEM = "system"
internal const val JSON_PACKAGE_VERSION = "version"
internal const val JSON_PACKAGE_INSTALLER = "installer"
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
index f58a29a..0d72253 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
@@ -131,6 +131,7 @@
fun onPackageBackedUp(
packageInfo: PackageInfo,
type: BackupType,
+ size: Long?,
metadataOutputStream: OutputStream,
) {
val packageName = packageInfo.packageName
@@ -143,12 +144,15 @@
metadata.packageMetadataMap[packageName]!!.time = now
metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA
metadata.packageMetadataMap[packageName]!!.backupType = type
+ // don't override a previous K/V size, if there were no K/V changes
+ if (size != null) metadata.packageMetadataMap[packageName]!!.size = size
} else {
metadata.packageMetadataMap[packageName] = PackageMetadata(
time = now,
state = APK_AND_DATA,
backupType = type,
- system = packageInfo.isSystemApp()
+ size = size,
+ system = packageInfo.isSystemApp(),
)
}
}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
index bbd6df1..fe1920b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt
@@ -120,6 +120,7 @@
// because when only backing up the APK for example, there's no type
else -> null
}
+ val pSize = p.optLong(JSON_PACKAGE_SIZE, -1L)
val pSystem = p.optBoolean(JSON_PACKAGE_SYSTEM, false)
val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L)
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER)
@@ -136,6 +137,7 @@
time = p.getLong(JSON_PACKAGE_TIME),
state = pState,
backupType = pBackupType,
+ size = if (pSize < 0L) null else pSize,
system = pSystem,
version = if (pVersion == 0L) null else pVersion,
installer = if (pInstaller == "") null else pInstaller,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
index bbed50c..ef9473b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt
@@ -49,6 +49,9 @@
if (packageMetadata.backupType != null) {
put(JSON_PACKAGE_BACKUP_TYPE, packageMetadata.backupType!!.name)
}
+ if (packageMetadata.size != null) {
+ put(JSON_PACKAGE_SIZE, packageMetadata.size)
+ }
if (packageMetadata.system) {
put(JSON_PACKAGE_SYSTEM, packageMetadata.system)
}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
index e185b79..3bf9445 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt
@@ -38,6 +38,7 @@
val icon: Drawable,
val name: String,
val time: Long,
+ val size: Long?,
val status: AppBackupState,
val isSpecial: Boolean = false,
) : AppListItem()
@@ -87,6 +88,7 @@
icon = getIcon(packageName),
name = context.getString(stringId),
time = metadata?.time ?: 0,
+ size = metadata?.size,
status = status,
isSpecial = true
)
@@ -111,6 +113,7 @@
icon = getIcon(it.packageName),
name = getAppName(context, it.packageName).toString(),
time = time,
+ size = metadata?.size,
status = status
)
}.sortedBy { it.name.lowercase(locale) }
@@ -125,6 +128,7 @@
icon = getIcon(it.packageName),
name = getAppName(context, it.packageName).toString(),
time = 0,
+ size = null,
status = FAILED_NOT_ALLOWED
)
}.sortedBy { it.name.lowercase(locale) }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt
index 5536f6b..b4db433 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt
@@ -3,6 +3,7 @@
import android.content.Intent
import android.net.Uri
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
@@ -116,7 +117,12 @@
setState(item.status, false)
}
if (item.status == SUCCEEDED) {
- appInfo.text = item.time.toRelativeTime(context)
+ appInfo.text = if (item.size == null) {
+ item.time.toRelativeTime(context)
+ } else {
+ item.time.toRelativeTime(context).toString() +
+ " (${formatShortFileSize(v.context, item.size)})"
+ }
appInfo.visibility = VISIBLE
}
switchView.visibility = INVISIBLE
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
index 87a1302..7025501 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
@@ -363,6 +363,7 @@
// getCurrentPackage() not-null because we have state, call before finishing
val packageInfo = kv.getCurrentPackage()!!
val packageName = packageInfo.packageName
+ val size = kv.getCurrentSize()
// tell K/V backup to finish
var result = kv.finishBackup()
if (result == TRANSPORT_OK) {
@@ -370,7 +371,7 @@
// call onPackageBackedUp for @pm@ only if we can do backups right now
if (!isPmBackup || settingsManager.canDoBackupNow()) {
try {
- onPackageBackedUp(packageInfo, BackupType.KV)
+ onPackageBackedUp(packageInfo, BackupType.KV, size)
} catch (e: Exception) {
Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e)
result = TRANSPORT_PACKAGE_REJECTED
@@ -396,10 +397,11 @@
// getCurrentPackage() not-null because we have state
val packageInfo = full.getCurrentPackage()!!
val packageName = packageInfo.packageName
+ val size = full.getCurrentSize()
// tell full backup to finish
var result = full.finishBackup()
try {
- onPackageBackedUp(packageInfo, BackupType.FULL)
+ onPackageBackedUp(packageInfo, BackupType.FULL, size)
} catch (e: Exception) {
Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e)
result = TRANSPORT_PACKAGE_REJECTED
@@ -470,9 +472,9 @@
}
}
- private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType) {
+ private suspend fun onPackageBackedUp(packageInfo: PackageInfo, type: BackupType, size: Long?) {
plugin.getMetadataOutputStream().use {
- metadataManager.onPackageBackedUp(packageInfo, type, it)
+ metadataManager.onPackageBackedUp(packageInfo, type, size, it)
}
}
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 48a42d6..d44cdeb 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
@@ -51,6 +51,8 @@
fun getCurrentPackage() = state?.packageInfo
+ fun getCurrentSize() = state?.size
+
fun getQuota(): Long {
return if (settingsManager.isQuotaUnlimited()) Long.MAX_VALUE else DEFAULT_QUOTA_FULL_BACKUP
}
@@ -190,7 +192,7 @@
}
fun finishBackup(): Int {
- Log.i(TAG, "Finish full backup of ${state!!.packageName}.")
+ Log.i(TAG, "Finish full backup of ${state!!.packageName}. Wrote ${state!!.size} bytes")
return clearState()
}
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 060f543..4467815 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
@@ -46,6 +46,10 @@
fun getCurrentPackage() = state?.packageInfo
+ fun getCurrentSize() = getCurrentPackage()?.let {
+ dbManager.getDbSize(it.packageName)
+ }
+
fun getQuota(): Long = if (settingsManager.isQuotaUnlimited()) {
Long.MAX_VALUE
} else {
@@ -252,7 +256,7 @@
}
}
}
- Log.d(TAG, "Uploaded db file for $packageName")
+ Log.d(TAG, "Uploaded db file for $packageName.")
}
private class KVOperation(
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVDbManager.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVDbManager.kt
index 95a026f..2c2253b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVDbManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVDbManager.kt
@@ -29,6 +29,11 @@
* Use only for backup.
*/
fun existsDb(packageName: String): Boolean
+
+ /**
+ * Returns the current size of the DB in bytes or null, if no DB exists.
+ */
+ fun getDbSize(packageName: String): Long?
fun deleteDb(packageName: String, isRestore: Boolean = false): Boolean
}
@@ -59,6 +64,11 @@
return getDbFile(packageName).isFile
}
+ override fun getDbSize(packageName: String): Long? {
+ val file = getDbFile(packageName)
+ return if (file.isFile) file.length() else null
+ }
+
override fun deleteDb(packageName: String, isRestore: Boolean): Boolean {
return getDbFile(packageName, isRestore).delete()
}
diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt
index 1a41a02..d127713 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt
@@ -249,6 +249,7 @@
time = time,
packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced
)
+ val size = Random.nextLong()
val packageMetadata = PackageMetadata(time)
updatedMetadata.packageMetadataMap[packageName] = packageMetadata
@@ -256,10 +257,15 @@
every { clock.time() } returns time
expectModifyMetadata(initialMetadata)
- manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
+ manager.onPackageBackedUp(packageInfo, BackupType.FULL, size, storageOutputStream)
assertEquals(
- packageMetadata.copy(state = APK_AND_DATA, backupType = BackupType.FULL, system = true),
+ packageMetadata.copy(
+ state = APK_AND_DATA,
+ backupType = BackupType.FULL,
+ size = size,
+ system = true,
+ ),
manager.getPackageMetadata(packageName)
)
assertEquals(time, manager.getLastBackupTime())
@@ -270,6 +276,7 @@
cacheOutputStream.close()
}
}
+
@Test
fun `test onPackageBackedUp() with D2D enabled`() {
expectReadFromCache()
@@ -278,7 +285,7 @@
every { settingsManager.d2dBackupsEnabled() } returns true
- manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
+ manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L, storageOutputStream)
assertTrue(initialMetadata.d2dBackup)
verify {
@@ -290,19 +297,20 @@
@Test
fun `test onPackageBackedUp() fails to write to storage`() {
val updateTime = time + 1
+ val size = Random.nextLong()
val updatedMetadata = initialMetadata.copy(
time = updateTime,
packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced
)
updatedMetadata.packageMetadataMap[packageName] =
- PackageMetadata(updateTime, APK_AND_DATA, BackupType.KV)
+ PackageMetadata(updateTime, APK_AND_DATA, BackupType.KV, size)
expectReadFromCache()
every { clock.time() } returns updateTime
every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException()
try {
- manager.onPackageBackedUp(packageInfo, BackupType.KV, storageOutputStream)
+ manager.onPackageBackedUp(packageInfo, BackupType.KV, size, storageOutputStream)
fail()
} catch (e: IOException) {
// expected
@@ -335,7 +343,7 @@
every { clock.time() } returns time
expectModifyMetadata(updatedMetadata)
- manager.onPackageBackedUp(packageInfo, BackupType.FULL, storageOutputStream)
+ manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L, storageOutputStream)
assertEquals(time, manager.getLastBackupTime())
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt
index 4712551..88a54d2 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt
@@ -14,6 +14,7 @@
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import kotlin.random.Random
+import kotlin.random.nextLong
@TestInstance(PER_CLASS)
internal class MetadataWriterDecoderTest {
@@ -81,11 +82,12 @@
time = Random.nextLong(),
state = QUOTA_EXCEEDED,
backupType = BackupType.FULL,
+ size = Random.nextLong(0..Long.MAX_VALUE),
system = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
- signatures = listOf(getRandomString())
+ signatures = listOf(getRandomString()),
)
)
put(
@@ -93,22 +95,24 @@
time = Random.nextLong(),
state = NO_DATA,
backupType = BackupType.KV,
+ size = null,
system = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
- signatures = listOf(getRandomString(), getRandomString())
+ signatures = listOf(getRandomString(), getRandomString()),
)
)
put(
getRandomString(), PackageMetadata(
time = 0L,
state = NOT_ALLOWED,
+ size = 0,
system = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
- signatures = listOf(getRandomString(), getRandomString())
+ signatures = listOf(getRandomString(), getRandomString()),
)
)
}
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 824230e..0ff406d 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
@@ -147,7 +147,12 @@
metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream)
} just Runs
every {
- metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
+ metadataManager.onPackageBackedUp(
+ packageInfo = packageInfo,
+ type = BackupType.KV,
+ size = more((appData.size + appData2.size).toLong()), // more because DB overhead
+ metadataOutputStream = metadataOutputStream,
+ )
} just Runs
// start K/V backup
@@ -216,7 +221,12 @@
backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
} returns metadataOutputStream
every {
- metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
+ metadataManager.onPackageBackedUp(
+ packageInfo = packageInfo,
+ type = BackupType.KV,
+ size = more(size.toLong()), // more than $size, because DB overhead
+ metadataOutputStream = metadataOutputStream,
+ )
} just Runs
// start K/V backup
@@ -289,7 +299,12 @@
)
} just Runs
every {
- metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
+ metadataManager.onPackageBackedUp(
+ packageInfo = packageInfo,
+ type = BackupType.FULL,
+ size = appData.size.toLong(),
+ metadataOutputStream = metadataOutputStream,
+ )
} just Runs
// perform backup to output stream
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt
index 42c7348..30d2aa1 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt
@@ -36,6 +36,7 @@
import java.io.IOException
import java.io.OutputStream
import kotlin.random.Random
+import kotlin.random.nextLong
@Suppress("BlockingMethodInNonBlockingContext")
internal class BackupCoordinatorTest : BackupTest() {
@@ -204,14 +205,22 @@
@Test
fun `finish backup delegates to KV plugin if it has state`() = runBlocking {
+ val size = 0L
+
every { kv.hasState() } returns true
every { full.hasState() } returns false
every { kv.getCurrentPackage() } returns packageInfo
coEvery { kv.finishBackup() } returns TRANSPORT_OK
every { settingsManager.getToken() } returns token
coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
+ every { kv.getCurrentSize() } returns size
every {
- metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, metadataOutputStream)
+ metadataManager.onPackageBackedUp(
+ packageInfo = packageInfo,
+ type = BackupType.KV,
+ size = size,
+ metadataOutputStream = metadataOutputStream,
+ )
} just Runs
every { metadataOutputStream.close() } just Runs
@@ -225,6 +234,7 @@
every { kv.hasState() } returns true
every { full.hasState() } returns false
every { kv.getCurrentPackage() } returns pmPackageInfo
+ every { kv.getCurrentSize() } returns 42L
coEvery { kv.finishBackup() } returns TRANSPORT_OK
every { settingsManager.canDoBackupNow() } returns false
@@ -235,6 +245,7 @@
@Test
fun `finish backup delegates to full plugin if it has state`() = runBlocking {
val result = Random.nextInt()
+ val size: Long? = null
every { kv.hasState() } returns false
every { full.hasState() } returns true
@@ -242,8 +253,14 @@
every { full.finishBackup() } returns result
every { settingsManager.getToken() } returns token
coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
+ every { full.getCurrentSize() } returns size
every {
- metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, metadataOutputStream)
+ metadataManager.onPackageBackedUp(
+ packageInfo = packageInfo,
+ type = BackupType.FULL,
+ size = size,
+ metadataOutputStream = metadataOutputStream,
+ )
} just Runs
every { metadataOutputStream.close() } just Runs
@@ -375,6 +392,7 @@
}
)
val packageMetadata: PackageMetadata = mockk()
+ val size = Random.nextLong(1L..Long.MAX_VALUE)
every { settingsManager.canDoBackupNow() } returns true
every { metadataManager.requiresInit } returns false
@@ -394,8 +412,14 @@
every { kv.hasState() } returns true
every { full.hasState() } returns false
every { kv.getCurrentPackage() } returns pmPackageInfo
+ every { kv.getCurrentSize() } returns size
every {
- metadataManager.onPackageBackedUp(pmPackageInfo, BackupType.KV, metadataOutputStream)
+ metadataManager.onPackageBackedUp(
+ pmPackageInfo,
+ BackupType.KV,
+ size,
+ metadataOutputStream,
+ )
} just Runs
coEvery { kv.finishBackup() } returns TRANSPORT_OK
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt
index 7173f2f..9a31d18 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/TestKvDbManager.kt
@@ -42,6 +42,10 @@
return db != null
}
+ override fun getDbSize(packageName: String): Long? {
+ return db?.serialize()?.toByteArray()?.size?.toLong()
+ }
+
override fun deleteDb(packageName: String, isRestore: Boolean): Boolean {
clearDb()
return true