Store total backup size and show it when restoring

This is done by storing also the APK sizes in addition to the app data size we already store.
This commit is contained in:
Torsten Grote 2024-04-29 11:50:37 -03:00 committed by Chirayu Desai
parent 1d8c438723
commit 7ec80d4ebb
13 changed files with 67 additions and 19 deletions

View file

@ -20,7 +20,12 @@ data class BackupMetadata(
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
internal var d2dBackup: Boolean = false,
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
)
) {
val size: Long?
get() = packageMetadataMap.values.sumOf { m ->
(m.size ?: 0L) + (m.splits?.sumOf { it.size ?: 0L } ?: 0L)
}
}
internal const val JSON_METADATA = "@meta@"
internal const val JSON_METADATA_VERSION = "version"
@ -89,6 +94,7 @@ data class PackageMetadata(
data class ApkSplit(
val name: String,
val size: Long?,
val sha256: String,
// There's also a revisionCode, but it doesn't seem to be used just yet
)

View file

@ -58,6 +58,8 @@ internal class MetadataManager(
return field
}
val backupSize: Long? get() = metadata.size
/**
* Call this when initializing a new device.
*

View file

@ -169,6 +169,9 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
val jsonApkSplit = jsonSplits.getJSONObject(i)
val apkSplit = ApkSplit(
name = jsonApkSplit.getString(JSON_PACKAGE_SPLIT_NAME),
size = jsonApkSplit.optLong(JSON_PACKAGE_SIZE, -1L).let {
if (it < 0L) null else it
},
sha256 = jsonApkSplit.getString(JSON_PACKAGE_SHA256)
)
splits.add(apkSplit)

View file

@ -61,6 +61,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
put(JSON_PACKAGE_SPLITS, JSONArray().apply {
for (split in splits) put(JSONObject().apply {
put(JSON_PACKAGE_SPLIT_NAME, split.name)
if (split.size != null) put(JSON_PACKAGE_SIZE, split.size)
put(JSON_PACKAGE_SHA256, split.sha256)
})
})

View file

@ -20,6 +20,9 @@ data class RestorableBackup(val backupMetadata: BackupMetadata) {
val time: Long
get() = backupMetadata.time
val size: Long?
get() = backupMetadata.size
val deviceName: String
get() = backupMetadata.deviceName

View file

@ -3,8 +3,11 @@ package com.stevesoltys.seedvault.restore
import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
import android.text.format.DateUtils.HOUR_IN_MILLIS
import android.text.format.DateUtils.getRelativeTimeSpanString
import android.text.format.Formatter
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.Adapter
@ -33,6 +36,7 @@ internal class RestoreSetAdapter(
private val titleView = v.requireViewById<TextView>(R.id.titleView)
private val subtitleView = v.requireViewById<TextView>(R.id.subtitleView)
private val sizeView = v.requireViewById<TextView>(R.id.sizeView)
internal fun bind(item: RestorableBackup) {
v.setOnClickListener { listener.onRestorableBackupClicked(item) }
@ -42,6 +46,16 @@ internal class RestoreSetAdapter(
val setup = getRelativeTime(item.token)
subtitleView.text =
v.context.getString(R.string.restore_restore_set_times, lastBackup, setup)
val size = item.size
if (size == null) {
sizeView.visibility = GONE
} else {
sizeView.text = v.context.getString(
R.string.restore_restore_set_size,
Formatter.formatShortFileSize(v.context, size),
)
sizeView.visibility = VISIBLE
}
}
private fun getRelativeTime(time: Long): CharSequence {

View file

@ -194,13 +194,16 @@ internal class ApkBackup(
// that we exceed the maximum file name length, so we use the hash instead.
// The downside is that we need to read the file two times.
val messageDigest = MessageDigest.getInstance("SHA-256")
getApkInputStream(sourceDir).use { inputStream ->
val size = getApkInputStream(sourceDir).use { inputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var readCount = 0
var bytes = inputStream.read(buffer)
while (bytes >= 0) {
readCount += bytes
messageDigest.update(buffer, 0, bytes)
bytes = inputStream.read(buffer)
}
readCount
}
val sha256 = messageDigest.digest().encodeBase64()
val name = crypto.getNameForApk(metadataManager.salt, packageName, splitName)
@ -210,7 +213,7 @@ internal class ApkBackup(
inputStream.copyTo(outputStream)
}
}
return ApkSplit(splitName, sha256)
return ApkSplit(splitName, size.toLong(), sha256)
}
}

View file

@ -33,14 +33,25 @@
<TextView
android:id="@+id/subtitleView"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:textColor="?android:attr/textColorTertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/titleView"
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="Yesterday, 8:25 AM" />
tools:text="@string/restore_restore_set_times" />
<TextView
android:id="@+id/sizeView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/titleView"
app:layout_constraintTop_toBottomOf="@+id/subtitleView"
tools:text="Size: 5 GB" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -195,6 +195,7 @@
<string name="restore_title">Restore from backup</string>
<string name="restore_choose_restore_set">Choose a backup to restore</string>
<string name="restore_restore_set_times">Last backup %1$s · First %2$s.</string>
<string name="restore_restore_set_size">Size: <xliff:g example="1 GB" id="size">%1$s</xliff:g></string>
<string name="restore_skip">Don\'t restore</string>
<string name="restore_skip_apps">Skip restoring apps</string>
<string name="restore_invalid_location_title">No backups found</string>

View file

@ -61,9 +61,13 @@ internal class MetadataWriterDecoderTest {
version = Random.nextLong(),
installer = getRandomString(),
splits = listOf(
ApkSplit(getRandomString(), getRandomString()),
ApkSplit(getRandomString(), getRandomString()),
ApkSplit(getRandomString(), getRandomString())
ApkSplit(getRandomString(), null, getRandomString()),
ApkSplit(getRandomString(), 0L, getRandomString()),
ApkSplit(
name = getRandomString(),
size = Random.nextLong(0, Long.MAX_VALUE),
sha256 = getRandomString(),
),
),
sha256 = getRandomString(),
signatures = listOf(getRandomString(), getRandomString())

View file

@ -76,7 +76,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
installer = getRandomString(),
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
signatures = listOf("AwIB"),
splits = listOf(ApkSplit(splitName, splitSha256))
splits = listOf(ApkSplit(splitName, Random.nextLong(), splitSha256))
)
private val packageMetadataMap: PackageMetadataMap = hashMapOf(packageName to packageMetadata)
private val installerName = packageMetadata.installer

View file

@ -269,8 +269,8 @@ internal class ApkRestoreTest : TransportTest() {
val split2Name = getRandomString()
packageMetadataMap[packageName] = packageMetadataMap[packageName]!!.copy(
splits = listOf(
ApkSplit(split1Name, getRandomBase64()),
ApkSplit(split2Name, getRandomBase64())
ApkSplit(split1Name, Random.nextLong(), getRandomBase64()),
ApkSplit(split2Name, Random.nextLong(), getRandomBase64())
)
)
@ -293,7 +293,7 @@ internal class ApkRestoreTest : TransportTest() {
// add one APK split to metadata
val splitName = getRandomString()
packageMetadataMap[packageName] = packageMetadataMap[packageName]!!.copy(
splits = listOf(ApkSplit(splitName, getRandomBase64(23)))
splits = listOf(ApkSplit(splitName, Random.nextLong(), getRandomBase64(23)))
)
// cache APK and get icon as well as app name
@ -318,7 +318,7 @@ internal class ApkRestoreTest : TransportTest() {
val splitName = getRandomString()
val sha256 = getRandomBase64(23)
packageMetadataMap[packageName] = packageMetadataMap[packageName]!!.copy(
splits = listOf(ApkSplit(splitName, sha256))
splits = listOf(ApkSplit(splitName, Random.nextLong(), sha256))
)
// cache APK and get icon as well as app name
@ -343,8 +343,8 @@ internal class ApkRestoreTest : TransportTest() {
val split2sha256 = "ZqZ1cVH47lXbEncWx-Pc4L6AdLZOIO2lQuXB5GypxB4"
packageMetadataMap[packageName] = packageMetadataMap[packageName]!!.copy(
splits = listOf(
ApkSplit(split1Name, split1sha256),
ApkSplit(split2Name, split2sha256)
ApkSplit(split1Name, Random.nextLong(), split1sha256),
ApkSplit(split2Name, Random.nextLong(), split2sha256)
)
)

View file

@ -208,8 +208,8 @@ internal class ApkBackupTest : BackupTest() {
version = packageInfo.longVersionCode,
installer = getRandomString(),
splits = listOf(
ApkSplit(split1Name, split1Sha256),
ApkSplit(split2Name, split2Sha256)
ApkSplit(split1Name, split1Bytes.size.toLong(), split1Sha256),
ApkSplit(split2Name, split2Bytes.size.toLong(), split2Sha256)
),
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
signatures = packageMetadata.signatures