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;
|
package com.cringe_studios.cringe_authenticator;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -9,7 +11,10 @@ import android.widget.AdapterView;
|
|||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResult;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
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.NamedFragment;
|
||||||
import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment;
|
import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment;
|
||||||
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
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.scanner.QRScannerContract;
|
||||||
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
||||||
import com.cringe_studios.cringe_authenticator.util.NavigationUtil;
|
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.StyledDialogBuilder;
|
||||||
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
|
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
|
||||||
import com.cringe_studios.cringe_authenticator_library.OTPType;
|
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;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class MainActivity extends BaseActivity {
|
public class MainActivity extends BaseActivity {
|
||||||
@ -42,9 +51,9 @@ public class MainActivity extends BaseActivity {
|
|||||||
|
|
||||||
private ActivityResultLauncher<Void> startQRCodeScan;
|
private ActivityResultLauncher<Void> startQRCodeScan;
|
||||||
|
|
||||||
private boolean unlocked;
|
private ActivityResultLauncher<PickVisualMediaRequest> pickQRCodeImage;
|
||||||
|
|
||||||
private long pauseTime;
|
private QRScanner qrScanner;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@ -56,6 +65,8 @@ public class MainActivity extends BaseActivity {
|
|||||||
|
|
||||||
setLocale(SettingsUtil.getLocale(this));
|
setLocale(SettingsUtil.getLocale(this));
|
||||||
|
|
||||||
|
qrScanner = new QRScanner();
|
||||||
|
|
||||||
startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> {
|
startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> {
|
||||||
if(obj == null) return; // Cancelled
|
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);
|
OTPDatabase.promptLoadDatabase(this, this::launchApp, this::finishAffinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
qrScanner.close();
|
||||||
|
}
|
||||||
|
|
||||||
public void setLocale(Locale locale) {
|
public void setLocale(Locale locale) {
|
||||||
Locale.setDefault(locale);
|
Locale.setDefault(locale);
|
||||||
Configuration config = new Configuration();
|
Configuration config = new Configuration();
|
||||||
@ -82,8 +135,6 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void launchApp() {
|
private void launchApp() {
|
||||||
unlocked = true;
|
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
@ -91,7 +142,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
|
|
||||||
binding.fabMenu.setOnClickListener(view -> NavigationUtil.navigate(this, MenuFragment.class, null));
|
binding.fabMenu.setOnClickListener(view -> NavigationUtil.navigate(this, MenuFragment.class, null));
|
||||||
binding.fabScan.setOnClickListener(view -> scanCode());
|
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());
|
binding.fabInput.setOnClickListener(view -> inputCode());
|
||||||
|
|
||||||
Fragment fragment = NavigationUtil.getCurrentFragment(this);
|
Fragment fragment = NavigationUtil.getCurrentFragment(this);
|
||||||
@ -148,6 +199,13 @@ public class MainActivity extends BaseActivity {
|
|||||||
startQRCodeScan.launch(null);
|
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() {
|
public void inputCode() {
|
||||||
DialogInputCodeChoiceBinding binding = DialogInputCodeChoiceBinding.inflate(getLayoutInflater());
|
DialogInputCodeChoiceBinding binding = DialogInputCodeChoiceBinding.inflate(getLayoutInflater());
|
||||||
|
|
||||||
@ -208,15 +266,11 @@ public class MainActivity extends BaseActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
this.pauseTime = System.currentTimeMillis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(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.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.media.Image;
|
import android.media.Image;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Toast;
|
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.databinding.ActivityQrScannerBinding;
|
||||||
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
||||||
import com.cringe_studios.cringe_authenticator.model.OTPMigrationPart;
|
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.StyledDialogBuilder;
|
||||||
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
|
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
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 com.google.mlkit.vision.common.InputImage;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -55,7 +49,7 @@ public class QRScannerActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
|
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
|
||||||
|
|
||||||
private BarcodeScanner scanner;
|
private QRScanner scanner;
|
||||||
|
|
||||||
private boolean process = true;
|
private boolean process = true;
|
||||||
|
|
||||||
@ -92,7 +86,7 @@ public class QRScannerActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}, ContextCompat.getMainExecutor(this));
|
}, ContextCompat.getMainExecutor(this));
|
||||||
|
|
||||||
scanner = BarcodeScanning.getClient();
|
scanner = new QRScanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
|
void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
|
||||||
@ -136,40 +130,27 @@ public class QRScannerActivity extends AppCompatActivity {
|
|||||||
Image mediaImage = image.getImage();
|
Image mediaImage = image.getImage();
|
||||||
if(mediaImage != null) {
|
if(mediaImage != null) {
|
||||||
InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees());
|
InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees());
|
||||||
scanner.process(input).addOnSuccessListener(barcodes -> {
|
scanner.scan(input, code -> {
|
||||||
image.close();
|
image.close();
|
||||||
|
|
||||||
if(barcodes.size() >= 1) {
|
if(code != null) {
|
||||||
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());
|
|
||||||
process = false;
|
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) {
|
private void importCode(DetectedCode code) {
|
||||||
if("otpauth-migration".equalsIgnoreCase(uri.getScheme())) {
|
if(code.isMigrationPart()) {
|
||||||
OTPMigrationPart part;
|
OTPMigrationPart part = code.getMigrationPart();
|
||||||
|
|
||||||
try {
|
|
||||||
part = OTPParser.parseMigration(uri);
|
|
||||||
}catch(IllegalArgumentException e) {
|
|
||||||
error(e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if((lastPart != null && part.getBatchIndex() != lastPart.getBatchIndex() + 1) || (lastPart == null && part.getBatchIndex() > 0)) {
|
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
|
// Not next batch, or first batch (if nothing was scanned yet), keep looking
|
||||||
@ -206,7 +187,6 @@ public class QRScannerActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,12 +197,18 @@ public class QRScannerActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
success(OTPParser.parse(uri));
|
success(code.getData());
|
||||||
}catch(IllegalArgumentException e) {
|
}catch(IllegalArgumentException e) {
|
||||||
error(e.getMessage());
|
error(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
scanner.close();
|
||||||
|
}
|
||||||
|
|
||||||
private void success(@NonNull OTPData... data) {
|
private void success(@NonNull OTPData... data) {
|
||||||
Intent result = new Intent();
|
Intent result = new Intent();
|
||||||
result.putExtra("data", data);
|
result.putExtra("data", data);
|
||||||
|
Loading…
Reference in New Issue
Block a user