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:
parent
491789e8e0
commit
1a7fdfa59a
14 changed files with 268 additions and 47 deletions
|
@ -62,3 +62,5 @@ class Backup : Application() {
|
|||
}
|
||||
|
||||
fun Uri.isOnExternalStorage() = authority == URI_AUTHORITY_EXTERNAL_STORAGE
|
||||
|
||||
fun isDebugBuild() = Build.TYPE == "userdebug"
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.stevesoltys.backup
|
|||
import android.app.backup.BackupProgress
|
||||
import android.app.backup.IBackupObserver
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import android.util.Log.INFO
|
||||
import android.util.Log.isLoggable
|
||||
|
@ -62,10 +63,12 @@ class NotificationBackupObserver(context: Context, private val userInitiated: Bo
|
|||
nm.onBackupFinished()
|
||||
}
|
||||
|
||||
private fun getAppName(packageId: String): CharSequence {
|
||||
private fun getAppName(packageId: String): CharSequence = getAppName(pm, packageId)
|
||||
|
||||
}
|
||||
|
||||
fun getAppName(pm: PackageManager, packageId: String): CharSequence {
|
||||
if (packageId == "@pm@") return packageId
|
||||
val appInfo = pm.getApplicationInfo(packageId, 0)
|
||||
return pm.getApplicationLabel(appInfo)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.stevesoltys.backup.restore
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.ui.BackupActivity
|
||||
|
@ -20,7 +21,13 @@ class RestoreActivity : BackupActivity() {
|
|||
|
||||
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() {
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
||||
}
|
|
@ -34,7 +34,7 @@ internal class RestoreSetAdapter(
|
|||
internal fun bind(item: RestoreSet) {
|
||||
v.setOnClickListener { listener.onRestoreSetClicked(item) }
|
||||
titleView.text = item.name
|
||||
subtitleView.text = item.device
|
||||
subtitleView.text = "Android Backup" // TODO change to backup date when available
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.stevesoltys.backup.restore
|
|||
|
||||
import android.app.backup.RestoreSet
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
|
@ -14,7 +13,7 @@ import androidx.lifecycle.ViewModelProviders
|
|||
import com.stevesoltys.backup.R
|
||||
import kotlinx.android.synthetic.main.fragment_restore_set.*
|
||||
|
||||
class RestoreSetFragment : Fragment(), RestoreSetClickListener {
|
||||
class RestoreSetFragment : Fragment() {
|
||||
|
||||
private lateinit var viewModel: RestoreViewModel
|
||||
|
||||
|
@ -34,8 +33,10 @@ class RestoreSetFragment : Fragment(), RestoreSetClickListener {
|
|||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (viewModel.recoveryCodeIsSet() && viewModel.validLocationIsSet()) {
|
||||
viewModel.loadRestoreSets()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRestoreSetsLoaded(result: RestoreSetResult) {
|
||||
if (result.hasError()) {
|
||||
|
@ -49,14 +50,10 @@ class RestoreSetFragment : Fragment(), RestoreSetClickListener {
|
|||
listView.visibility = VISIBLE
|
||||
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 {
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.app.backup.IRestoreSession
|
|||
import android.app.backup.RestoreSet
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.stevesoltys.backup.Backup
|
||||
|
@ -16,7 +17,7 @@ import com.stevesoltys.backup.ui.BackupViewModel
|
|||
|
||||
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
|
||||
|
||||
|
@ -27,8 +28,21 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
|
|||
private val mRestoreSets = MutableLiveData<RestoreSetResult>()
|
||||
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 {
|
||||
// TODO
|
||||
// TODO search if there's really a backup available in this location and see if we can decrypt it
|
||||
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() {
|
||||
super.onCleared()
|
||||
endSession()
|
||||
|
@ -63,8 +85,11 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
|
|||
observer = null
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private inner class RestoreObserver : IRestoreObserver.Stub() {
|
||||
|
||||
private var correctedNow: Int = -1
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -76,7 +101,7 @@ class RestoreViewModel(app: Application) : BackupViewModel(app) {
|
|||
*/
|
||||
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
|
||||
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 {
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
override fun restoreFinished(error: Int) {
|
||||
Log.e(TAG, "RESTORE FINISHED $error")
|
||||
override fun restoreFinished(result: Int) {
|
||||
mRestoreFinished.postValue(result)
|
||||
endSession()
|
||||
}
|
||||
|
||||
|
@ -129,3 +159,7 @@ internal class RestoreSetResult(
|
|||
|
||||
internal fun hasError(): Boolean = errorMsg != null
|
||||
}
|
||||
|
||||
internal class RestoreProgress(
|
||||
internal val nowBeingRestored: Int,
|
||||
internal val currentPackage: String)
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.stevesoltys.backup.header.UnsupportedVersionException
|
|||
import libcore.io.IoUtils.closeQuietly
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import javax.crypto.AEADBadTagException
|
||||
|
||||
private class KVRestoreState(
|
||||
internal val token: Long,
|
||||
|
@ -86,6 +87,9 @@ internal class KVRestore(
|
|||
} catch (e: UnsupportedVersionException) {
|
||||
Log.e(TAG, "Unsupported version in backup: ${e.version}", e)
|
||||
TRANSPORT_ERROR
|
||||
} catch (e: AEADBadTagException) {
|
||||
Log.e(TAG, "Decryption failed", e)
|
||||
TRANSPORT_ERROR
|
||||
} finally {
|
||||
this.state = null
|
||||
closeQuietly(data)
|
||||
|
|
|
@ -59,7 +59,9 @@ abstract class BackupActivity : AppCompatActivity() {
|
|||
|
||||
@CallSuper
|
||||
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")
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.backup.ui
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.Intent.*
|
||||
|
@ -8,23 +7,17 @@ import android.os.Bundle
|
|||
import android.provider.DocumentsContract.EXTRA_PROMPT
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.settings.SettingsViewModel
|
||||
|
||||
class BackupLocationFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private lateinit var viewModel: SettingsViewModel
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.backup_location, rootKey)
|
||||
|
||||
requireActivity().setTitle(R.string.settings_backup_location_title)
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
|
||||
|
||||
val externalStorage = Preference(requireContext()).apply {
|
||||
setIcon(R.drawable.ic_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)
|
||||
try {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
|
|||
* Will be set to true if this is the initial location.
|
||||
* 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>()
|
||||
internal val chooseBackupLocation: LiveEvent<Boolean> = mChooseBackupLocation
|
||||
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
||||
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
|
||||
|
||||
internal fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey()
|
||||
|
@ -52,12 +52,12 @@ abstract class BackupViewModel(protected val app: Application) : AndroidViewMode
|
|||
// store backup folder location in settings
|
||||
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
|
||||
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")
|
||||
} else {
|
||||
Log.w(TAG, "Location was rejected: $folderUri")
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.backup.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -11,6 +10,7 @@ import android.widget.Toast.LENGTH_LONG
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.isDebugBuild
|
||||
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
|
||||
import io.github.novacrypto.bip39.Validation.WordNotFoundException
|
||||
import kotlinx.android.synthetic.main.fragment_recovery_code_input.*
|
||||
|
@ -37,7 +37,7 @@ class RecoveryCodeInputFragment : Fragment() {
|
|||
}
|
||||
doneButton.setOnClickListener { done() }
|
||||
|
||||
if (Build.TYPE == "userdebug") debugPreFill()
|
||||
if (isDebugBuild()) debugPreFill()
|
||||
}
|
||||
|
||||
private fun getInput(): List<CharSequence> = ArrayList<String>(WORD_NUM).apply {
|
||||
|
|
110
app/src/main/res/layout/fragment_restore_progress.xml
Normal file
110
app/src/main/res/layout/fragment_restore_progress.xml
Normal 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>
|
|
@ -77,5 +77,11 @@
|
|||
<string name="restore_back">Don\'t restore</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_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>
|
||||
|
|
Loading…
Reference in a new issue