mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-16 15:02:54 +00:00
Add support for importing multiple QR code images in one go
This is also part of the preparation needed for scanning Google Authenticator Export QR codes from images.
This commit is contained in:
parent
b875baacef
commit
e46857a26e
36 changed files with 232 additions and 110 deletions
|
@ -0,0 +1,27 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
|
||||
public class SafHelper {
|
||||
private SafHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static String getFileName(Context context, Uri uri) {
|
||||
if (uri.getScheme() != null && uri.getScheme().equals("content")) {
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int i = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (i != -1) {
|
||||
return cursor.getString(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
@ -36,7 +32,6 @@ import java.io.FileInputStream;
|
|||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ImportEntriesActivity extends AegisActivity {
|
||||
|
@ -190,39 +185,11 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
|
||||
List<DatabaseImporterEntryException> errors = result.getErrors();
|
||||
if (errors.size() > 0) {
|
||||
showErrorDialog(errors);
|
||||
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
|
||||
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, message, errors, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
|
||||
.create());
|
||||
}
|
||||
|
||||
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
List<String> messages = new ArrayList<>();
|
||||
for (DatabaseImporterEntryException e : errors) {
|
||||
messages.add(e.getMessage());
|
||||
}
|
||||
|
||||
String message = TextUtils.join("\n\n", messages);
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", message);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, R.string.errors_copied, Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void showWipeEntriesDialog() {
|
||||
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
|
||||
R.string.dialog_wipe_entries_message,
|
||||
|
|
|
@ -4,10 +4,15 @@ import android.Manifest;
|
|||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -38,6 +43,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog;
|
|||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
|
@ -275,17 +281,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
|
||||
private void onScanResult(Intent data) {
|
||||
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
|
||||
if (entries.size() == 1) {
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
|
||||
} else {
|
||||
for (VaultEntry entry : entries) {
|
||||
_vaultManager.getVault().addEntry(entry);
|
||||
if (_loaded) {
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
if (entries != null) {
|
||||
importScannedEntries(entries);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,25 +308,78 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
|
||||
private void onScanImageResult(Intent intent) {
|
||||
startDecodeQrCodeImage(intent.getData());
|
||||
if (intent.getData() != null) {
|
||||
startDecodeQrCodeImages(Collections.singletonList(intent.getData()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.getClipData() != null) {
|
||||
ClipData data = intent.getClipData();
|
||||
|
||||
List<Uri> uris = new ArrayList<>();
|
||||
for (int i = 0; i < data.getItemCount(); i++) {
|
||||
ClipData.Item item = data.getItemAt(i);
|
||||
if (item.getUri() != null) {
|
||||
uris.add(item.getUri());
|
||||
}
|
||||
}
|
||||
|
||||
if (uris.size() > 0) {
|
||||
startDecodeQrCodeImages(uris);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startDecodeQrCodeImage(Uri uri) {
|
||||
QrDecodeTask task = new QrDecodeTask(this, (result) -> {
|
||||
if (result.getException() != null) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, result.getException());
|
||||
return;
|
||||
private static CharSequence buildImportError(String fileName, Throwable e) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(String.format("%s:\n%s", fileName, e));
|
||||
builder.setSpan(new StyleSpan(Typeface.BOLD), 0, fileName.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void startDecodeQrCodeImages(List<Uri> uris) {
|
||||
QrDecodeTask task = new QrDecodeTask(this, (results) -> {
|
||||
List<CharSequence> errors = new ArrayList<>();
|
||||
List<VaultEntry> entries = new ArrayList<>();
|
||||
for (QrDecodeTask.Result res : results) {
|
||||
if (res.getException() != null) {
|
||||
errors.add(buildImportError(res.getFileName(), res.getException()));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(res.getResult().getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
entries.add(entry);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
errors.add(buildImportError(res.getFileName(), e));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getResult().getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
final DialogInterface.OnClickListener dialogDismissHandler = (dialog, which) -> importScannedEntries(entries);
|
||||
if ((errors.size() > 0 && results.size() > 1) || errors.size() > 1) {
|
||||
Dialogs.showMultiMessageDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler);
|
||||
} else if (errors.size() > 0) {
|
||||
Dialogs.showErrorDialog(this, getString(R.string.unable_to_read_qrcode_file, results.get(0).getFileName()), errors.get(0), dialogDismissHandler);
|
||||
} else {
|
||||
importScannedEntries(entries);
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), uri);
|
||||
task.execute(getLifecycle(), uris);
|
||||
}
|
||||
|
||||
private void importScannedEntries(List<VaultEntry> entries) {
|
||||
if (entries.size() == 1) {
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
|
||||
} else if (entries.size() > 1) {
|
||||
for (VaultEntry entry: entries) {
|
||||
_vaultManager.getVault().addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
if (saveAndBackupVault()) {
|
||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSortCategoryMenu() {
|
||||
|
@ -368,9 +418,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
|
||||
private void startScanImageActivity() {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
fileIntent.setType("image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
|
||||
|
@ -421,7 +473,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
info = GoogleAuthInfo.parseUri(uri);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_process_deeplink, e);
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
|
@ -444,7 +496,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
intent.setAction(null);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
startDecodeQrCodeImage(uri);
|
||||
startDecodeQrCodeImages(Collections.singletonList(uri));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.content.res.ColorStateList;
|
|||
import android.graphics.Color;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -24,6 +25,7 @@ import android.widget.ListView;
|
|||
import android.widget.NumberPicker;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.StringRes;
|
||||
|
@ -44,6 +46,7 @@ import com.google.android.material.textfield.TextInputLayout;
|
|||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -340,6 +343,10 @@ public class Dialogs {
|
|||
showErrorDialog(context, message, e, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String message, Exception e) {
|
||||
showErrorDialog(context, message, e, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error) {
|
||||
showErrorDialog(context, message, error, null);
|
||||
}
|
||||
|
@ -348,7 +355,15 @@ public class Dialogs {
|
|||
showErrorDialog(context, message, e.toString(), listener);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String message, Exception e, DialogInterface.OnClickListener listener) {
|
||||
showErrorDialog(context, message, e.toString(), listener);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error, DialogInterface.OnClickListener listener) {
|
||||
showErrorDialog(context, context.getString(message), error, listener);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, String message, CharSequence error, DialogInterface.OnClickListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_error, null);
|
||||
TextView textDetails = view.findViewById(R.id.error_details);
|
||||
textDetails.setText(error);
|
||||
|
@ -388,6 +403,66 @@ public class Dialogs {
|
|||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showMultiMessageDialog(
|
||||
Context context, @StringRes int title, String message, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog, which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(context.getString(R.string.details), (dialog, which) -> {
|
||||
showDetailedMultiMessageDialog(context, title, messages, listener);
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
public static <T extends Throwable> void showMultiErrorDialog(
|
||||
Context context, @StringRes int title, String message, List<T> errors, DialogInterface.OnClickListener listener) {
|
||||
List<CharSequence> messages = new ArrayList<>();
|
||||
for (Throwable e : errors) {
|
||||
messages.add(e.toString());
|
||||
}
|
||||
|
||||
showMultiMessageDialog(context, title, message, messages, listener);
|
||||
}
|
||||
|
||||
private static void showDetailedMultiMessageDialog(
|
||||
Context context, @StringRes int title, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
for (CharSequence message : messages) {
|
||||
builder.append(message);
|
||||
builder.append("\n\n");
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(builder)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(android.R.string.copy, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
button.setOnClickListener(v -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", builder.toString());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(context, R.string.errors_copied, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showTimeSyncWarningDialog(Context context, Dialog.OnClickListener listener) {
|
||||
Preferences prefs = new Preferences(context);
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_time_sync, null);
|
||||
|
|
|
@ -5,12 +5,14 @@ import android.net.Uri;
|
|||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
|
||||
import com.google.zxing.Result;
|
||||
import com.beemdevelopment.aegis.helpers.SafHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class QrDecodeTask extends ProgressDialogTask<Uri, QrDecodeTask.Response> {
|
||||
public class QrDecodeTask extends ProgressDialogTask<List<Uri>, List<QrDecodeTask.Result>> {
|
||||
private final Callback _cb;
|
||||
|
||||
public QrDecodeTask(Context context, Callback cb) {
|
||||
|
@ -19,39 +21,61 @@ public class QrDecodeTask extends ProgressDialogTask<Uri, QrDecodeTask.Response>
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Response doInBackground(Uri... params) {
|
||||
protected List<Result> doInBackground(List<Uri>... params) {
|
||||
List<Result> res = new ArrayList<>();
|
||||
Context context = getDialog().getContext();
|
||||
|
||||
Uri uri = params[0];
|
||||
try (InputStream inStream = context.getContentResolver().openInputStream(uri)) {
|
||||
Result result = QrCodeHelper.decodeFromStream(inStream);
|
||||
return new Response(result, null);
|
||||
} catch (QrCodeHelper.DecodeError | IOException e) {
|
||||
e.printStackTrace();
|
||||
return new Response(null, e);
|
||||
List<Uri> uris = params[0];
|
||||
for (Uri uri : uris) {
|
||||
String fileName = SafHelper.getFileName(context, uri);
|
||||
if (uris.size() > 1) {
|
||||
publishProgress(context.getString(R.string.analyzing_qr_multiple, uris.indexOf(uri) + 1, uris.size(), fileName));
|
||||
}
|
||||
|
||||
try (InputStream inStream = context.getContentResolver().openInputStream(uri)) {
|
||||
com.google.zxing.Result result = QrCodeHelper.decodeFromStream(inStream);
|
||||
res.add(new Result(uri, fileName, result, null));
|
||||
} catch (QrCodeHelper.DecodeError | IOException e) {
|
||||
e.printStackTrace();
|
||||
res.add(new Result(uri, fileName, null, e));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Response result) {
|
||||
super.onPostExecute(result);
|
||||
_cb.onTaskFinished(result);
|
||||
protected void onPostExecute(List<Result> results) {
|
||||
super.onPostExecute(results);
|
||||
_cb.onTaskFinished(results);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(Response result);
|
||||
void onTaskFinished(List<Result> results);
|
||||
}
|
||||
|
||||
public static class Response {
|
||||
private final Result _result;
|
||||
public static class Result {
|
||||
private final Uri _uri;
|
||||
private final String _fileName;
|
||||
private final com.google.zxing.Result _result;
|
||||
private final Exception _e;
|
||||
|
||||
public Response(Result result, Exception e) {
|
||||
public Result(Uri uri, String fileName, com.google.zxing.Result result, Exception e) {
|
||||
_uri = uri;
|
||||
_fileName = fileName;
|
||||
_result = result;
|
||||
_e = e;
|
||||
}
|
||||
|
||||
public Result getResult() {
|
||||
public Uri getUri() {
|
||||
return _uri;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return _fileName;
|
||||
}
|
||||
|
||||
public com.google.zxing.Result getResult() {
|
||||
return _result;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue