Import OTP codes from QR code image
This commit is contained in:
parent
e6db8ba5d9
commit
2d82bd5725
@ -1,7 +1,9 @@
|
||||
package com.cringe_studios.cringe_authenticator;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@ -9,7 +11,10 @@ import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.PickVisualMediaRequest;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@ -25,6 +30,7 @@ import com.cringe_studios.cringe_authenticator.fragment.MenuFragment;
|
||||
import com.cringe_studios.cringe_authenticator.fragment.NamedFragment;
|
||||
import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment;
|
||||
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
||||
import com.cringe_studios.cringe_authenticator.scanner.QRScanner;
|
||||
import com.cringe_studios.cringe_authenticator.scanner.QRScannerContract;
|
||||
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
||||
import com.cringe_studios.cringe_authenticator.util.NavigationUtil;
|
||||
@ -33,7 +39,10 @@ import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
|
||||
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
|
||||
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
|
||||
import com.cringe_studios.cringe_authenticator_library.OTPType;
|
||||
import com.google.mlkit.vision.common.InputImage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MainActivity extends BaseActivity {
|
||||
@ -42,9 +51,9 @@ public class MainActivity extends BaseActivity {
|
||||
|
||||
private ActivityResultLauncher<Void> startQRCodeScan;
|
||||
|
||||
private boolean unlocked;
|
||||
private ActivityResultLauncher<PickVisualMediaRequest> pickQRCodeImage;
|
||||
|
||||
private long pauseTime;
|
||||
private QRScanner qrScanner;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@ -56,6 +65,8 @@ public class MainActivity extends BaseActivity {
|
||||
|
||||
setLocale(SettingsUtil.getLocale(this));
|
||||
|
||||
qrScanner = new QRScanner();
|
||||
|
||||
startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> {
|
||||
if(obj == null) return; // Cancelled
|
||||
|
||||
@ -71,9 +82,51 @@ public class MainActivity extends BaseActivity {
|
||||
}
|
||||
});
|
||||
|
||||
pickQRCodeImage = registerForActivityResult(new ActivityResultContracts.PickVisualMedia(), img -> {
|
||||
try {
|
||||
InputImage image = InputImage.fromFilePath(this, img);
|
||||
qrScanner.scan(image, code -> {
|
||||
if(code == null) {
|
||||
DialogUtil.showErrorDialog(this, "No codes were detected in the provided image");
|
||||
return;
|
||||
}
|
||||
|
||||
if(code.isMigrationPart()) {
|
||||
new StyledDialogBuilder(this) // TODO: duplicated from QRScannerActivity
|
||||
.setTitle(R.string.qr_scanner_migration_title)
|
||||
.setMessage(R.string.qr_scanner_migration_message)
|
||||
.setPositiveButton(R.string.yes, (d, which) -> {
|
||||
Fragment fragment = NavigationUtil.getCurrentFragment(this);
|
||||
if (fragment instanceof GroupFragment) {
|
||||
GroupFragment frag = (GroupFragment) fragment;
|
||||
for (OTPData dt : code.getOTPs()) frag.addOTP(dt);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.no, (d, which) -> {})
|
||||
.show()
|
||||
.setCanceledOnTouchOutside(false);
|
||||
}else {
|
||||
Fragment fragment = NavigationUtil.getCurrentFragment(this);
|
||||
if (fragment instanceof GroupFragment) {
|
||||
GroupFragment frag = (GroupFragment) fragment;
|
||||
for (OTPData dt : code.getOTPs()) frag.addOTP(dt);
|
||||
}
|
||||
}
|
||||
}, error -> DialogUtil.showErrorDialog(this, "Failed to detect code: " + error));
|
||||
} catch (IOException e) {
|
||||
DialogUtil.showErrorDialog(this, "Failed to read image: " + e);
|
||||
}
|
||||
});
|
||||
|
||||
OTPDatabase.promptLoadDatabase(this, this::launchApp, this::finishAffinity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
qrScanner.close();
|
||||
}
|
||||
|
||||
public void setLocale(Locale locale) {
|
||||
Locale.setDefault(locale);
|
||||
Configuration config = new Configuration();
|
||||
@ -82,8 +135,6 @@ public class MainActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void launchApp() {
|
||||
unlocked = true;
|
||||
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
@ -91,7 +142,7 @@ public class MainActivity extends BaseActivity {
|
||||
|
||||
binding.fabMenu.setOnClickListener(view -> NavigationUtil.navigate(this, MenuFragment.class, null));
|
||||
binding.fabScan.setOnClickListener(view -> scanCode());
|
||||
//binding.fabScanImage.setOnClickListener(view -> scanCode()); TODO: scan image
|
||||
binding.fabScanImage.setOnClickListener(view -> scanCodeFromImage()); // TODO: scan image
|
||||
binding.fabInput.setOnClickListener(view -> inputCode());
|
||||
|
||||
Fragment fragment = NavigationUtil.getCurrentFragment(this);
|
||||
@ -148,6 +199,13 @@ public class MainActivity extends BaseActivity {
|
||||
startQRCodeScan.launch(null);
|
||||
}
|
||||
|
||||
public void scanCodeFromImage() {
|
||||
Object mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE; // Cursed, but needs to happen because otherwise Android Studio complains about type casting even though it works
|
||||
pickQRCodeImage.launch(new PickVisualMediaRequest.Builder()
|
||||
.setMediaType((ActivityResultContracts.PickVisualMedia.VisualMediaType) mediaType)
|
||||
.build());
|
||||
}
|
||||
|
||||
public void inputCode() {
|
||||
DialogInputCodeChoiceBinding binding = DialogInputCodeChoiceBinding.inflate(getLayoutInflater());
|
||||
|
||||
@ -208,15 +266,11 @@ public class MainActivity extends BaseActivity {
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
this.pauseTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if(unlocked) {
|
||||
outState.putLong("pauseTime", pauseTime);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.cringe_studios.cringe_authenticator.scanner;
|
||||
|
||||
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
||||
import com.cringe_studios.cringe_authenticator.model.OTPMigrationPart;
|
||||
import com.cringe_studios.cringe_authenticator.proto.OTPMigration;
|
||||
|
||||
public class DetectedCode {
|
||||
|
||||
private OTPData data;
|
||||
private OTPMigrationPart migrationPart;
|
||||
|
||||
public DetectedCode(OTPData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public DetectedCode(OTPMigrationPart migrationPart) {
|
||||
this.migrationPart = migrationPart;
|
||||
}
|
||||
|
||||
public boolean isMigrationPart() {
|
||||
return migrationPart != null;
|
||||
}
|
||||
|
||||
public OTPData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public OTPData[] getOTPs() {
|
||||
return data != null ? new OTPData[] { data } : migrationPart.getOTPs();
|
||||
}
|
||||
|
||||
public OTPMigrationPart getMigrationPart() {
|
||||
return migrationPart;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package com.cringe_studios.cringe_authenticator.scanner;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.cringe_studios.cringe_authenticator.model.OTPMigrationPart;
|
||||
import com.cringe_studios.cringe_authenticator.util.OTPParser;
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner;
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning;
|
||||
import com.google.mlkit.vision.barcode.common.Barcode;
|
||||
import com.google.mlkit.vision.common.InputImage;
|
||||
|
||||
public class QRScanner {
|
||||
|
||||
private BarcodeScanner scanner;
|
||||
|
||||
public QRScanner() {
|
||||
scanner = BarcodeScanning.getClient();
|
||||
}
|
||||
|
||||
public void scan(InputImage image, Consumer<DetectedCode> onSuccess, Consumer<String> onFailure) {
|
||||
scanner.process(image).addOnSuccessListener(barcodes -> {
|
||||
if(barcodes.isEmpty()) {
|
||||
onSuccess.accept(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Barcode code = null;
|
||||
|
||||
for(Barcode c : barcodes) {
|
||||
if(c.getValueType() == Barcode.TYPE_TEXT) {
|
||||
code = c; // TODO: maybe consider whether this code is actually valid (for cases with multiple text QR codes in one image)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(code == null) {
|
||||
onSuccess.accept(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(code.getRawValue());
|
||||
Result r = parse(uri);
|
||||
if(r.code != null) {
|
||||
onSuccess.accept(r.code);
|
||||
}else {
|
||||
onFailure.accept(r.error);
|
||||
}
|
||||
})
|
||||
.addOnFailureListener(e -> onFailure.accept(e.toString()));
|
||||
}
|
||||
|
||||
private Result parse(Uri uri) {
|
||||
if("otpauth-migration".equalsIgnoreCase(uri.getScheme())) {
|
||||
OTPMigrationPart part;
|
||||
|
||||
try {
|
||||
part = OTPParser.parseMigration(uri);
|
||||
}catch(IllegalArgumentException e) {
|
||||
return new Result(e.getMessage());
|
||||
}
|
||||
|
||||
return new Result(new DetectedCode(part));
|
||||
}
|
||||
|
||||
try {
|
||||
return new Result(new DetectedCode(OTPParser.parse(uri)));
|
||||
}catch(IllegalArgumentException e) {
|
||||
return new Result(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
scanner.close();
|
||||
}
|
||||
|
||||
private static class Result {
|
||||
|
||||
public final DetectedCode code;
|
||||
public final String error;
|
||||
|
||||
private Result(@NonNull DetectedCode code) {
|
||||
this.code = code;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
private Result(String error) {
|
||||
this.error = error;
|
||||
this.code = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -5,10 +5,8 @@ import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.Image;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
@ -33,13 +31,9 @@ import com.cringe_studios.cringe_authenticator.R;
|
||||
import com.cringe_studios.cringe_authenticator.databinding.ActivityQrScannerBinding;
|
||||
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
||||
import com.cringe_studios.cringe_authenticator.model.OTPMigrationPart;
|
||||
import com.cringe_studios.cringe_authenticator.util.OTPParser;
|
||||
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
|
||||
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner;
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning;
|
||||
import com.google.mlkit.vision.barcode.common.Barcode;
|
||||
import com.google.mlkit.vision.common.InputImage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -55,7 +49,7 @@ public class QRScannerActivity extends AppCompatActivity {
|
||||
|
||||
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
|
||||
|
||||
private BarcodeScanner scanner;
|
||||
private QRScanner scanner;
|
||||
|
||||
private boolean process = true;
|
||||
|
||||
@ -92,7 +86,7 @@ public class QRScannerActivity extends AppCompatActivity {
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
|
||||
scanner = BarcodeScanning.getClient();
|
||||
scanner = new QRScanner();
|
||||
}
|
||||
|
||||
void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
|
||||
@ -136,40 +130,27 @@ public class QRScannerActivity extends AppCompatActivity {
|
||||
Image mediaImage = image.getImage();
|
||||
if(mediaImage != null) {
|
||||
InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees());
|
||||
scanner.process(input).addOnSuccessListener(barcodes -> {
|
||||
scanner.scan(input, code -> {
|
||||
image.close();
|
||||
|
||||
if(barcodes.size() >= 1) {
|
||||
Barcode code = null;
|
||||
|
||||
for(Barcode c : barcodes) {
|
||||
if(c.getValueType() == Barcode.TYPE_TEXT) {
|
||||
code = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(code == null) return;
|
||||
|
||||
Uri uri = Uri.parse(code.getRawValue());
|
||||
if(code != null) {
|
||||
process = false;
|
||||
importUri(uri);
|
||||
importCode(code);
|
||||
}
|
||||
}).addOnFailureListener(e -> {});
|
||||
}, error -> {
|
||||
image.close();
|
||||
error(error);
|
||||
});
|
||||
/*.addOnSuccessListener(barcodes -> {
|
||||
|
||||
}).addOnFailureListener(e -> error("Failed to scan code"));*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importUri(Uri uri) {
|
||||
if("otpauth-migration".equalsIgnoreCase(uri.getScheme())) {
|
||||
OTPMigrationPart part;
|
||||
|
||||
try {
|
||||
part = OTPParser.parseMigration(uri);
|
||||
}catch(IllegalArgumentException e) {
|
||||
error(e.getMessage());
|
||||
return;
|
||||
}
|
||||
private void importCode(DetectedCode code) {
|
||||
if(code.isMigrationPart()) {
|
||||
OTPMigrationPart part = code.getMigrationPart();
|
||||
|
||||
if((lastPart != null && part.getBatchIndex() != lastPart.getBatchIndex() + 1) || (lastPart == null && part.getBatchIndex() > 0)) {
|
||||
// Not next batch, or first batch (if nothing was scanned yet), keep looking
|
||||
@ -206,7 +187,6 @@ public class QRScannerActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -217,12 +197,18 @@ public class QRScannerActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
try {
|
||||
success(OTPParser.parse(uri));
|
||||
success(code.getData());
|
||||
}catch(IllegalArgumentException e) {
|
||||
error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
scanner.close();
|
||||
}
|
||||
|
||||
private void success(@NonNull OTPData... data) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("data", data);
|
||||
|
Loading…
Reference in New Issue
Block a user