Add a RestoreActivity that lists available RestoreSets (backups)

and allows the user to select one to get restored.
This commit is contained in:
Torsten Grote 2019-09-05 17:42:39 -03:00
parent aa3aad8fb3
commit 491789e8e0
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
28 changed files with 640 additions and 147 deletions

View file

@ -32,9 +32,20 @@
android:exported="true" />
<activity
android:name=".settings.RecoveryCodeActivity"
android:name=".ui.RecoveryCodeActivity"
android:label="@string/recovery_code_title" />
<activity
android:name=".restore.RestoreActivity"
android:exported="true"
android:label="@string/restore_title"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="com.stevesoltys.backup.restore.RESTORE_BACKUP" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.stevesoltys.backup.activity.MainActivity"
android:label="@string/app_name" />

View file

@ -0,0 +1,30 @@
package com.stevesoltys.backup.restore
import android.os.Bundle
import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.R
import com.stevesoltys.backup.ui.BackupActivity
import com.stevesoltys.backup.ui.BackupViewModel
class RestoreActivity : BackupActivity() {
private lateinit var viewModel: RestoreViewModel
override fun getViewModel(): BackupViewModel = viewModel
override fun getInitialFragment() = RestoreSetFragment()
override fun onCreate(savedInstanceState: Bundle?) {
viewModel = ViewModelProviders.of(this).get(RestoreViewModel::class.java)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fragment_container)
if (savedInstanceState == null) showFragment(getInitialFragment())
}
override fun onInvalidLocation() {
// TODO alert dialog?
}
}

View file

@ -0,0 +1,42 @@
package com.stevesoltys.backup.restore
import android.app.backup.RestoreSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.stevesoltys.backup.R
import com.stevesoltys.backup.restore.RestoreSetAdapter.RestoreSetViewHolder
internal class RestoreSetAdapter(
private val listener: RestoreSetClickListener,
private val items: Array<out RestoreSet>) : Adapter<RestoreSetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RestoreSetViewHolder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_restore_set, parent, false) as View
return RestoreSetViewHolder(v)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: RestoreSetViewHolder, position: Int) {
holder.bind(items[position])
}
inner class RestoreSetViewHolder(private val v: View) : ViewHolder(v) {
private val titleView = v.findViewById<TextView>(R.id.titleView)
private val subtitleView = v.findViewById<TextView>(R.id.subtitleView)
internal fun bind(item: RestoreSet) {
v.setOnClickListener { listener.onRestoreSetClicked(item) }
titleView.text = item.name
subtitleView.text = item.device
}
}
}

View file

@ -0,0 +1,64 @@
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
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.R
import kotlinx.android.synthetic.main.fragment_restore_set.*
class RestoreSetFragment : Fragment(), RestoreSetClickListener {
private lateinit var viewModel: RestoreViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_restore_set, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(requireActivity()).get(RestoreViewModel::class.java)
viewModel.restoreSets.observe(this, Observer { result -> onRestoreSetsLoaded(result) })
backView.setOnClickListener { requireActivity().finishAfterTransition() }
}
override fun onStart() {
super.onStart()
viewModel.loadRestoreSets()
}
private fun onRestoreSetsLoaded(result: RestoreSetResult) {
if (result.hasError()) {
errorView.visibility = VISIBLE
listView.visibility = INVISIBLE
progressBar.visibility = INVISIBLE
errorView.text = result.errorMsg
} else {
errorView.visibility = INVISIBLE
listView.visibility = VISIBLE
progressBar.visibility = INVISIBLE
listView.adapter = RestoreSetAdapter(this, result.sets)
}
}
override fun onRestoreSetClicked(set: RestoreSet) {
Log.e("TEST", "RESTORE SET CLICKED: ${set.name} ${set.device} ${set.token}")
}
}
internal interface RestoreSetClickListener {
fun onRestoreSetClicked(set: RestoreSet)
}

View file

@ -0,0 +1,131 @@
package com.stevesoltys.backup.restore
import android.app.Application
import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet
import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R
import com.stevesoltys.backup.session.backup.BackupMonitor
import com.stevesoltys.backup.transport.TRANSPORT_ID
import com.stevesoltys.backup.ui.BackupViewModel
private val TAG = RestoreViewModel::class.java.simpleName
class RestoreViewModel(app: Application) : BackupViewModel(app) {
private val backupManager = Backup.backupManager
private var session: IRestoreSession? = null
private var observer: RestoreObserver? = null
private val monitor = BackupMonitor()
private val mRestoreSets = MutableLiveData<RestoreSetResult>()
internal val restoreSets: LiveData<RestoreSetResult> get() = mRestoreSets
override fun acceptBackupLocation(folderUri: Uri): Boolean {
// TODO
return true
}
internal fun loadRestoreSets() {
val session = this.session ?: backupManager.beginRestoreSession(null, TRANSPORT_ID)
this.session = session
if (session == null) {
Log.e(TAG, "beginRestoreSession() returned null session")
mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_error))
return
}
val observer = this.observer ?: RestoreObserver()
this.observer = observer
val setResult = session.getAvailableRestoreSets(observer, monitor)
if (setResult != 0) {
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_error))
return
}
}
override fun onCleared() {
super.onCleared()
endSession()
}
private fun endSession() {
session?.endRestoreSession()
session = null
observer = null
}
private inner class RestoreObserver : IRestoreObserver.Stub() {
/**
* 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
* [IRestoreSession.getAvailableRestoreSets] method.
*
* @param restoreSets An array of [RestoreSet] objects
* describing all of the available datasets that are candidates for restoring to
* the current device. If no applicable datasets exist, restoreSets will be null.
*/
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
if (restoreSets == null || restoreSets.isEmpty()) {
mRestoreSets.value = RestoreSetResult(app.getString(R.string.restore_set_empty_result))
} else {
mRestoreSets.postValue(RestoreSetResult(restoreSets))
}
}
/**
* The restore operation has begun.
*
* @param numPackages The total number of packages being processed in this restore operation.
*/
override fun restoreStarting(numPackages: Int) {
Log.e(TAG, "RESTORE STARTING $numPackages")
}
/**
* An indication of which package is being restored currently,
* out of the total number provided in the [restoreStarting] callback.
* This method is not guaranteed to be called.
*
* @param nowBeingRestored The index, between 1 and the numPackages parameter
* to the [restoreStarting] callback, of the package now being restored.
* @param currentPackage The name of the package now being restored.
*/
override fun onUpdate(nowBeingRestored: Int, currentPackage: String) {
Log.e(TAG, "RESTORE UPDATE $nowBeingRestored $currentPackage")
}
/**
* The restore operation has completed.
*
* @param error 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")
endSession()
}
}
}
internal class RestoreSetResult(
internal val sets: Array<out RestoreSet>,
internal val errorMsg: String?) {
internal constructor(sets: Array<out RestoreSet>) : this(sets, null)
internal constructor(errorMsg: String) : this(emptyArray(), errorMsg)
internal fun hasError(): Boolean = errorMsg != null
}

View file

@ -1,85 +1,28 @@
package com.stevesoltys.backup.settings
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.LiveEventHandler
import com.stevesoltys.backup.R
import com.stevesoltys.backup.ui.BackupActivity
import com.stevesoltys.backup.ui.BackupViewModel
private val TAG = SettingsActivity::class.java.name
const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1
const val REQUEST_CODE_RECOVERY_CODE = 2
class SettingsActivity : AppCompatActivity() {
class SettingsActivity : BackupActivity() {
private lateinit var viewModel: SettingsViewModel
override fun getViewModel(): BackupViewModel = viewModel
override fun getInitialFragment() = SettingsFragment()
override fun onCreate(savedInstanceState: Bundle?) {
viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
viewModel.onLocationSet.observeEvent(this, LiveEventHandler { initialSetUp ->
if (initialSetUp) showFragment(SettingsFragment())
else supportFragmentManager.popBackStack()
})
viewModel.chooseBackupLocation.observeEvent(this, LiveEventHandler { show ->
if (show) showFragment(BackupLocationFragment(), true)
})
setContentView(R.layout.activity_fragment_container)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) showFragment(SettingsFragment())
}
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
if (resultCode != RESULT_OK) {
Log.w(TAG, "Error in activity result: $requestCode")
finishAfterTransition()
} else {
super.onActivityResult(requestCode, resultCode, result)
}
}
override fun onStart() {
super.onStart()
if (isFinishing) return
// check that backup is provisioned
if (!viewModel.recoveryCodeIsSet()) {
showRecoveryCodeActivity()
} else if (!viewModel.validLocationIsSet()) {
showFragment(BackupLocationFragment())
// remove potential error notifications
(application as Backup).notificationManager.onBackupErrorSeen()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
item.itemId == android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun showRecoveryCodeActivity() {
val intent = Intent(this, RecoveryCodeActivity::class.java)
startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE)
}
private fun showFragment(f: Fragment, addToBackStack: Boolean = false) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
.replace(R.id.fragment, f)
if (addToBackStack) fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
if (savedInstanceState == null) showFragment(getInitialFragment())
}
}

View file

@ -1,6 +1,7 @@
package com.stevesoltys.backup.settings
import android.content.Context.BACKUP_SERVICE
import android.content.Intent
import android.os.Bundle
import android.os.RemoteException
import android.provider.Settings
@ -17,6 +18,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R
import com.stevesoltys.backup.restore.RestoreActivity
private val TAG = SettingsFragment::class.java.name
@ -100,7 +102,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}
item.itemId == R.id.action_restore -> {
Toast.makeText(requireContext(), "Not yet implemented", LENGTH_SHORT).show()
startActivity(Intent(requireContext(), RestoreActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)

View file

@ -1,66 +1,10 @@
package com.stevesoltys.backup.settings
import android.app.Application
import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.LiveEvent
import com.stevesoltys.backup.MutableLiveEvent
import com.stevesoltys.backup.isOnExternalStorage
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService
import com.stevesoltys.backup.transport.requestBackup
import com.stevesoltys.backup.ui.BackupViewModel
private val TAG = SettingsViewModel::class.java.simpleName
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val app = application
private val locationWasSet = MutableLiveEvent<Boolean>()
/**
* 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<Boolean> = locationWasSet
private val mChooseBackupLocation = MutableLiveEvent<Boolean>()
internal val chooseBackupLocation: LiveEvent<Boolean> = mChooseBackupLocation
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey()
fun validLocationIsSet(): Boolean {
val uri = getBackupFolderUri(app) ?: return false
if (uri.isOnExternalStorage()) return true // might be a temporary failure
val file = DocumentFile.fromTreeUri(app, uri) ?: return false
return file.isDirectory
}
fun handleChooseFolderResult(result: Intent?) {
val folderUri = result?.data ?: return
// persist permission to access backup folder across reboots
val takeFlags = result.flags and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
app.contentResolver.takePersistableUriPermission(folderUri, takeFlags)
// check if this is initial set-up or a later change
val initialSetUp = !validLocationIsSet()
// store backup folder location in settings
setBackupFolderUri(app, folderUri)
// notify the UI that the location has been set
locationWasSet.setEvent(initialSetUp)
// stop backup service to be sure the old location will get updated
app.stopService(Intent(app, ConfigurableBackupTransportService::class.java))
Log.d(TAG, "New storage location chosen: $folderUri")
}
class SettingsViewModel(app: Application) : BackupViewModel(app) {
fun backupNow() = Thread { requestBackup(app) }.start()

View file

@ -12,6 +12,7 @@ import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.backup.settings.SettingsActivity
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
const val DEFAULT_RESTORE_SET_TOKEN: Long = 1
private const val TRANSPORT_DIRECTORY_NAME = "com.stevesoltys.backup.transport.ConfigurableBackupTransport"
@ -32,8 +33,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
}
override fun name(): String {
// TODO: Make this class non-static in ConfigurableBackupTransportService and use Context and a ComponentName.
return this.javaClass.name
return TRANSPORT_ID
}
override fun getTransportFlags(): Int {

View file

@ -0,0 +1,95 @@
package com.stevesoltys.backup.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R
const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1
const val REQUEST_CODE_RECOVERY_CODE = 2
private val TAG = BackupActivity::class.java.name
/**
* An Activity that requires the recovery code and the backup location to be set up
* before starting.
*/
abstract class BackupActivity : AppCompatActivity() {
protected abstract fun getViewModel(): BackupViewModel
protected abstract fun getInitialFragment(): Fragment
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getViewModel().onLocationSet.observeEvent(this, LiveEventHandler { result ->
if (result.validLocation) {
if (result.initialSetup) showFragment(getInitialFragment())
else supportFragmentManager.popBackStack()
} else onInvalidLocation()
})
getViewModel().chooseBackupLocation.observeEvent(this, LiveEventHandler { show ->
if (show) showFragment(BackupLocationFragment(), true)
})
}
@CallSuper
override fun onStart() {
super.onStart()
if (isFinishing) return
// check that backup is provisioned
if (!getViewModel().recoveryCodeIsSet()) {
showRecoveryCodeActivity()
} else if (!getViewModel().validLocationIsSet()) {
showFragment(BackupLocationFragment())
// remove potential error notifications
(application as Backup).notificationManager.onBackupErrorSeen()
}
}
@CallSuper
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
if (resultCode != RESULT_OK) {
Log.w(TAG, "Error in activity result: $requestCode")
finishAfterTransition()
} else {
super.onActivityResult(requestCode, resultCode, result)
}
}
@CallSuper
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
item.itemId == android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun showRecoveryCodeActivity() {
val intent = Intent(this, RecoveryCodeActivity::class.java)
startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE)
}
protected open fun onInvalidLocation() {
Toast.makeText(this, getString(R.string.settings_backup_location_invalid), LENGTH_LONG).show()
}
protected fun showFragment(f: Fragment, addToBackStack: Boolean = false) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
.replace(R.id.fragment, f)
if (addToBackStack) fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
}
}

View file

@ -1,4 +1,4 @@
package com.stevesoltys.backup.settings
package com.stevesoltys.backup.ui
import android.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException
@ -12,8 +12,7 @@ import androidx.lifecycle.ViewModelProviders
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.stevesoltys.backup.R
private val TAG = BackupLocationFragment::class.java.name
import com.stevesoltys.backup.settings.SettingsViewModel
class BackupLocationFragment : PreferenceFragmentCompat() {

View file

@ -0,0 +1,76 @@
package com.stevesoltys.backup.ui
import android.app.Application
import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.isOnExternalStorage
import com.stevesoltys.backup.settings.getBackupFolderUri
import com.stevesoltys.backup.settings.setBackupFolderUri
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService
private val TAG = BackupViewModel::class.java.simpleName
abstract class BackupViewModel(protected val app: Application) : AndroidViewModel(app) {
private val locationWasSet = MutableLiveEvent<LocationResult>()
/**
* 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
private val mChooseBackupLocation = MutableLiveEvent<Boolean>()
internal val chooseBackupLocation: LiveEvent<Boolean> = mChooseBackupLocation
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
internal fun recoveryCodeIsSet() = Backup.keyManager.hasBackupKey()
internal fun validLocationIsSet(): Boolean {
val uri = getBackupFolderUri(app) ?: return false
if (uri.isOnExternalStorage()) return true // might be a temporary failure
val file = DocumentFile.fromTreeUri(app, uri) ?: return false
return file.isDirectory
}
internal fun handleChooseFolderResult(result: Intent?) {
val folderUri = result?.data ?: return
// persist permission to access backup folder across reboots
val takeFlags = result.flags and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
app.contentResolver.takePersistableUriPermission(folderUri, takeFlags)
// check if this is initial set-up or a later change
val initialSetUp = !validLocationIsSet()
if (acceptBackupLocation(folderUri)) {
// 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))
Log.d(TAG, "New storage location chosen: $folderUri")
} else {
Log.w(TAG, "Location was rejected: $folderUri")
// notify the UI that the location was invalid
locationWasSet.setEvent(LocationResult(false, initialSetUp))
}
}
protected open fun acceptBackupLocation(folderUri: Uri): Boolean {
return true
}
}
class LocationResult(val validLocation: Boolean, val initialSetup: Boolean)

View file

@ -1,9 +1,9 @@
package com.stevesoltys.backup
package com.stevesoltys.backup.ui
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.stevesoltys.backup.LiveEvent.ConsumableEvent
import com.stevesoltys.backup.ui.LiveEvent.ConsumableEvent
open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() {

View file

@ -1,4 +1,4 @@
package com.stevesoltys.backup;
package com.stevesoltys.backup.ui;
public interface LiveEventHandler<T> {
void onEvent(T t);

View file

@ -1,4 +1,4 @@
package com.stevesoltys.backup
package com.stevesoltys.backup.ui
class MutableLiveEvent<T> : LiveEvent<T>() {

View file

@ -1,10 +1,9 @@
package com.stevesoltys.backup.settings
package com.stevesoltys.backup.ui
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.LiveEventHandler
import com.stevesoltys.backup.R
class RecoveryCodeActivity : AppCompatActivity() {

View file

@ -1,4 +1,4 @@
package com.stevesoltys.backup.settings
package com.stevesoltys.backup.ui
import android.view.LayoutInflater
import android.view.View

View file

@ -1,4 +1,4 @@
package com.stevesoltys.backup.settings
package com.stevesoltys.backup.ui
import android.os.Build
import android.os.Bundle
@ -57,7 +57,7 @@ class RecoveryCodeInputFragment : Fragment() {
}
private fun allFilledOut(input: List<CharSequence>): Boolean {
for (i in 0 until input.size) {
for (i in input.indices) {
if (input[i].isNotEmpty()) continue
showError(i, getString(R.string.recovery_code_error_empty_word))
return false
@ -96,7 +96,7 @@ class RecoveryCodeInputFragment : Fragment() {
private fun debugPreFill() {
val words = viewModel.wordList
for (i in 0 until words.size) {
for (i in words.indices) {
getWordLayout(i).editText!!.setText(words[i])
}
}

View file

@ -1,4 +1,4 @@
package com.stevesoltys.backup.settings
package com.stevesoltys.backup.ui
import android.content.res.Configuration
import android.os.Bundle

View file

@ -1,10 +1,8 @@
package com.stevesoltys.backup.settings
package com.stevesoltys.backup.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.LiveEvent
import com.stevesoltys.backup.MutableLiveEvent
import io.github.novacrypto.bip39.*
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
import io.github.novacrypto.bip39.Validation.InvalidWordCountException

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorSecondary"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorSecondary"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16,1L8,1C6.34,1 5,2.34 5,4v16c0,1.66 1.34,3 3,3h8c1.66,0 3,-1.34 3,-3L19,4c0,-1.66 -1.34,-3 -3,-3zM14,21h-4v-1h4v1zM17.25,18L6.75,18L6.75,4h10.5v14z" />
</vector>

View file

@ -9,7 +9,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".settings.RecoveryCodeInputFragment">
tools:context=".ui.RecoveryCodeInputFragment">
<ImageView
android:id="@+id/introIcon"

View file

@ -0,0 +1,81 @@
<?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_choose_restore_set"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/backView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:listitem="@layout/list_item_restore_set" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/backView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView" />
<TextView
android:id="@+id/errorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/holo_red_dark"
android:textSize="18sp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/backView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="There was an error retrieving your backups." />
<TextView
android:id="@+id/backView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/restore_back"
android:textColor="?android:colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/listView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,45 @@
<?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="wrap_content"
android:background="?android:selectableItemBackground"
tools:showIn="@layout/fragment_restore_set">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_phone_android"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/titleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="Pixel 2 XL backup" />
<TextView
android:id="@+id/subtitleView"
android:layout_width="wrap_content"
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_constraintStart_toStartOf="@+id/titleView"
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="Yesterday, 8:25 AM" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -29,6 +29,7 @@
<string name="settings_backup_location_picker">Choose backup location</string>
<string name="settings_backup_location_title">Backup Location</string>
<string name="settings_backup_location_info">Choose where to store your backups. More options might get added in the future.</string>
<string name="settings_backup_location_invalid">The chosen location can not be used.</string>
<string name="settings_backup_external_storage">External Storage</string>
<string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string>
<string name="settings_auto_restore_title">Automatic restore</string>
@ -70,4 +71,11 @@
<string name="notification_error_text">A device backup failed to run.</string>
<string name="notification_error_action">Fix</string>
<!-- Restore -->
<string name="restore_title">Restore from Backup</string>
<string name="restore_choose_restore_set">Choose a backup to 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_empty_result">No backups found at given location.</string>
</resources>

View file

@ -4,4 +4,9 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
</style>
<style name="AppTheme.NoActionBar" parent="AppTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>