From 7fd3810fbfd154637c5fe557ed6dd678aa2f2b2d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 7 Jun 2019 14:09:55 -0300 Subject: [PATCH 01/11] Add AndroidX dependencies --- app/build.gradle | 4 ++++ app/src/main/Android.mk | 3 +++ 2 files changed, 7 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index fc215d3b..2e122845 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,4 +79,8 @@ dependencies { ], dir: 'libs') implementation group: 'commons-io', name: 'commons-io', version: '2.6' + + // androidx uses old versions, but we need to use what AOSP currently supports + implementation 'androidx.preference:preference:1.0.0-alpha1' + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-alpha1' } diff --git a/app/src/main/Android.mk b/app/src/main/Android.mk index 28df4ae6..d555262b 100644 --- a/app/src/main/Android.mk +++ b/app/src/main/Android.mk @@ -32,4 +32,7 @@ LOCAL_CERTIFICATE := platform LOCAL_STATIC_JAVA_LIBRARIES := commons-io LOCAL_SRC_FILES := $(call all-java-files-under, java) LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res +LOCAL_STATIC_ANDROID_LIBRARIES := \ + androidx.preference_preference \ + androidx.lifecycle_lifecycle-extensions include $(BUILD_PACKAGE) From 3981d3d8ccefff50795dd9ff839eba3a03caa1d5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 3 Jul 2019 12:44:12 +0200 Subject: [PATCH 02/11] Use latest stable AndroidX libraries --- app/build.gradle | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e122845..086dede2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,9 +78,8 @@ dependencies { 'libcore.jar' ], dir: 'libs') - implementation group: 'commons-io', name: 'commons-io', version: '2.6' + implementation 'commons-io:commons-io:2.6' - // androidx uses old versions, but we need to use what AOSP currently supports - implementation 'androidx.preference:preference:1.0.0-alpha1' - implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-alpha1' + implementation 'androidx.preference:preference:1.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' } From b983414295f52e05b6ec27a820931a41ae1cceac Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 18 Jun 2019 17:39:46 -0300 Subject: [PATCH 03/11] Add custom settings UI --- app/src/main/AndroidManifest.xml | 4 + .../backup/settings/SettingsActivity.java | 52 +++++++++++++ .../backup/settings/SettingsFragment.java | 77 +++++++++++++++++++ .../ConfigurableBackupTransport.java | 10 +++ app/src/main/res/drawable/ic_cloud_upload.xml | 10 +++ app/src/main/res/drawable/ic_info_outline.xml | 10 +++ app/src/main/res/drawable/ic_storage.xml | 10 +++ app/src/main/res/layout/activity_settings.xml | 6 ++ app/src/main/res/menu/settings_menu.xml | 18 +++++ app/src/main/res/values/config.xml | 4 + app/src/main/res/values/strings.xml | 9 +++ app/src/main/res/values/styles.xml | 8 +- app/src/main/res/values/themes.xml | 7 ++ app/src/main/res/xml/settings.xml | 32 ++++++++ 14 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java create mode 100644 app/src/main/res/drawable/ic_cloud_upload.xml create mode 100644 app/src/main/res/drawable/ic_info_outline.xml create mode 100644 app/src/main/res/drawable/ic_storage.xml create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/menu/settings_menu.xml create mode 100644 app/src/main/res/values/config.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/settings.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e5e1297..cce2844f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,10 @@ android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning"> + + diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java new file mode 100644 index 00000000..51db87c4 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java @@ -0,0 +1,52 @@ +package com.stevesoltys.backup.settings; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.stevesoltys.backup.R; + +import static android.widget.Toast.LENGTH_SHORT; +import static java.util.Objects.requireNonNull; + +public class SettingsActivity extends AppCompatActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_settings); + + requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.settings_menu, menu); + if (getResources().getBoolean(R.bool.show_restore_in_settings)) { + menu.findItem(R.id.action_restore).setVisible(true); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } else if (item.getItemId() == R.id.action_backup) { + Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show(); + return true; + } else if (item.getItemId() == R.id.action_restore) { + Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java new file mode 100644 index 00000000..aa530dc6 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java @@ -0,0 +1,77 @@ +package com.stevesoltys.backup.settings; + +import android.app.backup.IBackupManager; +import android.content.ContentResolver; +import android.os.Bundle; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.TwoStatePreference; + +import com.stevesoltys.backup.R; + +import static android.content.Context.BACKUP_SERVICE; +import static android.os.ServiceManager.getService; +import static android.provider.Settings.Secure.BACKUP_AUTO_RESTORE; + +public class SettingsFragment extends PreferenceFragmentCompat { + + private final static String TAG = SettingsFragment.class.getSimpleName(); + + private IBackupManager backupManager; + + private TwoStatePreference backup; + private TwoStatePreference autoRestore; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.settings, rootKey); + + backupManager = IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)); + + backup = (TwoStatePreference) findPreference("backup"); + backup.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (boolean) newValue; + try { + backupManager.setBackupEnabled(enabled); + return true; + } catch (RemoteException e) { + e.printStackTrace(); + backup.setChecked(!enabled); + return false; + } + }); + + autoRestore = (TwoStatePreference) findPreference("auto_restore"); + autoRestore.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (boolean) newValue; + try { + backupManager.setAutoRestore(enabled); + return true; + } catch (RemoteException e) { + Log.e(TAG, "Error communicating with BackupManager", e); + autoRestore.setChecked(!enabled); + return false; + } + }); + } + + @Override + public void onStart() { + super.onStart(); + + try { + backup.setChecked(backupManager.isBackupEnabled()); + backup.setEnabled(true); + } catch (RemoteException e) { + Log.e(TAG, "Error communicating with BackupManager", e); + backup.setEnabled(false); + } + + ContentResolver resolver = requireContext().getContentResolver(); + autoRestore.setChecked(Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1); + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.java b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.java index b709bb13..522a6a14 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/ConfigurableBackupTransport.java @@ -4,11 +4,13 @@ import android.app.backup.BackupTransport; import android.app.backup.RestoreDescription; import android.app.backup.RestoreSet; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageInfo; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.util.Log; +import com.stevesoltys.backup.settings.SettingsActivity; import com.stevesoltys.backup.transport.component.BackupComponent; import com.stevesoltys.backup.transport.component.RestoreComponent; import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent; @@ -27,11 +29,14 @@ public class ConfigurableBackupTransport extends BackupTransport { private static final String TAG = TRANSPORT_DIRECTORY_NAME; + private final Context context; + private final BackupComponent backupComponent; private final RestoreComponent restoreComponent; ConfigurableBackupTransport(Context context) { + this.context = context; backupComponent = new ContentProviderBackupComponent(context); restoreComponent = new ContentProviderRestoreComponent(context); } @@ -57,6 +62,11 @@ public class ConfigurableBackupTransport extends BackupTransport { return 0; } + @Override + public Intent dataManagementIntent() { + return new Intent(context, SettingsActivity.class); + } + @Override public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup) { return true; diff --git a/app/src/main/res/drawable/ic_cloud_upload.xml b/app/src/main/res/drawable/ic_cloud_upload.xml new file mode 100644 index 00000000..aa601bab --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_upload.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_outline.xml b/app/src/main/res/drawable/ic_info_outline.xml new file mode 100644 index 00000000..1a849802 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_storage.xml b/app/src/main/res/drawable/ic_storage.xml new file mode 100644 index 00000000..713f2caf --- /dev/null +++ b/app/src/main/res/drawable/ic_storage.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 00000000..13b68c73 --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/settings_menu.xml b/app/src/main/res/menu/settings_menu.xml new file mode 100644 index 00000000..7a055c11 --- /dev/null +++ b/app/src/main/res/menu/settings_menu.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/config.xml b/app/src/main/res/values/config.xml new file mode 100644 index 00000000..313d635a --- /dev/null +++ b/app/src/main/res/values/config.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c68da057..9ec3386e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,4 +23,13 @@ Loading packages… Initializing… + + Backup my data + Backup location + External Storage + All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code. + Automatic restore + When reinstalling an app, restore backed up settings and data + Backup now + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9785e0c9..0d2c4cc4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,8 +1,4 @@ + - - - - + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..cd538f6f --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml new file mode 100644 index 00000000..b8a0c952 --- /dev/null +++ b/app/src/main/res/xml/settings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + From 3d5911d41d50f2359b7e31f00ac8cd7a9485542a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 3 Jul 2019 19:44:37 +0200 Subject: [PATCH 04/11] Add a SettingsViewModel in Kotlin including the Kotlin deps --- app/build.gradle | 7 ++- .../backup/settings/SettingsActivity.java | 51 +++++++++++++++++++ .../backup/settings/SettingsViewModel.kt | 28 ++++++++++ build.gradle | 5 ++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index 086dede2..a2bac220 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,8 @@ import groovy.xml.XmlUtil apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' android { @@ -78,8 +80,11 @@ dependencies { 'libcore.jar' ], dir: 'libs') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'commons-io:commons-io:2.6' - implementation 'androidx.preference:preference:1.0.0' + implementation "androidx.core:core-ktx:1.0.2" + implementation 'androidx.preference:preference-ktx:1.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' } diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java index 51db87c4..67e27d93 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java @@ -1,6 +1,10 @@ package com.stevesoltys.backup.settings; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -8,23 +12,44 @@ import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; import com.stevesoltys.backup.R; +import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE; +import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; +import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; +import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION; +import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; +import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE; import static java.util.Objects.requireNonNull; public class SettingsActivity extends AppCompatActivity { + private final static String TAG = SettingsActivity.class.getName(); + + private SettingsViewModel viewModel; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); + viewModel = ViewModelProviders.of(this).get(SettingsViewModel.class); + requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); } + @Override + protected void onStart() { + super.onStart(); + if (!viewModel.locationIsSet()) { + showChooseFolderActivity(); + } + } + @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); @@ -49,4 +74,30 @@ public class SettingsActivity extends AppCompatActivity { } return super.onOptionsItemSelected(item); } + + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent result) { + if (resultCode != Activity.RESULT_OK) { + Log.w(TAG, "Error in activity result: " + requestCode); + return; + } + + if (requestCode == OPEN_DOCUMENT_TREE_REQUEST_CODE) { + viewModel.handleChooseFolderResult(result); + } + } + + private void showChooseFolderActivity() { + Intent openTreeIntent = new Intent(ACTION_OPEN_DOCUMENT_TREE); + openTreeIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION | + FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION); + try { + Intent documentChooser = Intent.createChooser(openTreeIntent, "Select the backup location"); + startActivityForResult(documentChooser, OPEN_DOCUMENT_TREE_REQUEST_CODE); + } catch (ActivityNotFoundException ex) { + Toast.makeText(this, "Please install a file manager.", LENGTH_LONG).show(); + } + } + } diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt new file mode 100644 index 00000000..2fbc062c --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt @@ -0,0 +1,28 @@ +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 androidx.lifecycle.AndroidViewModel +import com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri +import com.stevesoltys.backup.settings.SettingsManager.setBackupFolderUri + +class SettingsViewModel(application: Application) : AndroidViewModel(application) { + + private val app = application + + fun locationIsSet() = getBackupFolderUri(getApplication()) != null + + 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) + + // store backup folder location in settings + setBackupFolderUri(app, folderUri) + } + +} diff --git a/build.gradle b/build.gradle index 656c3bdd..4aa96118 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + + ext.kotlin_version = '1.3.40' + repositories { jcenter() google() @@ -8,6 +11,7 @@ buildscript { dependencies { // newer versions require us to remove targetSdkVersion from Manifest classpath 'com.android.tools.build:gradle:3.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -16,6 +20,7 @@ buildscript { allprojects { repositories { + mavenCentral() jcenter() google() } From c801502e8158d55d82dd73ccd83fe4f6c93e9e1e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 4 Jul 2019 08:25:37 +0200 Subject: [PATCH 05/11] Migrate SettingsManager to Kotlin --- .../backup/activity/MainActivity.java | 2 +- .../activity/MainActivityController.java | 8 +-- .../CreateBackupActivityController.java | 8 ++- .../backup/settings/SettingsManager.java | 57 ------------------- .../backup/settings/SettingsManager.kt | 48 ++++++++++++++++ .../backup/settings/SettingsViewModel.kt | 2 - .../ContentProviderBackupComponent.java | 7 ++- 7 files changed, 62 insertions(+), 70 deletions(-) delete mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.java create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.kt diff --git a/app/src/main/java/com/stevesoltys/backup/activity/MainActivity.java b/app/src/main/java/com/stevesoltys/backup/activity/MainActivity.java index 3d666957..e11e8776 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/MainActivity.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/MainActivity.java @@ -11,7 +11,7 @@ import com.stevesoltys.backup.R; import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static com.stevesoltys.backup.settings.SettingsManager.areBackupsScheduled; +import static com.stevesoltys.backup.settings.SettingsManagerKt.areBackupsScheduled; public class MainActivity extends Activity implements View.OnClickListener { diff --git a/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java index 5f8cea31..76af03e4 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/MainActivityController.java @@ -24,10 +24,10 @@ import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION; import static com.stevesoltys.backup.Backup.JOB_ID_BACKGROUND_BACKUP; import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_BACKUP_REQUEST_CODE; import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE; -import static com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri; -import static com.stevesoltys.backup.settings.SettingsManager.getBackupPassword; -import static com.stevesoltys.backup.settings.SettingsManager.setBackupFolderUri; -import static com.stevesoltys.backup.settings.SettingsManager.setBackupsScheduled; +import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupFolderUri; +import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword; +import static com.stevesoltys.backup.settings.SettingsManagerKt.setBackupFolderUri; +import static com.stevesoltys.backup.settings.SettingsManagerKt.setBackupsScheduled; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.DAYS; diff --git a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java index db43cd92..dc43bf28 100644 --- a/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java +++ b/app/src/main/java/com/stevesoltys/backup/activity/backup/CreateBackupActivityController.java @@ -17,11 +17,13 @@ import com.stevesoltys.backup.R; import com.stevesoltys.backup.activity.PopupWindowUtil; import com.stevesoltys.backup.service.PackageService; import com.stevesoltys.backup.service.backup.BackupService; -import com.stevesoltys.backup.settings.SettingsManager; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword; +import static com.stevesoltys.backup.settings.SettingsManagerKt.setBackupPassword; + /** * @author Steve Soltys */ @@ -69,7 +71,7 @@ class CreateBackupActivityController { } void onCreateBackupButtonClicked(Set selectedPackages, Activity parent) { - String password = SettingsManager.getBackupPassword(parent); + String password = getBackupPassword(parent); if (password == null) { showEnterPasswordAlert(selectedPackages, parent); } else { @@ -114,7 +116,7 @@ class CreateBackupActivityController { String password = passwordTextView.getText().toString(); if (originalPassword.equals(password)) { - SettingsManager.setBackupPassword(parent, password); + setBackupPassword(parent, password); backupService.backupPackageData(selectedPackages, parent); } else { diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.java b/app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.java deleted file mode 100644 index fa4b41ad..00000000 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.stevesoltys.backup.settings; - -import android.annotation.Nullable; -import android.content.Context; -import android.net.Uri; - -import static android.preference.PreferenceManager.getDefaultSharedPreferences; - -public class SettingsManager { - - private static final String PREF_KEY_BACKUP_URI = "backupUri"; - private static final String PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword"; - private static final String PREF_KEY_BACKUPS_SCHEDULED = "backupsScheduled"; - - public static void setBackupFolderUri(Context context, Uri uri) { - getDefaultSharedPreferences(context) - .edit() - .putString(PREF_KEY_BACKUP_URI, uri.toString()) - .apply(); - } - - @Nullable - public static Uri getBackupFolderUri(Context context) { - String uriStr = getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_URI, null); - if (uriStr == null) return null; - return Uri.parse(uriStr); - } - - /** - * This is insecure and not supposed to be part of a release, - * but rather an intermediate step towards a generated passphrase. - */ - public static void setBackupPassword(Context context, String password) { - getDefaultSharedPreferences(context) - .edit() - .putString(PREF_KEY_BACKUP_PASSWORD, password) - .apply(); - } - - @Nullable - public static String getBackupPassword(Context context) { - return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null); - } - - public static void setBackupsScheduled(Context context) { - getDefaultSharedPreferences(context) - .edit() - .putBoolean(PREF_KEY_BACKUPS_SCHEDULED, true) - .apply(); - } - - @Nullable - public static Boolean areBackupsScheduled(Context context) { - return getDefaultSharedPreferences(context).getBoolean(PREF_KEY_BACKUPS_SCHEDULED, false); - } - -} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.kt new file mode 100644 index 00000000..2aa413ec --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsManager.kt @@ -0,0 +1,48 @@ +package com.stevesoltys.backup.settings + +import android.content.Context +import android.net.Uri +import android.preference.PreferenceManager.getDefaultSharedPreferences + +private const val PREF_KEY_BACKUP_URI = "backupUri" +private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword" +private const val PREF_KEY_BACKUPS_SCHEDULED = "backupsScheduled" + +fun setBackupFolderUri(context: Context, uri: Uri) { + getDefaultSharedPreferences(context) + .edit() + .putString(PREF_KEY_BACKUP_URI, uri.toString()) + .apply() +} + +fun getBackupFolderUri(context: Context): Uri? { + val uriStr = getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_URI, null) + ?: return null + return Uri.parse(uriStr) +} + +/** + * This is insecure and not supposed to be part of a release, + * but rather an intermediate step towards a generated passphrase. + */ +fun setBackupPassword(context: Context, password: String) { + getDefaultSharedPreferences(context) + .edit() + .putString(PREF_KEY_BACKUP_PASSWORD, password) + .apply() +} + +fun getBackupPassword(context: Context): String? { + return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null) +} + +fun setBackupsScheduled(context: Context) { + getDefaultSharedPreferences(context) + .edit() + .putBoolean(PREF_KEY_BACKUPS_SCHEDULED, true) + .apply() +} + +fun areBackupsScheduled(context: Context): Boolean { + return getDefaultSharedPreferences(context).getBoolean(PREF_KEY_BACKUPS_SCHEDULED, false) +} 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 2fbc062c..df47f1c7 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt @@ -5,8 +5,6 @@ import android.content.Intent import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION import androidx.lifecycle.AndroidViewModel -import com.stevesoltys.backup.settings.SettingsManager.getBackupFolderUri -import com.stevesoltys.backup.settings.SettingsManager.setBackupFolderUri class SettingsViewModel(application: Application) : AndroidViewModel(application) { diff --git a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java index bab06921..a951d50a 100644 --- a/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java +++ b/app/src/main/java/com/stevesoltys/backup/transport/component/provider/ContentProviderBackupComponent.java @@ -10,7 +10,6 @@ import android.util.Log; import com.stevesoltys.backup.security.CipherUtil; import com.stevesoltys.backup.security.KeyGenerator; -import com.stevesoltys.backup.settings.SettingsManager; import com.stevesoltys.backup.transport.component.BackupComponent; import org.apache.commons.io.IOUtils; @@ -42,6 +41,8 @@ import static android.provider.DocumentsContract.buildDocumentUriUsingTree; import static android.provider.DocumentsContract.createDocument; import static android.provider.DocumentsContract.getTreeDocumentId; import static com.stevesoltys.backup.activity.MainActivityController.DOCUMENT_MIME_TYPE; +import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupFolderUri; +import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword; import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_BACKUP_QUOTA; import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_FULL_BACKUP_DIRECTORY; import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_INCREMENTAL_BACKUP_DIRECTORY; @@ -286,13 +287,13 @@ public class ContentProviderBackupComponent implements BackupComponent { backupState.getOutputStream().write(backupState.getSalt()); backupState.getOutputStream().closeEntry(); - String password = requireNonNull(SettingsManager.getBackupPassword(context)); + String password = requireNonNull(getBackupPassword(context)); backupState.setSecretKey(KeyGenerator.generate(password, backupState.getSalt())); } } private void initializeOutputStream() throws IOException { - Uri folderUri = SettingsManager.getBackupFolderUri(context); + Uri folderUri = getBackupFolderUri(context); // TODO notify about failure with notification Uri fileUri = createBackupFile(folderUri); From ee6cf383125f0c113c15bc08f172c9000e4e1df5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 4 Jul 2019 08:38:29 +0200 Subject: [PATCH 06/11] Migrate SettingsActivity and Fragment to Kotlin --- .../backup/settings/SettingsActivity.java | 103 ------------------ .../backup/settings/SettingsActivity.kt | 91 ++++++++++++++++ .../backup/settings/SettingsFragment.java | 77 ------------- .../backup/settings/SettingsFragment.kt | 77 +++++++++++++ 4 files changed, 168 insertions(+), 180 deletions(-) delete mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt delete mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java deleted file mode 100644 index 67e27d93..00000000 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.stevesoltys.backup.settings; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProviders; - -import com.stevesoltys.backup.R; - -import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE; -import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; -import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; -import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION; -import static android.widget.Toast.LENGTH_LONG; -import static android.widget.Toast.LENGTH_SHORT; -import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE; -import static java.util.Objects.requireNonNull; - -public class SettingsActivity extends AppCompatActivity { - - private final static String TAG = SettingsActivity.class.getName(); - - private SettingsViewModel viewModel; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_settings); - - viewModel = ViewModelProviders.of(this).get(SettingsViewModel.class); - - requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); - } - - @Override - protected void onStart() { - super.onStart(); - if (!viewModel.locationIsSet()) { - showChooseFolderActivity(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.settings_menu, menu); - if (getResources().getBoolean(R.bool.show_restore_in_settings)) { - menu.findItem(R.id.action_restore).setVisible(true); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } else if (item.getItemId() == R.id.action_backup) { - Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show(); - return true; - } else if (item.getItemId() == R.id.action_restore) { - Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show(); - return true; - } - return super.onOptionsItemSelected(item); - } - - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent result) { - if (resultCode != Activity.RESULT_OK) { - Log.w(TAG, "Error in activity result: " + requestCode); - return; - } - - if (requestCode == OPEN_DOCUMENT_TREE_REQUEST_CODE) { - viewModel.handleChooseFolderResult(result); - } - } - - private void showChooseFolderActivity() { - Intent openTreeIntent = new Intent(ACTION_OPEN_DOCUMENT_TREE); - openTreeIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION | - FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION); - try { - Intent documentChooser = Intent.createChooser(openTreeIntent, "Select the backup location"); - startActivityForResult(documentChooser, OPEN_DOCUMENT_TREE_REQUEST_CODE); - } catch (ActivityNotFoundException ex) { - Toast.makeText(this, "Please install a file manager.", LENGTH_LONG).show(); - } - } - -} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt new file mode 100644 index 00000000..9cc06890 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt @@ -0,0 +1,91 @@ +package com.stevesoltys.backup.settings + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.Intent.* +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import android.widget.Toast.LENGTH_LONG +import android.widget.Toast.LENGTH_SHORT +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProviders +import com.stevesoltys.backup.R +import com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE + +private val TAG = SettingsActivity::class.java.name + +class SettingsActivity : AppCompatActivity() { + + private lateinit var viewModel: SettingsViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_settings) + + viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + } + + override fun onStart() { + super.onStart() + if (!viewModel.locationIsSet()) { + showChooseFolderActivity() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.settings_menu, menu) + if (resources.getBoolean(R.bool.show_restore_in_settings)) { + menu.findItem(R.id.action_restore).isVisible = true + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when { + item.itemId == android.R.id.home -> { + onBackPressed() + true + } + item.itemId == R.id.action_backup -> { + Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show() + true + } + item.itemId == R.id.action_restore -> { + Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + if (resultCode != RESULT_OK) { + Log.w(TAG, "Error in activity result: $requestCode") + return + } + + if (requestCode == OPEN_DOCUMENT_TREE_REQUEST_CODE) { + viewModel.handleChooseFolderResult(result) + } + } + + private fun showChooseFolderActivity() { + val openTreeIntent = Intent(ACTION_OPEN_DOCUMENT_TREE) + openTreeIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) + // TODO StringRes + try { + val documentChooser = createChooser(openTreeIntent, "Select the backup location") + startActivityForResult(documentChooser, OPEN_DOCUMENT_TREE_REQUEST_CODE) + } catch (ex: ActivityNotFoundException) { + Toast.makeText(this, "Please install a file manager.", LENGTH_LONG).show() + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java deleted file mode 100644 index aa530dc6..00000000 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.stevesoltys.backup.settings; - -import android.app.backup.IBackupManager; -import android.content.ContentResolver; -import android.os.Bundle; -import android.os.RemoteException; -import android.provider.Settings; -import android.util.Log; - -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.TwoStatePreference; - -import com.stevesoltys.backup.R; - -import static android.content.Context.BACKUP_SERVICE; -import static android.os.ServiceManager.getService; -import static android.provider.Settings.Secure.BACKUP_AUTO_RESTORE; - -public class SettingsFragment extends PreferenceFragmentCompat { - - private final static String TAG = SettingsFragment.class.getSimpleName(); - - private IBackupManager backupManager; - - private TwoStatePreference backup; - private TwoStatePreference autoRestore; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.settings, rootKey); - - backupManager = IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)); - - backup = (TwoStatePreference) findPreference("backup"); - backup.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (boolean) newValue; - try { - backupManager.setBackupEnabled(enabled); - return true; - } catch (RemoteException e) { - e.printStackTrace(); - backup.setChecked(!enabled); - return false; - } - }); - - autoRestore = (TwoStatePreference) findPreference("auto_restore"); - autoRestore.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (boolean) newValue; - try { - backupManager.setAutoRestore(enabled); - return true; - } catch (RemoteException e) { - Log.e(TAG, "Error communicating with BackupManager", e); - autoRestore.setChecked(!enabled); - return false; - } - }); - } - - @Override - public void onStart() { - super.onStart(); - - try { - backup.setChecked(backupManager.isBackupEnabled()); - backup.setEnabled(true); - } catch (RemoteException e) { - Log.e(TAG, "Error communicating with BackupManager", e); - backup.setEnabled(false); - } - - ContentResolver resolver = requireContext().getContentResolver(); - autoRestore.setChecked(Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1); - } - -} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt new file mode 100644 index 00000000..31e8432f --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt @@ -0,0 +1,77 @@ +package com.stevesoltys.backup.settings + +import android.app.backup.IBackupManager +import android.content.ContentResolver +import android.os.Bundle +import android.os.RemoteException +import android.provider.Settings +import android.util.Log + +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.TwoStatePreference + +import com.stevesoltys.backup.R + +import android.content.Context.BACKUP_SERVICE +import android.os.ServiceManager.getService +import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE +import androidx.preference.Preference +import androidx.preference.Preference.OnPreferenceChangeListener + +private val TAG = SettingsFragment::class.java.name + +class SettingsFragment : PreferenceFragmentCompat() { + + private lateinit var backupManager: IBackupManager + + private lateinit var backup: TwoStatePreference + private lateinit var autoRestore: TwoStatePreference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.settings, rootKey) + + backupManager = IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) + + backup = findPreference("backup") as TwoStatePreference + backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> + val enabled = newValue as Boolean + try { + backupManager.isBackupEnabled = enabled + return@OnPreferenceChangeListener true + } catch (e: RemoteException) { + e.printStackTrace() + backup.isChecked = !enabled + return@OnPreferenceChangeListener false + } + } + + autoRestore = findPreference("auto_restore") as TwoStatePreference + autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> + val enabled = newValue as Boolean + try { + backupManager.setAutoRestore(enabled) + return@OnPreferenceChangeListener true + } catch (e: RemoteException) { + Log.e(TAG, "Error communicating with BackupManager", e) + autoRestore.isChecked = !enabled + return@OnPreferenceChangeListener false + } + } + } + + override fun onStart() { + super.onStart() + + try { + backup.isChecked = backupManager.isBackupEnabled + backup.isEnabled = true + } catch (e: RemoteException) { + Log.e(TAG, "Error communicating with BackupManager", e) + backup.isEnabled = false + } + + val resolver = requireContext().contentResolver + autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1 + } + +} From 66c0919eb5e902d157be8413e76152deb5f99a56 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 5 Jul 2019 12:35:45 +0200 Subject: [PATCH 07/11] Let user write down recovery code on first start --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 10 +- .../java/com/stevesoltys/backup/LiveEvent.kt | 35 +++ .../stevesoltys/backup/LiveEventHandler.java | 5 + .../stevesoltys/backup/MutableLiveEvent.kt | 13 + .../backup/settings/RecoveryCodeActivity.kt | 55 ++++ .../backup/settings/RecoveryCodeAdapter.kt | 37 +++ .../settings/RecoveryCodeInputFragment.kt | 104 +++++++ .../settings/RecoveryCodeOutputFragment.kt | 45 +++ .../backup/settings/RecoveryCodeViewModel.kt | 54 ++++ .../backup/settings/SettingsActivity.kt | 37 ++- .../backup/settings/SettingsViewModel.kt | 1 + .../res/layout/activity_recovery_code.xml | 5 + .../layout/fragment_recovery_code_input.xml | 75 +++++ .../layout/fragment_recovery_code_output.xml | 83 ++++++ .../layout/list_item_recovery_code_output.xml | 36 +++ .../main/res/layout/recovery_code_input.xml | 263 ++++++++++++++++++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 23 ++ build.gradle | 2 +- 20 files changed, 873 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/LiveEvent.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java create mode 100644 app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt create mode 100644 app/src/main/res/layout/activity_recovery_code.xml create mode 100644 app/src/main/res/layout/fragment_recovery_code_input.xml create mode 100644 app/src/main/res/layout/fragment_recovery_code_output.xml create mode 100644 app/src/main/res/layout/list_item_recovery_code_output.xml create mode 100644 app/src/main/res/layout/recovery_code_input.xml diff --git a/app/build.gradle b/app/build.gradle index a2bac220..0545a964 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,8 +83,11 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'commons-io:commons-io:2.6' + implementation 'io.github.novacrypto:BIP39:2019.01.27' - implementation "androidx.core:core-ktx:1.0.2" + implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.preference:preference-ktx:1.0.0' + implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cce2844f..0bfb69e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,17 +18,21 @@ + + diff --git a/app/src/main/java/com/stevesoltys/backup/LiveEvent.kt b/app/src/main/java/com/stevesoltys/backup/LiveEvent.kt new file mode 100644 index 00000000..83aede27 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/LiveEvent.kt @@ -0,0 +1,35 @@ +package com.stevesoltys.backup + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.stevesoltys.backup.LiveEvent.ConsumableEvent + +open class LiveEvent : LiveData>() { + + fun observeEvent(owner: LifecycleOwner, handler: LiveEventHandler) { + val observer = LiveEventObserver(handler) + super.observe(owner, observer) + } + + class ConsumableEvent(private val content: T) { + private var consumed = false + + val contentIfNotConsumed: T? + get() { + if (consumed) return null + consumed = true + return content + } + } + + internal class LiveEventObserver(private val handler: LiveEventHandler) : Observer> { + override fun onChanged(consumableEvent: ConsumableEvent?) { + if (consumableEvent != null) { + val content = consumableEvent.contentIfNotConsumed + if (content != null) handler.onEvent(content) + } + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java b/app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java new file mode 100644 index 00000000..22d86af0 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/LiveEventHandler.java @@ -0,0 +1,5 @@ +package com.stevesoltys.backup; + +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/MutableLiveEvent.kt new file mode 100644 index 00000000..7086bd40 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt @@ -0,0 +1,13 @@ +package com.stevesoltys.backup + +class MutableLiveEvent : LiveEvent() { + + fun postEvent(value: T) { + super.postValue(ConsumableEvent(value)) + } + + fun setEvent(value: T) { + super.setValue(ConsumableEvent(value)) + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt new file mode 100644 index 00000000..0e737f94 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeActivity.kt @@ -0,0 +1,55 @@ +package com.stevesoltys.backup.settings + +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() { + + private lateinit var viewModel: RecoveryCodeViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_recovery_code) + + viewModel = ViewModelProviders.of(this).get(RecoveryCodeViewModel::class.java) + viewModel.confirmButtonClicked.observeEvent(this, LiveEventHandler { clicked -> + if (clicked) { + val tag = "Confirm" + supportFragmentManager.beginTransaction() + .replace(R.id.fragment, RecoveryCodeInputFragment(), tag) + .addToBackStack(tag) + .commit() + } + }) + viewModel.recoveryCodeSaved.observeEvent(this, LiveEventHandler { saved -> + if (saved) { + setResult(RESULT_OK) + finishAfterTransition() + } + }) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .add(R.id.fragment, RecoveryCodeOutputFragment(), "Code") + .commit() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when { + item.itemId == android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt new file mode 100644 index 00000000..cc4e009c --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeAdapter.kt @@ -0,0 +1,37 @@ +package com.stevesoltys.backup.settings + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import com.stevesoltys.backup.R + +class RecoveryCodeAdapter(private val items: List) : Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecoveryCodeViewHolder { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_recovery_code_output, parent, false) as View + return RecoveryCodeViewHolder(v) + } + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: RecoveryCodeViewHolder, position: Int) { + holder.bind(position + 1, items[position]) + } + +} + +class RecoveryCodeViewHolder(v: View) : RecyclerView.ViewHolder(v) { + + private val num = v.findViewById(R.id.num) + private val word = v.findViewById(R.id.word) + + internal fun bind(number: Int, item: CharSequence) { + num.text = number.toString() + word.text = item + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt new file mode 100644 index 00000000..26918c49 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeInputFragment.kt @@ -0,0 +1,104 @@ +package com.stevesoltys.backup.settings + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import android.widget.Toast +import android.widget.Toast.LENGTH_LONG +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProviders +import com.stevesoltys.backup.R +import io.github.novacrypto.bip39.Validation.InvalidChecksumException +import io.github.novacrypto.bip39.Validation.WordNotFoundException +import kotlinx.android.synthetic.main.fragment_recovery_code_input.* +import kotlinx.android.synthetic.main.recovery_code_input.* + +class RecoveryCodeInputFragment : Fragment() { + + private lateinit var viewModel: RecoveryCodeViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_recovery_code_input, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProviders.of(requireActivity()).get(RecoveryCodeViewModel::class.java) + + for (i in 0 until WORD_NUM) { + val wordLayout = getWordLayout(i) + wordLayout.editText!!.onFocusChangeListener = OnFocusChangeListener { _, focus -> + if (!focus) wordLayout.isErrorEnabled = false + } + } + doneButton.setOnClickListener { done() } + + if (Build.TYPE == "userdebug") debugPreFill() + } + + private fun getInput(): List = ArrayList(WORD_NUM).apply { + for (i in 0 until WORD_NUM) add(getWordLayout(i).editText!!.text.toString()) + } + + private fun done() { + val input = getInput() + if (!allFilledOut(input)) return + try { + viewModel.validateAndContinue(input) + } catch (e: InvalidChecksumException) { + Toast.makeText(context, R.string.recovery_code_error_checksum_word, LENGTH_LONG).show() + } catch (e: WordNotFoundException) { + showWrongWordError(input, e) + } + } + + private fun allFilledOut(input: List): Boolean { + for (i in 0 until input.size) { + if (input[i].isNotEmpty()) continue + showError(i, getString(R.string.recovery_code_error_empty_word)) + return false + } + return true + } + + private fun showWrongWordError(input: List, e: WordNotFoundException) { + val i = input.indexOf(e.word) + if (i == -1) throw AssertionError() + showError(i, getString(R.string.recovery_code_error_invalid_word, e.suggestion1, e.suggestion2)) + } + + private fun showError(i: Int, errorMsg: CharSequence) { + getWordLayout(i).apply { + error = errorMsg + requestFocus() + } + } + + private fun getWordLayout(i: Int) = when (i + 1) { + 1 -> wordLayout1 + 2 -> wordLayout2 + 3 -> wordLayout3 + 4 -> wordLayout4 + 5 -> wordLayout5 + 6 -> wordLayout6 + 7 -> wordLayout7 + 8 -> wordLayout8 + 9 -> wordLayout9 + 10 -> wordLayout10 + 11 -> wordLayout11 + 12 -> wordLayout12 + else -> throw IllegalArgumentException() + } + + private fun debugPreFill() { + val words = viewModel.wordList + for (i in 0 until words.size) { + 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/settings/RecoveryCodeOutputFragment.kt new file mode 100644 index 00000000..724cb5a1 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeOutputFragment.kt @@ -0,0 +1,45 @@ +package com.stevesoltys.backup.settings + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.stevesoltys.backup.R +import kotlinx.android.synthetic.main.fragment_recovery_code_output.* + +class RecoveryCodeOutputFragment : Fragment() { + + private lateinit var viewModel: RecoveryCodeViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_recovery_code_output, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProviders.of(requireActivity()).get(RecoveryCodeViewModel::class.java) + + setGridParameters(wordList) + wordList.adapter = RecoveryCodeAdapter(viewModel.wordList) + + confirmCodeButton.setOnClickListener { viewModel.onConfirmButtonClicked() } + } + + private fun setGridParameters(list: RecyclerView) { + val layoutManager = list.layoutManager as GridLayoutManager + if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + layoutManager.orientation = RecyclerView.VERTICAL + layoutManager.spanCount = 4 + } else { + layoutManager.orientation = RecyclerView.HORIZONTAL + layoutManager.spanCount = 6 + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt new file mode 100644 index 00000000..24743d84 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt @@ -0,0 +1,54 @@ +package com.stevesoltys.backup.settings + +import android.app.Application +import android.util.ByteStringUtils +import androidx.lifecycle.AndroidViewModel +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 +import io.github.novacrypto.bip39.Validation.UnexpectedWhiteSpaceException +import io.github.novacrypto.bip39.Validation.WordNotFoundException +import io.github.novacrypto.bip39.wordlists.English +import java.security.SecureRandom +import java.util.* + +internal const val WORD_NUM = 12 + +class RecoveryCodeViewModel(application: Application) : AndroidViewModel(application) { + + internal val wordList: List by lazy { + val items: ArrayList = ArrayList(WORD_NUM) + // TODO factor out entropy generation + val entropy = ByteArray(Words.TWELVE.byteLength()) + SecureRandom().nextBytes(entropy) + MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { + if (it != " ") items.add(it) + } + items + } + + private val mConfirmButtonClicked = MutableLiveEvent() + internal val confirmButtonClicked: LiveEvent = mConfirmButtonClicked + internal fun onConfirmButtonClicked() = mConfirmButtonClicked.setEvent(true) + + internal val recoveryCodeSaved = MutableLiveEvent() + + @Throws(WordNotFoundException::class, InvalidChecksumException::class) + fun validateAndContinue(input: List) { + try { + MnemonicValidator.ofWordList(English.INSTANCE).validate(input) + } catch (e: UnexpectedWhiteSpaceException) { + throw AssertionError(e) + } catch (e: InvalidWordCountException) { + throw AssertionError(e) + } + val mnemonic = input.joinToString(" ") + val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "") + // TODO use KeyManager to store secret + setBackupPassword(getApplication(), ByteStringUtils.toHexString(seed)) + recoveryCodeSaved.setEvent(true) + } + +} 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 9cc06890..3ffb4f4f 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt @@ -13,10 +13,13 @@ import android.widget.Toast.LENGTH_SHORT import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProviders import com.stevesoltys.backup.R -import com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE 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() { private lateinit var viewModel: SettingsViewModel @@ -31,9 +34,25 @@ class SettingsActivity : AppCompatActivity() { supportActionBar!!.setDisplayHomeAsUpEnabled(true) } + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + if (resultCode != RESULT_OK) { + Log.w(TAG, "Error in activity result: $requestCode") + finishAfterTransition() + } + + if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) { + viewModel.handleChooseFolderResult(result) + } + } + override fun onStart() { super.onStart() - if (!viewModel.locationIsSet()) { + if (isFinishing) return + + // check that backup is provisioned + if (!viewModel.recoveryCodeIsSet()) { + showRecoveryCodeActivity() + } else if (!viewModel.locationIsSet()) { showChooseFolderActivity() } } @@ -64,15 +83,9 @@ class SettingsActivity : AppCompatActivity() { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { - if (resultCode != RESULT_OK) { - Log.w(TAG, "Error in activity result: $requestCode") - return - } - - if (requestCode == OPEN_DOCUMENT_TREE_REQUEST_CODE) { - viewModel.handleChooseFolderResult(result) - } + private fun showRecoveryCodeActivity() { + val intent = Intent(this, RecoveryCodeActivity::class.java) + startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE) } private fun showChooseFolderActivity() { @@ -82,7 +95,7 @@ class SettingsActivity : AppCompatActivity() { // TODO StringRes try { val documentChooser = createChooser(openTreeIntent, "Select the backup location") - startActivityForResult(documentChooser, OPEN_DOCUMENT_TREE_REQUEST_CODE) + startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE) } catch (ex: ActivityNotFoundException) { Toast.makeText(this, "Please install a file manager.", LENGTH_LONG).show() } 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 df47f1c7..a49608f9 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt @@ -10,6 +10,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application private val app = application + fun recoveryCodeIsSet() = getBackupPassword(getApplication()) != null fun locationIsSet() = getBackupFolderUri(getApplication()) != null fun handleChooseFolderResult(result: Intent?) { diff --git a/app/src/main/res/layout/activity_recovery_code.xml b/app/src/main/res/layout/activity_recovery_code.xml new file mode 100644 index 00000000..d64f58e8 --- /dev/null +++ b/app/src/main/res/layout/activity_recovery_code.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recovery_code_input.xml b/app/src/main/res/layout/fragment_recovery_code_input.xml new file mode 100644 index 00000000..1442389a --- /dev/null +++ b/app/src/main/res/layout/fragment_recovery_code_input.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + +