How to Use Barcode Scanner in Android WebView

Google Chrome has getUserMedia support since v53 and WebAssembly support since v57, which makes it possible to use Dynamsoft Barcode Reader to scan barcodes and QR codes in the web context. The WebView component included in modern Android uses Chrome as its backend, so we can also run a barcode scanner in Android’s WebView.

In this article, we are going to talk about how to use the JavaScript version of Dynamsoft Barcode Reader to scan barcodes and QR codes in an Android WebView.

Use Barcode Scanner in Android WebView

New Project and Configure

  1. Create a new project with Android Studio.
  2. Open src\main\AndroidManifest.xml to add camera and Internet permissions.

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

Request Camera Permission

In MainActivity.java, request the camera permission when the app starts.

public class MainActivity extends AppCompatActivity  {
    private static final String[] CAMERA_PERMISSION = new String[]{Manifest.permission.CAMERA};
    private static final int CAMERA_REQUEST_CODE = 10;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (hasCameraPermission() == false) {
            requestPermission();
        }
    }
    
    private boolean hasCameraPermission() {
        return ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED;
    }

    private void requestPermission() {
        ActivityCompat.requestPermissions(
                this,
                CAMERA_PERMISSION,
                CAMERA_REQUEST_CODE
        );
    }
}

Design Layout

Here is the component tree of the layout:

component tree

The WebView control is used to load the JavaScript version of Dynamsoft Barcode Reader, open the camera, scan barcodes and then return the result.

Because we may need to scan multiple times, in order to quickly reopen the scanner, we put the WebView in the same activity. When we need to scan, we set it visible, otherwise, we set it invisible.

Set up WebView

Extra settings are needed for the WebView.

  1. Enable JavaScript.

     private void loadWebViewSettings(){
         WebSettings settings = webView.getSettings();
         settings.setJavaScriptEnabled(true);
     }
    
  2. Override the onPageFinished event so that we can know whether the page has finished loading.

     webView.setWebViewClient(new WebViewClient(){
         @Override
         public void onPageFinished(WebView view, String url) {
             pageFinished = true;
         }
     });
    
  3. Grant permission requests.

     webView.setWebChromeClient(new WebChromeClient() {
         @Override
         public void onPermissionRequest(final PermissionRequest request) {
             MainActivity.this.runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
                     request.grant(request.getResources());
                 }
             });
         }
     });
    
  4. Other optional settings:

     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
         webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
     }
    
     // Enable remote debugging via chrome://inspect
     if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
         WebView.setWebContentsDebuggingEnabled(true);
     }
    

Add Assets

We’ve created a demo web app using getUserMedia and Dynamsoft Barcode Reader in the previous article.

Here, we just create an assets folder under src/main, and put the files of the demo: scanner.html, scanner.css and scanner.js in it so that we can load the web page in WebView.

Let’s review the key parts of the demo first.

  1. Use getUserMedia to open the camera.

     function play(){
         var constraints = {
           video: {facingMode: { exact: "environment" }}, // use back camera
           audio: false
         }
         navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
           localStream = stream;
           var camera = document.getElementsByClassName("camera")[0];
           // Attach local stream to video element
           camera.srcObject = stream;
         }).catch(function(err) {
             console.error('getUserMediaError', err, err.stack);
         });
     }
    
  2. Start a decoding loop to use Dynamsoft Barcode Reader to decode the video.

     var decoding = false;
     var barcodeReader;
    
     //we have to initialize the Barcode Reader first.    
     async function init(){
       Dynamsoft.DBR.BarcodeReader.license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==";
       barcodeReader = await Dynamsoft.DBR.BarcodeReader.createInstance();
     }
        
     function startDecodingLoop(){
       decoding = false;
       clearInterval(interval);
       interval = setInterval(decode, 40);
     }
    
     async function decode(){
       if (decoding === false && barcodeReader) {
         var video = document.getElementsByClassName("camera")[0];
         decoding = true;
         var barcodes = await barcodeReader.decode(video);
         drawOverlay(barcodes);
         decoding = false;
       }
     }
    
  3. The app will draw barcode overlays with SVG if it finds barcodes.

     function drawOverlay(barcodes){
       var svg = document.getElementsByTagName("svg")[0];
       svg.innerHTML = "";
       for (var i=0;i<barcodes.length;i++) {
         var barcode = barcodes[i];
         console.log(barcode);
         var lr = barcode.localizationResult;
         var points = getPointsData(lr);
         var polygon = document.createElementNS("http://www.w3.org/2000/svg","polygon");
         polygon.setAttribute("points",points);
         polygon.setAttribute("class","barcode-polygon");
         var text = document.createElementNS("http://www.w3.org/2000/svg","text");
         text.innerHTML = barcode.barcodeText;
         text.setAttribute("x",lr.x1);
         text.setAttribute("y",lr.y1);
         text.setAttribute("fill","red");
         text.setAttribute("font-size","20");
         svg.append(polygon);
         svg.append(text);
       }
     }
    
     function getPointsData(lr){
       var pointsData = lr.x1+","+lr.y1 + " ";
       pointsData = pointsData+ lr.x2+","+lr.y2 + " ";
       pointsData = pointsData+ lr.x3+","+lr.y3 + " ";
       pointsData = pointsData+ lr.x4+","+lr.y4;
       return pointsData;
     }
    

Load Assets using HTTPS URL

We can then load the web page with WebView’s loadUrl method using the file protocol:

webView.loadUrl("file:android_asset/scanner.html");

However, we cannot host the library of Dynamsoft Barcode Reader locally as WebAssembly requires HTTP. We have to use the WebViewAssetLoader helper class to load local files including application’s static assets and resources using http(s):// URLs.

Here are the steps to load assets with WebViewAssetLoader.

  1. Add the WebViewAssetLoader dependency in the app’s build.gradle.

     implementation 'androidx.webkit:webkit:1.4.0'
    
  2. Update the WebView’s settings.

     final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
               .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this))
               .build();
                  
     webView.setWebViewClient(new WebViewClientCompat() {
       @Override
       @RequiresApi(21)
       public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
           return assetLoader.shouldInterceptRequest(request.getUrl());
       }
    
       @Override
       @SuppressWarnings("deprecation") // for API < 21
       public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
           return assetLoader.shouldInterceptRequest(Uri.parse(url));
       }
     });
    
     WebSettings webViewSettings = webView.getSettings();
     // Setting this off for security. Off by default for SDK versions >= 16.
     webViewSettings.setAllowFileAccessFromFileURLs(false);
     // Off by default, deprecated for SDK versions >= 30.
     webViewSettings.setAllowUniversalAccessFromFileURLs(false);
     // Keeping these off is less critical but still a good idea, especially if your app is not
     // using file:// or content:// URLs.
     webViewSettings.setAllowFileAccess(false);
     webViewSettings.setAllowContentAccess(false);
    
  3. Then, we can load the web page with the following URL:

     webView.loadUrl("https://appassets.androidplatform.net/assets/scanner.html");
    
  4. We can now use the library of Dynamsoft Barcode Reader locally instead of using a CDN.

    1. Download the library and put the dist folder in the assets folder.

    2. Modify scanner.html to use the local library.

      - <script src="https://unpkg.com/dynamsoft-javascript-barcode@9.0.2/dist/dbr.js"></script>
      + <script src="https://appassets.androidplatform.net/assets/dist/dbr.js"></script>
      

Call JavaScript Functions to Start Scanning from Java

Now that we can load the web page, we are going to further integrate the web barcode scanner in the Android project.

First, we need to call JavaScript functions from Java to control the scanner. Here are the JavaScript functions we are going to call from Java.

function resumeScan(){
  if (localStream) {
    var camera = document.getElementsByClassName("camera")[0];
    camera.play();
    startDecodingLoop();
  }
}

function pauseScan(){
  if (localStream) {
    clearInterval(interval);
    var camera = document.getElementsByClassName("camera")[0];
    camera.pause();
  }
}

function isCameraOpened(){
  if (localStream) {
    return "yes";
  }else{
    return "no";
  }
}

function startScan(){
  decoding = false;
  scannerContainer.style.display = "";
  home.style.display = "none";
  play(true);
}

We can use WebView’s evaluateJavascript to do this.

  1. Set the onclick event for the scan barcodes button to start or resume the scanner.

     Button scanBarcodesButton = findViewById(R.id.scanBarcodesButton);
     scanBarcodesButton.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View view) {
             if (pageFinished) {
                 webView.evaluateJavascript("javascript:isCameraOpened()", new ValueCallback<String>() {
                     @Override
                     public void onReceiveValue(String value) {
                         if (value.endsWith("\"yes\"")) {
                             resumeScan();
                         }else{
                             startScan();
                         }
                     }
                 });
    
                 webView.setVisibility(View.VISIBLE);
             }else{
                 Toast.makeText(ctx,"The web page has not been loaded.",Toast.LENGTH_SHORT).show();
             }
    
         }
     });
    
     private void startScan(){
         webView.evaluateJavascript("javascript:startScan()", new ValueCallback<String>() {
             @Override
             public void onReceiveValue(String value) {
             }
         });
     }
    
     private void resumeScan(){
         webView.evaluateJavascript("javascript:resumeScan()", new ValueCallback<String>() {
             @Override
             public void onReceiveValue(String value) {
             }
         });
     }
    
  2. Override the onBackPressed event so that when users press the back button, pause scan.

     @Override
     public void onBackPressed() {
         pauseScan();
         webView.setVisibility(View.INVISIBLE);
     }
        
     private void pauseScan(){
         webView.evaluateJavascript("javascript:pauseScan()", new ValueCallback<String>() {
             @Override
             public void onReceiveValue(String value) {
             }
         });
     }
    

Return the Barcode Results to Java

If the barcode scanner in the WebView finds a barcode, we need to pause scan and then display the barcode result in a native TextView. To do this, we need to call Java methods from JavaScript using WebView’s addJavascriptInterface method.

  1. Define a callback handler to return barcode results.

     class ScanHandler {
         public void onScanned(String result) {
    
         }
     }
    
  2. Define a new JSInterface class.

     public class JSInterface {
         private ScanHandler mHandler;
         JSInterface(ScanHandler handler){
           mHandler = handler;
         }
            
         @JavascriptInterface
         public void returnResult(String result) {
             mHandler.onScanned(result);
         }
     }
    
  3. Add the JavaScript interface.

     webView.addJavascriptInterface(new JSInterface(new ScanHandler (){
         @Override
         public void onScanned(String result){
             runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
                     webView.setVisibility(View.INVISIBLE);
                     textView.setText(result);
                 }
             });
         }
     }), "AndroidFunction");
    

    This will expose a global AndroidFunction object which has the functions defined in the JSInterface class.

  4. Call the Java function from JavaScript.

     async function decode(){
       if (decoding === false && barcodeReader) {
         var video = document.getElementsByClassName("camera")[0];
         decoding = true;
         var barcodes = await barcodeReader.decode(video);
         drawOverlay(barcodes);
         if (barcodes.length > 0) {
           setTimeout(function(){
             pauseScan();
             AndroidFunction.returnResult(barcodes[0].barcodeText);
           },timeoutAfterScan);
           return;
         }
         decoding = false;
       }
     }
    

Handle Lifecycle Events

We also need to handle lifecycle events.

When the activity is paused, stop scan. When the activity is resumed, if the webview is visible, start scan.

@Override
protected void onResume() {
    super.onResume();
    if (webView.getVisibility() == View.VISIBLE) {
        startScan();
    }
}

@Override
protected void onPause() {
    super.onPause();
    stopScan();
}

private void stopScan(){
    webView.evaluateJavascript("javascript:stopScan()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
        }
    });
}

The stopScan JavaScript function:

function stopScan(){
  if (localStream) {
    clearInterval(interval);
    stop();
    localStream = null;
  }
}

Here is a video of the final result:

Source Code

https://github.com/xulihang/Barcode-Scanner-WebView/