Show app backup check error screen
and error notification
This commit is contained in:
parent
26063a8ef0
commit
83974b4121
8 changed files with 147 additions and 24 deletions
|
@ -22,7 +22,9 @@ import org.calyxos.seedvault.core.toHexString
|
||||||
import java.security.DigestInputStream
|
import java.security.DigestInputStream
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import java.util.concurrent.ConcurrentSkipListSet
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
@ -36,6 +38,7 @@ internal class Checker(
|
||||||
) {
|
) {
|
||||||
private val log = KotlinLogging.logger { }
|
private val log = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
private var handleSize: Int? = null
|
||||||
private var snapshots: List<Snapshot>? = null
|
private var snapshots: List<Snapshot>? = null
|
||||||
private val concurrencyLimit: Int
|
private val concurrencyLimit: Int
|
||||||
get() {
|
get() {
|
||||||
|
@ -55,6 +58,7 @@ internal class Checker(
|
||||||
}
|
}
|
||||||
val snapshots = snapshotManager.onSnapshotsLoaded(handles)
|
val snapshots = snapshotManager.onSnapshotsLoaded(handles)
|
||||||
this.snapshots = snapshots // remember loaded snapshots
|
this.snapshots = snapshots // remember loaded snapshots
|
||||||
|
this.handleSize = handles.size // remember number of snapshot handles we had
|
||||||
|
|
||||||
// get total disk space used by snapshots
|
// get total disk space used by snapshots
|
||||||
val sizeMap = mutableMapOf<String, Int>()
|
val sizeMap = mutableMapOf<String, Int>()
|
||||||
|
@ -71,6 +75,10 @@ internal class Checker(
|
||||||
|
|
||||||
if (snapshots == null) getBackupSize() // just get size again to be sure we get snapshots
|
if (snapshots == null) getBackupSize() // just get size again to be sure we get snapshots
|
||||||
val snapshots = snapshots ?: error("Snapshots still null")
|
val snapshots = snapshots ?: error("Snapshots still null")
|
||||||
|
val handleSize = handleSize ?: error("Handle size still null")
|
||||||
|
check(handleSize >= snapshots.size) {
|
||||||
|
"Got $handleSize handles, but ${snapshots.size} snapshots."
|
||||||
|
}
|
||||||
val blobSample = getBlobSample(snapshots, percent)
|
val blobSample = getBlobSample(snapshots, percent)
|
||||||
val sampleSize = blobSample.values.sumOf { it.length.toLong() }
|
val sampleSize = blobSample.values.sumOf { it.length.toLong() }
|
||||||
log.info { "Blob sample has ${blobSample.size} blobs worth $sampleSize bytes." }
|
log.info { "Blob sample has ${blobSample.size} blobs worth $sampleSize bytes." }
|
||||||
|
@ -78,6 +86,7 @@ internal class Checker(
|
||||||
// check blobs concurrently
|
// check blobs concurrently
|
||||||
val semaphore = Semaphore(concurrencyLimit)
|
val semaphore = Semaphore(concurrencyLimit)
|
||||||
val size = AtomicLong()
|
val size = AtomicLong()
|
||||||
|
val badChunks = ConcurrentSkipListSet<String>()
|
||||||
val lastNotification = AtomicLong()
|
val lastNotification = AtomicLong()
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
@ -86,8 +95,13 @@ internal class Checker(
|
||||||
launch {
|
launch {
|
||||||
// suspend here until we get a permit from the semaphore (there's free workers)
|
// suspend here until we get a permit from the semaphore (there's free workers)
|
||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
// TODO record errors
|
try {
|
||||||
checkBlob(chunkId, blob)
|
checkBlob(chunkId, blob)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e) { "Error loading chunk $chunkId: " }
|
||||||
|
// TODO we could try differentiating transient backend issues
|
||||||
|
badChunks.add(chunkId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// keep track of how much we checked and for how long
|
// keep track of how much we checked and for how long
|
||||||
val newSize = size.addAndGet(blob.length.toLong())
|
val newSize = size.addAndGet(blob.length.toLong())
|
||||||
|
@ -106,10 +120,15 @@ internal class Checker(
|
||||||
if (sampleSize != size.get()) log.error {
|
if (sampleSize != size.get()) log.error {
|
||||||
"Checked ${size.get()} bytes, but expected $sampleSize"
|
"Checked ${size.get()} bytes, but expected $sampleSize"
|
||||||
}
|
}
|
||||||
val passedTime = System.currentTimeMillis() - startTime
|
val passedTime = max(System.currentTimeMillis() - startTime, 1000) // no div by zero
|
||||||
val bandwidth = size.get() / (passedTime.toDouble() / 1000).roundToLong()
|
val bandwidth = size.get() / (passedTime.toDouble() / 1000).roundToLong()
|
||||||
nm.onCheckComplete(size.get(), bandwidth)
|
checkerResult = if (badChunks.isEmpty() && handleSize == snapshots.size && handleSize > 0) {
|
||||||
checkerResult = CheckerResult.Success(snapshots, percent, size.get())
|
nm.onCheckComplete(size.get(), bandwidth)
|
||||||
|
CheckerResult.Success(snapshots, percent, size.get())
|
||||||
|
} else {
|
||||||
|
nm.onCheckFinishedWithError(size.get(), bandwidth)
|
||||||
|
CheckerResult.Error(handleSize, snapshots, badChunks)
|
||||||
|
}
|
||||||
this.snapshots = null
|
this.snapshots = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,31 @@ sealed class CheckerResult {
|
||||||
) : CheckerResult()
|
) : CheckerResult()
|
||||||
|
|
||||||
data class Error(
|
data class Error(
|
||||||
|
/**
|
||||||
|
* This number is greater than the size of [snapshots],
|
||||||
|
* if we could not read/decrypt one or more snapshots.
|
||||||
|
*/
|
||||||
|
val existingSnapshots: Int,
|
||||||
val snapshots: List<Snapshot>,
|
val snapshots: List<Snapshot>,
|
||||||
val errors: Map<String, Exception>,
|
/**
|
||||||
) : CheckerResult()
|
* The list of chunkIDs that had errors.
|
||||||
|
*/
|
||||||
|
val errorChunkIds: Set<String>,
|
||||||
|
) : CheckerResult() {
|
||||||
|
val goodSnapshots: List<Snapshot>
|
||||||
|
val badSnapshots: List<Snapshot>
|
||||||
|
|
||||||
|
init {
|
||||||
|
val good = mutableListOf<Snapshot>()
|
||||||
|
val bad = mutableListOf<Snapshot>()
|
||||||
|
snapshots.forEach { snapshot ->
|
||||||
|
val isGood = snapshot.blobsMap.keys.intersect(errorChunkIds).isEmpty()
|
||||||
|
if (isGood) good.add(snapshot) else bad.add(snapshot)
|
||||||
|
}
|
||||||
|
goodSnapshots = good
|
||||||
|
badSnapshots = bad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class GeneralError(val e: Exception) : CheckerResult()
|
data class GeneralError(val e: Exception) : CheckerResult()
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,11 @@ import android.view.View
|
||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
|
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||||
|
@ -40,6 +42,7 @@ internal class RestoreSetAdapter(
|
||||||
|
|
||||||
inner class RestoreSetViewHolder(private val v: View) : ViewHolder(v) {
|
inner class RestoreSetViewHolder(private val v: View) : ViewHolder(v) {
|
||||||
|
|
||||||
|
private val imageView = v.requireViewById<ImageView>(R.id.imageView)
|
||||||
private val titleView = v.requireViewById<TextView>(R.id.titleView)
|
private val titleView = v.requireViewById<TextView>(R.id.titleView)
|
||||||
private val appView = v.requireViewById<TextView>(R.id.appView)
|
private val appView = v.requireViewById<TextView>(R.id.appView)
|
||||||
private val apkView = v.requireViewById<TextView>(R.id.apkView)
|
private val apkView = v.requireViewById<TextView>(R.id.apkView)
|
||||||
|
@ -49,6 +52,11 @@ internal class RestoreSetAdapter(
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
v.setOnClickListener { listener.onRestorableBackupClicked(item) }
|
v.setOnClickListener { listener.onRestorableBackupClicked(item) }
|
||||||
}
|
}
|
||||||
|
if (item.canBeRestored) {
|
||||||
|
imageView.setImageResource(R.drawable.ic_phone_android)
|
||||||
|
} else {
|
||||||
|
imageView.setImageResource(R.drawable.ic_error_red)
|
||||||
|
}
|
||||||
titleView.text = item.name
|
titleView.text = item.name
|
||||||
|
|
||||||
appView.text = if (item.sizeAppData > 0) {
|
appView.text = if (item.sizeAppData > 0) {
|
||||||
|
@ -61,7 +69,9 @@ internal class RestoreSetAdapter(
|
||||||
v.context.getString(R.string.restore_restore_set_apps_no_size, item.numAppData)
|
v.context.getString(R.string.restore_restore_set_apps_no_size, item.numAppData)
|
||||||
}
|
}
|
||||||
appView.visibility = if (item.numAppData > 0) VISIBLE else GONE
|
appView.visibility = if (item.numAppData > 0) VISIBLE else GONE
|
||||||
apkView.text = if (item.sizeApks > 0) {
|
apkView.text = if (!item.canBeRestored) {
|
||||||
|
v.context.getString(R.string.restore_restore_set_can_not_get_restored)
|
||||||
|
} else if (item.sizeApks > 0) {
|
||||||
v.context.getString(
|
v.context.getString(
|
||||||
R.string.restore_restore_set_apks,
|
R.string.restore_restore_set_apks,
|
||||||
item.numApks,
|
item.numApks,
|
||||||
|
@ -70,7 +80,13 @@ internal class RestoreSetAdapter(
|
||||||
} else {
|
} else {
|
||||||
v.context.getString(R.string.restore_restore_set_apks_no_size, item.numApks)
|
v.context.getString(R.string.restore_restore_set_apks_no_size, item.numApks)
|
||||||
}
|
}
|
||||||
apkView.visibility = if (item.numApks > 0) VISIBLE else GONE
|
apkView.visibility = if (item.numApks > 0 || !item.canBeRestored) VISIBLE else GONE
|
||||||
|
val apkTextColor = if (item.canBeRestored) {
|
||||||
|
appView.currentTextColor
|
||||||
|
} else {
|
||||||
|
MaterialColors.getColor(apkView, R.attr.colorError)
|
||||||
|
}
|
||||||
|
apkView.setTextColor(apkTextColor)
|
||||||
timeView.text = getRelativeTime(item.time)
|
timeView.text = getRelativeTime(item.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,14 @@ data class RestorableBackup(
|
||||||
val backupMetadata: BackupMetadata,
|
val backupMetadata: BackupMetadata,
|
||||||
val repoId: String? = null,
|
val repoId: String? = null,
|
||||||
val snapshot: Snapshot? = null,
|
val snapshot: Snapshot? = null,
|
||||||
|
val canBeRestored: Boolean = true,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(repoId: String, snapshot: Snapshot) : this(
|
constructor(repoId: String, snapshot: Snapshot, canBeRestored: Boolean = true) : this(
|
||||||
backupMetadata = BackupMetadata.fromSnapshot(snapshot),
|
backupMetadata = BackupMetadata.fromSnapshot(snapshot),
|
||||||
repoId = repoId,
|
repoId = repoId,
|
||||||
snapshot = snapshot,
|
snapshot = snapshot,
|
||||||
|
canBeRestored = canBeRestored,
|
||||||
)
|
)
|
||||||
|
|
||||||
val name: String
|
val name: String
|
||||||
|
|
|
@ -7,6 +7,8 @@ package com.stevesoltys.seedvault.ui.check
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.Formatter.formatShortFileSize
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
@ -52,10 +54,7 @@ class AppCheckResultActivity : BackupActivity() {
|
||||||
private fun onActionReceived() {
|
private fun onActionReceived() {
|
||||||
when (val result = checker.checkerResult) {
|
when (val result = checker.checkerResult) {
|
||||||
is CheckerResult.Success -> onSuccess(result)
|
is CheckerResult.Success -> onSuccess(result)
|
||||||
is CheckerResult.Error -> {
|
is CheckerResult.Error -> onError(result)
|
||||||
// TODO
|
|
||||||
log.info { "snapshots: ${result.snapshots.size}, errors: ${result.errors.size}" }
|
|
||||||
}
|
|
||||||
is CheckerResult.GeneralError, null -> {
|
is CheckerResult.GeneralError, null -> {
|
||||||
// TODO
|
// TODO
|
||||||
if (result == null) log.error { "No more result" }
|
if (result == null) log.error { "No more result" }
|
||||||
|
@ -66,7 +65,7 @@ class AppCheckResultActivity : BackupActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSuccess(result: CheckerResult.Success) {
|
private fun onSuccess(result: CheckerResult.Success) {
|
||||||
setContentView(R.layout.activity_check_success)
|
setContentView(R.layout.activity_check_result)
|
||||||
val intro = getString(
|
val intro = getString(
|
||||||
R.string.backup_app_check_success_intro,
|
R.string.backup_app_check_success_intro,
|
||||||
result.snapshots.size,
|
result.snapshots.size,
|
||||||
|
@ -84,4 +83,42 @@ class AppCheckResultActivity : BackupActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onError(result: CheckerResult.Error) {
|
||||||
|
setContentView(R.layout.activity_check_result)
|
||||||
|
requireViewById<ImageView>(R.id.imageView).setImageResource(R.drawable.ic_cloud_error)
|
||||||
|
requireViewById<TextView>(R.id.titleView).setText(R.string.backup_app_check_error_title)
|
||||||
|
val disclaimerView = requireViewById<TextView>(R.id.disclaimerView)
|
||||||
|
|
||||||
|
val intro = if (result.existingSnapshots == 0) {
|
||||||
|
disclaimerView.visibility = GONE
|
||||||
|
getString(R.string.backup_app_check_error_no_snapshots)
|
||||||
|
} else if (result.snapshots.isEmpty()) {
|
||||||
|
disclaimerView.visibility = GONE
|
||||||
|
getString(
|
||||||
|
R.string.backup_app_check_error_only_broken_snapshots,
|
||||||
|
result.existingSnapshots,
|
||||||
|
)
|
||||||
|
} else if (result.existingSnapshots > result.snapshots.size) {
|
||||||
|
getString(
|
||||||
|
R.string.backup_app_check_error_some_snapshots,
|
||||||
|
result.existingSnapshots,
|
||||||
|
result.snapshots.size,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
getString(R.string.backup_app_check_error_read_all_snapshots, result.snapshots.size)
|
||||||
|
}
|
||||||
|
requireViewById<TextView>(R.id.introView).text = intro
|
||||||
|
|
||||||
|
val items = (result.goodSnapshots.map { snapshot ->
|
||||||
|
RestorableBackup("", snapshot)
|
||||||
|
} + result.badSnapshots.map { snapshot ->
|
||||||
|
RestorableBackup("", snapshot, false)
|
||||||
|
}).sortedByDescending { it.time }
|
||||||
|
val listView = requireViewById<RecyclerView>(R.id.listView)
|
||||||
|
listView.adapter = RestoreSetAdapter(
|
||||||
|
listener = null,
|
||||||
|
items = items,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -363,6 +363,31 @@ internal class BackupNotificationManager(private val context: Context) {
|
||||||
formatShortFileSize(context, size),
|
formatShortFileSize(context, size),
|
||||||
"${formatShortFileSize(context, speed)}/s",
|
"${formatShortFileSize(context, speed)}/s",
|
||||||
)
|
)
|
||||||
|
val notification = getOnCheckFinishedBuilder()
|
||||||
|
.setContentTitle(context.getString(R.string.notification_checking_finished_title))
|
||||||
|
.setContentText(text)
|
||||||
|
.setSmallIcon(R.drawable.ic_cloud_done)
|
||||||
|
.build()
|
||||||
|
nm.cancel(NOTIFICATION_ID_CHECKING)
|
||||||
|
nm.notify(NOTIFICATION_ID_CHECK_FINISHED, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCheckFinishedWithError(size: Long, speed: Long) {
|
||||||
|
val text = context.getString(
|
||||||
|
R.string.notification_checking_error_text,
|
||||||
|
formatShortFileSize(context, size),
|
||||||
|
"${formatShortFileSize(context, speed)}/s",
|
||||||
|
)
|
||||||
|
val notification = getOnCheckFinishedBuilder()
|
||||||
|
.setContentTitle(context.getString(R.string.notification_checking_error_title))
|
||||||
|
.setContentText(text)
|
||||||
|
.setSmallIcon(R.drawable.ic_cloud_error)
|
||||||
|
.build()
|
||||||
|
nm.cancel(NOTIFICATION_ID_CHECKING)
|
||||||
|
nm.notify(NOTIFICATION_ID_CHECK_FINISHED, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOnCheckFinishedBuilder(): Builder {
|
||||||
// the background activity launch (BAL) gets restricted for setDeleteIntent()
|
// the background activity launch (BAL) gets restricted for setDeleteIntent()
|
||||||
// if we don't use these special ActivityOptions, may cause issues in future SDKs
|
// if we don't use these special ActivityOptions, may cause issues in future SDKs
|
||||||
val options = ActivityOptions.makeBasic()
|
val options = ActivityOptions.makeBasic()
|
||||||
|
@ -381,17 +406,11 @@ internal class BackupNotificationManager(private val context: Context) {
|
||||||
val deleteIntent = getActivity(context, 2, dIntent, FLAG_IMMUTABLE, options)
|
val deleteIntent = getActivity(context, 2, dIntent, FLAG_IMMUTABLE, options)
|
||||||
val actionTitle = context.getString(R.string.notification_checking_action)
|
val actionTitle = context.getString(R.string.notification_checking_action)
|
||||||
val action = Action.Builder(null, actionTitle, contentIntent).build()
|
val action = Action.Builder(null, actionTitle, contentIntent).build()
|
||||||
val notification = Builder(context, CHANNEL_ID_CHECKING)
|
return Builder(context, CHANNEL_ID_CHECKING)
|
||||||
.setContentTitle(context.getString(R.string.notification_checking_finished_title))
|
|
||||||
.setContentText(text)
|
|
||||||
.setSmallIcon(R.drawable.ic_cloud_done)
|
|
||||||
.setContentIntent(contentIntent)
|
.setContentIntent(contentIntent)
|
||||||
.addAction(action)
|
.addAction(action)
|
||||||
.setDeleteIntent(deleteIntent)
|
.setDeleteIntent(deleteIntent)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
|
||||||
nm.cancel(NOTIFICATION_ID_CHECKING)
|
|
||||||
nm.notify(NOTIFICATION_ID_CHECK_FINISHED, notification)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCheckCompleteNotificationSeen() {
|
fun onCheckCompleteNotificationSeen() {
|
||||||
|
|
|
@ -51,10 +51,10 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="0dp"
|
android:layout_margin="0dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/disclaimerView"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/introView"
|
app:layout_constraintTop_toBottomOf="@+id/introView"
|
||||||
|
app:layout_constraintVertical_bias="0.0"
|
||||||
tools:itemCount="4"
|
tools:itemCount="4"
|
||||||
tools:listitem="@layout/list_item_restore_set" />
|
tools:listitem="@layout/list_item_restore_set" />
|
||||||
|
|
||||||
|
@ -70,7 +70,8 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/listView" />
|
app:layout_constraintTop_toBottomOf="@+id/listView"
|
||||||
|
app:layout_constraintVertical_bias="0.0" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -191,11 +191,17 @@
|
||||||
<string name="notification_checking_title">Checking app backups…</string>
|
<string name="notification_checking_title">Checking app backups…</string>
|
||||||
<string name="notification_checking_finished_title">App backup integrity verified</string>
|
<string name="notification_checking_finished_title">App backup integrity verified</string>
|
||||||
<string name="notification_checking_finished_text">Successfully checked %1$s at an average speed of %2$s.</string>
|
<string name="notification_checking_finished_text">Successfully checked %1$s at an average speed of %2$s.</string>
|
||||||
|
<string name="notification_checking_error_title">Backup errors found</string>
|
||||||
|
<string name="notification_checking_error_text">Tap for details. We checked %1$s at an average speed of %2$s.</string>
|
||||||
<string name="notification_checking_action">Details</string>
|
<string name="notification_checking_action">Details</string>
|
||||||
|
|
||||||
<string name="backup_app_check_success_intro">%1$d snapshots were found and %2$d%% of their data (%3$s) successfully verified:</string>
|
<string name="backup_app_check_success_intro">%1$d snapshots were found and %2$d%% of their data (%3$s) successfully verified:</string>
|
||||||
<string name="backup_app_check_success_disclaimer">Note: We can not verify whether apps include all of their data in the backup.</string>
|
<string name="backup_app_check_success_disclaimer">Note: We can not verify whether apps include all of their data in the backup.</string>
|
||||||
|
<string name="backup_app_check_error_title">@string/notification_checking_error_title</string>
|
||||||
<string name="backup_app_check_error_no_snapshots">We could not find any backup. Please run a successful backup first and then try checking again.</string>
|
<string name="backup_app_check_error_no_snapshots">We could not find any backup. Please run a successful backup first and then try checking again.</string>
|
||||||
|
<string name="backup_app_check_error_only_broken_snapshots">We found %1$d backup snapshots. However, all of them were corrupted. Please run a successful backup and then try checking again.</string>
|
||||||
|
<string name="backup_app_check_error_some_snapshots">We found %1$d backup snapshots. However, we could only read the following %2$d snapshots.</string>
|
||||||
|
<string name="backup_app_check_error_read_all_snapshots">We found %1$d backup snapshots. However, some had errors and can not be fully restored.</string>
|
||||||
|
|
||||||
<!-- App Backup and Restore State -->
|
<!-- App Backup and Restore State -->
|
||||||
|
|
||||||
|
@ -229,6 +235,7 @@
|
||||||
<string name="restore_restore_set_apps_no_size">Has user data for <xliff:g example="42" id="apps">%1$d</xliff:g> apps</string>
|
<string name="restore_restore_set_apps_no_size">Has user data for <xliff:g example="42" id="apps">%1$d</xliff:g> apps</string>
|
||||||
<string name="restore_restore_set_apks">Contains <xliff:g example="42" id="apps">%1$d</xliff:g> apps (<xliff:g example="1 GB" id="size">%2$s</xliff:g>)</string>
|
<string name="restore_restore_set_apks">Contains <xliff:g example="42" id="apps">%1$d</xliff:g> apps (<xliff:g example="1 GB" id="size">%2$s</xliff:g>)</string>
|
||||||
<string name="restore_restore_set_apks_no_size">Contains <xliff:g example="42" id="apps">%1$d</xliff:g> apps</string>
|
<string name="restore_restore_set_apks_no_size">Contains <xliff:g example="42" id="apps">%1$d</xliff:g> apps</string>
|
||||||
|
<string name="restore_restore_set_can_not_get_restored">Can not be (fully) restored.</string>
|
||||||
<string name="restore_skip">Don\'t restore</string>
|
<string name="restore_skip">Don\'t restore</string>
|
||||||
<string name="restore_skip_apps">Skip restoring apps</string>
|
<string name="restore_skip_apps">Skip restoring apps</string>
|
||||||
<string name="restore_invalid_location_title">No backups found</string>
|
<string name="restore_invalid_location_title">No backups found</string>
|
||||||
|
|
Loading…
Reference in a new issue