summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorNorbel AMBANUMBEN <aanorbel@gmail.com>2024-10-23 02:21:34 +0100
committerNorbel AMBANUMBEN <aanorbel@gmail.com>2024-10-23 02:21:34 +0100
commit3d1215f6d8e047421520590f78c78c67e3ac3891 (patch)
treeecf391b95879f58ff1a290c3cd56364df932ae71 /app
parenta0d90ddbe67c87aaa47805644b39e50966ac78fb (diff)
chore: boostrap scanner
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle4
-rw-r--r--app/src/main/AndroidManifest.xml9
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerActivity.java150
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerViewModel.java42
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java46
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java7
-rw-r--r--app/src/main/res/layout/a_scanner.xml18
7 files changed, 274 insertions, 2 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 2dffdd54..dbc3d926 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -422,6 +422,10 @@ dependencies {
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
+ implementation 'com.google.mlkit:barcode-scanning:17.3.0'
+ implementation 'androidx.camera:camera-camera2:1.3.4'
+ implementation 'androidx.camera:camera-lifecycle:1.3.4'
+ implementation 'androidx.camera:camera-view:1.3.4'
//implementation 'info.guardianproject:tor-android:0.4.5.7'
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" /> <!-- Used to show all apps in the allowed Apps selection -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
+ <uses-permission android:name="android.permission.CAMERA"/>
<application
android:name=".base.BitmaskApp"
android:allowBackup="false"
@@ -111,6 +114,10 @@
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
+ <activity android:name=".providersetup.activities.scanner.ScannerActivity"
+ android:launchMode="singleInstance"
+ android:screenOrientation="portrait"
+ android:exported="false" />
<activity
android:name=".base.MainActivity"
android:label="@string/app_name"
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerActivity.java
new file mode 100644
index 00000000..e3c081c2
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerActivity.java
@@ -0,0 +1,150 @@
+package se.leap.bitmaskclient.providersetup.activities.scanner;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.lifecycle.ViewModelProvider;
+
+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.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import se.leap.bitmaskclient.base.models.Introducer;
+import se.leap.bitmaskclient.databinding.AScannerBinding;
+
+@OptIn(markerClass = androidx.camera.core.ExperimentalGetImage.class)
+public class ScannerActivity extends AppCompatActivity {
+ public static final String INVITE_CODE = "invite_code";
+ private static final String TAG = ScannerActivity.class.getSimpleName();
+ private AScannerBinding binding;
+ private ProcessCameraProvider cameraProvider;
+ private Preview previewUseCase;
+ private CameraSelector cameraSelector;
+ private ImageAnalysis analysisUseCase;
+
+ public static Intent newIntent(Context context) {
+ return new Intent(context, ScannerActivity.class);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = AScannerBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ initCamera();
+ }
+
+ private void initCamera() {
+ cameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
+ new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(ScannerViewModel.class).getProcessCameraProvider().observe(this, cameraProvider -> {
+ 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<ProcessCameraProvider> cameraProviderLiveData;
+
+ public ScannerViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ public LiveData<ProcessCameraProvider> getProcessCameraProvider() {
+ if (cameraProviderLiveData == null) {
+ cameraProviderLiveData = new MutableLiveData<>();
+ ListenableFuture<ProcessCameraProvider> 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<Intent> scannerActivityResultLauncher;
+
private ProviderSelectionViewModel viewModel;
private ArrayList<RadioButton> 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".providersetup.activities.scanner.ScannerActivity">
+
+ <androidx.camera.view.PreviewView
+ android:id="@+id/previewView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file