diff options
author | Norbel AMBANUMBEN <aanorbel@gmail.com> | 2024-10-23 03:19:58 +0100 |
---|---|---|
committer | Norbel AMBANUMBEN <aanorbel@gmail.com> | 2024-10-23 03:19:58 +0100 |
commit | bc0ae60a26dd0c2b06ea42b2314d3aac7d9e7508 (patch) | |
tree | 25aa77cfc91d26d03fd4ebb0bb6d84cc4f9b3eb0 /app | |
parent | 3d1215f6d8e047421520590f78c78c67e3ac3891 (diff) |
update: camera scanner implementation
Diffstat (limited to 'app')
5 files changed, 134 insertions, 157 deletions
diff --git a/app/build.gradle b/app/build.gradle index dbc3d926..6a211461 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -423,9 +423,11 @@ dependencies { 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-core:1.3.4' 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 'androidx.camera:camera-mlkit-vision:1.4.0-rc04' //implementation 'info.guardianproject:tor-android:0.4.5.7' 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 index e3c081c2..0878bd11 100644 --- 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 @@ -1,41 +1,55 @@ package se.leap.bitmaskclient.providersetup.activities.scanner; +import static androidx.appcompat.app.ActionBar.DISPLAY_SHOW_CUSTOM; +import static androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED; + +import android.Manifest; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.Log; +import android.view.Gravity; +import android.view.MenuItem; import android.widget.Toast; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.OptIn; +import androidx.appcompat.app.ActionBar; 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 androidx.camera.mlkit.vision.MlKitAnalyzer; +import androidx.camera.view.LifecycleCameraController; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; import com.google.mlkit.vision.barcode.BarcodeScanner; +import com.google.mlkit.vision.barcode.BarcodeScannerOptions; 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.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.R; import se.leap.bitmaskclient.base.models.Introducer; +import se.leap.bitmaskclient.base.utils.ViewHelper; +import se.leap.bitmaskclient.base.views.ActionBarTitle; 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 static final String[] REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA}; + private static final int REQUEST_CODE_PERMISSIONS = 1; + private AScannerBinding binding; - private ProcessCameraProvider cameraProvider; - private Preview previewUseCase; - private CameraSelector cameraSelector; - private ImageAnalysis analysisUseCase; + private BarcodeScanner barcodeScanner; + private ExecutorService cameraExecutor; public static Intent newIntent(Context context) { return new Intent(context, ScannerActivity.class); @@ -46,105 +60,114 @@ public class ScannerActivity extends AppCompatActivity { 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(); - }); - } + setupActionBar(); + if (allPermissionsGranted()) { + startCamera(); + } else { + ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS); + } + cameraExecutor = Executors.newSingleThreadExecutor(); - private void bindCameraUseCases() { - bindPreviewUseCase(); - bindAnalyseUseCase(); } - - private void bindPreviewUseCase() { - if (cameraProvider == null) { - return; - } - if (previewUseCase != null) { - cameraProvider.unbind(previewUseCase); + private void setupActionBar() { + setSupportActionBar(binding.toolbar); + final ActionBar actionBar = getSupportActionBar(); + Context context = actionBar.getThemedContext(); + actionBar.setDisplayOptions(DISPLAY_SHOW_CUSTOM); + + ActionBarTitle actionBarTitle = new ActionBarTitle(context); + actionBarTitle.setTitleCaps(BuildConfig.actionbar_capitalize_title); + actionBarTitle.setTitle("Scan QR Code"); + + final Drawable upArrow = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_back, getTheme()); + actionBar.setHomeAsUpIndicator(upArrow); + + actionBar.setDisplayHomeAsUpEnabled(true); + ViewHelper.setActivityBarColor(this, R.color.bg_setup_status_bar, R.color.bg_setup_action_bar, R.color.colorActionBarTitleFont); + @ColorInt int titleColor = ContextCompat.getColor(context, R.color.colorActionBarTitleFont); + actionBarTitle.setTitleTextColor(titleColor); + + actionBarTitle.setCentered(BuildConfig.actionbar_center_title); + actionBarTitle.setSingleBoldTitle(); + if (BuildConfig.actionbar_center_title) { + ActionBar.LayoutParams params = new ActionBar.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT, ActionBar.LayoutParams.MATCH_PARENT, Gravity.CENTER); + actionBar.setCustomView(actionBarTitle, params); + } else { + actionBar.setCustomView(actionBarTitle); } + } - 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 boolean allPermissionsGranted() { + for (String permission : REQUIRED_PERMISSIONS) { + if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + return false; + } } + return true; } + private void startCamera() { + var cameraController = new LifecycleCameraController(getBaseContext()); + var previewView = binding.previewView; - 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(); + var options = new BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_QR_CODE).build(); + barcodeScanner = BarcodeScanning.getClient(options); - if (cameraProvider == null) { - return; - } - if (analysisUseCase != null) { - cameraProvider.unbind(analysisUseCase); - } + cameraController.setImageAnalysisAnalyzer(ContextCompat.getMainExecutor(this), new MlKitAnalyzer(Collections.singletonList(barcodeScanner), COORDINATE_SYSTEM_VIEW_REFERENCED, ContextCompat.getMainExecutor(this), result -> { + var barcodeResults = result.getValue(barcodeScanner); + if ((barcodeResults == null) || (barcodeResults.size() == 0) || (barcodeResults.get(0) == null)) { + previewView.getOverlay().clear(); + previewView.setOnTouchListener((v, event) -> false); //no-op + return; + } + try { + Introducer introducer = Introducer.fromUrl(barcodeResults.get(0).getRawValue()); + if (introducer != null && introducer.validate()) { + setResult(RESULT_OK, new Intent().putExtra(INVITE_CODE, introducer)); + } 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(); + } + previewView.setOnTouchListener((v, event) -> false); //no-op + previewView.getOverlay().clear(); - analysisUseCase = new ImageAnalysis.Builder().setTargetRotation(binding.previewView.getDisplay().getRotation()).build(); + finish(); + })); - // Initialize our background executor - ExecutorService cameraExecutor = Executors.newSingleThreadExecutor(); + cameraController.bindToLifecycle(this); + previewView.setController(cameraController); + } - analysisUseCase.setAnalyzer(cameraExecutor, imageProxy -> { - // Insert barcode scanning code here - processImageProxy(barcodeScanner, imageProxy); - }); + @Override + protected void onDestroy() { + super.onDestroy(); + cameraExecutor.shutdown(); + barcodeScanner.close(); + } - try { - cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase); - } catch (Exception exception) { - Log.e(TAG, exception.getMessage()); + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; } + return super.onOptionsItemSelected(item); } - 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(); - } + @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, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show(); + finish(); } - }).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 deleted file mode 100644 index d749e512..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/scanner/ScannerViewModel.java +++ /dev/null @@ -1,42 +0,0 @@ -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 8ccf7993..47f0e972 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 @@ -38,7 +38,6 @@ 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; @@ -112,25 +111,9 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc } 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 - ); - } - }); + binding.qrScanner.setOnClickListener(v -> scannerActivityResultLauncher.launch(ScannerActivity.newIntent(getContext()))); } - private boolean isCameraPermissionGranted() { - return ContextCompat.checkSelfPermission( - getContext(), - Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED; - } @Override public void onFragmentSelected() { diff --git a/app/src/main/res/layout/a_scanner.xml b/app/src/main/res/layout/a_scanner.xml index 5fb2db53..bf954d99 100644 --- a/app/src/main/res/layout/a_scanner.xml +++ b/app/src/main/res/layout/a_scanner.xml @@ -6,13 +6,24 @@ android:layout_height="match_parent" tools:context=".providersetup.activities.scanner.ScannerActivity"> + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:minHeight="?attr/actionBarSize" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorPrimary" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent"> + </androidx.appcompat.widget.Toolbar> + <androidx.camera.view.PreviewView android:id="@+id/previewView" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@+id/toolbar" /> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file |