diff options
author | binsky08 <timo@binsky.org> | 2022-08-28 16:13:59 +0300 |
---|---|---|
committer | binsky08 <timo@binsky.org> | 2022-08-28 16:13:59 +0300 |
commit | d40ba817045c2719158b3b4f53b87613f2617ccf (patch) | |
tree | e0aa1b28e603268b909507beefa3337ddd9d6d78 | |
parent | 965553254f126aa573c33a0b80e23d81d3cf388c (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>
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> + |