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:
parent
1d8c438723
commit
7ec80d4ebb
13 changed files with 67 additions and 19 deletions
|
@ -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
|
||||
)
|
||||
|
|
|
@ -58,6 +58,8 @@ internal class MetadataManager(
|
|||
return field
|
||||
}
|
||||
|
||||
val backupSize: Long? get() = metadata.size
|
||||
|
||||
/**
|
||||
* Call this when initializing a new device.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue