diff --git a/build.gradle b/build.gradle
index 5e4f4afb..1d806bc1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -477,6 +477,11 @@ android {
}
dependencies {
+ implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1'
+ def cameraXVersion = '1.3.4'
+ implementation "androidx.camera:camera-camera2:${cameraXVersion}"
+ implementation "androidx.camera:camera-lifecycle:${cameraXVersion}"
+ implementation "androidx.camera:camera-view:${cameraXVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.9.24')
implementation 'androidx.core:core:1.13.1'
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index d7c45d01..a6cee7ff 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -11,6 +11,9 @@
+
@@ -21,6 +24,9 @@
+
+
+
@@ -32,6 +38,9 @@
android:dataExtractionRules="@xml/backup_rules"
android:largeHeap="true"
tools:ignore="LockedOrientationActivity,UnusedAttribute">
+
@@ -65,6 +74,9 @@
+
diff --git a/src/main/java/org/medicmobile/webapp/mobile/BarcodeScanner.java b/src/main/java/org/medicmobile/webapp/mobile/BarcodeScanner.java
new file mode 100644
index 00000000..fe5a4b1b
--- /dev/null
+++ b/src/main/java/org/medicmobile/webapp/mobile/BarcodeScanner.java
@@ -0,0 +1,159 @@
+package org.medicmobile.webapp.mobile;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalGetImage;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageProxy;
+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 com.google.android.gms.tasks.Task;
+import com.google.common.util.concurrent.ListenableFuture;
+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.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import static org.medicmobile.webapp.mobile.EmbeddedBrowserActivity.RequestCode;
+
+public class BarcodeScanner extends AppCompatActivity {
+
+ private PreviewView scannerPreview;
+ private ExecutorService cameraExecutor;
+ private static final String TAG = "BarcodeScanner";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.barcode_scanner);
+ scannerPreview = findViewById(R.id.scannerPreview);
+ cameraExecutor = Executors.newSingleThreadExecutor();
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED) {
+ startCamera();
+ } else {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.CAMERA},
+ RequestCode.BARCODE_SCANNER.getCode());
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == RequestCode.BARCODE_SCANNER.getCode()
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED){
+ startCamera();
+ } else {
+ Toast.makeText(this, "Camera permission denied", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ cameraExecutor.shutdown();
+ }
+
+ private void startCamera() {
+ ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ bindPreview(cameraProvider);
+ } catch (ExecutionException | InterruptedException e){
+ Log.e(TAG, Objects.requireNonNull(e.getMessage()));
+ }
+ },
+ ContextCompat.getMainExecutor(this));
+ }
+
+ private void bindPreview(ProcessCameraProvider cameraProvider){
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(scannerPreview.getSurfaceProvider());
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+ .build();
+
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build();
+ imageAnalysis.setAnalyzer(cameraExecutor, this::imageAnalyzer);
+
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis);
+ }
+
+
+ @OptIn(markerClass = ExperimentalGetImage.class)
+ private void imageAnalyzer(ImageProxy imageProxy) {
+
+ if (imageProxy.getImage() != null) {
+ InputImage image = InputImage.fromMediaImage(imageProxy.getImage(), imageProxy.getImageInfo().getRotationDegrees());
+ BarcodeScannerOptions options = buildBarcodeOptions();
+ com.google.mlkit.vision.barcode.BarcodeScanner scanner = BarcodeScanning.getClient(options);
+
+ Task> result = scanner.process(image);
+ result.addOnSuccessListener(barcodes -> {
+ for (Barcode barcode : barcodes) {
+ String rawValue = barcode.getRawValue();
+ returnIntent(rawValue);
+ }
+ });
+ result.addOnFailureListener(e -> {
+ Log.e(TAG, Objects.requireNonNull(e.getMessage()));
+ Toast.makeText(this, "Failed to scan image", Toast.LENGTH_SHORT).show();
+ });
+
+ result.addOnCompleteListener(task -> {
+ imageProxy.close();
+ });
+ }
+ }
+
+ private void returnIntent(String barcode) {
+ if (barcode != null) {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra("CHT_BARCODE", barcode);
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
+ }
+
+ private BarcodeScannerOptions buildBarcodeOptions() {
+ return new BarcodeScannerOptions.Builder()
+ .setBarcodeFormats(
+ //formats to support
+ Barcode.FORMAT_QR_CODE,
+ Barcode.FORMAT_AZTEC,
+ Barcode.FORMAT_CODE_39,
+ Barcode.FORMAT_PDF417,
+ Barcode.FORMAT_EAN_13,
+ Barcode.FORMAT_CODE_128,
+ Barcode.FORMAT_UPC_E,
+ Barcode.FORMAT_UPC_A,
+ Barcode.FORMAT_EAN_8
+ )
+ .build();
+ }
+}
diff --git a/src/main/java/org/medicmobile/webapp/mobile/ChtExternalAppHandler.java b/src/main/java/org/medicmobile/webapp/mobile/ChtExternalAppHandler.java
index 2998a5d5..a8880696 100644
--- a/src/main/java/org/medicmobile/webapp/mobile/ChtExternalAppHandler.java
+++ b/src/main/java/org/medicmobile/webapp/mobile/ChtExternalAppHandler.java
@@ -76,6 +76,11 @@ void resumeActivity(int resultCode) {
this.lastIntent = null; // Cleaning as we don't need it anymore.
}
+ void triggerScanner() {
+ Intent intent = new Intent(context, BarcodeScanner.class);
+ this.context.startActivityForResult(intent, RequestCode.BARCODE_SCANNER.getCode());
+ }
+
//> PRIVATE
private void startActivity(Intent intent) {
diff --git a/src/main/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivity.java b/src/main/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivity.java
index 96f4e318..b1dcea7d 100644
--- a/src/main/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivity.java
+++ b/src/main/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivity.java
@@ -208,6 +208,9 @@ protected void onActivityResult(int requestCd, int resultCode, Intent intent) {
case ACCESS_SEND_SMS_PERMISSION:
this.smsSender.resumeProcess(resultCode);
return;
+ case BARCODE_SCANNER:
+ processChtBarcodeResult(resultCode, intent);
+ return;
default:
trace(this, "onActivityResult() :: no handling for requestCode=%s", requestCode.name());
}
@@ -283,6 +286,10 @@ private void locationRequestResolved() {
evaluateJavascript("window.CHTCore.AndroidApi.v1.locationPermissionRequestResolved();");
}
+ private void processChtBarcodeResult(int resultCode, Intent intentData) {
+ processChtExternalAppResult(resultCode, intentData);
+ }
+
private void processChtExternalAppResult(int resultCode, Intent intentData) {
String script = this.chtExternalAppHandler.processResult(resultCode, intentData);
trace(this, "ChtExternalAppHandler :: Executing JavaScript: %s", script);
@@ -410,7 +417,8 @@ public enum RequestCode {
ACCESS_SEND_SMS_PERMISSION(102),
CHT_EXTERNAL_APP_ACTIVITY(103),
GRAB_MRDT_PHOTO_ACTIVITY(104),
- FILE_PICKER_ACTIVITY(105);
+ FILE_PICKER_ACTIVITY(105),
+ BARCODE_SCANNER(106);
private final int requestCode;
diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java
index aac1e627..ffb11bc5 100644
--- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java
+++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java
@@ -31,6 +31,7 @@
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
import org.json.JSONException;
import org.json.JSONObject;
@@ -189,21 +190,27 @@ public void sms_send(String id, String destination, String content) throws Excep
@android.webkit.JavascriptInterface
public void launchExternalApp(String action, String category, String type, String extras, String uri, String packageName, String flags) {
try {
- JSONObject parsedExtras = extras == null ? null : new JSONObject(extras);
- Uri parsedUri = uri == null ? null : Uri.parse(uri);
- Integer parsedFlags = flags == null ? null : Integer.parseInt(flags);
-
- ChtExternalApp chtExternalApp = new ChtExternalApp
- .Builder()
- .setAction(action)
- .setCategory(category)
- .setType(type)
- .setExtras(parsedExtras)
- .setUri(parsedUri)
- .setPackageName(packageName)
- .setFlags(parsedFlags)
- .build();
- this.chtExternalAppHandler.startIntent(chtExternalApp);
+ //This implementation is hijacking this bridge
+ // Create a new webapp API specifically for this embedded barcode scanner
+ if(Objects.equals(action, "cht.android.SCAN_BARCODE")) {
+ this.chtExternalAppHandler.triggerScanner();
+ } else {
+ JSONObject parsedExtras = extras == null ? null : new JSONObject(extras);
+ Uri parsedUri = uri == null ? null : Uri.parse(uri);
+ Integer parsedFlags = flags == null ? null : Integer.parseInt(flags);
+
+ ChtExternalApp chtExternalApp = new ChtExternalApp
+ .Builder()
+ .setAction(action)
+ .setCategory(category)
+ .setType(type)
+ .setExtras(parsedExtras)
+ .setUri(parsedUri)
+ .setPackageName(packageName)
+ .setFlags(parsedFlags)
+ .build();
+ this.chtExternalAppHandler.startIntent(chtExternalApp);
+ }
} catch (Exception ex) {
logException(ex);
diff --git a/src/main/res/layout/barcode_scanner.xml b/src/main/res/layout/barcode_scanner.xml
new file mode 100644
index 00000000..f4cb6ff7
--- /dev/null
+++ b/src/main/res/layout/barcode_scanner.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/src/main/res/layout/connection_error.xml b/src/main/res/layout/connection_error.xml
index 910509d1..cd91f105 100644
--- a/src/main/res/layout/connection_error.xml
+++ b/src/main/res/layout/connection_error.xml
@@ -1,5 +1,6 @@
+ android:textAllCaps="true"
+ tools:ignore="UsingOnClickInXml" />
+ android:textAllCaps="true"
+ tools:ignore="UsingOnClickInXml" />
diff --git a/src/main/res/layout/custom_server_form.xml b/src/main/res/layout/custom_server_form.xml
index a0c34725..1f85d632 100644
--- a/src/main/res/layout/custom_server_form.xml
+++ b/src/main/res/layout/custom_server_form.xml
@@ -22,7 +22,7 @@
android:layout_alignParentBottom="true"
android:onClick="cancelSettingsEdit"
android:text="@string/btnCancel"
- tools:ignore="OnClick" />
+ tools:ignore="OnClick,UsingOnClickInXml" />
@@ -32,5 +32,5 @@
android:layout_alignParentBottom="true"
android:onClick="verifyAndSave"
android:text="@string/btnSave"
- tools:ignore="OnClick" />
+ tools:ignore="OnClick,UsingOnClickInXml" />
diff --git a/src/main/res/layout/free_space_warning.xml b/src/main/res/layout/free_space_warning.xml
index 08203107..50192b3a 100644
--- a/src/main/res/layout/free_space_warning.xml
+++ b/src/main/res/layout/free_space_warning.xml
@@ -37,7 +37,7 @@
style="@style/standardButton"
android:text="@string/btnQuit"
android:onClick="evtQuit"
- tools:ignore="OnClick"
+ tools:ignore="OnClick,UsingOnClickInXml"
android:layout_toStartOf="@+id/btnFreeSpaceContinue"
android:layout_alignParentBottom="true"/>