diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4665b8cb..fa57332b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,9 +32,20 @@
android:exported="true" />
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt
new file mode 100644
index 00000000..09c73839
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreActivity.kt
@@ -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?
+ }
+
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt
new file mode 100644
index 00000000..b3f74c2d
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetAdapter.kt
@@ -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) : Adapter() {
+
+ 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(R.id.titleView)
+ private val subtitleView = v.findViewById(R.id.subtitleView)
+
+ internal fun bind(item: RestoreSet) {
+ v.setOnClickListener { listener.onRestoreSetClicked(item) }
+ titleView.text = item.name
+ subtitleView.text = item.device
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt
new file mode 100644
index 00000000..4b7120b4
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreSetFragment.kt
@@ -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)
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt
new file mode 100644
index 00000000..cc66dae9
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/restore/RestoreViewModel.kt
@@ -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()
+ internal val restoreSets: LiveData 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?) {
+ 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,
+ internal val errorMsg: String?) {
+
+ internal constructor(sets: Array) : this(sets, null)
+
+ internal constructor(errorMsg: String) : this(emptyArray(), errorMsg)
+
+ internal fun hasError(): Boolean = errorMsg != null
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt
index 36f52c99..8aefd061 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt
@@ -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())
}
}
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt
index e36627d7..9415b38c 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt
@@ -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)
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt
index 6840dabd..fadfb8e2 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt
@@ -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()
- /**
- * 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 = locationWasSet
-
- private val mChooseBackupLocation = MutableLiveEvent()
- internal val chooseBackupLocation: LiveEvent = 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()
diff --git a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt
index bfc32f0b..bee1f86f 100644
--- a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt
+++ b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.kt
@@ -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 {
diff --git a/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt
new file mode 100644
index 00000000..3df956fd
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupActivity.kt
@@ -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()
+ }
+
+}
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt
similarity index 95%
rename from app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt
index eab22e01..d58fbff7 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupLocationFragment.kt
@@ -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() {
diff --git a/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt b/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt
new file mode 100644
index 00000000..7f622afc
--- /dev/null
+++ b/app/src/main/java/com/stevesoltys/backup/ui/BackupViewModel.kt
@@ -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()
+ /**
+ * 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 = locationWasSet
+
+ private val mChooseBackupLocation = MutableLiveEvent()
+ internal val chooseBackupLocation: LiveEvent = 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)
diff --git a/app/src/main/java/com/stevesoltys/backup/LiveEvent.kt b/app/src/main/java/com/stevesoltys/backup/ui/LiveEvent.kt
similarity index 91%
rename from app/src/main/java/com/stevesoltys/backup/LiveEvent.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/LiveEvent.kt
index 83aede27..b01d2840 100644
--- a/app/src/main/java/com/stevesoltys/backup/LiveEvent.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/LiveEvent.kt
@@ -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 : LiveData>() {
diff --git a/app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java b/app/src/main/java/com/stevesoltys/backup/ui/LiveEventHandler.java
similarity index 65%
rename from app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java
rename to app/src/main/java/com/stevesoltys/backup/ui/LiveEventHandler.java
index 22d86af0..5070ddf4 100644
--- a/app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java
+++ b/app/src/main/java/com/stevesoltys/backup/ui/LiveEventHandler.java
@@ -1,4 +1,4 @@
-package com.stevesoltys.backup;
+package com.stevesoltys.backup.ui;
public interface LiveEventHandler {
void onEvent(T t);
diff --git a/app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt b/app/src/main/java/com/stevesoltys/backup/ui/MutableLiveEvent.kt
similarity index 86%
rename from app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/MutableLiveEvent.kt
index 7086bd40..ff279f1a 100644
--- a/app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/MutableLiveEvent.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.backup
+package com.stevesoltys.backup.ui
class MutableLiveEvent : LiveEvent() {
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeActivity.kt
similarity index 95%
rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeActivity.kt
index 0e737f94..69e1673a 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeActivity.kt
@@ -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() {
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeAdapter.kt
similarity index 96%
rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeAdapter.kt
index cc4e009c..28f7ecdd 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeAdapter.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.backup.settings
+package com.stevesoltys.backup.ui
import android.view.LayoutInflater
import android.view.View
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt
similarity index 96%
rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt
index 26918c49..d631ac1b 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeInputFragment.kt
@@ -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): 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])
}
}
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeOutputFragment.kt
similarity index 97%
rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeOutputFragment.kt
index 724cb5a1..03297b53 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeOutputFragment.kt
@@ -1,4 +1,4 @@
-package com.stevesoltys.backup.settings
+package com.stevesoltys.backup.ui
import android.content.res.Configuration
import android.os.Bundle
diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeViewModel.kt
similarity index 94%
rename from app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt
rename to app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeViewModel.kt
index 0083e9a0..a4653e0a 100644
--- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/backup/ui/RecoveryCodeViewModel.kt
@@ -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
diff --git a/app/src/main/res/drawable/ic_cloud_download.xml b/app/src/main/res/drawable/ic_cloud_download.xml
new file mode 100644
index 00000000..6a3f5f94
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cloud_download.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_phone_android.xml b/app/src/main/res/drawable/ic_phone_android.xml
new file mode 100644
index 00000000..c3cf49d9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_phone_android.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_fragment_container.xml
similarity index 100%
rename from app/src/main/res/layout/activity_settings.xml
rename to app/src/main/res/layout/activity_fragment_container.xml
diff --git a/app/src/main/res/layout/fragment_recovery_code_input.xml b/app/src/main/res/layout/fragment_recovery_code_input.xml
index 1442389a..b478d509 100644
--- a/app/src/main/res/layout/fragment_recovery_code_input.xml
+++ b/app/src/main/res/layout/fragment_recovery_code_input.xml
@@ -9,7 +9,7 @@
+ tools:context=".ui.RecoveryCodeInputFragment">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_item_restore_set.xml b/app/src/main/res/layout/list_item_restore_set.xml
new file mode 100644
index 00000000..a05234a9
--- /dev/null
+++ b/app/src/main/res/layout/list_item_restore_set.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index dce1cdd0..2a5a3668 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
Choose backup location
Backup Location
Choose where to store your backups. More options might get added in the future.
+ The chosen location can not be used.
External Storage
All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.
Automatic restore
@@ -70,4 +71,11 @@
A device backup failed to run.
Fix
+
+ Restore from Backup
+ Choose a backup to restore
+ Don\'t restore
+ An error occurred loading the backups.
+ No backups found at given location.
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index cd538f6f..96232824 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -4,4 +4,9 @@
- @style/PreferenceThemeOverlay
+
+