diff --git a/.github/ISSUE_TEMPLATE/thumbnail-request.md b/.github/ISSUE_TEMPLATE/thumbnail-request.md index 1dd56474..cae56f17 100644 --- a/.github/ISSUE_TEMPLATE/thumbnail-request.md +++ b/.github/ISSUE_TEMPLATE/thumbnail-request.md @@ -7,7 +7,7 @@ assignees: flocke, RichyHBM, ziegenberg --- -Please fill in **all the information below** to help speed up the process! + **Name**: **Website**: diff --git a/CHANGELOG.md b/CHANGELOG.md index bc570ef0..3a1b7126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * Bug fix: Crash when clicking on an entry multiple times (Issue #631, PR #634 by @jsoberg) * Bug fix: Crash during settings changes (Issue #639, PR #640 by @jsoberg) * Bug fix: Don't use regionalized language codes (Issue #567) + * Bug fix: Report backup failure correctly (Issue #671) * Internal: Update Gradle, build tools and dependencies * Internal: Min API Level set to 22 (Lollipop 5.1) * Internal: Refactoring and reducing build warnings diff --git a/README.md b/README.md index c184e65e..ccec411a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # andOTP - Android OTP Authenticator [![Build Status](https://travis-ci.org/andOTP/andOTP.svg?branch=master)](https://travis-ci.org/andOTP/andOTP) -[![Current release](https://img.shields.io/github/release/andOTP/andOTP/all.svg)](https://github.com/andOTP/andOTP/releases/download/v0.8.0-beta2/andOTP_v0.8.0-beta2.apk) +[![Current release](https://img.shields.io/github/release/andOTP/andOTP/all.svg)](https://github.com/andOTP/andOTP/releases/download/v0.8.0/andOTP_v0.8.0.apk) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/andotp/localized.svg)](https://crowdin.com/project/andotp) [![Chat - Telegram](https://img.shields.io/badge/chat-Telegram-blue.svg)](https://t.me/andOTP) [![Chat - Matrix](https://img.shields.io/badge/chat-Matrix-blue.svg)](https://matrix.to/#/#andOTP:privacytools.io) diff --git a/app/build.gradle b/app/build.gradle index 3284f7c6..57f121a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.shadowice.flocke.andotp" minSdkVersion 22 targetSdkVersion 29 - versionCode 34 - versionName "0.8.0-beta2" + versionCode 35 + versionName "0.8.0" vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -57,12 +57,12 @@ android { dependencies { def lifecycle_version = "2.2.0" - implementation 'androidx.media:media:1.2.0' + implementation 'androidx.media:media:1.2.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.lifecycle:lifecycle-process:${lifecycle_version}" implementation "androidx.lifecycle:lifecycle-common-java8:${lifecycle_version}" implementation 'com.google.android.material:material:1.2.1' @@ -71,13 +71,14 @@ dependencies { implementation "commons-codec:commons-codec:1.15" implementation "com.github.aakira:expandable-layout:1.6.0" implementation "com.heinrichreimersoftware:material-intro:2.0.0" - implementation "com.journeyapps:zxing-android-embedded:3.6.0" + implementation("com.journeyapps:zxing-android-embedded:4.1.0"){ transitive = false } + implementation "com.google.zxing:core:3.3.0" // Keep pinned to 3.3.0 to support SDK versions below 24 implementation "com.vanniktech:vntnumberpickerpreference:1.0.0" implementation "me.zhanghai.android.materialprogressbar:library:1.6.1" implementation "org.sufficientlysecure:openpgp-api:12.0" implementation "com.leinardi.android:speed-dial:3.1.1" - implementation "com.mikepenz:aboutlibraries:6.2.3" + implementation "com.mikepenz:aboutlibraries:6.2.3" // Supporting a newer version will need some work androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bbc7ed07..96a1996a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ android:installLocation="auto" package="org.shadowice.flocke.andotp"> + + { + if (settings.getRelockOnScreenOff()) { + cancelBackgroundTask(); + } + }); + + observer = new ProcessLifecycleObserver(); + ProcessLifecycleOwner.get().getLifecycle() + .addObserver(observer); + + getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + + private void initToolbar() { Toolbar toolbar = findViewById(R.id.container_toolbar); toolbar.setNavigationIcon(null); setSupportActionBar(toolbar); + } + private void initPasswordViews() { ViewStub stub = findViewById(R.id.container_stub); stub.setLayoutResource(R.layout.content_authenticate); View v = stub.inflate(); - Intent callingIntent = getIntent(); - int labelMsg = callingIntent.getIntExtra(Constants.EXTRA_AUTH_MESSAGE, R.string.auth_msg_authenticate); - newEncryption = callingIntent.getStringExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION); + initPasswordLabelView(v); + initPasswordLayoutView(v); + initPasswordInputView(v); + initUnlockViews(v); + } + private void initPasswordLabelView(View v) { + int labelMsg = getIntent().getIntExtra(Constants.EXTRA_AUTH_MESSAGE, R.string.auth_msg_authenticate); TextView passwordLabel = v.findViewById(R.id.passwordLabel); - TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout); - passwordInput = v.findViewById(R.id.passwordEdit); - - if (settings.getBlockAccessibility()) - passwordLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && settings.getBlockAutofill()) - passwordLayout.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS); - passwordLabel.setText(labelMsg); + } - authMethod = settings.getAuthMethod(); - password = settings.getAuthCredentials(); - - if (password.isEmpty()) { - password = settings.getOldCredentials(authMethod); - oldPassword = true; + private void initPasswordLayoutView(View v) { + passwordLayout = v.findViewById(R.id.passwordLayout); + int hintResId = (authMethod == AuthMethod.PASSWORD) ? R.string.auth_hint_password : R.string.auth_hint_pin; + passwordLayout.setHint(getString(hintResId)); + if (settings.getBlockAccessibility()) { + passwordLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } - - if (authMethod == AuthMethod.PASSWORD) { - if (password.isEmpty()) { - Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show(); - finishWithResult(true, null); - } else { - passwordLayout.setHint(getString(R.string.auth_hint_password)); - passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } - } else if (authMethod == AuthMethod.PIN) { - if (password.isEmpty()) { - Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show(); - finishWithResult(true, null); - } else { - passwordLayout.setHint(getString(R.string.auth_hint_pin)); - passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - } - } else { - finishWithResult(true, null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && settings.getBlockAutofill()) { + passwordLayout.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS); } + } + private void initPasswordInputView(View v) { + passwordInput = v.findViewById(R.id.passwordEdit); + int inputType = (authMethod == AuthMethod.PASSWORD) + ? (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) + : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + passwordInput.setInputType(inputType); passwordInput.setTransformationMethod(new PasswordTransformationMethod()); passwordInput.setOnEditorActionListener(this); + } - Button unlockButton = v.findViewById(R.id.buttonUnlock); + private void initUnlockViews(View v) { + unlockButton = v.findViewById(R.id.buttonUnlock); unlockButton.setOnClickListener(this); + unlockProgress = v.findViewById(R.id.unlockProgress); + } - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + private void cancelBackgroundTask() { + TaskFragment taskFragment = findTaskFragment(); + if (taskFragment != null) { + taskFragment.task.cancel(); + } + setupUiForTaskState(false); + } + + private class ProcessLifecycleObserver implements DefaultLifecycleObserver { + @Override + public void onStop(LifecycleOwner owner) { + if (settings.getRelockOnBackground()) { + cancelBackgroundTask(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + checkBackgroundTask(); + } + + private void checkBackgroundTask() { + TaskFragment taskFragment = findTaskFragment(); + if (taskFragment != null) { + if (taskFragment.task.isCanceled()) { + // The task was canceled, so remove the task fragment and reset password input. + getFragmentManager().beginTransaction() + .remove(taskFragment) + .commit(); + resetPasswordInput(); + } else { + taskFragment.task.setCallback(this::handleResult); + setupUiForTaskState(true); + } + } + } + + private void resetPasswordInput() { + passwordInput.setText(""); + passwordInput.requestFocus(); + InputMethodManager keyboard = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + keyboard.showSoftInput(passwordInput, 0); + } + + @Nullable + private TaskFragment findTaskFragment() { + return (TaskFragment) getFragmentManager().findFragmentByTag(TAG_TASK_FRAGMENT); + } + + private void setupUiForTaskState(boolean isTaskRunning) { + passwordLayout.setEnabled(!isTaskRunning); + passwordInput.setEnabled(!isTaskRunning); + unlockButton.setEnabled(!isTaskRunning); + unlockButton.setVisibility(isTaskRunning? View.INVISIBLE : View.VISIBLE); + unlockProgress.setVisibility(isTaskRunning ? View.VISIBLE : View.GONE); } @Override public void onClick(View view) { - checkPassword(passwordInput.getText().toString()); + Editable text = passwordInput.getText(); + startAuthTask(text != null ? text.toString() : ""); } @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (EditorActionHelper.isActionDoneOrKeyboardEnter(actionId, event)) { - checkPassword(v.getText().toString()); + startAuthTask(v.getText().toString()); return true; } - return false; } - public void checkPassword(String plainPassword) { - if (! oldPassword) { - try { - EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), settings.getIterations()); - byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE); + private void startAuthTask(String plainPassword) { + TaskFragment taskFragment = findTaskFragment(); + // Don't start a task if we already have an active task running. + if (taskFragment == null || taskFragment.task.isCanceled()) { + AuthenticationTask task = new AuthenticationTask(this, isAuthUpgrade, existingAuthCredentials, plainPassword); + task.setCallback(this::handleResult); - if (Arrays.equals(passwordArray, credentials.password)) { - finishWithResult(true, credentials.key); - } else { - finishWithResult(false, null); - } - } catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) { - e.printStackTrace(); - finishWithResult(false, null); - } - } else { - String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); - - if (hashedPassword.equals(password)) { - byte[] key = settings.setAuthCredentials(plainPassword); - - if (key == null) - Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); - - if (authMethod == AuthMethod.PASSWORD) - settings.removeAuthPasswordHash(); - else if (authMethod == AuthMethod.PIN) - settings.removeAuthPINHash(); - - finishWithResult(true, key); - } else { - finishWithResult(false, null); + if (taskFragment == null) { + taskFragment = new TaskFragment(); + getFragmentManager() + .beginTransaction() + .add(taskFragment, TAG_TASK_FRAGMENT) + .commit(); } + taskFragment.startTask(task); + setupUiForTaskState(true); } } - // End with a result - public void finishWithResult(boolean success, byte[] key) { + private void handleResult(Result result) { + if (result.authUpgradeFailed) { + Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); + } + finishWithResult(result.encryptionKey != null, result.encryptionKey); + } + + private void finishWithResult(boolean success, byte[] encryptionKey) { Intent data = new Intent(); - - if (newEncryption != null && ! newEncryption.isEmpty()) + if (newEncryption != null && !newEncryption.isEmpty()) data.putExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION, newEncryption); - - if (key != null) - data.putExtra(Constants.EXTRA_AUTH_PASSWORD_KEY, key); - + if (encryptionKey != null) + data.putExtra(Constants.EXTRA_AUTH_PASSWORD_KEY, encryptionKey); if (success) setResult(RESULT_OK, data); - finish(); } - // Go back to the main activity @Override public void onBackPressed() { finishWithResult(false, null); super.onBackPressed(); } + + @Override + protected void onPause() { + super.onPause(); + // We don't want the task to callback to a dead activity and cause a memory leak, so null it here. + TaskFragment taskFragment = findTaskFragment(); + if (taskFragment != null) { + taskFragment.task.setCallback(null); + } + } + + @Override + protected void onDestroy() { + ProcessLifecycleOwner.get().getLifecycle() + .removeObserver(observer); + super.onDestroy(); + } + + @Override + protected boolean shouldDestroyOnScreenOff() { + return false; + } + + /** Retained instance fragment to hold a running {@link AuthenticationTask} between configuration changes.*/ + public static class TaskFragment extends Fragment { + + AuthenticationTask task; + + public TaskFragment() { + super(); + setRetainInstance(true); + } + + public void startTask(@NonNull AuthenticationTask task) { + this.task = task; + task.execute(); + } + } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java index 7cab245c..9bef95c7 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java @@ -23,6 +23,7 @@ package org.shadowice.flocke.andotp.Activities; import android.app.AlertDialog; +import android.app.Fragment; import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.DialogInterface; @@ -30,17 +31,25 @@ import android.content.Intent; import android.content.IntentSender; import android.net.Uri; import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.ViewStub; -import android.widget.LinearLayout; -import android.widget.Switch; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ProgressBar; +import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import com.google.android.material.switchmaterial.SwitchMaterial; + import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpApi; @@ -48,26 +57,35 @@ import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.shadowice.flocke.andotp.Database.Entry; import org.shadowice.flocke.andotp.Dialogs.PasswordEntryDialog; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Tasks.EncryptedBackupTask; +import org.shadowice.flocke.andotp.Tasks.EncryptedRestoreTask; +import org.shadowice.flocke.andotp.Tasks.GenericBackupTask; +import org.shadowice.flocke.andotp.Tasks.GenericRestoreTask; +import org.shadowice.flocke.andotp.Tasks.PGPBackupTask; +import org.shadowice.flocke.andotp.Tasks.PGPRestoreTask; +import org.shadowice.flocke.andotp.Tasks.PlainTextBackupTask; +import org.shadowice.flocke.andotp.Tasks.PlainTextRestoreTask; import org.shadowice.flocke.andotp.Utilities.BackupHelper; import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; -import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper; import org.shadowice.flocke.andotp.Utilities.Tools; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import javax.crypto.SecretKey; public class BackupActivity extends BaseActivity { private final static String TAG = BackupActivity.class.getSimpleName(); + private static final String TAG_BACKUP_TASK_FRAGMENT = "BackupActivity.BackupTaskFragmentTag"; + private static final String TAG_RESTORE_TASK_FRAGMENT = "BackupActivity.RestoreTaskFragmentTag"; + + private Constants.BackupType backupType = Constants.BackupType.ENCRYPTED; private SecretKey encryptionKey = null; private OpenPgpServiceConnection pgpServiceConnection; @@ -76,9 +94,17 @@ public class BackupActivity extends BaseActivity { private Uri encryptTargetFile; private Uri decryptSourceFile; - private Switch replace; + private Button btnBackup; + private Button btnRestore; + private TextView txtBackupLabel; + private TextView txtBackupWarning; + private SwitchMaterial swReplace; + private CheckBox chkOldFormat; + private ProgressBar progressBackup; + private ProgressBar progressRestore; private boolean reload = false; + private boolean allowExit = true; @Override protected void onCreate(Bundle savedInstanceState) { @@ -98,96 +124,119 @@ public class BackupActivity extends BaseActivity { byte[] keyMaterial = callingIntent.getByteArrayExtra(Constants.EXTRA_BACKUP_ENCRYPTION_KEY); encryptionKey = EncryptionHelper.generateSymmetricKey(keyMaterial); - // Plain-text + Spinner spBackupType = v.findViewById(R.id.backupType); + btnBackup = v.findViewById(R.id.buttonBackup); + btnRestore = v.findViewById(R.id.buttonRestore); + txtBackupLabel = v.findViewById(R.id.backupLabel); + txtBackupWarning = v.findViewById(R.id.backupErrorLabel); + swReplace = v.findViewById(R.id.backup_replace); + chkOldFormat = v.findViewById(R.id.restoreOldCrypt); + progressBackup = v.findViewById(R.id.progressBarBackup); + progressRestore = v.findViewById(R.id.progressBarRestore); - LinearLayout backupPlain = v.findViewById(R.id.button_backup_plain); - LinearLayout restorePlain = v.findViewById(R.id.button_restore_plain); + setupBackupType(settings.getDefaultBackupType()); + spBackupType.setSelection(backupType.ordinal()); - backupPlain.setOnClickListener(new View.OnClickListener() { + spBackupType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onClick(View view) { - backupPlainWithWarning(); + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + Constants.BackupType type = Constants.BackupType.values()[i]; + setupBackupType(type); } + + @Override + public void onNothingSelected(AdapterView adapterView) { } }); - restorePlain.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN); - } - }); - - // Password - - TextView cryptSetup = v.findViewById(R.id.msg_crypt_setup); - LinearLayout backupCrypt = v.findViewById(R.id.button_backup_crypt); - LinearLayout restoreCrypt = v.findViewById(R.id.button_restore_crypt); - LinearLayout restoreCryptOld = v.findViewById(R.id.button_restore_crypt_old); - - if (settings.getBackupPasswordEnc().isEmpty()) { - cryptSetup.setVisibility(View.VISIBLE); - } else { - cryptSetup.setVisibility(View.GONE); - } - - backupCrypt.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - showSaveFileSelector(Constants.BACKUP_MIMETYPE_CRYPT, Constants.BackupType.ENCRYPTED, Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT); - } - }); - - restoreCrypt.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT); - } - }); - - restoreCryptOld.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT_OLD); - } - }); - - // OpenPGP - - String PGPProvider = settings.getOpenPGPProvider(); - pgpEncryptionUserIDs = settings.getOpenPGPEncryptionUserIDs(); - - TextView setupPGP = v.findViewById(R.id.msg_openpgp_setup); - LinearLayout backupPGP = v.findViewById(R.id.button_backup_openpgp); - LinearLayout restorePGP = v.findViewById(R.id.button_restore_openpgp); - - if (TextUtils.isEmpty(PGPProvider)) { - setupPGP.setVisibility(View.VISIBLE); - backupPGP.setVisibility(View.GONE); - restorePGP.setVisibility(View.GONE); - } else if (TextUtils.isEmpty(pgpEncryptionUserIDs)){ - setupPGP.setVisibility(View.VISIBLE); - setupPGP.setText(R.string.backup_desc_openpgp_keyid); - backupPGP.setVisibility(View.GONE); - } else { - pgpServiceConnection = new OpenPgpServiceConnection(BackupActivity.this.getApplicationContext(), PGPProvider); - pgpServiceConnection.bindToService(); - - backupPGP.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { + btnBackup.setOnClickListener(view -> { + switch (backupType) { + case PLAIN_TEXT: + backupPlainWithWarning(); + break; + case ENCRYPTED: + showSaveFileSelector(Constants.BACKUP_MIMETYPE_CRYPT, Constants.BackupType.ENCRYPTED, Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT); + break; + case OPEN_PGP: showSaveFileSelector(Constants.BACKUP_MIMETYPE_PGP, Constants.BackupType.OPEN_PGP, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP); - } - }); + break; + } + }); - restorePGP.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { + btnRestore.setOnClickListener(view -> { + switch (backupType) { + case PLAIN_TEXT: + showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN); + break; + case ENCRYPTED: + if (chkOldFormat.isChecked()) + showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT_OLD); + else + showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT); + break; + case OPEN_PGP: showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP); + break; + } + }); + } + + private void setupBackupType(Constants.BackupType type) { + switch (type) { + case PLAIN_TEXT: + txtBackupLabel.setText(R.string.backup_label_warning_plain); + + chkOldFormat.setVisibility(View.GONE); + txtBackupWarning.setVisibility(View.GONE); + + btnBackup.setEnabled(true); + btnRestore.setEnabled(true); + + break; + case ENCRYPTED: + txtBackupLabel.setText(R.string.backup_label_crypt); + + chkOldFormat.setVisibility(View.VISIBLE); + txtBackupWarning.setVisibility(View.GONE); + + btnBackup.setEnabled(true); + btnRestore.setEnabled(true); + + break; + case OPEN_PGP: + txtBackupLabel.setText(R.string.backup_label_pgp); + + chkOldFormat.setVisibility(View.GONE); + + String PGPProvider = settings.getOpenPGPProvider(); + pgpEncryptionUserIDs = settings.getOpenPGPEncryptionUserIDs(); + + if (TextUtils.isEmpty(PGPProvider)) { + txtBackupWarning.setText(R.string.backup_desc_openpgp_provider); + txtBackupWarning.setVisibility(View.VISIBLE); + + btnBackup.setEnabled(false); + btnRestore.setEnabled(false); + } else if (TextUtils.isEmpty(pgpEncryptionUserIDs)){ + txtBackupWarning.setText(R.string.backup_desc_openpgp_keyid); + txtBackupWarning.setVisibility(View.VISIBLE); + + btnBackup.setEnabled(false); + btnRestore.setEnabled(false); + } else { + txtBackupWarning.setVisibility(View.GONE); + + btnBackup.setEnabled(true); + btnRestore.setEnabled(true); + + pgpServiceConnection = new OpenPgpServiceConnection(BackupActivity.this.getApplicationContext(), PGPProvider); + pgpServiceConnection.bindToService(); } - }); + + break; } - replace = v.findViewById(R.id.backup_replace); + backupType = type; + settings.setDefaultBackupType(type); } // End with a result @@ -201,14 +250,18 @@ public class BackupActivity extends BaseActivity { // Go back to the main activity @Override public boolean onSupportNavigateUp() { - finishWithResult(); + if (allowExit) + finishWithResult(); + return true; } @Override public void onBackPressed() { - finishWithResult(); - super.onBackPressed(); + if (allowExit) { + finishWithResult(); + super.onBackPressed(); + } } @Override @@ -219,6 +272,85 @@ public class BackupActivity extends BaseActivity { pgpServiceConnection.unbindFromService(); } + // TODO: Show more information about the finished backup (e.g. a notification with the file name) + private void notifyBackupState(int msgId) { + Toast.makeText(this, msgId, Toast.LENGTH_LONG).show(); + } + + private void handleBackupTaskResult(GenericBackupTask.BackupTaskResult result) { + showBackupProgress(false); + + if (result.messageId != 0) + notifyBackupState(result.messageId); + else + if (!result.success) + notifyBackupState(R.string.backup_toast_export_failed); + + // Clean up the task fragment + BackupTaskFragment backupTaskFragment = findBackupTaskFragment(); + if (backupTaskFragment != null) { + getFragmentManager().beginTransaction() + .remove(backupTaskFragment) + .commit(); + } + + if (result.success) + finishWithResult(); + } + + private void handleRestoreTaskResult(GenericRestoreTask.RestoreTaskResult result) { + if (result.success) { + if (result.isPGP) { + InputStream is = new ByteArrayInputStream(result.payload.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService()); + Intent resultIntent = api.executeApi(result.decryptIntent, is, os); + + handleOpenPGPResult(resultIntent, os, result.uri, Constants.INTENT_BACKUP_DECRYPT_PGP); + } else { + restoreEntries(result.payload, false); + } + } else { + if (result.messageId != 0) + notifyBackupState(result.messageId); + else + notifyBackupState(R.string.backup_toast_import_failed); + } + + showRestoreProgress(false); + + // Clean up the task fragment + RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment(); + if (restoreTaskFragment != null) { + getFragmentManager().beginTransaction() + .remove(restoreTaskFragment) + .commit(); + } + + if (result.success && !result.isPGP) + finishWithResult(); + } + + private void toggleInProgressMode(boolean running) { + allowExit = !running; + + btnBackup.setEnabled(!running); + btnRestore.setEnabled(!running); + chkOldFormat.setEnabled(!running); + swReplace.setEnabled(!running); + } + + private void showBackupProgress(boolean running) { + toggleInProgressMode(running); + progressBackup.setVisibility(running ? View.VISIBLE : View.GONE); + } + + private void showRestoreProgress(boolean running) { + toggleInProgressMode(running); + progressRestore.setVisibility(running ? View.VISIBLE : View.GONE); + } + // Get the result from external activities @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { @@ -291,26 +423,11 @@ public class BackupActivity extends BaseActivity { } else { if (settings.isBackupLocationSet()) { if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN) { - BackupHelper.BackupFile plainBackupFile = BackupHelper.backupFile(this, settings.getBackupLocation(), Constants.BackupType.PLAIN_TEXT); - - if (plainBackupFile.file != null) - doBackupPlain(plainBackupFile.file.getUri()); - else - Toast.makeText(this, plainBackupFile.errorMessage, Toast.LENGTH_LONG).show(); + doBackupPlain(null); } else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT) { - BackupHelper.BackupFile cryptBackupFile = BackupHelper.backupFile(this, settings.getBackupLocation(), Constants.BackupType.ENCRYPTED); - - if (cryptBackupFile.file != null) - doBackupCrypt(cryptBackupFile.file.getUri()); - else - Toast.makeText(this, cryptBackupFile.errorMessage, Toast.LENGTH_LONG).show(); + doBackupCrypt(null); } else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP) { - BackupHelper.BackupFile pgpBackupFile = BackupHelper.backupFile(this, settings.getBackupLocation(), Constants.BackupType.OPEN_PGP); - - if (pgpBackupFile.file != null) - backupEncryptedWithPGP(pgpBackupFile.file.getUri(), null); - else - Toast.makeText(this, pgpBackupFile.errorMessage, Toast.LENGTH_LONG).show(); + backupEncryptedWithPGP(null, null); } } else { Toast.makeText(this, R.string.backup_toast_no_location, Toast.LENGTH_LONG).show(); @@ -318,11 +435,11 @@ public class BackupActivity extends BaseActivity { } } - private void restoreEntries(String text) { + private void restoreEntries(String text, boolean finish) { ArrayList entries = DatabaseHelper.stringToEntries(text); if (entries.size() > 0) { - if (! replace.isChecked()) { + if (! swReplace.isChecked()) { ArrayList currentEntries = DatabaseHelper.loadDatabase(this, encryptionKey); entries.removeAll(currentEntries); @@ -332,7 +449,9 @@ public class BackupActivity extends BaseActivity { if (DatabaseHelper.saveDatabase(this, entries, encryptionKey)) { reload = true; Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show(); - finishWithResult(); + + if (finish) + finishWithResult(); } else { Toast.makeText(this, R.string.backup_toast_import_save_failed, Toast.LENGTH_LONG).show(); } @@ -345,9 +464,10 @@ public class BackupActivity extends BaseActivity { private void doRestorePlain(Uri uri) { if (Tools.isExternalStorageReadable()) { - String content = StorageAccessHelper.loadFileString(this, uri); + PlainTextRestoreTask task = new PlainTextRestoreTask(this, uri); + task.setCallback(this::handleRestoreTaskResult); - restoreEntries(content); + startRestoreTask(task); } else { Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show(); } @@ -357,15 +477,13 @@ public class BackupActivity extends BaseActivity { if (Tools.isExternalStorageWritable()) { ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); - if (StorageAccessHelper.saveFile(this, uri, DatabaseHelper.entriesToString(entries))) - Toast.makeText(this, R.string.backup_toast_export_success, Toast.LENGTH_LONG).show(); - else - Toast.makeText(this, R.string.backup_toast_export_failed, Toast.LENGTH_LONG).show(); + PlainTextBackupTask task = new PlainTextBackupTask(this, entries, uri); + task.setCallback(this::handleBackupTaskResult); + + startBackupTask(task); } else { Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show(); } - - finishWithResult(); } private void backupPlainWithWarning() { @@ -408,39 +526,10 @@ public class BackupActivity extends BaseActivity { private void doRestoreCryptWithPassword(Uri uri, String password, boolean old_format) { if (Tools.isExternalStorageReadable()) { - boolean success = true; - String decryptedString = ""; + EncryptedRestoreTask task = new EncryptedRestoreTask(this, uri, password, old_format); + task.setCallback(this::handleRestoreTaskResult); - try { - byte[] data = StorageAccessHelper.loadFile(this, uri); - - if (old_format) { - SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); - byte[] decrypted = EncryptionHelper.decrypt(key, data); - - decryptedString = new String(decrypted, StandardCharsets.UTF_8); - } else { - byte[] iterBytes = Arrays.copyOfRange(data, 0, Constants.INT_LENGTH); - byte[] salt = Arrays.copyOfRange(data, Constants.INT_LENGTH, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH); - byte[] encrypted = Arrays.copyOfRange(data, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH, data.length); - - int iter = ByteBuffer.wrap(iterBytes).getInt(); - - SecretKey key = EncryptionHelper.generateSymmetricKeyPBKDF2(password, iter, salt); - - byte[] decrypted = EncryptionHelper.decrypt(key, encrypted); - decryptedString = new String(decrypted, StandardCharsets.UTF_8); - } - } catch (Exception e) { - success = false; - e.printStackTrace(); - } - - if (success) { - restoreEntries(decryptedString); - } else { - Toast.makeText(this,R.string.backup_toast_import_decryption_failed, Toast.LENGTH_LONG).show(); - } + startRestoreTask(task); } else { Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show(); } @@ -464,19 +553,15 @@ public class BackupActivity extends BaseActivity { private void doBackupCryptWithPassword(Uri uri, String password) { if (Tools.isExternalStorageWritable()) { + ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); - boolean success = BackupHelper.backupToFile(this, uri, password, encryptionKey); + EncryptedBackupTask task = new EncryptedBackupTask(this, entries, password, uri); + task.setCallback(this::handleBackupTaskResult); - if (success) { - Toast.makeText(this, R.string.backup_toast_export_success, Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(this, R.string.backup_toast_export_failed, Toast.LENGTH_LONG).show(); - } + startBackupTask(task); } else { Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show(); } - - finishWithResult(); } /* OpenPGP backup functions */ @@ -485,28 +570,21 @@ public class BackupActivity extends BaseActivity { if (decryptIntent == null) decryptIntent = new Intent(OpenPgpApi.ACTION_DECRYPT_VERIFY); - String input = StorageAccessHelper.loadFileString(this, uri); + PGPRestoreTask task = new PGPRestoreTask(this, uri, decryptIntent); + task.setCallback(this::handleRestoreTaskResult); - InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService()); - Intent result = api.executeApi(decryptIntent, is, os); - handleOpenPGPResult(result, os, uri, Constants.INTENT_BACKUP_DECRYPT_PGP); + startRestoreTask(task); } private void doBackupEncrypted(Uri uri, String data) { if (Tools.isExternalStorageWritable()) { - boolean success = StorageAccessHelper.saveFile(this, uri, data); + PGPBackupTask task = new PGPBackupTask(this, data, uri); + task.setCallback(this::handleBackupTaskResult); - if (success) - Toast.makeText(this, R.string.backup_toast_export_success, Toast.LENGTH_LONG).show(); - else - Toast.makeText(this, R.string.backup_toast_export_failed, Toast.LENGTH_LONG).show(); + startBackupTask(task); } else { Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show(); } - - finishWithResult(); } private void backupEncryptedWithPGP(Uri uri, Intent encryptIntent) { @@ -549,12 +627,12 @@ public class BackupActivity extends BaseActivity { OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); if (sigResult.getResult() == OpenPgpSignatureResult.RESULT_VALID_KEY_CONFIRMED) { - restoreEntries(outputStreamToString(os)); + restoreEntries(outputStreamToString(os), true); } else { Toast.makeText(this, R.string.backup_toast_openpgp_not_verified, Toast.LENGTH_LONG).show(); } } else { - restoreEntries(outputStreamToString(os)); + restoreEntries(outputStreamToString(os), true); } } } @@ -578,4 +656,145 @@ public class BackupActivity extends BaseActivity { Toast.makeText(this, String.format(getString(R.string.backup_toast_openpgp_error), error.getMessage()), Toast.LENGTH_LONG).show(); } } + + @Nullable + private BackupTaskFragment findBackupTaskFragment() { + return (BackupTaskFragment) getFragmentManager().findFragmentByTag(TAG_BACKUP_TASK_FRAGMENT); + } + + @Nullable + private RestoreTaskFragment findRestoreTaskFragment() { + return (RestoreTaskFragment) getFragmentManager().findFragmentByTag(TAG_RESTORE_TASK_FRAGMENT); + } + + private void startBackupTask(GenericBackupTask task) { + BackupTaskFragment backupTaskFragment = findBackupTaskFragment(); + RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment(); + + // Don't start a task if we already have an active task running (backup or restore). + if ((backupTaskFragment == null || backupTaskFragment.task.isCanceled()) && (restoreTaskFragment == null || restoreTaskFragment.task.isCanceled())) { + if (backupTaskFragment == null) { + backupTaskFragment = new BackupTaskFragment(); + getFragmentManager() + .beginTransaction() + .add(backupTaskFragment, TAG_BACKUP_TASK_FRAGMENT) + .commit(); + } + + backupTaskFragment.startTask(task); + + showBackupProgress(true); + } + } + + private void startRestoreTask(GenericRestoreTask task) { + BackupTaskFragment backupTaskFragment = findBackupTaskFragment(); + RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment(); + + // Don't start a task if we already have an active task running (backup or restore). + if ((backupTaskFragment == null || backupTaskFragment.task.isCanceled()) && (restoreTaskFragment == null || restoreTaskFragment.task.isCanceled())) { + if (restoreTaskFragment == null) { + restoreTaskFragment = new RestoreTaskFragment(); + getFragmentManager() + .beginTransaction() + .add(restoreTaskFragment, TAG_RESTORE_TASK_FRAGMENT) + .commit(); + } + + restoreTaskFragment.startTask(task); + + showRestoreProgress(true); + } + } + + private void checkBackgroundBackupTask() { + BackupTaskFragment backupTaskFragment = findBackupTaskFragment(); + + if (backupTaskFragment != null) { + if (backupTaskFragment.task.isCanceled()) { + // The task was canceled or has finished, so remove the task fragment. + getFragmentManager().beginTransaction() + .remove(backupTaskFragment) + .commit(); + } else { + backupTaskFragment.task.setCallback(this::handleBackupTaskResult); + showBackupProgress(true); + } + } + + } + + private void checkBackgroundRestoreTask() { + RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment(); + + if (restoreTaskFragment != null) { + if (restoreTaskFragment.task.isCanceled()) { + // The task was canceled or has finished, so remove the task fragment. + getFragmentManager().beginTransaction() + .remove(restoreTaskFragment) + .commit(); + } else { + restoreTaskFragment.task.setCallback(this::handleRestoreTaskResult); + showRestoreProgress(true); + } + } + } + + @Override + protected void onPause() { + super.onPause(); + + // We don't want the task to callback to a dead activity and cause a memory leak, so null it here. + BackupTaskFragment backupTaskFragment = findBackupTaskFragment(); + RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment(); + + if (backupTaskFragment != null) + backupTaskFragment.task.setCallback(null); + + if (restoreTaskFragment != null) + restoreTaskFragment.task.setCallback(null); + } + + @Override + public void onResume() { + super.onResume(); + + checkBackgroundBackupTask(); + checkBackgroundRestoreTask(); + } + + @Override + protected boolean shouldDestroyOnScreenOff() { + return allowExit; // Don't destroy the backup activity as long as a backup task is running + } + + /** Retained instance fragment to hold a running {@link GenericBackupTask} between configuration changes.*/ + public static class BackupTaskFragment extends Fragment { + GenericBackupTask task; + + public BackupTaskFragment() { + super(); + setRetainInstance(true); + } + + public void startTask(@NonNull GenericBackupTask task) { + this.task = task; + this.task.execute(); + } + } + + /** Retained instance fragment to hold a running {@link GenericRestoreTask} between configuration changes.*/ + public static class RestoreTaskFragment extends Fragment { + GenericRestoreTask task; + + public RestoreTaskFragment() { + super(); + setRetainInstance(true); + } + + public void startTask(@NonNull GenericRestoreTask task) { + this.task = task; + this.task.execute(); + } + } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BaseActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BaseActivity.java index 108adca3..1a3c7a71 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BaseActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BaseActivity.java @@ -43,15 +43,9 @@ public abstract class BaseActivity extends ThemedActivity { @Override protected void onDestroy() { unregisterReceiver(screenOffReceiver); - super.onDestroy(); } - private void destroyIfNotMain() { - if (getClass() != MainActivity.class) - finish(); - } - public void setBroadcastCallback(BroadcastReceivedCallback cb) { this.broadcastReceivedCallback = cb; } @@ -62,14 +56,20 @@ public abstract class BaseActivity extends ThemedActivity { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { - if (broadcastReceivedCallback != null) + if (broadcastReceivedCallback != null) { broadcastReceivedCallback.onReceivedScreenOff(); - - destroyIfNotMain(); + } + if (shouldDestroyOnScreenOff()) { + finish(); + } } } } + protected boolean shouldDestroyOnScreenOff() { + return true; + } + interface BroadcastReceivedCallback { void onReceivedScreenOff(); } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/IntroScreenActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/IntroScreenActivity.java index aa26e08c..793b2b20 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/IntroScreenActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/IntroScreenActivity.java @@ -42,6 +42,7 @@ import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Spinner; +import android.widget.Switch; import android.widget.TextView; import androidx.annotation.NonNull; @@ -66,6 +67,7 @@ public class IntroScreenActivity extends IntroActivity { private EncryptionFragment encryptionFragment; private AuthenticationFragment authenticationFragment; + private AndroidSyncFragment androidSyncFragment; private void saveSettings() { Constants.EncryptionType encryptionType = encryptionFragment.getEncryptionType(); @@ -73,6 +75,7 @@ public class IntroScreenActivity extends IntroActivity { settings.setEncryption(encryptionType); settings.setAuthMethod(authMethod); + settings.setAndroidBackupServiceEnabled(androidSyncFragment.getSyncEnabled()); if (authMethod == Constants.AuthMethod.PASSWORD || authMethod == Constants.AuthMethod.PIN) { String password = authenticationFragment.getPassword(); @@ -89,6 +92,7 @@ public class IntroScreenActivity extends IntroActivity { encryptionFragment = new EncryptionFragment(); authenticationFragment = new AuthenticationFragment(); + androidSyncFragment = new AndroidSyncFragment(encryptionFragment); encryptionFragment.setEncryptionChangedCallback(newEncryptionType -> authenticationFragment.updateEncryptionType(newEncryptionType)); @@ -121,6 +125,13 @@ public class IntroScreenActivity extends IntroActivity { .build() ); + addSlide(new FragmentSlide.Builder() + .background(R.color.colorPrimary) + .backgroundDark(R.color.colorPrimaryDark) + .fragment(androidSyncFragment) + .build() + ); + addSlide(new SimpleSlide.Builder() .title(R.string.intro_slide4_title) .description(R.string.intro_slide4_desc) @@ -130,6 +141,7 @@ public class IntroScreenActivity extends IntroActivity { .build() ); + addOnNavigationBlockedListener((position, direction) -> { if (position == 2) authenticationFragment.flashWarning(); @@ -138,7 +150,7 @@ public class IntroScreenActivity extends IntroActivity { addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageSelected(int position) { - if (position == 3) + if (position == getSlides().size() - 1) saveSettings(); } @@ -223,6 +235,39 @@ public class IntroScreenActivity extends IntroActivity { } } + public static class AndroidSyncFragment extends SlideFragment { + private Switch introAndroidSync; + private EncryptionFragment encryptionFragment; + + public AndroidSyncFragment(EncryptionFragment encryptionFragment) { + this.encryptionFragment = encryptionFragment; + } + + public boolean getSyncEnabled() + { + return introAndroidSync.isChecked(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.component_intro_android_sync, container, false); + + introAndroidSync = root.findViewById(R.id.introAndroidSync); + introAndroidSync.setOnCheckedChangeListener((compoundButton, b) -> { + compoundButton.setText( b ? + R.string.settings_toast_android_sync_enabled : + R.string.settings_toast_android_sync_disabled + ); + }); + + introAndroidSync.setChecked(encryptionFragment.getEncryptionType() != Constants.EncryptionType.KEYSTORE); + introAndroidSync.setEnabled(encryptionFragment.getEncryptionType() != Constants.EncryptionType.KEYSTORE); + + return root; + } + } + public static class AuthenticationFragment extends SlideFragment implements TextView.OnEditorActionListener { private Constants.EncryptionType encryptionType = Constants.EncryptionType.KEYSTORE; @@ -232,6 +277,7 @@ public class IntroScreenActivity extends IntroActivity { private String lengthWarning = ""; private String noPasswordWarning = ""; private String confirmPasswordWarning = ""; + private String passwordMismatchWarning = ""; private TextView desc = null; private Spinner selection = null; @@ -389,6 +435,7 @@ public class IntroScreenActivity extends IntroActivity { lengthWarning = getString(R.string.settings_label_short_password, minLength); noPasswordWarning = getString(R.string.intro_slide3_warn_no_password); confirmPasswordWarning = getString(R.string.intro_slide3_warn_confirm_password); + passwordMismatchWarning = getString(R.string.intro_slide3_warn_password_mismatch); focusOnPasswordInput(); } @@ -415,6 +462,7 @@ public class IntroScreenActivity extends IntroActivity { lengthWarning = getString(R.string.settings_label_short_pin, minLength); noPasswordWarning = getString(R.string.intro_slide3_warn_no_pin); confirmPasswordWarning = getString(R.string.intro_slide3_warn_confirm_pin); + passwordMismatchWarning = getString(R.string.intro_slide3_warn_pin_mismatch); focusOnPasswordInput(); } @@ -479,6 +527,9 @@ public class IntroScreenActivity extends IntroActivity { if (! confirm.isEmpty() && confirm.equals(password)) { hideWarning(); return true; + } else if (! confirm.isEmpty() && ! confirm.equals(password)) { + updateWarning(passwordMismatchWarning); + return false; } else { updateWarning(confirmPasswordWarning); return false; diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java index e4e96dd7..82e1ff7a 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java @@ -34,6 +34,8 @@ import android.os.Bundle; import android.os.CountDownTimer; import android.os.Handler; import android.preference.PreferenceManager; + +import androidx.annotation.Nullable; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.appcompat.app.ActionBarDrawerToggle; @@ -56,6 +58,7 @@ import android.widget.AdapterView; import android.widget.CheckedTextView; import android.widget.ListView; import android.widget.ProgressBar; +import android.widget.TextView; import android.widget.Toast; import com.google.zxing.integration.android.IntentIntegrator; @@ -114,6 +117,8 @@ public class MainActivity extends BaseActivity private String filterString; private CountDownTimer countDownTimer; + private ProgressBar progressBar; + private TextView emptyListView; // QR code scanning private void scanQRCode(){ @@ -245,8 +250,8 @@ public class MainActivity extends BaseActivity } }); - final ProgressBar progressBar = findViewById(R.id.progressBar); - progressBar.setVisibility(settings.isHideGlobalTimeoutEnabled() ? View.GONE : View.VISIBLE); + progressBar = findViewById(R.id.progressBar); + emptyListView = findViewById(R.id.emptyListView); RecyclerView recList = findViewById(R.id.cardList); recList.setHasFixedSize(true); @@ -256,6 +261,43 @@ public class MainActivity extends BaseActivity tagsDrawerAdapter = new TagsAdapter(this, new HashMap()); adapter = new EntriesCardAdapter(this, tagsDrawerAdapter); + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + super.onChanged(); + hideProgressBar(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + super.onItemRangeChanged(positionStart, itemCount); + hideProgressBar(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + super.onItemRangeChanged(positionStart, itemCount, payload); + hideProgressBar(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + super.onItemRangeInserted(positionStart, itemCount); + hideProgressBar(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + super.onItemRangeRemoved(positionStart, itemCount); + hideProgressBar(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + super.onItemRangeMoved(fromPosition, toPosition, itemCount); + hideProgressBar(); + } + }); if (savedInstanceState != null) { byte[] encKey = savedInstanceState.getByteArray("encKey"); @@ -586,6 +628,10 @@ public class MainActivity extends BaseActivity } }); + if (settings.isFocusSearchOnStartEnabled()) { + searchItem.expandActionView(); + } + return true; } @@ -894,4 +940,20 @@ public class MainActivity extends BaseActivity settings.unregisterPreferenceChangeListener(this); super.onDestroy(); } -} \ No newline at end of file + + /** + * This function will hide the progress bar if the token list is empty along with + * showing a view which has instruction on how to add the tokens + * */ + private void hideProgressBar(){ + int itemCount = adapter.getItemCount(); + progressBar.setVisibility((settings.isHideGlobalTimeoutEnabled() || itemCount <= 0) ? View.GONE : View.VISIBLE); + emptyListView.setVisibility(itemCount > 0 ? View.GONE : View.VISIBLE); + + } + + @Override + protected boolean shouldDestroyOnScreenOff() { + return false; + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java index 9ee85cfa..236bb05d 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java @@ -40,6 +40,7 @@ import android.preference.PreferenceManager; import androidx.appcompat.widget.Toolbar; import android.provider.DocumentsContract; +import android.util.Log; import android.view.ViewStub; import android.widget.Toast; @@ -168,6 +169,13 @@ public class SettingsActivity extends BaseActivity if (fragment.useAndroidSync != null) fragment.useAndroidSync.setEnabled(true); } + } else if(key.equals(getString(R.string.settings_key_enable_android_backup_service))) + { + Log.d(SettingsActivity.class.getSimpleName(), "onSharedPreferenceChanged called modifying settings_key_enable_android_backup_service service is now: " + + (settings.getAndroidBackupServiceEnabled() ? "enabled" : "disabled")); + + int message = settings.getAndroidBackupServiceEnabled() ? R.string.settings_toast_android_sync_enabled : R.string.settings_toast_android_sync_disabled; + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } fragment.updateAutoBackup(); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java index 00eeddbb..b53445de 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java @@ -30,7 +30,6 @@ import android.preference.DialogPreference; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; -import android.text.method.PasswordTransformationMethod; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; @@ -69,7 +68,7 @@ public class CredentialsPreference extends DialogPreference boolean testEncryptionChange(byte[] newKey); } - private List entries; + private final List entries; private static final List entryValues = Arrays.asList( AuthMethod.NONE, AuthMethod.PASSWORD, @@ -79,7 +78,7 @@ public class CredentialsPreference extends DialogPreference private int minLength = 0; - private Settings settings; + private final Settings settings; private AuthMethod value = AuthMethod.NONE; private EncryptionChangeCallback encryptionChangeCallback = null; @@ -233,77 +232,65 @@ public class CredentialsPreference extends DialogPreference if (password.length() >= minLength) { toShortWarning.setVisibility(View.GONE); - String confirm = passwordConfirm.getEditableText().toString(); - if (!password.isEmpty() && !confirm.isEmpty() && password.equals(confirm)) { - btnSave.setEnabled(true); - } else { - btnSave.setEnabled(false); - } + boolean canSave = !password.isEmpty() && !confirm.isEmpty() && password.equals(confirm); + btnSave.setEnabled(canSave); } else { toShortWarning.setVisibility(View.VISIBLE); } } private void updateLayout() { - if (value == AuthMethod.NONE) { + if (value == AuthMethod.NONE || value == AuthMethod.DEVICE) { credentialsLayout.setVisibility(View.GONE); - - if (getDialog() != null) - UIHelper.hideKeyboard(getContext(), getDialog().getCurrentFocus()); - - btnSave.setEnabled(true); - } else if (value == AuthMethod.PASSWORD) { - credentialsLayout.setVisibility(View.VISIBLE); - - passwordLayout.setHint(getContext().getString(R.string.settings_hint_password)); - passwordConfirm.setHint(R.string.settings_hint_password_confirm); - - ConfirmedPasswordTransformationHelper.setup(passwordLayout, passwordInput, passwordConfirm); - - passwordInput.setTransformationMethod(new PasswordTransformationMethod()); - passwordConfirm.setTransformationMethod(new PasswordTransformationMethod()); - - minLength = Constants.AUTH_MIN_PASSWORD_LENGTH; - toShortWarning.setText(getContext().getString(R.string.settings_label_short_password, minLength)); - - passwordInput.requestFocus(); - UIHelper.showKeyboard(getContext(), passwordInput); - - btnSave.setEnabled(false); - } else if (value == AuthMethod.PIN) { - credentialsLayout.setVisibility(View.VISIBLE); - - passwordLayout.setHint(getContext().getString(R.string.settings_hint_pin)); - passwordConfirm.setHint(R.string.settings_hint_pin_confirm); - - passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - passwordConfirm.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - - ConfirmedPasswordTransformationHelper.setup(passwordLayout, passwordInput, passwordConfirm); - - minLength = Constants.AUTH_MIN_PIN_LENGTH; - toShortWarning.setText(getContext().getString(R.string.settings_label_short_pin, minLength)); - - passwordInput.requestFocus(); - UIHelper.showKeyboard(getContext(), passwordInput); - - btnSave.setEnabled(false); - } else if (value == AuthMethod.DEVICE) { - credentialsLayout.setVisibility(View.GONE); - - if (getDialog() != null) - UIHelper.hideKeyboard(getContext(), getDialog().getCurrentFocus()); - + if (getDialog() != null) { + UIHelper.hideKeyboard(getContext(), passwordInput); + } btnSave.setEnabled(true); + } else if (value == AuthMethod.PASSWORD || value == AuthMethod.PIN) { + prepareAuthMethodInputFields(); } } + private void prepareAuthMethodInputFields() { + if (value != AuthMethod.PIN && value != AuthMethod.PASSWORD) { + return; + } + boolean isPassword = value == AuthMethod.PASSWORD; + + credentialsLayout.setVisibility(View.VISIBLE); + int layoutHintRes = isPassword ? R.string.settings_hint_password : R.string.settings_hint_pin; + passwordLayout.setHint(getContext().getString(layoutHintRes)); + int confirmHintRes = isPassword ? R.string.settings_hint_password_confirm : R.string.settings_hint_pin_confirm; + passwordConfirm.setHint(confirmHintRes); + + int inputType = isPassword ? + (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) : + (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + passwordInput.setInputType(inputType); + passwordConfirm.setInputType(inputType); + ConfirmedPasswordTransformationHelper.setup(passwordLayout, passwordInput, passwordConfirm); + + minLength = isPassword ? Constants.AUTH_MIN_PASSWORD_LENGTH : Constants.AUTH_MIN_PIN_LENGTH; + int shortWarningRes = isPassword ? R.string.settings_label_short_password : R.string.settings_label_short_pin; + toShortWarning.setText(getContext().getString(shortWarningRes, minLength)); + + passwordInput.requestFocus(); + UIHelper.showKeyboard(getContext(), passwordInput); + btnSave.setEnabled(false); + } + @Override public void onItemClick(AdapterView parent, View view, int position, long id) { value = entryValues.get(position); updateLayout(); + clearInputFields(); + } + + private void clearInputFields() { + passwordInput.setText(null); + passwordConfirm.setText(null); } @Override diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/AuthenticationTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/AuthenticationTask.java new file mode 100644 index 00000000..13c2f069 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/AuthenticationTask.java @@ -0,0 +1,114 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Base64; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.shadowice.flocke.andotp.Tasks.AuthenticationTask.Result; +import org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper.PBKDF2Credentials; +import org.shadowice.flocke.andotp.Utilities.Settings; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; + +public class AuthenticationTask extends UiBasedBackgroundTask { + + private final Settings settings; + + private final boolean isAuthUpgrade; + private final String existingAuthCredentials; + private final String plainPassword; + + /** + * @param context Context to be used to query settings (the application Context will be used to avoid memory leaks). + * @param isAuthUpgrade true if this is an authentication upgrade and new credentials should be saved, false if this is just confirmation. + * @param existingAuthCredentials The existing hashed authentication credentials that we have stored. + * @param plainPassword The plaintext user-entered password to check authentication with. */ + public AuthenticationTask(Context context, boolean isAuthUpgrade, String existingAuthCredentials, String plainPassword) { + super(Result.failure()); + Context applicationContext = context.getApplicationContext(); + this.settings = new Settings(applicationContext); + + this.isAuthUpgrade = isAuthUpgrade; + this.existingAuthCredentials = existingAuthCredentials; + this.plainPassword = plainPassword; + } + + @Override + @NonNull + protected Result doInBackground() { + if (isAuthUpgrade) { + return upgradeAuthentication(); + } else { + return confirmAuthentication(); + } + } + + @NonNull + private Result upgradeAuthentication() { + String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); + if (!hashedPassword.equals(existingAuthCredentials)) + return Result.failure(); + + byte[] key = settings.setAuthCredentials(plainPassword); + + AuthMethod authMethod = settings.getAuthMethod(); + if (authMethod == AuthMethod.PASSWORD) + settings.removeAuthPasswordHash(); + else if (authMethod == AuthMethod.PIN) + settings.removeAuthPINHash(); + + if (key == null) + return Result.upgradeFailure(); + else + return Result.success(key); + } + + @NonNull + private Result confirmAuthentication() { + try { + PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), settings.getIterations()); + byte[] passwordArray = Base64.decode(existingAuthCredentials, Base64.URL_SAFE); + + if (Arrays.equals(passwordArray, credentials.password)) { + return Result.success(credentials.key); + } + return Result.failure(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) { + Log.e("AuthenticationTask", "Problem decoding password", e); + return Result.failure(); + } + } + + public static class Result { + @Nullable + public final byte[] encryptionKey; + public final boolean authUpgradeFailed; + + public Result(@Nullable byte[] encryptionKey, boolean authUpgradeFailed) { + this.encryptionKey = encryptionKey; + this.authUpgradeFailed = authUpgradeFailed; + } + + public static Result success(byte[] encryptionKey) { + return new Result(encryptionKey, false); + } + + public static Result upgradeFailure() { + return new Result(null, true); + } + + public static Result failure() { + return new Result(null, false); + } + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/EncryptedBackupTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/EncryptedBackupTask.java new file mode 100644 index 00000000..f810a34d --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/EncryptedBackupTask.java @@ -0,0 +1,37 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.shadowice.flocke.andotp.Database.Entry; +import org.shadowice.flocke.andotp.Utilities.BackupHelper; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; + +import java.util.ArrayList; + +public class EncryptedBackupTask extends GenericBackupTask { + private final String password; + private final ArrayList entries; + + public EncryptedBackupTask(Context context, ArrayList entries, String password, @Nullable Uri uri) { + super(context, uri); + this.entries = entries; + this.password = password; + } + + @Override + @NonNull + protected Constants.BackupType getBackupType() { + return Constants.BackupType.ENCRYPTED; + } + + @Override + protected boolean doBackup() { + String payload = DatabaseHelper.entriesToString(entries); + return BackupHelper.backupToFile(applicationContext, uri, password, payload); + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/EncryptedRestoreTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/EncryptedRestoreTask.java new file mode 100644 index 00000000..bcd947e1 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/EncryptedRestoreTask.java @@ -0,0 +1,66 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import javax.crypto.SecretKey; + +public class EncryptedRestoreTask extends GenericRestoreTask { + private final String password; + private final boolean oldFormat; + + public EncryptedRestoreTask(Context context, Uri uri, String password, boolean oldFormat) { + super(context, uri); + this.password = password; + this.oldFormat = oldFormat; + } + + @Override + @NonNull + protected RestoreTaskResult doInBackground() { + boolean success = true; + String decryptedString = ""; + + try { + byte[] data = StorageAccessHelper.loadFile(applicationContext, uri); + + if (oldFormat) { + SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); + byte[] decrypted = EncryptionHelper.decrypt(key, data); + + decryptedString = new String(decrypted, StandardCharsets.UTF_8); + } else { + byte[] iterBytes = Arrays.copyOfRange(data, 0, Constants.INT_LENGTH); + byte[] salt = Arrays.copyOfRange(data, Constants.INT_LENGTH, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH); + byte[] encrypted = Arrays.copyOfRange(data, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH, data.length); + + int iter = ByteBuffer.wrap(iterBytes).getInt(); + + SecretKey key = EncryptionHelper.generateSymmetricKeyPBKDF2(password, iter, salt); + + byte[] decrypted = EncryptionHelper.decrypt(key, encrypted); + decryptedString = new String(decrypted, StandardCharsets.UTF_8); + } + } catch (Exception e) { + success = false; + e.printStackTrace(); + } + + if (success) { + return RestoreTaskResult.success(decryptedString); + } else { + return RestoreTaskResult.failure(R.string.backup_toast_import_decryption_failed); + } + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/GenericBackupTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/GenericBackupTask.java new file mode 100644 index 00000000..3d6f756b --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/GenericBackupTask.java @@ -0,0 +1,72 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.BackupHelper; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.Settings; + +public abstract class GenericBackupTask extends UiBasedBackgroundTask { + protected final Context applicationContext; + protected final Settings settings; + protected final Constants.BackupType type; + protected Uri uri; + + public GenericBackupTask(Context context, @Nullable Uri uri) { + super(BackupTaskResult.failure()); + + this.applicationContext = context.getApplicationContext(); + this.settings = new Settings(applicationContext); + + this.type = getBackupType(); + this.uri = uri; + } + + @Override + @NonNull + protected BackupTaskResult doInBackground() { + if (uri == null) { + BackupHelper.BackupFile backupFile = BackupHelper.backupFile(applicationContext, settings.getBackupLocation(), type); + + if (backupFile.file == null) + return new BackupTaskResult(false, backupFile.errorMessage); + + uri = backupFile.file.getUri(); + } + + boolean success = doBackup(); + + if (success) + return BackupTaskResult.success(); + else + return BackupTaskResult.failure(); + } + + @NonNull + protected abstract Constants.BackupType getBackupType(); + protected abstract boolean doBackup(); + + + public static class BackupTaskResult { + public final boolean success; + public final int messageId; + + public BackupTaskResult(boolean success, int messageId) { + this.success = success; + this.messageId = messageId; + } + + public static BackupTaskResult success() { + return new BackupTaskResult(true, R.string.backup_toast_export_success); + } + + public static BackupTaskResult failure() { + return new BackupTaskResult(false, R.string.backup_toast_export_failed); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/GenericRestoreTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/GenericRestoreTask.java new file mode 100644 index 00000000..c7b094c8 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/GenericRestoreTask.java @@ -0,0 +1,63 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Settings; + +public abstract class GenericRestoreTask extends UiBasedBackgroundTask { + protected final Context applicationContext; + protected final Settings settings; + protected Uri uri; + + public GenericRestoreTask(Context context, @Nullable Uri uri) { + super(GenericRestoreTask.RestoreTaskResult.failure(R.string.backup_toast_import_failed)); + + this.applicationContext = context.getApplicationContext(); + this.settings = new Settings(applicationContext); + + this.uri = uri; + } + + @Override + @NonNull + protected abstract RestoreTaskResult doInBackground(); + + public static class RestoreTaskResult { + public final boolean success; + public final String payload; + public final int messageId; + + public boolean isPGP = false; + public Intent decryptIntent = null; + public Uri uri = null; + + public RestoreTaskResult(boolean success, String payload, int messageId) { + this.success = success; + this.payload = payload; + this.messageId = messageId; + } + + public RestoreTaskResult(boolean success, String payload, int messageId, boolean isPGP, Intent decryptIntent, Uri uri) { + this.success = success; + this.payload = payload; + this.messageId = messageId; + this.isPGP = isPGP; + this.decryptIntent = decryptIntent; + this.uri = uri; + } + + public static RestoreTaskResult success(String payload) { + return new RestoreTaskResult(true, payload, 0); + } + + public static RestoreTaskResult failure(int messageId) { + return new RestoreTaskResult(false, null, messageId); + } + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PGPBackupTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PGPBackupTask.java new file mode 100644 index 00000000..1d900614 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PGPBackupTask.java @@ -0,0 +1,30 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper; + +public class PGPBackupTask extends GenericBackupTask { + private final String payload; + + public PGPBackupTask(Context context, String payload, @Nullable Uri uri) { + super(context, uri); + this.payload = payload; + } + + @Override + @NonNull + protected Constants.BackupType getBackupType() { + return Constants.BackupType.OPEN_PGP; + } + + @Override + protected boolean doBackup() { + return StorageAccessHelper.saveFile(applicationContext, uri, payload); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PGPRestoreTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PGPRestoreTask.java new file mode 100644 index 00000000..250a861f --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PGPRestoreTask.java @@ -0,0 +1,26 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper; + +public class PGPRestoreTask extends GenericRestoreTask { + private final Intent decryptIntent; + + public PGPRestoreTask(Context context, Uri uri, Intent decryptIntent) { + super(context, uri); + this.decryptIntent = decryptIntent; + } + + @Override + @NonNull + protected RestoreTaskResult doInBackground() { + String data = StorageAccessHelper.loadFileString(applicationContext, uri); + + return new RestoreTaskResult(true, data, 0, true, decryptIntent, uri); + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PlainTextBackupTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PlainTextBackupTask.java new file mode 100644 index 00000000..c4cab579 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PlainTextBackupTask.java @@ -0,0 +1,35 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.shadowice.flocke.andotp.Database.Entry; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; +import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper; + +import java.util.ArrayList; + +public class PlainTextBackupTask extends GenericBackupTask { + private final ArrayList entries; + + public PlainTextBackupTask(Context context, ArrayList entries, @Nullable Uri uri) { + super(context, uri); + this.entries = entries; + } + + @Override + @NonNull + protected Constants.BackupType getBackupType() { + return Constants.BackupType.PLAIN_TEXT; + } + + @Override + protected boolean doBackup() { + String payload = DatabaseHelper.entriesToString(entries); + return StorageAccessHelper.saveFile(applicationContext, uri, payload); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PlainTextRestoreTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PlainTextRestoreTask.java new file mode 100644 index 00000000..200b9ac4 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/PlainTextRestoreTask.java @@ -0,0 +1,21 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper; + +public class PlainTextRestoreTask extends GenericRestoreTask { + public PlainTextRestoreTask(Context context, Uri uri) { + super(context, uri); + } + + @Override + @NonNull + protected RestoreTaskResult doInBackground() { + String data = StorageAccessHelper.loadFileString(applicationContext, uri); + return RestoreTaskResult.success(data); + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Tasks/UiBasedBackgroundTask.java b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/UiBasedBackgroundTask.java new file mode 100644 index 00000000..4293eb5a --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Tasks/UiBasedBackgroundTask.java @@ -0,0 +1,109 @@ +package org.shadowice.flocke.andotp.Tasks; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.AnyThread; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** Encapsulates a background task that needs to communicate back to the UI (on the main thread) to + * provide a result. */ +public abstract class UiBasedBackgroundTask { + + private final Result failedResult; + private final ExecutorService executor; + private final Handler mainThreadHandler; + + private final Object callbackLock = new Object(); + @Nullable + private UiCallback callback; + @Nullable + private Result awaitedResult; + + private volatile boolean isCanceled = false; + + /** @param failedResult The result to return if the task fails (throws an exception or returns null). */ + public UiBasedBackgroundTask(@NonNull Result failedResult) { + this.failedResult = failedResult; + this.executor = Executors.newSingleThreadExecutor(); + this.mainThreadHandler = new Handler(Looper.getMainLooper()); + } + + /** @param callback If null, any results which may arrive from a currently executing task will + * be stored until a new callback is set. */ + public void setCallback(@Nullable UiCallback callback) { + synchronized (callbackLock) { + // Don't bother doing anything if the task was canceled. + if (isCanceled()) { + return; + } + this.callback = callback; + // If we have an awaited result and are setting a new callback, publish the result immediately. + if (awaitedResult != null && callback != null) { + emitResultOnMainThread(callback, awaitedResult); + } + } + } + + private void emitResultOnMainThread(@NonNull UiCallback callback, @NonNull Result result) { + mainThreadHandler.post(() -> callback.onResult(result)); + this.callback = null; + this.awaitedResult = null; + } + + /** Executed the task on a background thread. Safe to call from the main thread. */ + @AnyThread + public void execute() { + executor.execute(this::runTask); + } + + private void runTask() { + Result result = failedResult; + try { + result = doInBackground(); + } catch (Exception e) { + Log.e("UiBasedBackgroundTask", "Problem running background task", e); + } + + synchronized (callbackLock) { + // Don't bother issuing callback or storing result if this task is canceled. + if (isCanceled()) { + return; + } + if (callback != null) { + emitResultOnMainThread(callback, result); + } else { + awaitedResult = result; + } + } + } + + /** Work to be done in a background thread. + * @return Return the result from this task's execution. + * @throws Exception If an Exception is thrown from this task's execution, it will be logged + * and the provided default Result will be returned. */ + @NonNull + protected abstract Result doInBackground() throws Exception; + + @AnyThread + public boolean isCanceled() { + return isCanceled; + } + + @AnyThread + public void cancel() { + isCanceled = true; + } + + @FunctionalInterface + public interface UiCallback { + @MainThread + void onResult(@NonNull Result result); + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupAgent.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupAgent.java index ead9e95f..9d154eb9 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupAgent.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupAgent.java @@ -29,6 +29,8 @@ import android.app.backup.BackupDataOutput; import android.app.backup.FileBackupHelper; import android.app.backup.SharedPreferencesBackupHelper; import android.os.ParcelFileDescriptor; +import android.util.Log; + import java.io.IOException; @@ -44,19 +46,29 @@ public class BackupAgent extends BackupAgentHelper { @Override public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException { Settings settings = new Settings(this); + StringBuilder stringBuilder = new StringBuilder("onBackup called with the backup service set to "); + stringBuilder.append(settings.getAndroidBackupServiceEnabled() ? "enabled" : "disabled"); if(settings.getAndroidBackupServiceEnabled()) { synchronized (DatabaseHelper.DatabaseFileLock) { + stringBuilder.append(" calling parent onBackup"); super.onBackup(oldState, data, newState); } } + Log.d(BackupAgent.class.getSimpleName(), stringBuilder.toString()); } @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException { + Settings settings = new Settings(this); + StringBuilder stringBuilder = new StringBuilder("onRestore called with the backup service set to "); + stringBuilder.append(settings.getAndroidBackupServiceEnabled() ? "enabled" : "disabled"); + synchronized (DatabaseHelper.DatabaseFileLock) { + stringBuilder.append(" but restore happens regardless, calling parent onRestore"); super.onRestore(data, appVersionCode, newState); } + Log.d(BackupAgent.class.getSimpleName(), stringBuilder.toString()); } @Override diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupHelper.java index ade1bcc8..f6377303 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/BackupHelper.java @@ -101,11 +101,17 @@ public class BackupHelper { return Constants.BackupType.UNAVAILABLE; } - public static boolean backupToFile(Context context, Uri uri, String password, SecretKey encryptionKey) - { + public static boolean backupToFile(Context context, Uri uri, String password, SecretKey encryptionKey) { ArrayList entries = DatabaseHelper.loadDatabase(context, encryptionKey); String plain = DatabaseHelper.entriesToString(entries); + return backupToFile(context, uri, password, plain); + } + + public static boolean backupToFile(Context context, Uri uri, String password, String plain) + { + boolean success = true; + try { int iter = EncryptionHelper.generateRandomIterations(); byte[] salt = EncryptionHelper.generateRandom(Constants.ENCRYPTION_IV_LENGTH); @@ -120,12 +126,12 @@ public class BackupHelper { System.arraycopy(salt, 0, data, Constants.INT_LENGTH, Constants.ENCRYPTION_IV_LENGTH); System.arraycopy(encrypted, 0, data, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH, encrypted.length); - StorageAccessHelper.saveFile(context, uri, data); + success = StorageAccessHelper.saveFile(context, uri, data); } catch (Exception e) { e.printStackTrace(); - return false; + success = false; } - return true; + return success; } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EntryThumbnail.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EntryThumbnail.java index dd27bb50..7c13a172 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EntryThumbnail.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EntryThumbnail.java @@ -79,6 +79,7 @@ public class EntryThumbnail { AirBNB(R.drawable.thumb_airbnb), Airbrake(R.drawable.thumb_airbrake), AirTable(R.drawable.thumb_airtable), + AirVPN(R.drawable.thumb_airvpn), AllegroPl(R.drawable.thumb_allegropl), Alwaysdata(R.drawable.thumb_alwaysdata), Amazon(R.drawable.thumb_amazon), @@ -86,16 +87,19 @@ public class EntryThumbnail { AngelList(R.drawable.thumb_angellist), AnimeBytes(R.drawable.thumb_animebytes), Anonaddy(R.drawable.thumb_anonaddy), + AnyDesk(R.drawable.thumb_anydesk), Apache(R.drawable.thumb_apache), Apple(R.drawable.thumb_apple), Appveyor(R.drawable.thumb_appveyor), ArenaNet(R.drawable.thumb_arenanet), AtlanticNet(R.drawable.thumb_atlantic_net), Atlassian(R.drawable.thumb_atlassian), + Autodesk(R.drawable.thumb_autodesk), AutoDNS(R.drawable.thumb_autodns), AVM(R.drawable.thumb_avm), Backblaze(R.drawable.thumb_backblaze), BattleNet(R.drawable.thumb_battlenet), + BestBuy(R.drawable.thumb_bestbuy), Betterment(R.drawable.thumb_betterment), Binance(R.drawable.thumb_binance), BitBucket(R.drawable.thumb_bitbucket), @@ -109,14 +113,18 @@ public class EntryThumbnail { Bitwala(R.drawable.thumb_bitwala), Bitwarden(R.drawable.thumb_bitwarden), BlockchainInfo(R.drawable.thumb_blockchain_info), + Boxcryptor(R.drawable.thumb_boxcryptor), + BraveRewards(R.drawable.thumb_braverewards), Bugcrowd(R.drawable.thumb_bugcrowd), CEXio(R.drawable.thumb_cexio), ChurchTools(R.drawable.thumb_church_tools), Cisco(R.drawable.thumb_cisco), + Citrix(R.drawable.thumb_citrix), CloudDownload(R.drawable.thumb_cloud_download), Cloudflare(R.drawable.thumb_cloudflare), Clubhouse(R.drawable.thumb_clubhouse), Cobranded(R.drawable.thumb_cobranded), + Codeberg(R.drawable.thumb_codeberg), Codegiant(R.drawable.thumb_codegiant), Coinbase(R.drawable.thumb_coinbase), CoinJar(R.drawable.thumb_coinjar), @@ -124,12 +132,14 @@ public class EntryThumbnail { ComputerBase(R.drawable.thumb_computerbase), ConnectWiseManage(R.drawable.thumb_connectwise_manage), CozyCloud(R.drawable.thumb_cozycloud), + CrowdSupply(R.drawable.thumb_crowd_supply), Crowdin(R.drawable.thumb_crowdin), Cyon(R.drawable.thumb_cyon), Dashlane(R.drawable.thumb_dashlane), Debian(R.drawable.thumb_debian), Degiro(R.drawable.thumb_degiro), Denic(R.drawable.thumb_denic), + Deribit(R.drawable.thumb_deribit), Diaspora(R.drawable.thumb_diaspora), Digidentity(R.drawable.thumb_digidentity), DigitalOcean(R.drawable.thumb_digital_ocean), @@ -152,6 +162,7 @@ public class EntryThumbnail { FACEIT(R.drawable.thumb_faceit), Fanatical(R.drawable.thumb_fanatical), Fastmail(R.drawable.thumb_fastmail), + FidelityInvestments(R.drawable.thumb_fidelity_investments), Figma(R.drawable.thumb_figma), Fingerprint(R.drawable.thumb_fingerprint), Finnair(R.drawable.thumb_finnair), @@ -168,6 +179,7 @@ public class EntryThumbnail { GitHub(R.drawable.thumb_github), GitLab(R.drawable.thumb_gitlab), GMX(R.drawable.thumb_gmx), + GoatCounter(R.drawable.thumb_goatcounter), GoDaddy(R.drawable.thumb_godaddy), Gogs(R.drawable.thumb_gogs), Google(R.drawable.thumb_google), @@ -180,12 +192,14 @@ public class EntryThumbnail { HitBTC(R.drawable.thumb_hitbtc), HMRC(R.drawable.thumb_hmrc), HomeAssistant(R.drawable.thumb_home_assistant), + HostingDE(R.drawable.thumb_hosting_de), Hover(R.drawable.thumb_hover), HumbleBundle(R.drawable.thumb_humblebundle), HurricaneElectric(R.drawable.thumb_hurricane_electric), IBM(R.drawable.thumb_ibm), Iconomi(R.drawable.thumb_iconomi), IFTTT(R.drawable.thumb_ifttt), + ImmoScout24(R.drawable.thumb_immo_scout24), Infomaniak(R.drawable.thumb_infomaniak), Instagram(R.drawable.thumb_instagram), INWX(R.drawable.thumb_inwx), @@ -193,7 +207,9 @@ public class EntryThumbnail { Jagex(R.drawable.thumb_jagex), JetBrains(R.drawable.thumb_jetbrains), Joomla(R.drawable.thumb_joomla), + KDE(R.drawable.thumb_kde), Keeper(R.drawable.thumb_keeper), + Keycloak(R.drawable.thumb_keycloak), Kickstarter(R.drawable.thumb_kickstarter), Kraken(R.drawable.thumb_kraken), Kucoin(R.drawable.thumb_kucoin), @@ -209,17 +225,23 @@ public class EntryThumbnail { LocalMonero(R.drawable.thumb_localmonero), LoginGov(R.drawable.thumb_login_gov), LogMeIn(R.drawable.thumb_logmein), + LuaDNS(R.drawable.thumb_luadns), + MailDE(R.drawable.thumb_mail_de), Mailbox(R.drawable.thumb_mailbox), Mailchimp(R.drawable.thumb_mailchimp), Mailcow(R.drawable.thumb_mailcow), Mailgun(R.drawable.thumb_mailgun), Mailru(R.drawable.thumb_mailru), + Malwaretips(R.drawable.thumb_malwaretips), + Mangadex(R.drawable.thumb_mangadex), Mapbox(R.drawable.thumb_mapbox), Mastodon(R.drawable.thumb_mastodon), Matomo(R.drawable.thumb_matomo), + Mattermost(R.drawable.thumb_mattermost), Mediawiki(R.mipmap.thumb_mediawiki, AssetType.Bitmap), Mega(R.drawable.thumb_mega), MercadoLibre(R.drawable.thumb_mercadolibre), + Mercury(R.drawable.thumb_mercury), Microsoft(R.drawable.thumb_microsoft), MicrosoftTeams(R.drawable.thumb_microsoft_teams), Migadu(R.drawable.thumb_migadu), @@ -229,11 +251,14 @@ public class EntryThumbnail { Mixer(R.drawable.thumb_mixer), MongoDB(R.drawable.thumb_mongodb), MVPSnet(R.drawable.thumb_mvpsnet), + MyGovAU(R.drawable.thumb_my_gov_au), NameCheap(R.drawable.thumb_namecheap), NameCom(R.drawable.thumb_namecom), NAS(R.drawable.thumb_nas), netcup(R.drawable.thumb_netcup), + netlify(R.drawable.thumb_netlify), NextCloud(R.drawable.thumb_nextcloud), + NextDNS(R.drawable.thumb_nextdns), Nintendo(R.drawable.thumb_nintendo), NoStarchPress(R.drawable.thumb_no_starch_press), NOYB(R.drawable.thumb_noyb), @@ -257,8 +282,11 @@ public class EntryThumbnail { PaySafe(R.drawable.thumb_paysafecard), PayWithPrivacy(R.drawable.thumb_paywithprivacy), PCloud(R.drawable.thumb_pcloud), + PeeringDB(R.drawable.thumb_peeringdb), Phabricator(R.drawable.thumb_phabricator), phpMyAdmin(R.drawable.thumb_phpmyadmin), + PlayStation(R.drawable.thumb_playstation), + Plex(R.drawable.thumb_plex), Pluralsight(R.drawable.thumb_pluralsight), Plurk(R.drawable.thumb_plurk), Posteo(R.drawable.thumb_posteo), @@ -281,15 +309,19 @@ public class EntryThumbnail { RSS(R.drawable.thumb_rss), Samsung(R.drawable.thumb_samsung), SAP(R.drawable.thumb_sap), + Saxobank(R.drawable.thumb_saxobank), Scaleway(R.drawable.thumb_scaleway), School(R.drawable.thumb_school), Sciebo(R.drawable.thumb_sciebo), + SelfWealth(R.drawable.thumb_selfwealth), Seafile(R.mipmap.thumb_seafile, AssetType.Bitmap), Sentry(R.drawable.thumb_sentry), Sevdesk(R.drawable.thumb_sevdesk), + SimpleLogin(R.drawable.thumb_simple_login), Skrill(R.drawable.thumb_skrill), Slack(R.drawable.thumb_slack), Snapchat(R.drawable.thumb_snapchat), + SOGo(R.drawable.thumb_sogo), Sophos(R.drawable.thumb_sophos), SourceForge(R.drawable.thumb_sourceforge), Squarespace(R.drawable.thumb_squarespace), @@ -297,14 +329,18 @@ public class EntryThumbnail { StarCitizen(R.drawable.thumb_starcitizen), Steam(R.drawable.thumb_steam), Stripe(R.drawable.thumb_stripe), + Sumologic(R.drawable.thumb_sumologic), Sync(R.drawable.thumb_sync), Synology(R.drawable.thumb_synology), TeaHub(R.drawable.thumb_teahub), TeamViewer(R.drawable.thumb_teamviewer), Terminal(R.drawable.thumb_terminal), + TeslaMotors(R.drawable.thumb_tesla_motors), + TMobile(R.drawable.thumb_t_mobile), TransIP(R.drawable.thumb_transip), Trello(R.drawable.thumb_trello), Tumblr(R.drawable.thumb_tumblr), + TUBerlin(R.drawable.thumb_tu_berlin), TurboTax(R.drawable.thumb_turbotax), Tutanota(R.drawable.thumb_tutanota), TUWien(R.drawable.thumb_tuwien_ac_at), @@ -314,8 +350,8 @@ public class EntryThumbnail { Uber(R.drawable.thumb_uber), UbiquitiNetworks(R.drawable.thumb_ubnt), Ubisoft(R.drawable.thumb_ubisoft), - Unity(R.drawable.thumb_unity), UbuntuOne(R.drawable.thumb_ubuntu_one), + Unity(R.drawable.thumb_unity), Uphold(R.drawable.thumb_uphold), USAA(R.drawable.thumb_usaa), VagrantCloud(R.drawable.thumb_vagrant_cloud), @@ -326,9 +362,10 @@ public class EntryThumbnail { Wallet(R.drawable.thumb_wallet), Wargaming(R.drawable.thumb_wargaming), Wasabi(R.drawable.thumb_wasabi), - Weclapp(R.drawable.thumb_weclapp), WebDe(R.drawable.thumb_web_de), + Weclapp(R.drawable.thumb_weclapp), Wikimedia(R.drawable.thumb_wikimedia), + Wildduck(R.drawable.thumb_wildduck), Wordfence(R.drawable.thumb_wordfence), Wordpress(R.drawable.thumb_wordpress), Workplace(R.drawable.thumb_workplace), @@ -341,8 +378,8 @@ public class EntryThumbnail { Zoho(R.drawable.thumb_zoho), Zoom(R.drawable.thumb_zoom); - private int resource; - private AssetType assetType; + private final int resource; + private final AssetType assetType; EntryThumbnails(int resource) { this.resource = resource; diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java index e57ee557..c8853c3e 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java @@ -576,6 +576,10 @@ public class Settings { return getBoolean(R.string.settings_key_show_individual_timeouts, false); } + public boolean isFocusSearchOnStartEnabled() { + return getBoolean(R.string.settings_key_focus_search_on_start, false); + } + public Constants.TapMode getTapSingle() { String singleTap = getString(R.string.settings_key_tap_single, R.string.settings_default_tap_single); return Constants.TapMode.valueOf(singleTap.toUpperCase(Locale.ENGLISH)); @@ -601,4 +605,13 @@ public class Settings { public boolean getBlockAutofill() { return getBoolean(R.string.settings_key_block_autofill, false); } + + public void setDefaultBackupType(Constants.BackupType type) { + setString(R.string.settings_key_backup_default_type, type.name().toLowerCase(Locale.ENGLISH)); + } + + public Constants.BackupType getDefaultBackupType() { + String defaultType = getString(R.string.settings_key_backup_default_type, Constants.BackupType.ENCRYPTED.name()); + return Constants.BackupType.valueOf(defaultType.toUpperCase(Locale.ENGLISH)); + } } diff --git a/app/src/main/res/drawable/thumb_airvpn.xml b/app/src/main/res/drawable/thumb_airvpn.xml new file mode 100644 index 00000000..58b98ff9 --- /dev/null +++ b/app/src/main/res/drawable/thumb_airvpn.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/thumb_anydesk.xml b/app/src/main/res/drawable/thumb_anydesk.xml new file mode 100644 index 00000000..595eaeed --- /dev/null +++ b/app/src/main/res/drawable/thumb_anydesk.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_autodesk.xml b/app/src/main/res/drawable/thumb_autodesk.xml new file mode 100644 index 00000000..65a46b68 --- /dev/null +++ b/app/src/main/res/drawable/thumb_autodesk.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_bestbuy.xml b/app/src/main/res/drawable/thumb_bestbuy.xml new file mode 100644 index 00000000..447964a4 --- /dev/null +++ b/app/src/main/res/drawable/thumb_bestbuy.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_boxcryptor.xml b/app/src/main/res/drawable/thumb_boxcryptor.xml new file mode 100644 index 00000000..637dc6e3 --- /dev/null +++ b/app/src/main/res/drawable/thumb_boxcryptor.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_braverewards.xml b/app/src/main/res/drawable/thumb_braverewards.xml new file mode 100644 index 00000000..a540f208 --- /dev/null +++ b/app/src/main/res/drawable/thumb_braverewards.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/thumb_citrix.xml b/app/src/main/res/drawable/thumb_citrix.xml new file mode 100644 index 00000000..e3cc1035 --- /dev/null +++ b/app/src/main/res/drawable/thumb_citrix.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_codeberg.xml b/app/src/main/res/drawable/thumb_codeberg.xml new file mode 100644 index 00000000..4f0bba55 --- /dev/null +++ b/app/src/main/res/drawable/thumb_codeberg.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_crowd_supply.xml b/app/src/main/res/drawable/thumb_crowd_supply.xml new file mode 100644 index 00000000..04e65b37 --- /dev/null +++ b/app/src/main/res/drawable/thumb_crowd_supply.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_dashlane.xml b/app/src/main/res/drawable/thumb_dashlane.xml index 6b7bea2c..7067e236 100644 --- a/app/src/main/res/drawable/thumb_dashlane.xml +++ b/app/src/main/res/drawable/thumb_dashlane.xml @@ -1,9 +1,24 @@ - - \ No newline at end of file + android:width="91.03dp" + android:height="122.87dp" + android:viewportWidth="91.03" + android:viewportHeight="122.87"> + + + + + + + diff --git a/app/src/main/res/drawable/thumb_deribit.xml b/app/src/main/res/drawable/thumb_deribit.xml new file mode 100644 index 00000000..73339e85 --- /dev/null +++ b/app/src/main/res/drawable/thumb_deribit.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/thumb_fidelity_investments.xml b/app/src/main/res/drawable/thumb_fidelity_investments.xml new file mode 100644 index 00000000..7dddd7a5 --- /dev/null +++ b/app/src/main/res/drawable/thumb_fidelity_investments.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/thumb_goatcounter.xml b/app/src/main/res/drawable/thumb_goatcounter.xml new file mode 100644 index 00000000..6c48dcc8 --- /dev/null +++ b/app/src/main/res/drawable/thumb_goatcounter.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_hosting_de.xml b/app/src/main/res/drawable/thumb_hosting_de.xml new file mode 100644 index 00000000..6c3957f0 --- /dev/null +++ b/app/src/main/res/drawable/thumb_hosting_de.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/thumb_immo_scout24.xml b/app/src/main/res/drawable/thumb_immo_scout24.xml new file mode 100644 index 00000000..2961240a --- /dev/null +++ b/app/src/main/res/drawable/thumb_immo_scout24.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_kde.xml b/app/src/main/res/drawable/thumb_kde.xml new file mode 100644 index 00000000..227859fa --- /dev/null +++ b/app/src/main/res/drawable/thumb_kde.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_keycloak.xml b/app/src/main/res/drawable/thumb_keycloak.xml new file mode 100644 index 00000000..f70d92ad --- /dev/null +++ b/app/src/main/res/drawable/thumb_keycloak.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_luadns.xml b/app/src/main/res/drawable/thumb_luadns.xml new file mode 100644 index 00000000..6575299c --- /dev/null +++ b/app/src/main/res/drawable/thumb_luadns.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_mail_de.xml b/app/src/main/res/drawable/thumb_mail_de.xml new file mode 100644 index 00000000..b019c420 --- /dev/null +++ b/app/src/main/res/drawable/thumb_mail_de.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_malwaretips.xml b/app/src/main/res/drawable/thumb_malwaretips.xml new file mode 100644 index 00000000..7f5e7a08 --- /dev/null +++ b/app/src/main/res/drawable/thumb_malwaretips.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_mangadex.xml b/app/src/main/res/drawable/thumb_mangadex.xml new file mode 100644 index 00000000..fcdb3329 --- /dev/null +++ b/app/src/main/res/drawable/thumb_mangadex.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_mattermost.xml b/app/src/main/res/drawable/thumb_mattermost.xml new file mode 100644 index 00000000..8a0e2f88 --- /dev/null +++ b/app/src/main/res/drawable/thumb_mattermost.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_mercury.xml b/app/src/main/res/drawable/thumb_mercury.xml new file mode 100644 index 00000000..32b2e5d7 --- /dev/null +++ b/app/src/main/res/drawable/thumb_mercury.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/thumb_my_gov_au.xml b/app/src/main/res/drawable/thumb_my_gov_au.xml new file mode 100644 index 00000000..b06246a0 --- /dev/null +++ b/app/src/main/res/drawable/thumb_my_gov_au.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_netlify.xml b/app/src/main/res/drawable/thumb_netlify.xml new file mode 100644 index 00000000..214cd98a --- /dev/null +++ b/app/src/main/res/drawable/thumb_netlify.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_nextdns.xml b/app/src/main/res/drawable/thumb_nextdns.xml new file mode 100644 index 00000000..a729fc74 --- /dev/null +++ b/app/src/main/res/drawable/thumb_nextdns.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_peeringdb.xml b/app/src/main/res/drawable/thumb_peeringdb.xml new file mode 100644 index 00000000..f37a8e09 --- /dev/null +++ b/app/src/main/res/drawable/thumb_peeringdb.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_playstation.xml b/app/src/main/res/drawable/thumb_playstation.xml new file mode 100644 index 00000000..51e2463a --- /dev/null +++ b/app/src/main/res/drawable/thumb_playstation.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/thumb_plex.xml b/app/src/main/res/drawable/thumb_plex.xml new file mode 100644 index 00000000..09c4b76b --- /dev/null +++ b/app/src/main/res/drawable/thumb_plex.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_saxobank.xml b/app/src/main/res/drawable/thumb_saxobank.xml new file mode 100644 index 00000000..c4d33497 --- /dev/null +++ b/app/src/main/res/drawable/thumb_saxobank.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_selfwealth.xml b/app/src/main/res/drawable/thumb_selfwealth.xml new file mode 100644 index 00000000..5ecb1971 --- /dev/null +++ b/app/src/main/res/drawable/thumb_selfwealth.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/thumb_simple_login.xml b/app/src/main/res/drawable/thumb_simple_login.xml new file mode 100644 index 00000000..e5d64b6a --- /dev/null +++ b/app/src/main/res/drawable/thumb_simple_login.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_sogo.xml b/app/src/main/res/drawable/thumb_sogo.xml new file mode 100644 index 00000000..ecff3302 --- /dev/null +++ b/app/src/main/res/drawable/thumb_sogo.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/thumb_sumologic.xml b/app/src/main/res/drawable/thumb_sumologic.xml new file mode 100644 index 00000000..0df9164e --- /dev/null +++ b/app/src/main/res/drawable/thumb_sumologic.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_t_mobile.xml b/app/src/main/res/drawable/thumb_t_mobile.xml new file mode 100644 index 00000000..babb7f82 --- /dev/null +++ b/app/src/main/res/drawable/thumb_t_mobile.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/thumb_tesla_motors.xml b/app/src/main/res/drawable/thumb_tesla_motors.xml new file mode 100644 index 00000000..06564e82 --- /dev/null +++ b/app/src/main/res/drawable/thumb_tesla_motors.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_tu_berlin.xml b/app/src/main/res/drawable/thumb_tu_berlin.xml new file mode 100644 index 00000000..32a06cda --- /dev/null +++ b/app/src/main/res/drawable/thumb_tu_berlin.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/thumb_wildduck.xml b/app/src/main/res/drawable/thumb_wildduck.xml new file mode 100644 index 00000000..a1763514 --- /dev/null +++ b/app/src/main/res/drawable/thumb_wildduck.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index adf47974..eef7e889 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -66,7 +66,19 @@ android:paddingTop="@dimen/activity_margin_xsmall" android:paddingBottom="@dimen/fab_recyclerview_padding" android:clipToPadding="false" /> - + diff --git a/app/src/main/res/layout/component_intro_android_sync.xml b/app/src/main/res/layout/component_intro_android_sync.xml new file mode 100644 index 00000000..0bc7a73f --- /dev/null +++ b/app/src/main/res/layout/component_intro_android_sync.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_authenticate.xml b/app/src/main/res/layout/content_authenticate.xml index 21f9006b..0075336f 100644 --- a/app/src/main/res/layout/content_authenticate.xml +++ b/app/src/main/res/layout/content_authenticate.xml @@ -34,12 +34,27 @@ -