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 + +