Skip to content

QR Code Scanning

This document explains how QR code scanning is implemented and exposed to the WebView layer. It covers:

  • The bridge API used by JavaScript to start a scan
  • How permissions are handled
  • How the Huawei ScanKit activity is launched
  • How the scan result is delivered back to JavaScript

The QR scanning feature is implemented in BaseWebViewActivity and exposed to the WebView via bridgeWebView.

Overview

The QR scanning flow is:

┌──────────┐
│   START  │
└────┬─────┘
     │
     ▼
┌─────────────────────────┐
│ Check Permissions       │
│ - CAMERA                │
│ - READ_MEDIA            │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ Register Callback       │
│ scanQrcodeResultCallback│
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ startQrCodeScan(999)    │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ Open Camera Scanner     │
│                         │
│  ┌─────────────────┐    │
│  │  [QR Frame]     │    │
│  │                 │    │
│  │  Point camera   │    │
│  │  at QR code     │    │
│  └─────────────────┘    │
└────┬────────────────────┘
     │
     │ (User scans or cancels)
     │
     ├────────────┬─────────────┐
     ▼            ▼             ▼
  Success      Cancel         Error
     │
     ▼
┌─────────────────────────┐
│ Decode QR Data          │
│ Extract value           │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ scanQrcodeResultCallback│
│ {                       │
│   value: "QR_CONTENT",  │
│   requestCode: 999      │
│ }                       │
└────┬────────────────────┘
     │
     ▼
┌─────────────────────────┐
│ Process QR Data         │
│ in JavaScript           │
│ - URL redirect          │
│ - Parse JSON            │
│ - etc.                  │
└─────────────────────────┘
     │
     ▼
┌──────────┐
│   END    │
└──────────┘
  1. JavaScript calls the native bridge method startQrCodeScan.
  2. Native code checks and requests the necessary permissions (camera, etc.).
  3. If permissions are granted, native configures Huawei ScanKit and launches the ScanKitActivity.
  4. The user scans a QR code (or cancels).
  5. BaseWebViewActivity.onActivityResult() receives the result, extracts the scanned value, and sends it back to JavaScript via scanQrcodeResultCallBack.

The flow is fully event-driven: the WebView triggers the scan and receives the result asynchronously through a callback.

Native–WebView Bridge API

startQrCodeScan (JS → Native)

Bridge handler name: startQrCodeScan Defined in: registerMethod() inside BaseWebViewActivity

Signature on the native side:

bridgeWebView.registerHandler("startQrCodeScan", new BridgeHandler() {
    @Override
    public void handler(String requestCode, CallBackFunction function) {
        // ...
    }
});

Parameters

requestCode (String)

- Optional, sent from JavaScript.
- If empty, REQUEST_CODE_SCAN_ONE defaults to 0.
- If non-empty, it is parsed as an integer and stored in REQUEST_CODE_SCAN_ONE.
- This allows the JS side to distinguish between multiple scan requests if needed.

Example native logic:

REQUEST_CODE_SCAN_ONE = TextUtils.isEmpty(requestCode)
        ? 0
        : Integer.valueOf(requestCode);

Immediate Native Behavior

Inside handler(...), native code:

  1. Requests runtime permissions using XXPermissions (with a custom PermissionInterceptor).
  2. If not all required permissions are granted, it immediately calls back to JS with a permission error:
    function.onCallBack(gson.toJson(Result.fail(PERMISSION_ERROR, false)));
    return;
    
  3. If permissions are granted, it builds a Huawei ScanKit configuration and launches the scan activity.

Permissions Handling

Permissions are requested via XXPermissions:

XXPermissions.with(BaseWebViewActivity.this)
    // (Camera and any related permissions are configured here)
    .interceptor(new PermissionInterceptor())
    .request(new OnPermissionCallback() {
        @Override
        public void onGranted(@NonNull List<String> permissions, boolean allGranted) {
            if (!allGranted) {
                function.onCallBack(gson.toJson(Result.fail(PERMISSION_ERROR, false)));
                return;
            }
            // Permissions granted → proceed to launch scanner
            // ...
        }
    });

Key points:

  • Permissions are requested before any scan UI is launched.
  • If the user denies any required permission, the WebView receives a response with Result.fail(PERMISSION_ERROR, false) and no scan activity is started.
  • The actual permission list (camera, storage, etc.) is configured in the native code and can be adjusted as needed.

Launching the Scan UI (Huawei ScanKit)

Once permissions are granted, the code constructs an HmsScanAnalyzerOptions and starts the Huawei ScanKit activity:

HmsScanAnalyzerOptions.Creator creator = new HmsScanAnalyzerOptions.Creator();

// Accept all types of barcodes / QR codes
creator.setHmsScanTypes(HmsScan.ALL_SCAN_TYPE);

// Show the built-in scan guide overlay
creator.setShowGuide(true);

// Allow scanning from gallery (photo mode)
creator.setPhotoMode(true);

HmsScanAnalyzerOptions hmsScanAnalyzerOptions = creator.create();

// Launch ScanKit activity
Intent intent = new Intent(BaseWebViewActivity.this, ScanKitActivity.class);
if (intent != null) {
    intent.putExtra("ScanFormatValue", hmsScanAnalyzerOptions.mode);
}

startActivityForResult(intent, REQUEST_CODE_SCAN_ONE);

Notes:

  • HmsScan.ALL_SCAN_TYPE enables recognition of multiple barcode formats, not just QR codes.
  • setShowGuide(true) displays a guide on top of the camera preview to help user alignment.
  • setPhotoMode(true) allows the user to select an existing image from the gallery and scan it.
  • The REQUEST_CODE_SCAN_ONE is used later in onActivityResult to identify the result.

If any exception occurs while building options or starting the activity, it is caught and logged:

} 
catch (Exception e) {
    e.printStackTrace();
}

Receiving the Scan Result (Native)

The scan result is handled in onActivityResult:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == REQUEST_CODE_SCAN_ONE && resultCode == RESULT_OK) {
        HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT);
        Toaster.show(obj.originalValue);

        if (obj != null) {
            Map<String, Object> map = new HashMap<>();
            map.put("value", obj.originalValue);
            map.put("requestCode", REQUEST_CODE_SCAN_ONE);

            bridgeWebView.callHandler("scanQrcodeResultCallBack",
                    gson.toJson(Result.ok(map)),
                    new CallBackFunction() {
                        @Override
                        public void onCallBack(String data) {
                        }
                    });
        } else {
            bridgeWebView.callHandler("scanQrcodeResultCallBack",
                    gson.toJson(Result.fail()),
                    new CallBackFunction() {
                        @Override
                        public void onCallBack(String data) {
                        }
                    });
        }
    }

    // ... (other onActivityResult branches: picture choose, OCR, etc.)
}

Important details

  • Only when requestCode == REQUEST_CODE_SCAN_ONE and resultCode == RESULT_OK do we treat it as a valid scan result.
  • HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT); pulls the HmsScan result object from the intent.
  • obj.originalValue contains the string value encoded in the QR code / barcode.
  • A small toast (Toaster.show(obj.originalValue);) is shown for debugging / quick feedback.
  • If obj is non-null:

    • A map is created with:
      • "value" → the scanned string (obj.originalValue)
      • "requestCode" → the request code used (so JS can correlate the response)
    • The map is wrapped with Result.ok(map) and sent back to JS via scanQrcodeResultCallBack.
  • If obj is null:

    • Native calls scanQrcodeResultCallBack with Result.fail().

Cancellation handling:

- If the user cancels the ScanKit screen, `resultCode` will not be `RESULT_OK`, so the QR branch is skipped.
- In that case, no success payload is sent; the JS side should handle this by treating “no result received” as a canceled operation or by adding a separate mechanism if needed.

JavaScript Side Usage

1. Register the result callback

In your WebView JavaScript, you must register a handler to receive scan results

bridgeWebView.registerHandler("scanQrcodeResultCallBack", function (data) {
  const res = JSON.parse(data);

  // Depending on your Result implementation, this may look like:
  // { success: true/false, code: ..., data: { value, requestCode } }

  if (res.success) {
    const value = res.data.value;
    const reqCode = res.data.requestCode;
    console.log("QR scan success:", value, "(req:", reqCode, ")");

    // TODO: handle the scanned value (navigate, fill form, etc.)
  } else {
    console.warn("QR scan failed", res);
    // TODO: show error message or retry option
  }
});

2. Start a QR scan from JS

When you want to start scanning:

function startQrScan() {
  const REQUEST_CODE_QR = 1001; // any integer you want to track this call

  bridgeWebView.callHandler(
    "startQrCodeScan",
    String(REQUEST_CODE_QR), // this becomes requestCode in native
    function (response) {
      // Initial response from native (permission / immediate errors)
      const res = JSON.parse(response || "{}");

      if (res.success === false) {
        console.error("Failed to start QR scan:", res);
        // Probably permission denied or some immediate error.
      } else {
        // In most cases, you simply wait for scanQrcodeResultCallBack.
        console.log("QR scan started; waiting for result...");
      }
    }
  );
}

Flow from JS perspective:

  1. Call startQrCodeScan.
  2. Optionally inspect the immediate response (for permission errors).
  3. Wait for scanQrcodeResultCallBack with the final result.

Result Payload Structure

All responses are wrapped in the internal Result class before being sent to JS, for example:

  • Success (conceptual shape):

        {
      "success": true,
      "code": 0,
      "data": {
        "value": "scanned-qr-content",
        "requestCode": 1001
      }
    }
    

  • On Faliure

    {
      "success": false,
      "code": "<error-code>",
      "message": "<human-readable message>"
    }
    

The exact keys (code, message, etc.) depend on your Result implementation, but the important part is that data.value holds the scanned string and data.requestCode lets you map a result to a specific scan request.

Error Handling & Edge Cases

  • Permissions denied

    • startQrCodeScan returns immediately with Result.fail(PERMISSION_ERROR, false).
    • The scan UI is never opened.
    • JS should show a message asking the user to grant camera permission.
  • Scan returns null result (obj == null)

    • scanQrcodeResultCallBack is called with Result.fail().
    • JS should treat this as “scan failed” and allow retry.
  • User cancels ScanKit

    • resultCode is not RESULT_OK.
    • QR branch is skipped in onActivityResult.
    • No success payload is sent; JS should handle this as “no result” (e.g. by timeout, or by user flow).
  • Unexpected exceptions when launching ScanKit

    • Currently only logged via e.printStackTrace().
    • You may want to extend this to send a failure Result back to JS for better UX.

Summary

  • The QR scanning implementation provides a clean, high-level API to the WebView:

    • JS entry point: startQrCodeScan
    • Native scanner: Huawei ScanKit via ScanKitActivity
    • Result callback to JS: scanQrcodeResultCallBack with a Result-wrapped payload containing the scanned value and requestCode.

This keeps all camera and ScanKit details in the native layer, while keeping the WebView integration simple and predictable.