Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/passman-android.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbinsky08 <timo@binsky.org>2022-08-28 16:13:59 +0300
committerbinsky08 <timo@binsky.org>2022-08-28 16:13:59 +0300
commitd40ba817045c2719158b3b4f53b87613f2617ccf (patch)
treee0aa1b28e603268b909507beefa3337ddd9d6d78
parent965553254f126aa573c33a0b80e23d81d3cf388c (diff)
implement own qr code scan analyzer and activity to get a better scanning behaviour (with support for all android devices)
Signed-off-by: binsky08 <timo@binsky.org>
-rw-r--r--app/build.gradle9
-rw-r--r--app/src/main/AndroidManifest.xml28
-rw-r--r--app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java24
-rw-r--r--app/src/main/java/es/wolfi/app/passman/activities/ScanQRCodeActivity.java151
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java3
-rw-r--r--app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java3
-rw-r--r--app/src/main/java/es/wolfi/utils/QrCodeAnalyzer.java71
-rw-r--r--app/src/main/res/anim/scanner.xml12
-rw-r--r--app/src/main/res/layout/activity_scan_qrcode.xml20
9 files changed, 280 insertions, 41 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 4007b45..ed4050b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -102,6 +102,7 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0', {
exclude group: 'com.android.support', module: 'support-annotations'
})
@@ -120,8 +121,12 @@ dependencies {
implementation 'com.caverock:androidsvg:1.4'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
implementation 'com.vdurmont:semver4j:3.1.0'
- implementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false }
- implementation 'com.google.zxing:core:3.4.0'
+ // Version 3.4.0 contains a crashing bug before api level 24
+ implementation 'com.google.zxing:core:3.3.3'
+ implementation("androidx.camera:camera-core:1.1.0")
+ implementation("androidx.camera:camera-camera2:1.1.0")
+ implementation("androidx.camera:camera-lifecycle:1.1.0")
+ implementation("androidx.camera:camera-view:1.1.0")
testImplementation 'junit:junit:4.13.2'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4045fda..8c29c7f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,29 +2,34 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="es.wolfi.app.passman">
- <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
+
<queries>
<package android:name="com.nextcloud.client" />
<package android:name="com.nextcloud.android.beta" />
</queries>
+
<application
android:allowBackup="true"
+ android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:supportsRtl="true"
- android:hardwareAccelerated="true"
android:largeHeap="true"
- android:theme="@style/AppTheme"
+ android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
- android:networkSecurityConfig="@xml/network_security_config">
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name=".activities.ScanQRCodeActivity"
+ android:exported="false" />
<activity
android:name=".activities.PasswordListActivity"
+ android:exported="true"
android:label="@string/app_name"
- android:theme="@style/AppTheme.NoActionBar"
- android:exported="true">
+ android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -41,9 +46,9 @@
<service
android:name=".autofill.CredentialAutofillService"
+ android:exported="true"
android:label="Passman Credential Autofill Service"
- android:permission="android.permission.BIND_AUTOFILL_SERVICE"
- android:exported="true">
+ android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data
android:name="android.autofill"
android:resource="@xml/autofill_service" />
@@ -56,8 +61,7 @@
<receiver
android:name=".PassmanReceiver"
android:enabled="true"
- android:exported="true">
- </receiver>
+ android:exported="true"></receiver>
</application>
</manifest> \ No newline at end of file
diff --git a/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java b/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java
index 8679458..0fadfd0 100644
--- a/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java
+++ b/app/src/main/java/es/wolfi/app/passman/activities/PasswordListActivity.java
@@ -22,8 +22,6 @@
package es.wolfi.app.passman.activities;
-import static com.google.zxing.integration.android.IntentIntegrator.parseActivityResult;
-
import android.app.KeyguardManager;
import android.app.ProgressDialog;
import android.content.ClipData;
@@ -58,9 +56,6 @@ import androidx.core.graphics.drawable.IconCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
-import com.google.zxing.integration.android.IntentResult;
-import com.journeyapps.barcodescanner.ScanContract;
-import com.journeyapps.barcodescanner.ScanOptions;
import com.koushikdutta.async.future.FutureCallback;
import org.json.JSONException;
@@ -762,14 +757,7 @@ public class PasswordListActivity extends AppCompatActivity implements
}
public void scanQRCodeForOTP(int requestCode) {
- ScanOptions scanOptions = new ScanOptions();
- scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE); // optional
- scanOptions.setOrientationLocked(false); // allow barcode scanner in portrait mode
-
- ScanContract scanContract = new ScanContract();
- Intent intent = scanContract.createIntent(this, scanOptions);
-
- startActivityForResult(intent, requestCode);
+ startActivityForResult(new Intent(this, ScanQRCodeActivity.class), requestCode);
}
@Override
@@ -797,30 +785,24 @@ public class PasswordListActivity extends AppCompatActivity implements
if (requestCode == REQUEST_CODE_SCAN_QR_CODE_FOR_OTP_EDIT) { // scan qr code as otp config in credential edit
if (resultCode != RESULT_OK) {
- Log.e("otp qr scan", "failed");
return;
}
CredentialEditFragment credentialEditFragment = (CredentialEditFragment) getSupportFragmentManager().findFragmentByTag("credentialEdit");
if (credentialEditFragment != null) {
- IntentResult result = parseActivityResult(resultCode, data);
- credentialEditFragment.processScannedQRCodeData(result.getContents());
+ credentialEditFragment.processScannedQRCodeData(data.getData().toString());
}
- Log.d("otp qr scan", "successful");
}
if (requestCode == REQUEST_CODE_SCAN_QR_CODE_FOR_OTP_ADD) { // scan qr code as otp config in credential add
if (resultCode != RESULT_OK) {
- Log.e("otp qr scan", "failed");
return;
}
CredentialAddFragment credentialAddFragment = (CredentialAddFragment) getSupportFragmentManager().findFragmentByTag("credentialAdd");
if (credentialAddFragment != null) {
- IntentResult result = parseActivityResult(resultCode, data);
- credentialAddFragment.processScannedQRCodeData(result.getContents());
+ credentialAddFragment.processScannedQRCodeData(data.getData().toString());
}
- Log.d("otp qr scan", "successful");
}
// Following cases should only be handled on positive result
diff --git a/app/src/main/java/es/wolfi/app/passman/activities/ScanQRCodeActivity.java b/app/src/main/java/es/wolfi/app/passman/activities/ScanQRCodeActivity.java
new file mode 100644
index 0000000..74b89cd
--- /dev/null
+++ b/app/src/main/java/es/wolfi/app/passman/activities/ScanQRCodeActivity.java
@@ -0,0 +1,151 @@
+package es.wolfi.app.passman.activities;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Surface;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.PreviewView;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.zxing.Result;
+import com.koushikdutta.async.future.FutureCallback;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import es.wolfi.app.passman.R;
+import es.wolfi.utils.QrCodeAnalyzer;
+
+public class ScanQRCodeActivity extends AppCompatActivity {
+
+ public final static String LOG_TAG = ScanQRCodeActivity.class.getSimpleName();
+
+ private static final int REQUEST_CODE_PERMISSIONS = 10;
+ private static final String[] REQUIRED_PERMISSIONS = {Manifest.permission.CAMERA};
+ private static final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+ PreviewView previewView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_scan_qrcode);
+
+ previewView = findViewById(R.id.preview);
+
+ if (allPermissionsGranted()) {
+ startCamera();
+ } else {
+ ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
+ }
+ }
+
+ /**
+ * Process result from permission request dialog box.
+ * If the request has been granted, start Camera. Otherwise display a toast.
+ */
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ startCamera();
+ } else {
+ Toast.makeText(this, "Camera permission denied", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+ }
+
+ /**
+ * Check if the required camera permission have been granted
+ */
+ private boolean allPermissionsGranted() {
+ for (String permission : REQUIRED_PERMISSIONS) {
+ if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void startCamera() {
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ Context c = this;
+
+ Animation animation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.scanner);
+ findViewById(R.id.bar).startAnimation(animation);
+
+ cameraProviderFuture.addListener(new Runnable() {
+ @Override
+ public void run() {
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+ .build();
+
+ Preview preview = new Preview.Builder()
+ .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+ .setTargetRotation(Surface.ROTATION_0).build();
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
+
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
+ .setBackgroundExecutor(mExecutor)
+ .setTargetAspectRatio(AspectRatio.RATIO_16_9)
+ .setTargetRotation(Surface.ROTATION_0).build();
+
+ imageAnalysis.setAnalyzer(mExecutor, new QrCodeAnalyzer(new FutureCallback<Result>() {
+ @Override
+ public void onCompleted(Exception e, Result result) {
+ if (result != null) {
+ Intent intent = new Intent();
+ intent.setData(Uri.parse(result.getText()));
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+ if (e != null) {
+ e.printStackTrace();
+ Log.e(LOG_TAG, "error parsing qr code", e);
+
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(c, "Error parsing qr code", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+ }));
+
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ cameraProvider.unbindAll();
+ cameraProvider.bindToLifecycle((LifecycleOwner) c, cameraSelector, preview, imageAnalysis);
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java
index 315cea7..7c11dfb 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialAddFragment.java
@@ -151,9 +151,6 @@ public class CredentialAddFragment extends Fragment implements View.OnClickListe
AppCompatImageButton scanOtpQRCodeButton = (AppCompatImageButton) view.findViewById(R.id.scanOtpQRCodeButton);
scanOtpQRCodeButton.setOnClickListener(this.getScanOtpQRCodeButtonListener());
- if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
- scanOtpQRCodeButton.setVisibility(View.INVISIBLE);
- }
otpEditCollapseExtendedButton = (AppCompatImageButton) view.findViewById(R.id.otpEditCollapseExtendedButton);
otpEditCollapseExtendedButton.setOnClickListener(this.getOtpEditCollapseExtendedButtonListener());
diff --git a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java
index 8853914..f35af48 100644
--- a/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java
+++ b/app/src/main/java/es/wolfi/app/passman/fragments/CredentialEditFragment.java
@@ -163,9 +163,6 @@ public class CredentialEditFragment extends Fragment implements View.OnClickList
AppCompatImageButton scanOtpQRCodeButton = (AppCompatImageButton) view.findViewById(R.id.scanOtpQRCodeButton);
scanOtpQRCodeButton.setOnClickListener(this.getScanOtpQRCodeButtonListener());
- if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
- scanOtpQRCodeButton.setVisibility(View.INVISIBLE);
- }
otpEditCollapseExtendedButton = (AppCompatImageButton) view.findViewById(R.id.otpEditCollapseExtendedButton);
otpEditCollapseExtendedButton.setOnClickListener(this.getOtpEditCollapseExtendedButtonListener());
diff --git a/app/src/main/java/es/wolfi/utils/QrCodeAnalyzer.java b/app/src/main/java/es/wolfi/utils/QrCodeAnalyzer.java
new file mode 100644
index 0000000..1589e0a
--- /dev/null
+++ b/app/src/main/java/es/wolfi/utils/QrCodeAnalyzer.java
@@ -0,0 +1,71 @@
+package es.wolfi.utils;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageProxy;
+
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.ChecksumException;
+import com.google.zxing.FormatException;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.Result;
+import com.google.zxing.common.HybridBinarizer;
+import com.google.zxing.qrcode.QRCodeReader;
+import com.koushikdutta.async.future.FutureCallback;
+
+import java.nio.ByteBuffer;
+
+public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
+
+ public final static String LOG_TAG = QrCodeAnalyzer.class.getSimpleName();
+
+ private final QRCodeReader qrCodeReader = new QRCodeReader();
+ private final FutureCallback<Result> callback;
+ private boolean foundToken = false;
+
+ public QrCodeAnalyzer(FutureCallback<Result> callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void analyze(@NonNull ImageProxy image) {
+ if (foundToken) {
+ return;
+ }
+
+ Result result = null;
+ Exception exception = null;
+
+ ByteBuffer buffer = image.getPlanes()[0].getBuffer();
+
+ byte[] imageData = new byte[buffer.remaining()];
+ buffer.get(imageData);
+
+ PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
+ imageData,
+ image.getWidth(),
+ image.getHeight(),
+ 0,
+ 0,
+ image.getWidth(),
+ image.getHeight(),
+ false
+ );
+
+ try {
+ result = qrCodeReader.decode(new BinaryBitmap(new HybridBinarizer(source)));
+ foundToken = true;
+ } catch (NotFoundException | ChecksumException ignored) {
+ // Whenever reader fails to detect a QR code in image it throws NotFoundException
+ // Whenever reader detect a QR code with inconsistent QR points it throws ChecksumException
+ } catch (FormatException e) {
+ exception = e;
+ } finally {
+ qrCodeReader.reset();
+ }
+
+ image.close();
+ callback.onCompleted(exception, result);
+ }
+}
diff --git a/app/src/main/res/anim/scanner.xml b/app/src/main/res/anim/scanner.xml
new file mode 100644
index 0000000..0ca598f
--- /dev/null
+++ b/app/src/main/res/anim/scanner.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="false"
+ android:interpolator="@android:anim/accelerate_decelerate_interpolator">
+
+ <translate
+ android:duration="2000"
+ android:fromYDelta="15%p"
+ android:repeatCount="infinite"
+ android:repeatMode="reverse"
+ android:toYDelta="85%p" />
+</set>
diff --git a/app/src/main/res/layout/activity_scan_qrcode.xml b/app/src/main/res/layout/activity_scan_qrcode.xml
new file mode 100644
index 0000000..c02f966
--- /dev/null
+++ b/app/src/main/res/layout/activity_scan_qrcode.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+
+ <androidx.camera.view.PreviewView
+ android:id="@+id/preview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <View
+ android:id="@+id/bar"
+ android:layout_width="match_parent"
+ android:layout_height="6dp"
+ android:alpha="0.3"
+ android:background="@android:color/black"
+ android:visibility="visible" />
+</FrameLayout>
+