Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Barcode scanner #383

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 12 additions & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<uses-feature android:name="android.hardware.location" android:required="false"/>
<uses-feature android:name="android.hardware.location.gps" android:required="false"/>
<uses-feature android:name="android.hardware.location.network" android:required="false"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
Expand All @@ -21,6 +24,9 @@
<!-- READ_EXTERNAL_STORAGE is required if users want to include photos from their phone -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>

<!-- BARCODE_SCANNER -->
<uses-permission android:name="android.permission.CAMERA" />

<!-- SEND_SMS is required if users want to send reports as SMS.To able the feature add the following line:
<uses-permission android:name="android.permission.SEND_SMS"/>
-->
Expand All @@ -32,6 +38,9 @@
android:dataExtractionRules="@xml/backup_rules"
android:largeHeap="true"
tools:ignore="LockedOrientationActivity,UnusedAttribute">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode" />
<activity android:name="StartupActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
Expand Down Expand Up @@ -65,6 +74,9 @@
<activity android:name="DomainVerificationActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi" />
<activity
android:theme="@style/Theme.AppCompat.Light"
android:name=".BarcodeScanner" />
<activity android:name="AppUrlIntentActivity"
android:launchMode="singleInstance"
android:exported="true">
Expand Down
159 changes: 159 additions & 0 deletions src/main/java/org/medicmobile/webapp/mobile/BarcodeScanner.java
Original file line number Diff line number Diff line change
@@ -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<ProcessCameraProvider> 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<List<Barcode>> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions src/main/res/layout/barcode_scanner.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">

<androidx.camera.view.PreviewView
android:id="@+id/scannerPreview"
android:layout_width="350dp"
android:layout_height="470dp"
android:layout_gravity="center"
/>

</LinearLayout>
7 changes: 5 additions & 2 deletions src/main/res/layout/connection_error.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
Expand Down Expand Up @@ -48,7 +49,8 @@
style="@style/borderlessButton"
android:onClick="onClickMoreInfo"
android:text="@string/btnMoreInfo"
android:textAllCaps="true" />
android:textAllCaps="true"
tools:ignore="UsingOnClickInXml" />

<View
android:layout_width="0dp"
Expand All @@ -60,7 +62,8 @@
style="@style/borderlessButton"
android:onClick="onClickRetry"
android:text="@string/btnRetry"
android:textAllCaps="true" />
android:textAllCaps="true"
tools:ignore="UsingOnClickInXml" />
</LinearLayout>
</RelativeLayout>

Expand Down
4 changes: 2 additions & 2 deletions src/main/res/layout/custom_server_form.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
android:layout_alignParentBottom="true"
android:onClick="cancelSettingsEdit"
android:text="@string/btnCancel"
tools:ignore="OnClick" />
tools:ignore="OnClick,UsingOnClickInXml" />
<!-- OnClick is ignored because lint generates error
incorrectly. Test with future build tool versions
to see if this exception can be removed -->
Expand All @@ -32,5 +32,5 @@
android:layout_alignParentBottom="true"
android:onClick="verifyAndSave"
android:text="@string/btnSave"
tools:ignore="OnClick" />
tools:ignore="OnClick,UsingOnClickInXml" />
</RelativeLayout>
4 changes: 2 additions & 2 deletions src/main/res/layout/free_space_warning.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
<!-- OnClick is ignored because lint generates error
Expand All @@ -47,7 +47,7 @@
style="@style/standardButton"
android:text="@string/btnContinue"
android:onClick="evtContinue"
tools:ignore="OnClick"
tools:ignore="OnClick,UsingOnClickInXml"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
Expand Down
Loading
Loading