How to Test QR Code Scanning on Android (Complete Guide)
QR codes have become a ubiquitous bridge between physical touchpoints and digital experiences. In Android applications they enable quick login, payment initiation, device pairing, coupon redemption, a
Why QR Code Scanning Matters in Android Apps
QR codes have become a ubiquitous bridge between physical touchpoints and digital experiences. In Android applications they enable quick login, payment initiation, device pairing, coupon redemption, and AR content launch. When the scanner fails, users abandon the flow, support tickets rise, and brand trust erodes.
Testing QR code scanning is not just about confirming that a camera can read a pattern; it is about validating that the entire pipeline—from image acquisition, through decoding, to business‑logic handling—works under the varied conditions users encounter in the wild. A single missed edge case can produce a silent failure: the app shows a loading spinner forever, or worse, it misinterprets data and triggers an unintended action (e.g., sending money to the wrong address).
Because the scanner relies on hardware (camera, focus, exposure), software (decoder libraries, permission handling), and environmental factors (lighting, angle, glare), it is a classic source of intermittent bugs that slip through scripted regression suites. A disciplined test strategy therefore combines deterministic checks with exploratory, persona‑driven techniques that mimic real‑world usage patterns.
Common Failure Modes in Production
Understanding where QR scanners break helps prioritize test effort. The following categories capture the majority of field‑observed defects:
| Category | Typical Symptom | Root Cause |
|---|---|---|
| Permission mishandling | Scanner never launches; toast says “Camera permission denied” | Manifest missing CAMERA or runtime request not handled |
| Focus/exposure issues | Blurry preview, decoder returns null | Fixed‑focus camera, lack of tap‑to‑focus, or exposure lock not released |
| Decoder library limits | Valid QR returns empty string; invalid QR accepted | Out‑of‑date ZXing, ML Kit model mismatch, or incorrect barcode format flags |
| UI thread blocking | ANR after scanning, UI freezes | Heavy image processing on main thread |
| Incorrect intent handling | Scan result opens wrong activity or crashes | Mismatch between scanned data format and expected intent extras |
| Glare/reflection | Scan fails on glossy surfaces or under bright light | Auto‑exposure cannot compensate; need manual EV adjustment |
| Accessibility gaps | TalkBack does not announce scan status | Missing content descriptions on scanner view or result dialog |
| Security oversights | QR triggers arbitrary intent or web load without validation | Trusting scanned URL without scheme whitelist or intent filter validation |
| Locale/encoding problems | Non‑Latin characters appear garbled | Decoder assumes UTF‑8 but data encoded in ISO‑8859‑1 or Shift_JIS |
| Power‑saving interference | Scanner stops after a few seconds when battery saver is on | Camera preview paused by system power manager |
Each of these can be reproduced in a lab setting with the right tools, but many only manifest under specific combinations (e.g., low‑light + glare + battery saver). The test matrix below enumerates the dimensions to cover.
Test Matrix for QR Code Scanning
The matrix combines functional, non‑functional, and security dimensions. Use it as a checklist when designing test cases; each cell represents a distinct scenario to verify.
| Dimension | Happy Path | Error Path | Edge Case | Accessibility | Security / Privacy |
|---|---|---|---|---|---|
| Input | Valid QR encoding a URL, plain text, or Wi‑Fi config | Malformed QR (missing format indicator) | QR with excessive version (e.g., version 40) | QR with low contrast modules (light gray on white) | QR containing JavaScript:alert(1) or intent:#Intent;action=android.intent.action.VIEW;S.browser_fallback_url=http://evil.com;end |
| Camera Settings | Auto‑focus enabled, default exposure | Camera disabled via device policy | Manual focus set to infinity; exposure locked at -2 EV | Preview shown with TalkBack label “Scanner view” | Camera permission requested only when scanner button pressed |
| Lighting | Uniform indoor lighting (~300 lux) | Dim environment (<50 lux) | Direct sunlight causing overexposure | High‑contrast mode enabled (system setting) | QR displayed on reflective surface (glass) to test glare handling |
| Angle & Distance | QR centered, 0° tilt, 10‑15 cm distance | QR rotated 45°, 90° | QR partially occluded (20% covered by finger) | QR presented via accessibility service overlay | QR printed on curved surface (bottle) to test distortion |
| Decoder Library | ZXing core 3.4.0, ML Kit Barcode Scanning v16.0.0 | Library missing required format (e.g., PDF417) | Custom decoder with slow algorithm | Decoder output announced via AccessibilityEvent | Library version known to have CVE (e.g., ZXing <3.3.3) |
| Threading | Decode runs on AsyncTask/Executor, UI remains responsive | Decode on main thread causing >16ms frame drop | Decode spawned on background thread but result posted via Handler causing leak | Result delivered via AccessibilityAnnouncement | Background decode continues after activity is finished (leak) |
| Result Handling | URL opened in Custom Tabs with validation | Invalid URL scheme triggers error dialog | Result contains newline injection (\n) that breaks parser | Result spoken aloud with appropriate pitch | Result triggers implicit intent without whitelist check |
| Device State | Battery >80%, normal performance mode | Battery saver ON, CPU throttled | Device in Doze mode, app in background | Font size set to largest, screen magnifier enabled | Device admin policy disables camera for non‑system apps |
| Network | Online, Wi‑Fi, successful fetch after scan | No network, offline fallback shown | Captive portal redirect after scanning URL | Network state announced via Accessibility | QR contains internal IP address; app attempts to fetch without validation |
| Lifecycle | Scanner launched from foreground activity, result returned via onActivityResult | Scanner launched from fragment, result lost due to fragment recreation | Activity recreated (rotation) mid‑scan, preview survives | Scanner view restored correctly after rotation | Scanner continues after onPause leading to leaked camera |
| Internationalization | QR encodes UTF‑8 English text | QR encodes UTF‑8 Japanese, Arabic, or emoji | QR encodes mixed script with RTL markers | Layout mirrors correctly for RTL locales | QR contains locale‑specific payload that could trigger locale‑based code path |
Each row can be expanded into multiple test cases (e.g., varying angle from 0° to 90° in 15° steps). The matrix makes it easy to see where automation can cover large swaths (happy path, many error paths) and where manual or exploratory testing is essential (glare, angle, accessibility).
Manual Testing Approach
A disciplined manual session starts with a test device matrix (different Android versions, OEM camera hardware, and form factors) and a set of printed or displayed QR codes that exercise the matrix dimensions.
1. Prepare the Device Lab
- Devices: At least one phone running Android 8.0 (API 26), one on Android 11 (API 30), and one on Android 13 (API 33). Include a device with a fixed‑focus camera (e.g., low‑end Android Go) and one with OIS/laser focus.
- Environment: A lighting rig with adjustable lux levels (0–1000 lux) and a rotatable platform for angle testing.
- QR Set: Generate codes using an online tool or
zxingCLI. Store them as PNG files and print on matte paper, glossy photo paper, and a transparent sheet for glare tests. Include codes that encode: - Simple URL (
https://example.com) - Wi‑Fi config (
WIFI:T:WPA;S:mynet;P:secret;;) - vCard (
BEGIN:VCARD...END:VCARD) - Malformed data (missing length indicator)
- JavaScript payload (
javascript:alert(1))
2. Baseline Permission Check
- Launch the app, navigate to the scanner screen.
- Observe system permission dialog.
- Deny permission → verify that the app shows a clear inline message (“Camera permission required to scan”).
- Grant permission via Settings → repeat scan → confirm scanner opens.
3. Happy‑Path Validation
- Place a matte‑printed URL QR at the center of the preview, 12 cm away.
- Tap the scan button (if present) or rely on auto‑detect.
- Verify:
- Decoder returns the exact string.
- App launches the URL in a Custom Tab or internal WebView.
- No ANR (monitor via
adb shell dumpsys gfxinfo– frame‑time should stay under 16 ms). - TalkBack announces “Scan successful, opening example.com”.
4. Error‑Path Injection
- Swap the QR for a malformed version (e.g., remove the version field).
- Expect:
- Decoder returns null or throws
FormatException. - UI shows a user‑friendly error (“Unable to read code”).
- No crash; logcat shows no
NullPointerExceptionfrom decoder.
5. Edge‑Case Exploration
| Sub‑test | Procedure | Pass Criteria |
|---|---|---|
| Low Light | Dim lux to 30, place QR, enable torch if available. | Scanner either succeeds (with torch) or shows a clear “Insufficient light” message. |
| Glare | Place QR on glossy sheet, shine a 45° lamp to create specular highlight. | Scanner detects code despite glare, or falls back to manual re‑position prompt. |
| Angle | Rotate QR to 30°, 45°, 60° while keeping distance constant. | Decode succeeds up to at least 45° tilt; beyond that, a graceful degradation message appears. |
| Occlusion | Cover 20% of QR with a finger or sticker. | Decoder fails but does not crash; UI invites user to reveal full code. |
| High Version | Generate version‑40 QR (177 × 177 modules). | Decoder processes without OutOfMemoryError; result matches source. |
| Battery Saver | Enable Android Battery Saver, repeat happy‑path. | Scanner still works; preview frame rate may drop but no ANR. |
| Doze Mode | Put device idle for >15 min, then trigger scan via notification. | Scanner wakes, acquires camera, and returns result within 2 s. |
| Font Size / Magnifier | Set system font to largest, enable magnification gesture. | All scanner UI elements remain readable and tappable; TalkBack reads labels correctly. |
| RTL Layout | Switch device language to Arabic (right‑to‑left). | Scanner view mirrors correctly; preview not flipped horizontally. |
| Malicious Intent | Scan QR containing intent:#Intent;action=android.intent.action.VIEW;S.browser_fallback_url=http://evil.com;end. | App validates scheme; either blocks the intent or opens it in a sandboxed WebView with no navigation to external domains. |
| Encoding Test | Scan QR with Japanese UTF‑8 text “こんにちは”. | Result string matches original; displayed correctly in UI (no mojibake). |
During each sub‑test, capture logcat (adb logcat -v time | grep -i qr) to verify that no unexpected exceptions are thrown and that the decoder logs appropriate messages.
6. Accessibility Walk‑through
- Enable TalkBack.
- Navigate to scanner screen using swipe gestures.
- Confirm that the scanner view has a content description like “Scanner, double tap to start”.
- While scanning, listen for announcements: “Scanning…”, “Scan successful”, or error messages.
- After result, ensure that the result dialog is focusable and that actions (Open, Copy, Cancel) are announced.
7. Security & Privacy Checks
- URL Whitelist: Attempt to scan a QR with a disallowed domain (e.g.,
http://localhost:8080). Verify that the app blocks navigation and shows a warning. - Intent Sanitization: Scan a QR that encodes an intent to
Settings.ACTION_APPLICATION_DETAILS_SETTINGS. Confirm that the app does not launch system settings unless explicitly allowed. - Data Minimization: After a successful scan, ensure that the raw image bytes are not persisted to disk or logged. Use
adb shell run-asto inspect any leftover files.ls /data/data/ /files - Camera Release: After scanning or canceling, run
adb shell dumpsys media.cameraand verify that the camera ID is no longer held by the app’s PID.
8. Post‑Test Cleanup
- Clear app data (
adb shell pm clear) to ensure no state influences subsequent runs. - Rotate device orientation and repeat a subset of tests to confirm lifecycle robustness.
Manual testing, while time‑consuming, is invaluable for catching issues that depend on subtle sensor behavior, lighting physics, or human perception—areas where automated scripts often make unrealistic assumptions.
Automated Testing Approaches
Automation shines for repeatable happy‑path and many error‑path checks. Android offers several layers that can be combined to achieve high coverage without flakiness.
Unit‑Level Validation of Decoder Logic
If your app wraps a decoder (ZXing, ML Kit) in a utility class, unit‑test it with pure Java/Kotlin:
class QrDecoderTest {
private val decoder = QrDecoderImpl() // wraps ZXing
@Test
fun `valid url returns string`() {
val bytes = encodeQr("https://example.com") // helper that returns ByteArray
assertEquals("https://example.com", decoder.decode(bytes))
}
@Test
fun `malformed qr returns null`() {
val bad = encodeQr("INVALID") // missing length indicator
assertNull(decoder.decode(bad))
}
@Test
fun `utf8 japanese preserved`() {
val src = "こんにちは"
val bytes = encodeQr(src)
assertEquals(src, decoder.decode(bytes))
}
}
Run these tests on every CI build; they guard against regressions in the decoding layer independent of UI.
Instrumented UI Tests with Espresso
Espresso can drive the scanner UI, inject bitmap, bypassing the camera entirely. This yields deterministic, fast tests.
- Add a test‑only interface to your scanner fragment/activity that accepts a
Bitmapfor simulated scanning:
class QrScannerFragment : Fragment(R.layout.fragment_qr_scanner) {
var testBitmap: Bitmap? = null // visible only in test source set
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.btn_scan).setOnClickListener {
val bmp = testBitmap ?: takePreviewFrame() // real camera path
val result = QrDecoderImpl().decode(bmp)
onResultReceived(result)
}
}
}
- Write the Espresso test:
@RunWith(AndroidJUnit4::class)
class QrScannerEspressoTest {
@get:Rule
val activityRule = ActivityScenarioRule(QrScannerActivity::class.java)
@Test
fun happyPathWithEspresso() {
// Prepare a bitmap that encodes a known URL
val url = "https://susatest.com/demo"
val bmp = encodeQrToBitmap(url)
// Inject bitmap into fragment
onView(withId(R.id.fragment_container))
.perform(replaceFragment(QrScannerFragment::class.java))
onView(withId(R.id.qr_scanner_fragment))
.perform(setTestBitmap(bmp)) // custom ViewAction
// Trigger scan
onView(withId(R.id.btn_scan)).perform(click())
// Verify result: Custom Tab opened with correct URL
intended(hasData(Uri.parse(url)))
intended(hasAction(Intent.ACTION_VIEW))
}
@Test
fun errorPathShowsMessage() {
val badBmp = encodeQrToBitmap("!!INVALID!!")
onView(withId(R.id.qr_scanner_fragment))
.perform(setTestBitmap(badBmp))
onView(withId(R.id.btn_scan)).perform(click())
onView(withText(R.string.error_unreadable))
.check(matches(isDisplayed()))
}
}
The setTestBitmap ViewAction simply assigns the bitmap to the fragment’s testBitmap field. Because the camera is never used, the test runs in <200 ms on any emulator or device.
UI Automator for System‑Level Scenarios
When you need to test interactions that cross app boundaries (e.g., launching a Custom Tab, handling an intent from another app), UI Automator is appropriate.
@RunWith(AndroidJUnit4.class)
public class QrScannerUiAutomatorTest {
private UiDevice device;
@Before
public void setUp() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
@Test
public void scanLaunchesCustomTab() throws Exception {
// Assume the app is already launched and on scanner screen
UiObject scanBtn = device.findObject(new UiSelector().resourceId("com.example.app:id/btn_scan"));
scanBtn.click();
// Wait for Custom Tab toolbar to appear
UiObject customTab = device.wait(Until.findObject(
new UiSelector().descriptionContains("Open in Chrome")), 5000);
assertTrue(customTab.exists());
// Verify URL in the Custom Tab's address bar (requires accessibility service)
UiObject addressBar = device.findObject(new UiSelector()
.resourceId("com.android.chrome:id/url_bar"));
assertEquals("https://susatest.com/demo", addressBar.getText());
}
}
UI Automator tests are slower but validate the full intent‑resolution chain, which pure Espresso cannot.
Leveraging Firebase Test Lab for Device Matrix
To gain confidence across OEM camera hardware, upload your APK (or App Bundle) to Firebase Test Lab and run the Espresso/UI Automator suite on a selection of devices:
gcloud firebase test android run \
--type instrumentation \
--app app-debug.apk \
--test app-debug-test.apk \
--device model=Pixel3,version=30,locale=en,orientation=portrait \
--device model=SamsungGalaxyS9,version=28,locale=en,orientation=landscape \
--timeout 30m
Test Lab provides video logs, allowing you to visually confirm that the preview behaves correctly under each device’s camera characteristics.
Mocking the Camera with AndroidX Test’s CameraXTestUtil
If your app uses CameraX, you can supply a fake ImageAnalysis analyzer that feeds pre‑generated frames:
@ExperimentalCoroutinesApi
class FakeImageAnalyzer(private val bitmap: Bitmap) : ImageAnalysis.Analyzer {
override fun analyze(imageProxy: ImageProxy) {
val bitmapRef = imageProxy.toBitmap()
// Replace the frame with our test bitmap
imageProxy.close()
// Deliver the bitmap to the decoder on the main thread
Handler(Looper.getMainLooper()).post {
QrDecoderImpl().decode(bitmap)
}
}
}
In your test, set the analyzer to the fake instance, then trigger the scan flow. This technique validates the full CameraX lifecycle without needing a physical device.
Continuous Integration Checklist
Add the following steps to your CI pipeline:
- Run unit tests (
./gradlew test). - Run Espresso suite on emulator (
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.notAnnotation=ui.automator). - Run UI Automator subset on a single device in Test Lab (to keep cost low).
- Archive test artifacts (screenshots, logcat) forflaky‑test analysis.
Automation catches regressions quickly, but it should be complemented by the manual and exploratory sessions described earlier to cover the unpredictable real‑world variables.
Tooling Specific to Android QR Scanning
Choosing the right decoder and auxiliary libraries influences both functionality and testability. Below is a comparison of the most common options.
| Library | License | Supported Formats | Camera Integration | Size Impact | Notable Pros | Cons / Gotchas |
|---|---|---|---|---|---|---|
| ZXing (“Zebra Crossing”) | Apache 2.0 | QR Code, Data Matrix, Aztec, PDF417, UPC/EAN, etc. | Works with Camera1, Camera2, CameraX via CaptureActivity | ~350 KB (core) + camera shim | Mature, extensive format support, easy to bundle | Larger APK if you include the full android-integration module; UI‑heavy default activity |
| ML Kit Barcode Scanning (Google) | Proprietary (free tier) | QR Code, Data Matrix, Aztec, PDF417, UPC/EAN, Code 128, etc. | Works with CameraX (BarcodeScanning) via ProcessImage | ~1 MB (dynamic feature download) | On‑device model, no network needed after download, excellent speed, auto‑focus handling | Requires Play Services; APK size increase if bundled; limited customization of decoder parameters |
| Vision Library (Deprecated) | Apache 2.0 | QR, Data Matrix, Aztec, UPC/EAN | Works with CameraSource | ~500 KB | Simple API, good docs | Officially deprecated; will be removed in future SDKs |
| Dynamsoft Barcode Reader | Commercial | 20+ barcode types | CameraX, Camera2 | ~2 MB (native libs) | High performance, supports damaged codes | License cost, adds native .so files increasing APK size |
| Custom ZXing Core Only | Apache 2.0 | QR Code (if you limit) | You supply bitmap from any source (Camera, ImageReader, etc.) | ~100 KB | Minimal footprint, full control over threading & power usage | You must build your own preview UI and permission handling |
When to Choose Which
- Rapid prototype / internal tool: ML Kit offers the fastest integration with minimal boilerplate.
- Public‑facing app where APK size matters: Use ZXing core only and build a thin CameraX preview; you avoid the heavy
android-integrationactivity. - Need to support legacy barcode types (PDF417, Data Matrix) on low‑end devices: ZXing full‑featured module is still the most reliable.
- Enterprise with strict licensing: Evaluate commercial SDKs only after verifying that open‑source options cannot meet performance or damage‑tolerance requirements.
Debugging & Inspection Tools
adb shell dumpsys media.camera– shows which processes hold the camera, useful for verifying release after scan.Systrace– capture frames during a 10‑second trace while scanning reveals if the decoder is blocking the UI thread.GPU Inspector– if you render a custom preview with OpenGL, verify that texture uploads aren’t causing jank.LeakCanary– place it in your test variant to catch anyImageAnalyzerorCameraProviderleaks.StethoorFlipper– expose a runtime inspector to check the decoded string in real time without breaking UI flow.
These tools are indispensable when you suspect a threading or resource‑leak issue that only manifests under specific device load.
Edge Cases That Only Show Up in Production
Even with exhaustive lab testing, certain conditions surface only after the app reaches real users. Knowing where to look helps you prioritize monitoring and field‑testing.
1. Variable Auto‑Focus Behaviors
Some OEMs expose a “continuous focus” mode that constantly adjusts lens position, causing the preview to jitter. If your decoder assumes a static frame for a few hundred milliseconds, you may see intermittent null results.
Mitigation:
- Use a sliding‑window approach: collect N consecutive frames, attempt decode on each, and accept the first successful result.
- Expose a setting to disable continuous focus on devices known to be problematic (read
Build.MANUFACTURERandBuild.MODEL).
2. Infrared (IR) Filters on Front‑Facing Cameras
Many front‑face cameras have an IR cut filter that reduces sensitivity to certain wavelengths. QR codes printed with IR‑absorbent ink (sometimes used for secure tickets) become invisible to the front camera, leading to false “no code found”.
Mitigation:
- Document which camera (rear vs front) is used for scanning.
- If front‑camera scanning is a feature, provide a fallback to rear camera or instruct users to avoid IR‑filtered codes.
3. Thermal Shutdown on Prolonged Use
In hot environments or when the device is charging, the camera subsystem may throttle or shut down to prevent overheating. The preview may freeze, yet the app still believes it is scanning.
Mitigation:
- Monitor
CameraDevice.StateCallback.onErrorforCameraDevice.StateCallback.ERROR_CAMERA_DEVICE. - Show a toast: “Camera too hot – please cool device and try again.”
- Log the event for backend analytics to detect geographic hotspots.
4. Multi‑Window and Picture‑In‑Picture (PIP) Modes
When the app is not the top‑most focused window (e.g., user splits screen with a chat app), some manufacturers pause the camera pipeline to save power. The scanner may appear to work but actually returns stale frames.
Mitigation:
- Listen to
onPause/onResumeandonStop/onStart. - In
onPause, release the camera immediately; inonResume, re‑initialize. - Show a UI hint: “Scanner paused while another app is active.”
5. NFC Interference on Certain Chipsets
A few Android devices share the same power rail for NFC and camera; activating NFC (e.g., for a payment tap) can cause a brief voltage dip that results in a corrupted frame, producing a false decode.
Mitigation:
- If your app uses both NFC and QR scanning, serialize the operations: disable NFC listener while the scanner active, or vice‑versa.
- Add unit test that simulates rapid toggling and asserts no corrupted frames are passed to decoder.
6. User‑Generated QR Codes with Low Error Correction
When users create their own QR codes (e.g., via a sharing feature), they may select the lowest error‑correction level (L) to maximize data capacity. Such codes are far more susceptible to smudge or partial occlusion.
Mitigation:
- In your QR‑generation flow, default to at least
Q(25 % recovery) orH(30 %). - Offer an advanced setting for power users who truly need maximum capacity, with a warning about fragility.
7. Accessibility Overlays That Block the Preview
Screen‑magnifier gestures or color‑inversion overlays can alter the preview image before it reaches the decoder, causing false negatives.
Mitigation:
- Test with TalkBack, Magnification Gestures, and Color Correction enabled.
- If you detect that an accessibility service is active that modifies the camera feed (check
AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS), consider offering a “high contrast” preview mode that bypasses system color transforms.
8. Network Captive Portals After Scanning a URL
Scanning a QR that encodes a URL often leads to a captive portal (e.g., airport Wi‑Fi). If your app immediately tries to fetch a JSON endpoint without handling the redirect, you may appear‑as‑success (HTTP 200 with login page) and then misinterpret data.
Mitigation:
- After launching the URL, monitor the WebView’s
onPageFinishedand check the final URL against a known pattern (e.g., does it still contain the expected path?). - If the final URL deviates, show a warning: “You appear to be behind a login page; please authenticate first.”
9. Battery‑Optimization Aggressive Doze
On some devices, Doze can defer alarm‑based jobs that you might use to periodically re‑try scanning after a failure. The user perceives the scanner as “stuck”.
Mitigation:
- Use
WorkManagerwithsetExpedited(true)for immediate retries, or rely on user‑initiated scan rather than background polling. - Log when a retry is postponed due to battery optimization and surface a tip in settings: “Allow background work for faster retries.”
10. Multi‑Language Input Methods Causing Unexpected Characters
When a QR encodes a string that includes locale‑specific formatting characters (e.g., Arabic-Indic digits), some IMEs may auto‑convert them during copy‑paste, leading to mismatched validation.
Mitigation:
- Treat the raw byte array from the decoder as the source of truth; avoid converting to
Stringvianew String(bytes, Charset.defaultCharset())without specifyingUTF-8. - Explicitly use
String(bytes, StandardCharsets.UTF_8)and compare against expected UTF‑8 sequences.
By incorporating checks for these production‑only phenomena into your test plan—either via automated assertions on device state or via manual exploratory sessions—you dramatically reduce the chance of a nasty surprise after release.
Accessibility and Security Considerations
Accessibility and security are often treated as afterthoughts, yet they directly affect the trustworthiness of a QR scanner.
Accessibility Checklist
| Item | How to Test | Pass Criteria |
|---|---|---|
| Content description on scanner view | TalkBack, navigate to scanner | “Scanner, double tap to start” (or similar) |
| Live region for status updates | Enable TalkBack, start scan | Announces “Scanning…”, then either “Scan successful” or error |
| Contrast ratio of overlay UI | Use Android Studio’s Layout Inspector or external contrast scanner | Minimum 4.5:1 for text, 3:1 for large text |
| Touch target size | Measure with UI Automator or manual ruler | Minimum 48 dp × 48 dp |
| Navigation order | TalkBack swipe left/right | Focus moves logically from preview → button → result dialog |
| Error announcement | Trigger a malformed QR | TalkBack reads the error message (not just beep) |
| Screen reader compatibility with result | After successful scan, navigate to result view | URL or text is read fully, character by character if needed |
| Reduced motion | Enable “Remove animations” in Accessibility | No disruptive animations that could cause vestibular discomfort |
| Font scaling | Set font size to largest, verify UI | All text scales, no clipping, buttons remain tappable |
Implementing these checks early prevents costly redesigns later. Tools like Accessibility Test Framework (ATF) in Espresso can automate many of them:
@Test
fun scannerHasContentDescription() {
onView(withId(R.id.scanner_preview))
.check(matches(hasContentDescription(containsString("Scanner"))))
}
Security Checklist
| Threat | Test | Expected Outcome |
|---|---|---|
| Arbitrary intent injection | Scan QR containing intent:#Intent;action=android.intent.action.CALL;S.tel=911;end | App blocks intent or shows a confirmation dialog |
| Open redirect | Scan http://example.com/https://evil.com | App validates that the hostname matches an allowlist before loading |
| Data exfiltration via logs | Scan a QR with personal data, inspect logcat | No personal data appears in logs (adb logcat) |
| Camera leakage | Scan, then press Home, run dumpsys media.camera | Camera is not held by the app’s PID |
| Clipboard abuse | After scan, automatically copy result to clipboard, then read via another app | Ensure the clipboard content is cleared after a short timeout or only when user explicitly taps “Copy” |
| WebView JavaScript injection | Scan javascript:alert('XSS') | WebView has JavaScript disabled or uses a safe WebViewClient that overrides shouldOverrideUrlLoading to block javascript: schemes |
| Permission creep | Manifest only requests CAMERA; no INTERNET unless needed | Verify with apkanalyzer or apktool that no extra dangerous permissions are present |
Automated security tests can be written with MobSF or **Q
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free