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"/>