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" /> android:exported="true" />
<activity <activity
android:name=".settings.RecoveryCodeActivity" android:name=".ui.RecoveryCodeActivity"
android:label="@string/recovery_code_title" /> 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 <activity
android:name="com.stevesoltys.backup.activity.MainActivity" android:name="com.stevesoltys.backup.activity.MainActivity"
android:label="@string/app_name" /> 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 package com.stevesoltys.backup.settings
import android.content.Intent
import android.os.Bundle 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 androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.LiveEventHandler
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.ui.BackupActivity
import com.stevesoltys.backup.ui.BackupViewModel
private val TAG = SettingsActivity::class.java.name class SettingsActivity : BackupActivity() {
const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1
const val REQUEST_CODE_RECOVERY_CODE = 2
class SettingsActivity : AppCompatActivity() {
private lateinit var viewModel: SettingsViewModel private lateinit var viewModel: SettingsViewModel
override fun getViewModel(): BackupViewModel = viewModel
override fun getInitialFragment() = SettingsFragment()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_fragment_container)
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)
})
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) showFragment(SettingsFragment()) if (savedInstanceState == null) showFragment(getInitialFragment())
}
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()
} }
} }

View file

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

View file

@ -1,66 +1,10 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.app.Application 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.transport.requestBackup
import com.stevesoltys.backup.ui.BackupViewModel
private val TAG = SettingsViewModel::class.java.simpleName class SettingsViewModel(app: Application) : BackupViewModel(app) {
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")
}
fun backupNow() = Thread { requestBackup(app) }.start() fun backupNow() = Thread { requestBackup(app) }.start()

View file

@ -12,6 +12,7 @@ import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.backup.settings.SettingsActivity import com.stevesoltys.backup.settings.SettingsActivity
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
const val DEFAULT_RESTORE_SET_TOKEN: Long = 1 const val DEFAULT_RESTORE_SET_TOKEN: Long = 1
private const val TRANSPORT_DIRECTORY_NAME = "com.stevesoltys.backup.transport.ConfigurableBackupTransport" 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 { override fun name(): String {
// TODO: Make this class non-static in ConfigurableBackupTransportService and use Context and a ComponentName. return TRANSPORT_ID
return this.javaClass.name
} }
override fun getTransportFlags(): Int { 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.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -12,8 +12,7 @@ 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
private val TAG = BackupLocationFragment::class.java.name
class BackupLocationFragment : PreferenceFragmentCompat() { 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.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.stevesoltys.backup.LiveEvent.ConsumableEvent import com.stevesoltys.backup.ui.LiveEvent.ConsumableEvent
open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() { 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> { public interface LiveEventHandler<T> {
void onEvent(T 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>() { 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.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.LiveEventHandler
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
class RecoveryCodeActivity : AppCompatActivity() { 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.LayoutInflater
import android.view.View 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.Build
import android.os.Bundle import android.os.Bundle
@ -57,7 +57,7 @@ class RecoveryCodeInputFragment : Fragment() {
} }
private fun allFilledOut(input: List<CharSequence>): Boolean { private fun allFilledOut(input: List<CharSequence>): Boolean {
for (i in 0 until input.size) { for (i in input.indices) {
if (input[i].isNotEmpty()) continue if (input[i].isNotEmpty()) continue
showError(i, getString(R.string.recovery_code_error_empty_word)) showError(i, getString(R.string.recovery_code_error_empty_word))
return false return false
@ -96,7 +96,7 @@ class RecoveryCodeInputFragment : Fragment() {
private fun debugPreFill() { private fun debugPreFill() {
val words = viewModel.wordList val words = viewModel.wordList
for (i in 0 until words.size) { for (i in words.indices) {
getWordLayout(i).editText!!.setText(words[i]) 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.content.res.Configuration
import android.os.Bundle 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 android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.backup.Backup 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.*
import io.github.novacrypto.bip39.Validation.InvalidChecksumException import io.github.novacrypto.bip39.Validation.InvalidChecksumException
import io.github.novacrypto.bip39.Validation.InvalidWordCountException 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 <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:context=".settings.RecoveryCodeInputFragment"> tools:context=".ui.RecoveryCodeInputFragment">
<ImageView <ImageView
android:id="@+id/introIcon" 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_picker">Choose backup location</string>
<string name="settings_backup_location_title">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_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_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_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> <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_text">A device backup failed to run.</string>
<string name="notification_error_action">Fix</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> </resources>

View file

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