From 3d1215f6d8e047421520590f78c78c67e3ac3891 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Wed, 23 Oct 2024 02:21:34 +0100 Subject: chore: boostrap scanner --- app/src/main/AndroidManifest.xml | 9 +- .../activities/scanner/ScannerActivity.java | 150 +++++++++++++++++++++ .../activities/scanner/ScannerViewModel.java | 42 ++++++ .../fragments/ProviderSelectionFragment.java | 46 ++++++- .../viewmodel/ProviderSelectionViewModel.java | 7 + app/src/main/res/layout/a_scanner.xml | 18 +++ 6 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerViewModel.java create mode 100644 app/src/main/res/layout/a_scanner.xml (limited to 'app/src/main') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 08433778..3a762180 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,10 @@ android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> - + + + { + this.cameraProvider = cameraProvider; + bindCameraUseCases(); + }); + } + + private void bindCameraUseCases() { + bindPreviewUseCase(); + bindAnalyseUseCase(); + } + + + private void bindPreviewUseCase() { + if (cameraProvider == null) { + return; + } + if (previewUseCase != null) { + cameraProvider.unbind(previewUseCase); + } + + previewUseCase = new Preview.Builder().setTargetRotation(binding.previewView.getDisplay().getRotation()).build(); + previewUseCase.setSurfaceProvider(binding.previewView.getSurfaceProvider()); + + try { + cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase); + } catch (Exception exception) { + Log.e(TAG, exception.getMessage()); + } + } + + + private void bindAnalyseUseCase() { + // Note that if you know which format of barcode your app is dealing with, detection will be + // faster to specify the supported barcode formats one by one, e.g. + // BarcodeScannerOptions.Builder() + // .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + // .build(); + BarcodeScanner barcodeScanner = BarcodeScanning.getClient(); + + if (cameraProvider == null) { + return; + } + if (analysisUseCase != null) { + cameraProvider.unbind(analysisUseCase); + } + + analysisUseCase = new ImageAnalysis.Builder().setTargetRotation(binding.previewView.getDisplay().getRotation()).build(); + + // Initialize our background executor + ExecutorService cameraExecutor = Executors.newSingleThreadExecutor(); + + analysisUseCase.setAnalyzer(cameraExecutor, imageProxy -> { + // Insert barcode scanning code here + processImageProxy(barcodeScanner, imageProxy); + }); + + try { + cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase); + } catch (Exception exception) { + Log.e(TAG, exception.getMessage()); + } + } + + private void processImageProxy(BarcodeScanner barcodeScanner, ImageProxy imageProxy) { + InputImage inputImage = InputImage.fromMediaImage(imageProxy.getImage(), imageProxy.getImageInfo().getRotationDegrees()); + + barcodeScanner.process(inputImage).addOnSuccessListener(barcodes -> { + for (Barcode barcode : barcodes) { + try { + Introducer introducer = Introducer.fromUrl(barcode.getRawValue()); + if (introducer != null && introducer.validate()) { + imageProxy.close(); + setResult(RESULT_OK, new Intent().putExtra(INVITE_CODE, introducer)); + finish(); + } else { + Toast.makeText(this, "Invalid introducer", Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + Toast.makeText(this, "Invalid introducer", Toast.LENGTH_SHORT).show(); + } + } + }).addOnFailureListener(e -> { + Log.e(TAG, e.getMessage()); + }).addOnCompleteListener(task -> { + // When the image is from CameraX analysis use case, must call image.close() on received + // images when finished using them. Otherwise, new images may not be received or the camera + // may stall. + imageProxy.close(); + }); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerViewModel.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerViewModel.java new file mode 100644 index 00000000..d749e512 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerViewModel.java @@ -0,0 +1,42 @@ +package se.leap.bitmaskclient.providersetup.activities.scanner; + +import android.app.Application; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.concurrent.ExecutionException; + +public class ScannerViewModel extends AndroidViewModel { + private static final String TAG = ScannerViewModel.class.getSimpleName(); + private MutableLiveData cameraProviderLiveData; + + public ScannerViewModel(@NonNull Application application) { + super(application); + } + + public LiveData getProcessCameraProvider() { + if (cameraProviderLiveData == null) { + cameraProviderLiveData = new MutableLiveData<>(); + ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(getApplication()); + cameraProviderFuture.addListener(() -> { + try { + cameraProviderLiveData.setValue(cameraProviderFuture.get()); + } catch (ExecutionException e) { + // Handle any errors (including cancellation) here. + Log.e(TAG, "Unhandled exception", e); + } catch (InterruptedException e) { + Log.e(TAG, "Unhandled exception", e); + } + }, ContextCompat.getMainExecutor(getApplication())); + } + return cameraProviderLiveData; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java index e5c19e28..8ccf7993 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java @@ -3,6 +3,10 @@ package se.leap.bitmaskclient.providersetup.fragments; import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.ADD_PROVIDER; import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.INVITE_CODE_PROVIDER; +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Typeface; import android.os.Bundle; import android.text.Editable; @@ -12,14 +16,18 @@ import android.view.View; import android.view.ViewGroup; import android.widget.RadioButton; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; -import java.net.URISyntaxException; import java.util.ArrayList; import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.providersetup.activities.scanner.ScannerActivity; import se.leap.bitmaskclient.base.models.Introducer; import se.leap.bitmaskclient.base.models.Provider; import se.leap.bitmaskclient.base.utils.ViewHelper; @@ -30,6 +38,9 @@ import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelection public class ProviderSelectionFragment extends BaseSetupFragment implements CancelCallback { + private static final int PERMISSION_GRANTED_REQUEST_CODE = 1; + private ActivityResultLauncher scannerActivityResultLauncher; + private ProviderSelectionViewModel viewModel; private ArrayList radioButtons; @@ -49,6 +60,15 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc new ProviderSelectionViewModelFactory( getContext().getApplicationContext().getAssets())). get(ProviderSelectionViewModel.class); + scannerActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + if (data != null) { + Introducer introducer = data.getParcelableExtra(ScannerActivity.INVITE_CODE); + binding.editCustomProvider.setText(introducer.toUrl()); + } + } + }); } @Override @@ -80,6 +100,7 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility()); binding.syntaxCheck.setVisibility(viewModel.getEditProviderVisibility()); + binding.qrScanner.setVisibility(viewModel.getQrScannerVisibility()); return binding.getRoot(); } @@ -87,6 +108,28 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setupActivityCallback.registerCancelCallback(this); + initQrScanner(); + } + + private void initQrScanner() { + binding.qrScanner.setOnClickListener(v -> { + if (isCameraPermissionGranted()) { + scannerActivityResultLauncher.launch(ScannerActivity.newIntent(getContext())); + } else { + ActivityCompat.requestPermissions( + getActivity(), + new String[]{Manifest.permission.CAMERA}, + PERMISSION_GRANTED_REQUEST_CODE + ); + } + }); + } + + private boolean isCameraPermissionGranted() { + return ContextCompat.checkSelfPermission( + getContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED; } @Override @@ -102,6 +145,7 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc binding.providerDescription.setText(viewModel.getProviderDescription(getContext())); binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility()); binding.syntaxCheck.setVisibility(viewModel.getEditProviderVisibility()); + binding.qrScanner.setVisibility(viewModel.getQrScannerVisibility()); if (viewModel.getCustomUrl() == null || viewModel.getCustomUrl().isEmpty()) { binding.syntaxCheckResult.setText(""); binding.syntaxCheckResult.setTextColor(getResources().getColor(R.color.color_font_btn)); diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java index 58b43fbd..00117336 100644 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java @@ -86,6 +86,13 @@ public class ProviderSelectionViewModel extends ViewModel { return provider.getDescription(); } + public int getQrScannerVisibility() { + if (selected == INVITE_CODE_PROVIDER) { + return View.VISIBLE; + } + return View.GONE; + } + public int getEditProviderVisibility() { if (selected == ADD_PROVIDER) { return View.VISIBLE; diff --git a/app/src/main/res/layout/a_scanner.xml b/app/src/main/res/layout/a_scanner.xml new file mode 100644 index 00000000..5fb2db53 --- /dev/null +++ b/app/src/main/res/layout/a_scanner.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file -- cgit v1.2.3