Implement restoring of backup and show progress in UI

Note that the progress view is not exact as the progress reporting of
AOSP seems to be buggy.
This commit is contained in:
Torsten Grote 2019-09-06 12:36:51 -03:00
parent 491789e8e0
commit 1a7fdfa59a
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
14 changed files with 268 additions and 47 deletions

View file

@ -62,3 +62,5 @@ class Backup : Application() {
} }
fun Uri.isOnExternalStorage() = authority == URI_AUTHORITY_EXTERNAL_STORAGE fun Uri.isOnExternalStorage() = authority == URI_AUTHORITY_EXTERNAL_STORAGE
fun isDebugBuild() = Build.TYPE == "userdebug"

View file

@ -3,6 +3,7 @@ package com.stevesoltys.backup
import android.app.backup.BackupProgress import android.app.backup.BackupProgress
import android.app.backup.IBackupObserver import android.app.backup.IBackupObserver
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.util.Log import android.util.Log
import android.util.Log.INFO import android.util.Log.INFO
import android.util.Log.isLoggable import android.util.Log.isLoggable
@ -62,10 +63,12 @@ class NotificationBackupObserver(context: Context, private val userInitiated: Bo
nm.onBackupFinished() nm.onBackupFinished()
} }
private fun getAppName(packageId: String): CharSequence { private fun getAppName(packageId: String): CharSequence = getAppName(pm, packageId)
if (packageId == "@pm@") return packageId
val appInfo = pm.getApplicationInfo(packageId, 0)
return pm.getApplicationLabel(appInfo)
}
} }
fun getAppName(pm: PackageManager, packageId: String): CharSequence {
if (packageId == "@pm@") return packageId
val appInfo = pm.getApplicationInfo(packageId, 0)
return pm.getApplicationLabel(appInfo)
}

View file

@ -1,6 +1,7 @@
package com.stevesoltys.backup.restore package com.stevesoltys.backup.restore
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.ui.BackupActivity import com.stevesoltys.backup.ui.BackupActivity
@ -20,7 +21,13 @@ class RestoreActivity : BackupActivity() {
setContentView(R.layout.activity_fragment_container) setContentView(R.layout.activity_fragment_container)
if (savedInstanceState == null) showFragment(getInitialFragment()) viewModel.chosenRestoreSet.observe(this, Observer { set ->
if (set != null) showFragment(RestoreProgressFragment())
})
if (savedInstanceState == null && viewModel.validLocationIsSet()) {
showFragment(getInitialFragment())
}
} }
override fun onInvalidLocation() { override fun onInvalidLocation() {

View file

@ -0,0 +1,70 @@
package com.stevesoltys.backup.restore
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.R
import com.stevesoltys.backup.getAppName
import com.stevesoltys.backup.isDebugBuild
import kotlinx.android.synthetic.main.fragment_restore_progress.*
class RestoreProgressFragment : Fragment() {
private lateinit var viewModel: RestoreViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_restore_progress, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// decryption will fail when the device is locked, so keep the screen on to prevent locking
requireActivity().window.addFlags(FLAG_KEEP_SCREEN_ON)
viewModel = ViewModelProviders.of(requireActivity()).get(RestoreViewModel::class.java)
viewModel.numPackages.observe(this, Observer { numPackages ->
progressBar.min = 0
progressBar.max = numPackages
})
viewModel.chosenRestoreSet.observe(this, Observer { set ->
backupNameView.text = set.device
})
viewModel.restoreProgress.observe(this, Observer { progress ->
progressBar.progress = progress.nowBeingRestored
val appName = getAppName(requireActivity().packageManager, progress.currentPackage)
val displayName = if (isDebugBuild()) "$appName (${progress.currentPackage})" else appName
currentPackageView.text = getString(R.string.restore_current_package, displayName)
})
viewModel.restoreFinished.observe(this, Observer { finished ->
progressBarIndefinite.visibility = INVISIBLE
progressBar.progress = viewModel.numPackages.value ?: progressBar.max
button.visibility = VISIBLE
if (finished == 0) {
// success
currentPackageView.text = getString(R.string.restore_finished_success)
warningView.visibility = VISIBLE
} else {
// error
currentPackageView.text = getString(R.string.restore_finished_error)
currentPackageView.setTextColor(warningView.textColors)
}
activity?.window?.clearFlags(FLAG_KEEP_SCREEN_ON)
})
button.setOnClickListener { requireActivity().finishAfterTransition() }
}
}

View file

@ -34,7 +34,7 @@ internal class RestoreSetAdapter(
internal fun bind(item: RestoreSet) { internal fun bind(item: RestoreSet) {
v.setOnClickListener { listener.onRestoreSetClicked(item) } v.setOnClickListener { listener.onRestoreSetClicked(item) }
titleView.text = item.name titleView.text = item.name
subtitleView.text = item.device subtitleView.text = "Android Backup" // TODO change to backup date when available
} }
} }

View file

@ -2,7 +2,6 @@ package com.stevesoltys.backup.restore
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
@ -14,7 +13,7 @@ import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import kotlinx.android.synthetic.main.fragment_restore_set.* import kotlinx.android.synthetic.main.fragment_restore_set.*
class RestoreSetFragment : Fragment(), RestoreSetClickListener { class RestoreSetFragment : Fragment() {
private lateinit var viewModel: RestoreViewModel private lateinit var viewModel: RestoreViewModel
@ -34,7 +33,9 @@ class RestoreSetFragment : Fragment(), RestoreSetClickListener {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
viewModel.loadRestoreSets() if (viewModel.recoveryCodeIsSet() && viewModel.validLocationIsSet()) {
viewModel.loadRestoreSets()
}
} }
private fun onRestoreSetsLoaded(result: RestoreSetResult) { private fun onRestoreSetsLoaded(result: RestoreSetResult) {
@ -49,14 +50,10 @@ class RestoreSetFragment : Fragment(), RestoreSetClickListener {
listView.visibility = VISIBLE listView.visibility = VISIBLE
progressBar.visibility = INVISIBLE progressBar.visibility = INVISIBLE
listView.adapter = RestoreSetAdapter(this, result.sets) listView.adapter = RestoreSetAdapter(viewModel, result.sets)
} }
} }
override fun onRestoreSetClicked(set: RestoreSet) {
Log.e("TEST", "RESTORE SET CLICKED: ${set.name} ${set.device} ${set.token}")
}
} }
internal interface RestoreSetClickListener { internal interface RestoreSetClickListener {

View file

@ -6,6 +6,7 @@ import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.stevesoltys.backup.Backup import com.stevesoltys.backup.Backup
@ -16,7 +17,7 @@ import com.stevesoltys.backup.ui.BackupViewModel
private val TAG = RestoreViewModel::class.java.simpleName private val TAG = RestoreViewModel::class.java.simpleName
class RestoreViewModel(app: Application) : BackupViewModel(app) { class RestoreViewModel(app: Application) : BackupViewModel(app), RestoreSetClickListener {
private val backupManager = Backup.backupManager private val backupManager = Backup.backupManager
@ -27,8 +28,21 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
private val mRestoreSets = MutableLiveData<RestoreSetResult>() private val mRestoreSets = MutableLiveData<RestoreSetResult>()
internal val restoreSets: LiveData<RestoreSetResult> get() = mRestoreSets internal val restoreSets: LiveData<RestoreSetResult> get() = mRestoreSets
private val mChosenRestoreSet = MutableLiveData<RestoreSet>()
internal val chosenRestoreSet: LiveData<RestoreSet> get() = mChosenRestoreSet
private var mNumPackages = MutableLiveData<Int>()
internal val numPackages: LiveData<Int> get() = mNumPackages
private val mRestoreProgress = MutableLiveData<RestoreProgress>()
internal val restoreProgress: LiveData<RestoreProgress> get() = mRestoreProgress
private val mRestoreFinished = MutableLiveData<Int>()
// Zero on success; a nonzero error code if the restore operation as a whole failed.
internal val restoreFinished: LiveData<Int> get() = mRestoreFinished
override fun acceptBackupLocation(folderUri: Uri): Boolean { override fun acceptBackupLocation(folderUri: Uri): Boolean {
// TODO // TODO search if there's really a backup available in this location and see if we can decrypt it
return true return true
} }
@ -52,6 +66,14 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
} }
} }
override fun onRestoreSetClicked(set: RestoreSet) {
val session = this.session
check(session != null)
session.restoreAll(set.token, observer, monitor)
mChosenRestoreSet.value = set
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
endSession() endSession()
@ -63,8 +85,11 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
observer = null observer = null
} }
@WorkerThread
private inner class RestoreObserver : IRestoreObserver.Stub() { private inner class RestoreObserver : IRestoreObserver.Stub() {
private var correctedNow: Int = -1
/** /**
* Supply a list of the restore datasets available from the current transport. * Supply a list of the restore datasets available from the current transport.
* This method is invoked as a callback following the application's use of the * This method is invoked as a callback following the application's use of the
@ -76,7 +101,7 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
*/ */
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) { override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
if (restoreSets == null || restoreSets.isEmpty()) { if (restoreSets == null || restoreSets.isEmpty()) {
mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_empty_result)) mRestoreSets.postValue(RestoreSetResult(app.getString(R.string.restore_set_empty_result)))
} else { } else {
mRestoreSets.postValue(RestoreSetResult(restoreSets)) mRestoreSets.postValue(RestoreSetResult(restoreSets))
} }
@ -88,7 +113,7 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
* @param numPackages The total number of packages being processed in this restore operation. * @param numPackages The total number of packages being processed in this restore operation.
*/ */
override fun restoreStarting(numPackages: Int) { override fun restoreStarting(numPackages: Int) {
Log.e(TAG, "RESTORE STARTING $numPackages") mNumPackages.postValue(numPackages)
} }
/** /**
@ -101,17 +126,22 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
* @param currentPackage The name of the package now being restored. * @param currentPackage The name of the package now being restored.
*/ */
override fun onUpdate(nowBeingRestored: Int, currentPackage: String) { override fun onUpdate(nowBeingRestored: Int, currentPackage: String) {
Log.e(TAG, "RESTORE UPDATE $nowBeingRestored $currentPackage") if (nowBeingRestored <= correctedNow) {
correctedNow += 1
} else {
correctedNow = nowBeingRestored
}
mRestoreProgress.postValue(RestoreProgress(correctedNow, currentPackage))
} }
/** /**
* The restore operation has completed. * The restore operation has completed.
* *
* @param error Zero on success; a nonzero error code if the restore operation * @param result Zero on success; a nonzero error code if the restore operation
* as a whole failed. * as a whole failed.
*/ */
override fun restoreFinished(error: Int) { override fun restoreFinished(result: Int) {
Log.e(TAG, "RESTORE FINISHED $error") mRestoreFinished.postValue(result)
endSession() endSession()
} }
@ -129,3 +159,7 @@ internal class RestoreSetResult(
internal fun hasError(): Boolean = errorMsg != null internal fun hasError(): Boolean = errorMsg != null
} }
internal class RestoreProgress(
internal val nowBeingRestored: Int,
internal val currentPackage: String)

View file

@ -13,6 +13,7 @@ import com.stevesoltys.backup.header.UnsupportedVersionException
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import javax.crypto.AEADBadTagException
private class KVRestoreState( private class KVRestoreState(
internal val token: Long, internal val token: Long,
@ -86,6 +87,9 @@ internal class KVRestore(
} catch (e: UnsupportedVersionException) { } catch (e: UnsupportedVersionException) {
Log.e(TAG, "Unsupported version in backup: ${e.version}", e) Log.e(TAG, "Unsupported version in backup: ${e.version}", e)
TRANSPORT_ERROR TRANSPORT_ERROR
} catch (e: AEADBadTagException) {
Log.e(TAG, "Decryption failed", e)
TRANSPORT_ERROR
} finally { } finally {
this.state = null this.state = null
closeQuietly(data) closeQuietly(data)

View file

@ -59,7 +59,9 @@ abstract class BackupActivity : AppCompatActivity() {
@CallSuper @CallSuper
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
if (resultCode != RESULT_OK) { if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) {
getViewModel().handleChooseFolderResult(result)
} else if (resultCode != RESULT_OK) {
Log.w(TAG, "Error in activity result: $requestCode") Log.w(TAG, "Error in activity result: $requestCode")
finishAfterTransition() finishAfterTransition()
} else { } else {

View file

@ -1,6 +1,5 @@
package com.stevesoltys.backup.ui package com.stevesoltys.backup.ui
import android.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.Intent.* import android.content.Intent.*
@ -8,23 +7,17 @@ import android.os.Bundle
import android.provider.DocumentsContract.EXTRA_PROMPT import android.provider.DocumentsContract.EXTRA_PROMPT
import android.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.lifecycle.ViewModelProviders
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.settings.SettingsViewModel
class BackupLocationFragment : PreferenceFragmentCompat() { class BackupLocationFragment : PreferenceFragmentCompat() {
private lateinit var viewModel: SettingsViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.backup_location, rootKey) setPreferencesFromResource(R.xml.backup_location, rootKey)
requireActivity().setTitle(R.string.settings_backup_location_title) requireActivity().setTitle(R.string.settings_backup_location_title)
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
val externalStorage = Preference(requireContext()).apply { val externalStorage = Preference(requireContext()).apply {
setIcon(R.drawable.ic_storage) setIcon(R.drawable.ic_storage)
setTitle(R.string.settings_backup_external_storage) setTitle(R.string.settings_backup_external_storage)
@ -43,18 +36,11 @@ class BackupLocationFragment : PreferenceFragmentCompat() {
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
try { try {
val documentChooser = createChooser(openTreeIntent, null) val documentChooser = createChooser(openTreeIntent, null)
startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE) // start from the activity context, so we can receive and handle the result there
requireActivity().startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE)
} catch (ex: ActivityNotFoundException) { } catch (ex: ActivityNotFoundException) {
Toast.makeText(requireContext(), "Please install a file manager.", LENGTH_LONG).show() Toast.makeText(requireContext(), "Please install a file manager.", LENGTH_LONG).show()
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) {
viewModel.handleChooseFolderResult(result)
} else {
super.onActivityResult(requestCode, resultCode, result)
}
}
} }

View file

@ -23,10 +23,10 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
* Will be set to true if this is the initial location. * Will be set to true if this is the initial location.
* It will be false if an existing location was changed. * It will be false if an existing location was changed.
*/ */
internal val onLocationSet: LiveEvent<LocationResult> = locationWasSet internal val onLocationSet: LiveEvent<LocationResult> get() = locationWasSet
private val mChooseBackupLocation = MutableLiveEvent<Boolean>() private val mChooseBackupLocation = MutableLiveEvent<Boolean>()
internal val chooseBackupLocation: LiveEvent<Boolean> = mChooseBackupLocation internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true) internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
internal fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey() internal fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey()
@ -52,12 +52,12 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
// store backup folder location in settings // store backup folder location in settings
setBackupFolderUri(app, folderUri) setBackupFolderUri(app, folderUri)
// notify the UI that the location has been set
locationWasSet.setEvent(LocationResult(true, initialSetUp))
// stop backup service to be sure the old location will get updated // stop backup service to be sure the old location will get updated
app.stopService(Intent(app, ConfigurableBackupTransportService::class.java)) app.stopService(Intent(app, ConfigurableBackupTransportService::class.java))
// notify the UI that the location has been set
locationWasSet.setEvent(LocationResult(true, initialSetUp))
Log.d(TAG, "New storage location chosen: $folderUri") Log.d(TAG, "New storage location chosen: $folderUri")
} else { } else {
Log.w(TAG, "Location was rejected: $folderUri") Log.w(TAG, "Location was rejected: $folderUri")

View file

@ -1,6 +1,5 @@
package com.stevesoltys.backup.ui package com.stevesoltys.backup.ui
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -11,6 +10,7 @@ import android.widget.Toast.LENGTH_LONG
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.isDebugBuild
import io.github.novacrypto.bip39.Validation.InvalidChecksumException import io.github.novacrypto.bip39.Validation.InvalidChecksumException
import io.github.novacrypto.bip39.Validation.WordNotFoundException import io.github.novacrypto.bip39.Validation.WordNotFoundException
import kotlinx.android.synthetic.main.fragment_recovery_code_input.* import kotlinx.android.synthetic.main.fragment_recovery_code_input.*
@ -37,7 +37,7 @@ class RecoveryCodeInputFragment : Fragment() {
} }
doneButton.setOnClickListener { done() } doneButton.setOnClickListener { done() }
if (Build.TYPE == "userdebug") debugPreFill() if (isDebugBuild()) debugPreFill()
} }
private fun getInput(): List<CharSequence> = ArrayList<String>(WORD_NUM).apply { private fun getInput(): List<CharSequence> = ArrayList<String>(WORD_NUM).apply {

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="16dp"
android:tint="?android:colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_cloud_download"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/titleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/restore_restoring"
android:textColor="?android:textColorSecondary"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<TextView
android:id="@+id/backupNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="?android:textColorTertiary"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="Pixel 2 XL" />
<TextView
android:id="@+id/currentPackageView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:textColor="?android:textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
tools:text="@string/restore_current_package" />
<ProgressBar
android:id="@+id/progressBarIndefinite"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/currentPackageView" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBarIndefinite"
tools:progress="50" />
<TextView
android:id="@+id/warningView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:text="@string/restore_finished_warning_only_installed"
android:textColor="@android:color/holo_red_dark"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar"
tools:visibility="visible" />
<Button
android:id="@+id/button"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/restore_finished_button"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/warningView"
app:layout_constraintVertical_bias="1.0"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -77,5 +77,11 @@
<string name="restore_back">Don\'t restore</string> <string name="restore_back">Don\'t restore</string>
<string name="restore_set_error">An error occurred loading the backups.</string> <string name="restore_set_error">An error occurred loading the backups.</string>
<string name="restore_set_empty_result">No backups found at given location.</string> <string name="restore_set_empty_result">No backups found at given location.</string>
<string name="restore_restoring">Restoring Backup</string>
<string name="restore_current_package">Restoring %s…</string>
<string name="restore_finished_success">Restore complete.</string>
<string name="restore_finished_error">An error occurred while restoring the backup.</string>
<string name="restore_finished_warning_only_installed">Note that we could only restore data for apps that are already installed.\n\nWhen you install more apps, we will try to restore their data and settings from this backup. So please do not delete it as long as it might still be needed.</string>
<string name="restore_finished_button">Finish</string>
</resources> </resources>