commit
d41ad38a78
48 changed files with 1651 additions and 178 deletions
22
.travis.yml
22
.travis.yml
|
@ -3,6 +3,28 @@ android:
|
||||||
components:
|
components:
|
||||||
- build-tools-28.0.3
|
- build-tools-28.0.3
|
||||||
- android-28
|
- android-28
|
||||||
|
|
||||||
|
licenses:
|
||||||
|
- android-sdk-license-.+
|
||||||
|
- '.+'
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- mkdir "$ANDROID_HOME/licenses" || true
|
||||||
|
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" >> "$ANDROID_HOME/licenses/android-sdk-license"
|
||||||
|
- echo -e "\nd56f5187479451eabf01fb78af6dfcb131a6481e" >> "$ANDROID_HOME/licenses/android-sdk-license"
|
||||||
|
- echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license"
|
||||||
|
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||||
|
|
||||||
|
before_cache:
|
||||||
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
|
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.gradle/caches/
|
||||||
|
- $HOME/.gradle/wrapper/
|
||||||
|
- $HOME/.android/build-cache
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: releases
|
provider: releases
|
||||||
api_key:
|
api_key:
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import groovy.xml.XmlUtil
|
import groovy.xml.XmlUtil
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
@ -78,5 +80,14 @@ dependencies {
|
||||||
'libcore.jar'
|
'libcore.jar'
|
||||||
], dir: 'libs')
|
], dir: 'libs')
|
||||||
|
|
||||||
implementation group: 'commons-io', name: 'commons-io', version: '2.6'
|
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.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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,21 +18,24 @@
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Backup"
|
android:name=".Backup"
|
||||||
android:supportsRtl="true"
|
android:allowBackup="false"
|
||||||
android:theme="@style/AppTheme"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:allowBackup="false"
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsActivity"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".settings.RecoveryCodeActivity"
|
||||||
|
android:label="@string/recovery_code_title" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.stevesoltys.backup.activity.MainActivity"
|
android:name="com.stevesoltys.backup.activity.MainActivity"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name"/>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.stevesoltys.backup.activity.backup.CreateBackupActivity"
|
android:name="com.stevesoltys.backup.activity.backup.CreateBackupActivity"
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package com.stevesoltys.backup;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Steve Soltys
|
|
||||||
*/
|
|
||||||
public class Backup extends Application {
|
|
||||||
|
|
||||||
public static final int JOB_ID_BACKGROUND_BACKUP = 1;
|
|
||||||
|
|
||||||
}
|
|
22
app/src/main/java/com/stevesoltys/backup/Backup.kt
Normal file
22
app/src/main/java/com/stevesoltys/backup/Backup.kt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package com.stevesoltys.backup
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.app.backup.IBackupManager
|
||||||
|
import android.content.Context.BACKUP_SERVICE
|
||||||
|
import android.os.ServiceManager.getService
|
||||||
|
|
||||||
|
const val JOB_ID_BACKGROUND_BACKUP = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Steve Soltys
|
||||||
|
* @author Torsten Grote
|
||||||
|
*/
|
||||||
|
class Backup : Application() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val backupManager: IBackupManager by lazy {
|
||||||
|
IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
app/src/main/java/com/stevesoltys/backup/LiveEvent.kt
Normal file
35
app/src/main/java/com/stevesoltys/backup/LiveEvent.kt
Normal file
|
@ -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<T> : LiveData<ConsumableEvent<T>>() {
|
||||||
|
|
||||||
|
fun observeEvent(owner: LifecycleOwner, handler: LiveEventHandler<in T>) {
|
||||||
|
val observer = LiveEventObserver(handler)
|
||||||
|
super.observe(owner, observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsumableEvent<T>(private val content: T) {
|
||||||
|
private var consumed = false
|
||||||
|
|
||||||
|
val contentIfNotConsumed: T?
|
||||||
|
get() {
|
||||||
|
if (consumed) return null
|
||||||
|
consumed = true
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LiveEventObserver<T>(private val handler: LiveEventHandler<in T>) : Observer<ConsumableEvent<T>> {
|
||||||
|
override fun onChanged(consumableEvent: ConsumableEvent<T>?) {
|
||||||
|
if (consumableEvent != null) {
|
||||||
|
val content = consumableEvent.contentIfNotConsumed
|
||||||
|
if (content != null) handler.onEvent(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.stevesoltys.backup;
|
||||||
|
|
||||||
|
public interface LiveEventHandler<T> {
|
||||||
|
void onEvent(T t);
|
||||||
|
}
|
13
app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt
Normal file
13
app/src/main/java/com/stevesoltys/backup/MutableLiveEvent.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package com.stevesoltys.backup
|
||||||
|
|
||||||
|
class MutableLiveEvent<T> : LiveEvent<T>() {
|
||||||
|
|
||||||
|
fun postEvent(value: T) {
|
||||||
|
super.postValue(ConsumableEvent(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEvent(value: T) {
|
||||||
|
super.setValue(ConsumableEvent(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.stevesoltys.backup
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.NotificationManager.IMPORTANCE_MIN
|
||||||
|
import android.app.backup.BackupManager
|
||||||
|
import android.app.backup.BackupProgress
|
||||||
|
import android.app.backup.IBackupObserver
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Log.INFO
|
||||||
|
import android.util.Log.isLoggable
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
||||||
|
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService.getBackupTransport
|
||||||
|
|
||||||
|
private const val CHANNEL_ID = "NotificationBackupObserver"
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
|
||||||
|
private val TAG = NotificationBackupObserver::class.java.name
|
||||||
|
|
||||||
|
class NotificationBackupObserver(
|
||||||
|
private val context: Context,
|
||||||
|
private val userInitiated: Boolean) : IBackupObserver.Stub() {
|
||||||
|
|
||||||
|
private val pm = context.packageManager
|
||||||
|
private val nm = context.getSystemService(NotificationManager::class.java).apply {
|
||||||
|
val title = context.getString(R.string.notification_channel_title)
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, title, IMPORTANCE_MIN).apply {
|
||||||
|
enableVibration(false)
|
||||||
|
}
|
||||||
|
createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
private val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
|
||||||
|
setSmallIcon(R.drawable.ic_cloud_upload)
|
||||||
|
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method could be called several times for packages with full data backup.
|
||||||
|
* It will tell how much of backup data is already saved and how much is expected.
|
||||||
|
*
|
||||||
|
* @param currentBackupPackage The name of the package that now being backed up.
|
||||||
|
* @param backupProgress Current progress of backup for the package.
|
||||||
|
*/
|
||||||
|
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
||||||
|
val transferred = backupProgress.bytesTransferred
|
||||||
|
val expected = backupProgress.bytesExpected
|
||||||
|
if (isLoggable(TAG, INFO)) {
|
||||||
|
Log.i(TAG, "Update. Target: $currentBackupPackage, $transferred/$expected")
|
||||||
|
}
|
||||||
|
val notification = notificationBuilder.apply {
|
||||||
|
setContentTitle(context.getString(R.string.notification_title))
|
||||||
|
setContentText(getAppName(currentBackupPackage))
|
||||||
|
setProgress(expected.toInt(), transferred.toInt(), false)
|
||||||
|
}.build()
|
||||||
|
nm.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup of one package or initialization of one transport has completed. This
|
||||||
|
* method will be called at most one time for each package or transport, and might not
|
||||||
|
* be not called if the operation fails before backupFinished(); for example, if the
|
||||||
|
* requested package/transport does not exist.
|
||||||
|
*
|
||||||
|
* @param target The name of the package that was backed up, or of the transport
|
||||||
|
* that was initialized
|
||||||
|
* @param status Zero on success; a nonzero error code if the backup operation failed.
|
||||||
|
*/
|
||||||
|
override fun onResult(target: String, status: Int) {
|
||||||
|
if (isLoggable(TAG, INFO)) {
|
||||||
|
Log.i(TAG, "Completed. Target: $target, status: $status")
|
||||||
|
}
|
||||||
|
val title = context.getString(
|
||||||
|
if (status == 0) R.string.notification_backup_result_complete
|
||||||
|
else R.string.notification_backup_result_error
|
||||||
|
)
|
||||||
|
val notification = notificationBuilder.apply {
|
||||||
|
setContentTitle(title)
|
||||||
|
setContentText(getAppName(target))
|
||||||
|
}.build()
|
||||||
|
nm.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The backup process has completed. This method will always be called,
|
||||||
|
* even if no individual package backup operations were attempted.
|
||||||
|
*
|
||||||
|
* @param status Zero on success; a nonzero error code if the backup operation
|
||||||
|
* as a whole failed.
|
||||||
|
*/
|
||||||
|
override fun backupFinished(status: Int) {
|
||||||
|
if (isLoggable(TAG, INFO)) {
|
||||||
|
Log.i(TAG, "Backup finished. Status: $status")
|
||||||
|
}
|
||||||
|
if (status == BackupManager.SUCCESS) getBackupTransport(context).backupFinished()
|
||||||
|
nm.cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAppName(packageId: String): CharSequence {
|
||||||
|
val appInfo = pm.getApplicationInfo(packageId, 0)
|
||||||
|
return pm.getApplicationLabel(appInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import com.stevesoltys.backup.R;
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
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 {
|
public class MainActivity extends Activity implements View.OnClickListener {
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,13 @@ import static android.content.Intent.CATEGORY_OPENABLE;
|
||||||
import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
|
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_READ_URI_PERMISSION;
|
||||||
import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
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.BackupKt.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_BACKUP_REQUEST_CODE;
|
||||||
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_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.SettingsManagerKt.getBackupFolderUri;
|
||||||
import static com.stevesoltys.backup.settings.SettingsManager.getBackupPassword;
|
import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword;
|
||||||
import static com.stevesoltys.backup.settings.SettingsManager.setBackupFolderUri;
|
import static com.stevesoltys.backup.settings.SettingsManagerKt.setBackupFolderUri;
|
||||||
import static com.stevesoltys.backup.settings.SettingsManager.setBackupsScheduled;
|
import static com.stevesoltys.backup.settings.SettingsManagerKt.setBackupsScheduled;
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static java.util.concurrent.TimeUnit.DAYS;
|
import static java.util.concurrent.TimeUnit.DAYS;
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,13 @@ import com.stevesoltys.backup.R;
|
||||||
import com.stevesoltys.backup.activity.PopupWindowUtil;
|
import com.stevesoltys.backup.activity.PopupWindowUtil;
|
||||||
import com.stevesoltys.backup.service.PackageService;
|
import com.stevesoltys.backup.service.PackageService;
|
||||||
import com.stevesoltys.backup.service.backup.BackupService;
|
import com.stevesoltys.backup.service.backup.BackupService;
|
||||||
import com.stevesoltys.backup.settings.SettingsManager;
|
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
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
|
* @author Steve Soltys
|
||||||
*/
|
*/
|
||||||
|
@ -69,7 +71,7 @@ class CreateBackupActivityController {
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) {
|
void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) {
|
||||||
String password = SettingsManager.getBackupPassword(parent);
|
String password = getBackupPassword(parent);
|
||||||
if (password == null) {
|
if (password == null) {
|
||||||
showEnterPasswordAlert(selectedPackages, parent);
|
showEnterPasswordAlert(selectedPackages, parent);
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,7 +116,7 @@ class CreateBackupActivityController {
|
||||||
String password = passwordTextView.getText().toString();
|
String password = passwordTextView.getText().toString();
|
||||||
|
|
||||||
if (originalPassword.equals(password)) {
|
if (originalPassword.equals(password)) {
|
||||||
SettingsManager.setBackupPassword(parent, password);
|
setBackupPassword(parent, password);
|
||||||
backupService.backupPackageData(selectedPackages, parent);
|
backupService.backupPackageData(selectedPackages, parent);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.stevesoltys.backup.security
|
||||||
|
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.security.keystore.KeyProperties.*
|
||||||
|
import android.security.keystore.KeyProtection
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.KeyStore.SecretKeyEntry
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
private const val KEY_SIZE = 256
|
||||||
|
private const val KEY_ALIAS = "com.stevesoltys.backup"
|
||||||
|
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||||
|
|
||||||
|
object KeyManager {
|
||||||
|
|
||||||
|
private val keyStore by lazy {
|
||||||
|
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
|
||||||
|
load(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun storeBackupKey(seed: ByteArray) {
|
||||||
|
if (seed.size < KEY_SIZE / 8) throw IllegalArgumentException()
|
||||||
|
// TODO check if using first 256 of 512 bytes produced by PBKDF2WithHmacSHA512 is safe!
|
||||||
|
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE / 8, "AES")
|
||||||
|
val ksEntry = SecretKeyEntry(secretKeySpec)
|
||||||
|
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasBackupKey() = keyStore.containsAlias(KEY_ALIAS) &&
|
||||||
|
keyStore.entryInstanceOf(KEY_ALIAS, SecretKeyEntry::class.java)
|
||||||
|
|
||||||
|
fun getBackupKey(): SecretKey {
|
||||||
|
val ksEntry = keyStore.getEntry(KEY_ALIAS, null) as SecretKeyEntry
|
||||||
|
return ksEntry.secretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKeyProtection(): KeyProtection {
|
||||||
|
val builder = KeyProtection.Builder(PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
|
||||||
|
.setBlockModes(BLOCK_MODE_GCM)
|
||||||
|
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
||||||
|
.setRandomizedEncryptionRequired(true)
|
||||||
|
if (SDK_INT >= 28) builder.setUnlockedDeviceRequired(true)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,61 +0,0 @@
|
||||||
package com.stevesoltys.backup.service.backup;
|
|
||||||
|
|
||||||
import android.app.backup.BackupManager;
|
|
||||||
import android.app.backup.IBackupManager;
|
|
||||||
import android.app.job.JobParameters;
|
|
||||||
import android.app.job.JobService;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.stevesoltys.backup.service.PackageService;
|
|
||||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService;
|
|
||||||
|
|
||||||
import static android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP;
|
|
||||||
import static android.os.ServiceManager.getService;
|
|
||||||
|
|
||||||
public class BackupJobService extends JobService {
|
|
||||||
|
|
||||||
private final static String TAG = BackupJobService.class.getName();
|
|
||||||
|
|
||||||
private final IBackupManager backupManager;
|
|
||||||
private final PackageService packageService = new PackageService();
|
|
||||||
|
|
||||||
public BackupJobService() {
|
|
||||||
backupManager = IBackupManager.Stub.asInterface(getService("backup"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onStartJob(JobParameters params) {
|
|
||||||
Log.i(TAG, "Triggering full backup");
|
|
||||||
startService(new Intent(this, ConfigurableBackupTransportService.class));
|
|
||||||
try {
|
|
||||||
String[] packages = packageService.getEligiblePackages();
|
|
||||||
// TODO use an observer to know when backups fail
|
|
||||||
int result = backupManager.requestBackup(packages, null, null, FLAG_NON_INCREMENTAL_BACKUP);
|
|
||||||
if (result == BackupManager.SUCCESS) {
|
|
||||||
Log.i(TAG, "Backup succeeded ");
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Backup failed: " + result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO show notification on backup error
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.e(TAG, "Error during backup: ", e);
|
|
||||||
} finally {
|
|
||||||
jobFinished(params, false);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onStopJob(JobParameters params) {
|
|
||||||
try {
|
|
||||||
backupManager.cancelBackups();
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.e(TAG, "Error cancelling backup: ", e);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.stevesoltys.backup.service.backup
|
||||||
|
|
||||||
|
import android.app.backup.BackupManager
|
||||||
|
import android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP
|
||||||
|
import android.app.backup.BackupTransport.FLAG_USER_INITIATED
|
||||||
|
import android.app.job.JobParameters
|
||||||
|
import android.app.job.JobService
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.BACKUP_SERVICE
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.stevesoltys.backup.Backup
|
||||||
|
import com.stevesoltys.backup.NotificationBackupObserver
|
||||||
|
import com.stevesoltys.backup.service.PackageService
|
||||||
|
import com.stevesoltys.backup.session.backup.BackupMonitor
|
||||||
|
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService
|
||||||
|
|
||||||
|
private val TAG = BackupJobService::class.java.name
|
||||||
|
|
||||||
|
// TODO might not be needed, if the OS really schedules backups on its own
|
||||||
|
class BackupJobService : JobService() {
|
||||||
|
|
||||||
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
|
Log.i(TAG, "Triggering full backup")
|
||||||
|
try {
|
||||||
|
requestFullBackup(this)
|
||||||
|
} finally {
|
||||||
|
jobFinished(params, false)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopJob(params: JobParameters): Boolean {
|
||||||
|
try {
|
||||||
|
Backup.backupManager.cancelBackups()
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
Log.e(TAG, "Error cancelling backup: ", e)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun requestFullBackup(context: Context) {
|
||||||
|
context.startService(Intent(context, ConfigurableBackupTransportService::class.java))
|
||||||
|
val observer = NotificationBackupObserver(context, true)
|
||||||
|
val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
|
||||||
|
val packages = PackageService().eligiblePackages
|
||||||
|
val result = try {
|
||||||
|
Backup.backupManager.requestBackup(packages, observer, BackupMonitor(), flags)
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
// TODO show notification on backup error
|
||||||
|
Log.e(TAG, "Error during backup: ", e)
|
||||||
|
}
|
||||||
|
if (result == BackupManager.SUCCESS) {
|
||||||
|
Log.i(TAG, "Backup succeeded ")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Backup failed: $result")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
package com.stevesoltys.backup.session.backup;
|
|
||||||
|
|
||||||
import android.app.backup.IBackupManagerMonitor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY;
|
|
||||||
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID;
|
|
||||||
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME;
|
|
||||||
|
|
||||||
class BackupMonitor extends IBackupManagerMonitor.Stub {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(Bundle bundle) {
|
|
||||||
Log.d("BackupMonitor", "ID: " + bundle.getInt(EXTRA_LOG_EVENT_ID));
|
|
||||||
Log.d("BackupMonitor", "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1));
|
|
||||||
Log.d("BackupMonitor", "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.stevesoltys.backup.session.backup
|
||||||
|
|
||||||
|
import android.app.backup.BackupManagerMonitor.*
|
||||||
|
import android.app.backup.IBackupManagerMonitor
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Log.DEBUG
|
||||||
|
|
||||||
|
private val TAG = BackupMonitor::class.java.name
|
||||||
|
|
||||||
|
class BackupMonitor : IBackupManagerMonitor.Stub() {
|
||||||
|
|
||||||
|
override fun onEvent(bundle: Bundle) {
|
||||||
|
if (!Log.isLoggable(TAG, DEBUG)) return
|
||||||
|
Log.d(TAG, "ID: " + bundle.getInt(EXTRA_LOG_EVENT_ID))
|
||||||
|
Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1))
|
||||||
|
Log.d(TAG, "PACKAGE: " + bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.stevesoltys.backup.settings
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.*
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import android.widget.Toast.LENGTH_LONG
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import com.stevesoltys.backup.R
|
||||||
|
|
||||||
|
private val TAG = BackupLocationFragment::class.java.name
|
||||||
|
|
||||||
|
class BackupLocationFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
private lateinit var viewModel: SettingsViewModel
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.backup_location, rootKey)
|
||||||
|
|
||||||
|
requireActivity().setTitle(R.string.settings_backup_location_title)
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
|
||||||
|
|
||||||
|
val externalStorage = Preference(requireContext()).apply {
|
||||||
|
setIcon(R.drawable.ic_storage)
|
||||||
|
setTitle(R.string.settings_backup_external_storage)
|
||||||
|
setOnPreferenceClickListener {
|
||||||
|
showChooseFolderActivity()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preferenceScreen.addPreference(externalStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
try {
|
||||||
|
val documentChooser = createChooser(openTreeIntent, null)
|
||||||
|
startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE)
|
||||||
|
} catch (ex: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(requireContext(), "Please install a file manager.", LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
|
||||||
|
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) {
|
||||||
|
viewModel.handleChooseFolderResult(result)
|
||||||
|
} else {
|
||||||
|
super.onActivityResult(requestCode, resultCode, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<CharSequence>) : Adapter<RecoveryCodeViewHolder>() {
|
||||||
|
|
||||||
|
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<TextView>(R.id.num)
|
||||||
|
private val word = v.findViewById<TextView>(R.id.word)
|
||||||
|
|
||||||
|
internal fun bind(number: Int, item: CharSequence) {
|
||||||
|
num.text = number.toString()
|
||||||
|
word.text = item
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<CharSequence> = ArrayList<String>(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<CharSequence>): 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<CharSequence>, 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.stevesoltys.backup.settings
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.ByteStringUtils.toHexString
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import com.stevesoltys.backup.LiveEvent
|
||||||
|
import com.stevesoltys.backup.MutableLiveEvent
|
||||||
|
import com.stevesoltys.backup.security.KeyManager
|
||||||
|
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<CharSequence> by lazy {
|
||||||
|
val items: ArrayList<CharSequence> = 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<Boolean>()
|
||||||
|
internal val confirmButtonClicked: LiveEvent<Boolean> = mConfirmButtonClicked
|
||||||
|
internal fun onConfirmButtonClicked() = mConfirmButtonClicked.setEvent(true)
|
||||||
|
|
||||||
|
private val mRecoveryCodeSaved = MutableLiveEvent<Boolean>()
|
||||||
|
internal val recoveryCodeSaved: LiveEvent<Boolean> = mRecoveryCodeSaved
|
||||||
|
|
||||||
|
@Throws(WordNotFoundException::class, InvalidChecksumException::class)
|
||||||
|
fun validateAndContinue(input: List<CharSequence>) {
|
||||||
|
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, "")
|
||||||
|
KeyManager.storeBackupKey(seed)
|
||||||
|
|
||||||
|
// TODO remove once encryption/decryption uses key from KeyStore
|
||||||
|
setBackupPassword(getApplication(), toHexString(seed))
|
||||||
|
|
||||||
|
mRecoveryCodeSaved.setEvent(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
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.LiveEventHandler
|
||||||
|
import com.stevesoltys.backup.R
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_settings)
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
|
||||||
|
viewModel.onLocationSet.observeEvent(this, LiveEventHandler { wasEmptyBefore ->
|
||||||
|
if (wasEmptyBefore) showFragment(SettingsFragment())
|
||||||
|
else supportFragmentManager.popBackStack()
|
||||||
|
})
|
||||||
|
viewModel.chooseBackupLocation.observeEvent(this, LiveEventHandler { show ->
|
||||||
|
if (show) showFragment(BackupLocationFragment(), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
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.locationIsSet()) {
|
||||||
|
showFragment(BackupLocationFragment())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.stevesoltys.backup.settings
|
||||||
|
|
||||||
|
import android.content.Context.BACKUP_SERVICE
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.preference.Preference.OnPreferenceChangeListener
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.TwoStatePreference
|
||||||
|
import com.stevesoltys.backup.Backup
|
||||||
|
import com.stevesoltys.backup.R
|
||||||
|
|
||||||
|
private val TAG = SettingsFragment::class.java.name
|
||||||
|
|
||||||
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
private val backupManager = Backup.backupManager
|
||||||
|
|
||||||
|
private lateinit var viewModel: SettingsViewModel
|
||||||
|
|
||||||
|
private lateinit var backup: TwoStatePreference
|
||||||
|
private lateinit var autoRestore: TwoStatePreference
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupLocation = findPreference("backup_location")
|
||||||
|
backupLocation.setOnPreferenceClickListener {
|
||||||
|
viewModel.chooseBackupLocation()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// we need to re-set the title when returning to this fragment
|
||||||
|
requireActivity().setTitle(R.string.app_name)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
inflater.inflate(R.menu.settings_menu, menu)
|
||||||
|
if (resources.getBoolean(R.bool.show_restore_in_settings)) {
|
||||||
|
menu.findItem(R.id.action_restore).isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
|
||||||
|
item.itemId == R.id.action_backup -> {
|
||||||
|
viewModel.backupNow()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
item.itemId == R.id.action_restore -> {
|
||||||
|
Toast.makeText(requireContext(), "Not yet implemented", Toast.LENGTH_SHORT).show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
@Deprecated("Replaced by KeyManager#storeBackupKey()")
|
||||||
|
fun setBackupPassword(context: Context, password: String) {
|
||||||
|
getDefaultSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString(PREF_KEY_BACKUP_PASSWORD, password)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Replaced by KeyManager#getBackupKey()")
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
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.LiveEvent
|
||||||
|
import com.stevesoltys.backup.MutableLiveEvent
|
||||||
|
import com.stevesoltys.backup.security.KeyManager
|
||||||
|
import com.stevesoltys.backup.service.backup.requestFullBackup
|
||||||
|
|
||||||
|
private val TAG = SettingsViewModel::class.java.name
|
||||||
|
|
||||||
|
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val app = application
|
||||||
|
|
||||||
|
private val locationWasSet = MutableLiveEvent<Boolean>()
|
||||||
|
/**
|
||||||
|
* Will be set to true if this is the initial location.
|
||||||
|
* It will be false if an existing location was changed.
|
||||||
|
*/
|
||||||
|
internal val onLocationSet: LiveEvent<Boolean> = locationWasSet
|
||||||
|
|
||||||
|
private val mChooseBackupLocation = MutableLiveEvent<Boolean>()
|
||||||
|
internal val chooseBackupLocation: LiveEvent<Boolean> = mChooseBackupLocation
|
||||||
|
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
|
||||||
|
|
||||||
|
fun recoveryCodeIsSet() = KeyManager.hasBackupKey()
|
||||||
|
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)
|
||||||
|
|
||||||
|
// check if this is initial set-up or a later change
|
||||||
|
val wasEmptyBefore = getBackupFolderUri(app) == null
|
||||||
|
|
||||||
|
// store backup folder location in settings
|
||||||
|
setBackupFolderUri(app, folderUri)
|
||||||
|
|
||||||
|
// notify the UI that the location has been set
|
||||||
|
locationWasSet.setEvent(wasEmptyBefore)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backupNow() = Thread { requestFullBackup(app) }.start()
|
||||||
|
|
||||||
|
}
|
|
@ -4,11 +4,13 @@ import android.app.backup.BackupTransport;
|
||||||
import android.app.backup.RestoreDescription;
|
import android.app.backup.RestoreDescription;
|
||||||
import android.app.backup.RestoreSet;
|
import android.app.backup.RestoreSet;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.stevesoltys.backup.settings.SettingsActivity;
|
||||||
import com.stevesoltys.backup.transport.component.BackupComponent;
|
import com.stevesoltys.backup.transport.component.BackupComponent;
|
||||||
import com.stevesoltys.backup.transport.component.RestoreComponent;
|
import com.stevesoltys.backup.transport.component.RestoreComponent;
|
||||||
import com.stevesoltys.backup.transport.component.provider.ContentProviderBackupComponent;
|
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 static final String TAG = TRANSPORT_DIRECTORY_NAME;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
private final BackupComponent backupComponent;
|
private final BackupComponent backupComponent;
|
||||||
|
|
||||||
private final RestoreComponent restoreComponent;
|
private final RestoreComponent restoreComponent;
|
||||||
|
|
||||||
ConfigurableBackupTransport(Context context) {
|
ConfigurableBackupTransport(Context context) {
|
||||||
|
this.context = context;
|
||||||
backupComponent = new ContentProviderBackupComponent(context);
|
backupComponent = new ContentProviderBackupComponent(context);
|
||||||
restoreComponent = new ContentProviderRestoreComponent(context);
|
restoreComponent = new ContentProviderRestoreComponent(context);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +62,11 @@ public class ConfigurableBackupTransport extends BackupTransport {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Intent dataManagementIntent() {
|
||||||
|
return new Intent(context, SettingsActivity.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup) {
|
public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -10,7 +10,6 @@ import android.util.Log;
|
||||||
|
|
||||||
import com.stevesoltys.backup.security.CipherUtil;
|
import com.stevesoltys.backup.security.CipherUtil;
|
||||||
import com.stevesoltys.backup.security.KeyGenerator;
|
import com.stevesoltys.backup.security.KeyGenerator;
|
||||||
import com.stevesoltys.backup.settings.SettingsManager;
|
|
||||||
import com.stevesoltys.backup.transport.component.BackupComponent;
|
import com.stevesoltys.backup.transport.component.BackupComponent;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
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.createDocument;
|
||||||
import static android.provider.DocumentsContract.getTreeDocumentId;
|
import static android.provider.DocumentsContract.getTreeDocumentId;
|
||||||
import static com.stevesoltys.backup.activity.MainActivityController.DOCUMENT_MIME_TYPE;
|
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_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_FULL_BACKUP_DIRECTORY;
|
||||||
import static com.stevesoltys.backup.transport.component.provider.ContentProviderBackupConstants.DEFAULT_INCREMENTAL_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().write(backupState.getSalt());
|
||||||
backupState.getOutputStream().closeEntry();
|
backupState.getOutputStream().closeEntry();
|
||||||
|
|
||||||
String password = requireNonNull(SettingsManager.getBackupPassword(context));
|
String password = requireNonNull(getBackupPassword(context));
|
||||||
backupState.setSecretKey(KeyGenerator.generate(password, backupState.getSalt()));
|
backupState.setSecretKey(KeyGenerator.generate(password, backupState.getSalt()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeOutputStream() throws IOException {
|
private void initializeOutputStream() throws IOException {
|
||||||
Uri folderUri = SettingsManager.getBackupFolderUri(context);
|
Uri folderUri = getBackupFolderUri(context);
|
||||||
// TODO notify about failure with notification
|
// TODO notify about failure with notification
|
||||||
Uri fileUri = createBackupFile(folderUri);
|
Uri fileUri = createBackupFile(folderUri);
|
||||||
|
|
||||||
|
|
10
app/src/main/res/drawable/ic_cloud_upload.xml
Normal file
10
app/src/main/res/drawable/ic_cloud_upload.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:attr/textColorSecondary"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_info_outline.xml
Normal file
10
app/src/main/res/drawable/ic_info_outline.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:attr/textColorSecondary"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_storage.xml
Normal file
10
app/src/main/res/drawable/ic_storage.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:attr/textColorSecondary"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M2,20h20v-4L2,16v4zM4,17h2v2L4,19v-2zM2,4v4h20L22,4L2,4zM6,7L4,7L4,5h2v2zM2,14h20v-4L2,10v4zM4,11h2v2L4,13v-2z" />
|
||||||
|
</vector>
|
5
app/src/main/res/layout/activity_recovery_code.xml
Normal file
5
app/src/main/res/layout/activity_recovery_code.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/fragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
5
app/src/main/res/layout/activity_settings.xml
Normal file
5
app/src/main/res/layout/activity_settings.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/fragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
75
app/src/main/res/layout/fragment_recovery_code_input.xml
Normal file
75
app/src/main/res/layout/fragment_recovery_code_input.xml
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:context=".settings.RecoveryCodeInputFragment">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/introIcon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:src="@drawable/ic_info_outline"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/introText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/recovery_code_confirm_intro"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/introIcon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="@color/divider"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/introText" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/wordList"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/doneButton"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/divider">
|
||||||
|
|
||||||
|
<include layout="@layout/recovery_code_input" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/doneButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/recovery_code_done_button"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
83
app/src/main/res/layout/fragment_recovery_code_output.xml
Normal file
83
app/src/main/res/layout/fragment_recovery_code_output.xml
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".settings.RecoveryCodeFragment">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/introIcon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:src="@drawable/ic_info_outline"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/introText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/recovery_code_12_word_intro"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/introIcon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/introText2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/recovery_code_write_it_down"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/introIcon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/introText" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="@color/divider"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/introText2" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/wordList"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/confirmCodeButton"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/divider"
|
||||||
|
app:spanCount="6"
|
||||||
|
tools:itemCount="12"
|
||||||
|
tools:listitem="@layout/list_item_recovery_code_output" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/confirmCodeButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/recovery_code_confirm_button"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
36
app/src/main/res/layout/list_item_recovery_code_output.xml
Normal file
36
app/src/main/res/layout/list_item_recovery_code_output.xml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
tools:showIn="@layout/fragment_recovery_code_output">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/num"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:gravity="center_vertical|end"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/word"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="1." />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/word"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:autoSizeTextType="uniform"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/num"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Test1" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
263
app/src/main/res/layout/recovery_code_input.xml
Normal file
263
app/src/main/res/layout/recovery_code_input.xml
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"
|
||||||
|
tools:showIn="@layout/fragment_recovery_code_input">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_1"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout2"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/wordLayout7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="spread_inside">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput1"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput2" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_2"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout3"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/wordLayout7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout1">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput3" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout3"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_3"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout4"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/wordLayout7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout2">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput3"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput4" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout4"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_4"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout5"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/wordLayout7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout3">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput4"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput5" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout5"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_5"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout6"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/wordLayout7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout4">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput5"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput6" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout6"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_6"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/wordLayout7"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout5">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput6"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput7" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout7"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_7"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout8"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/wordLayout1"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="spread_inside">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput7"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput8" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout8"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_8"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout9"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/wordLayout1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout7">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput8"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput9" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout9"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_9"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout10"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/wordLayout1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout8">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput9"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput10" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout10"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_10"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout11"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/wordLayout1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout9">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput10"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput11" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout11"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_11"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/wordLayout12"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/wordLayout1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout10">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput11"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionNext|flagNavigateNext|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:nextFocusForward="@+id/wordInput12" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/wordLayout12"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recovery_code_input_hint_12"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/wordLayout1"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/wordLayout11">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/wordInput12"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionDone|flagNoPersonalizedLearning"
|
||||||
|
android:inputType="textAutoComplete" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</merge>
|
18
app/src/main/res/menu/settings_menu.xml
Normal file
18
app/src/main/res/menu/settings_menu.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_backup"
|
||||||
|
android:title="@string/settings_backup_now"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_restore"
|
||||||
|
android:title="@string/restore_backup_button"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="never"
|
||||||
|
tools:visible="true" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -3,4 +3,6 @@
|
||||||
<color name="colorPrimary">#3F51B5</color>
|
<color name="colorPrimary">#3F51B5</color>
|
||||||
<color name="colorPrimaryDark">#303F9F</color>
|
<color name="colorPrimaryDark">#303F9F</color>
|
||||||
<color name="colorAccent">#FF4081</color>
|
<color name="colorAccent">#FF4081</color>
|
||||||
|
|
||||||
|
<color name="divider">#8A000000</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
4
app/src/main/res/values/config.xml
Normal file
4
app/src/main/res/values/config.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<bool name="show_restore_in_settings">true</bool>
|
||||||
|
</resources>
|
|
@ -23,4 +23,44 @@
|
||||||
<string name="loading_packages">Loading packages…</string>
|
<string name="loading_packages">Loading packages…</string>
|
||||||
<string name="initializing">Initializing…</string>
|
<string name="initializing">Initializing…</string>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="settings_backup">Backup my data</string>
|
||||||
|
<string name="settings_backup_location">Backup location</string>
|
||||||
|
<string name="settings_backup_location_title">Backup Location</string>
|
||||||
|
<string name="settings_backup_location_info">Choose where to store your backups. More options might get added in the future.</string>
|
||||||
|
<string name="settings_backup_external_storage">External Storage</string>
|
||||||
|
<string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string>
|
||||||
|
<string name="settings_auto_restore_title">Automatic restore</string>
|
||||||
|
<string name="settings_auto_restore_summary">When reinstalling an app, restore backed up settings and data</string>
|
||||||
|
<string name="settings_backup_now">Backup now</string>
|
||||||
|
|
||||||
|
<!-- Recovery Code -->
|
||||||
|
<string name="recovery_code_title">Recovery Code</string>
|
||||||
|
<string name="recovery_code_12_word_intro">You need your 12-word recovery code to restore backed up data.</string>
|
||||||
|
<string name="recovery_code_write_it_down">Write it down on paper now!</string>
|
||||||
|
<string name="recovery_code_confirm_button">Confirm Code</string>
|
||||||
|
<string name="recovery_code_confirm_intro">Enter your 12-word recovery code to ensure that it will work when you need it.</string>
|
||||||
|
<string name="recovery_code_done_button">Done</string>
|
||||||
|
<string name="recovery_code_input_hint_1">Word 1</string>
|
||||||
|
<string name="recovery_code_input_hint_2">Word 2</string>
|
||||||
|
<string name="recovery_code_input_hint_3">Word 3</string>
|
||||||
|
<string name="recovery_code_input_hint_4">Word 4</string>
|
||||||
|
<string name="recovery_code_input_hint_5">Word 5</string>
|
||||||
|
<string name="recovery_code_input_hint_6">Word 6</string>
|
||||||
|
<string name="recovery_code_input_hint_7">Word 7</string>
|
||||||
|
<string name="recovery_code_input_hint_8">Word 8</string>
|
||||||
|
<string name="recovery_code_input_hint_9">Word 9</string>
|
||||||
|
<string name="recovery_code_input_hint_10">Word 10</string>
|
||||||
|
<string name="recovery_code_input_hint_11">Word 11</string>
|
||||||
|
<string name="recovery_code_input_hint_12">Word 12</string>
|
||||||
|
<string name="recovery_code_error_empty_word">You forgot to enter this word.</string>
|
||||||
|
<string name="recovery_code_error_invalid_word">Wrong word. Did you mean %1$s or %2$s?</string>
|
||||||
|
<string name="recovery_code_error_checksum_word">We are so sorry! An unexpected error occurred.</string>
|
||||||
|
|
||||||
|
<!-- Notification -->
|
||||||
|
<string name="notification_channel_title">Backup Notification</string>
|
||||||
|
<string name="notification_title">Backup running</string>
|
||||||
|
<string name="notification_backup_result_complete">Backup complete</string>
|
||||||
|
<string name="notification_backup_result_error">Backup failed</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
</resources>
|
||||||
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
7
app/src/main/res/values/themes.xml
Normal file
7
app/src/main/res/values/themes.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
|
||||||
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
11
app/src/main/res/xml/backup_location.xml
Normal file
11
app/src/main/res/xml/backup_location.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:allowDividerAbove="true"
|
||||||
|
app:allowDividerBelow="false"
|
||||||
|
app:icon="@drawable/ic_info_outline"
|
||||||
|
app:selectable="false"
|
||||||
|
app:order="1337"
|
||||||
|
app:summary="@string/settings_backup_location_info" />
|
||||||
|
|
||||||
|
</androidx.preference.PreferenceScreen>
|
31
app/src/main/res/xml/settings.xml
Normal file
31
app/src/main/res/xml/settings.xml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
|
app:icon="@drawable/ic_cloud_upload"
|
||||||
|
app:key="backup"
|
||||||
|
app:persistent="false"
|
||||||
|
app:title="@string/settings_backup" />
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:dependency="backup"
|
||||||
|
app:icon="@drawable/ic_storage"
|
||||||
|
app:key="backup_location"
|
||||||
|
app:summary="@string/settings_backup_external_storage"
|
||||||
|
app:title="@string/settings_backup_location" />
|
||||||
|
|
||||||
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
|
app:dependency="backup"
|
||||||
|
app:key="auto_restore"
|
||||||
|
app:persistent="false"
|
||||||
|
app:summary="@string/settings_auto_restore_summary"
|
||||||
|
app:title="@string/settings_auto_restore_title" />
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:allowDividerAbove="true"
|
||||||
|
app:allowDividerBelow="false"
|
||||||
|
app:dependency="backup"
|
||||||
|
app:icon="@drawable/ic_info_outline"
|
||||||
|
app:selectable="false"
|
||||||
|
app:summary="@string/settings_info" />
|
||||||
|
|
||||||
|
</androidx.preference.PreferenceScreen>
|
|
@ -1,6 +1,9 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
|
||||||
|
ext.kotlin_version = '1.3.41'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
google()
|
google()
|
||||||
|
@ -8,6 +11,7 @@ buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
// newer versions require us to remove targetSdkVersion from Manifest
|
// newer versions require us to remove targetSdkVersion from Manifest
|
||||||
classpath 'com.android.tools.build:gradle:3.1.0'
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
@ -16,6 +20,7 @@ buildscript {
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
jcenter()
|
jcenter()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue