Common Battery Drain in Qr Code Apps: Causes and Fixes

QR code apps spend most of their time in the camera preview loop, performing image processing, and occasionally making network calls to validate or retrieve data. Battery drain appears when any of the

April 21, 2026 · 6 min read · Common Issues

What causes battery drain in QR code apps (technical root causes)

QR code apps spend most of their time in the camera preview loop, performing image processing, and occasionally making network calls to validate or retrieve data. Battery drain appears when any of these activities run longer or more frequently than necessary.

Root causeWhy it hurts battery
Continuous high‑resolution previewThe Camera2 API keeps the sensor active at 30‑60 fps. If the app never throttles frame rate or resolution when idle, the ISP and CPU stay powered.
Blocking image‑processing on the UI threadRunning ZXing, ML Kit, or custom OpenCV filters on the main thread forces the CPU to stay at high frequency, preventing deep sleep.
Unbounded background servicesSome apps launch a foreground service to keep the camera alive while the app is in the background (e.g., for “scan‑while‑locked” features). If the service isn’t stopped promptly, the sensor and wake‑lock persist.
Frequent network pollingAfter a successful scan, the app may poll a server every few seconds to check for updated coupon validity or location‑based offers. Each radio wake‑up adds ~10‑15 mA draw.
Excessive wake‑locksHolding a PARTIAL_WAKE_LOCK while waiting for a scan prevents the device from entering sleep, even when the screen is off.
Inefficient bitmap handlingDecoding large preview frames into full‑size Bitmaps without scaling creates GC pressure and forces the CPU to spend time in garbage collection.
Overlay rendering with heavy shadersDrawing animated scan lines or AR overlays using custom GLSL shaders that run every frame can push the GPU to its max frequency.

These causes are amplified in QR code apps because the core interaction loop (camera → process → UI) is tight; any inefficiency repeats dozens of times per second.

Real‑world impact (user complaints, store ratings, revenue loss)

5‑7 specific examples of how battery drain manifests in QR code apps

  1. Never‑stopping high‑resolution preview – The app requests CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE and starts preview at 4080×3060, 30 fps, even though the QR detector only needs a 640×480 region of interest.
  2. Blocking ZXing decode on UI thread – After each preview frame, the app calls new MultiFormatReader().decode(bitmap) on the main thread, causing UI jank and keeping the CPU at 1.8 GHz.
  3. Foreground service with wake‑lock for “scan‑while‑locked” – A service acquires PARTIAL_WAKE_LOCK and keeps the camera open when the screen is off, but never releases the lock after a successful scan.
  4. Polling server every 2 seconds for coupon validity – Using WorkManager with a periodic request set to 2 seconds, causing the cellular radio to wake every cycle.
  5. Full‑size Bitmap allocation per frame – The preview callback converts each Image to a Bitmap via ImageConverter.toBitmap(image) without scaling, allocating ~5 MB per frame at 30 fps.
  6. GPU‑intensive animated overlay – A custom GLSurfaceView draws a pulsating scan line using a fragment shader that performs 12 texture lookups per fragment, pushing GPU utilization to 95 %.
  7. Unregistered broadcast receivers keeping alarms alive – The app registers a BOOT_COMPLETED receiver that sets an inexact repeating alarm to restart the camera service, but the alarm is never canceled on app exit, causing periodic wake‑ups.

How to detect battery drain (tools, techniques, what to look for)

Profiling on device

Automated detection with SUSATest

  1. Upload the APK – SUSATest’s autonomous explorer launches the app using each of the 10 user personas (e.g., “impatient” will rapidly tap scan, “elderly” will linger on the preview screen).
  2. Cross‑session learning – On the first run, SUSATest builds a baseline of camera preview frame rate and CPU usage. Subsequent runs compare against this baseline; a >20 % increase in sensor‑on time triggers a battery‑drain anomaly flag.
  3. Flow tracking – Define a flow like “open app → point at QR code → wait for result → close”. SUSATest records the average duration the camera stays active per flow iteration; deviations indicate a leak (e.g., service not stopped).
  4. Coverage analytics – The tool highlights UI elements never interacted with (e.g., a hidden “settings” button that launches a background service). If such elements are tied to battery‑heavy code, they become a target for inspection.
  5. Generated regression scripts – After each run, SUSATest outputs Appium scripts that assert:

These assertions can be added to your CI pipeline (GitHub Actions) via the susatest-agent CLI:


susatest run --apk app.apk --flows qr_scan_flow.json --assertions battery

If any assertion fails, the job is marked unstable, prompting a fix before merge.

How to fix each example (code‑level guidance)

ExampleFix
Never‑stopping high‑resolution previewRequest a preview size that matches the detector’s ROI. Use CameraCharacteristics.getScalerStreamConfigurationMap() to pick the smallest size ≥ 640×480. If you need higher resolution for framing, switch to it only when the user taps a “manual focus” button.
Blocking ZXing decode on UI threadOffload decoding to a ExecutorService or Kotlin coroutine with Dispatchers.Default. Post the result back to the main thread via a Handler or LiveData. Consider using ML Kit’s barcode API, which already runs on a background thread.
Foreground service with wake‑lockBind the service to the UI lifecycle: start it in onResume() and stop it in onPause(). Release the wake‑lock immediately after a successful scan (wakeLock.release()). If you truly need background scanning, use a JobIntentService with a strict setMinimumLatency and ensure the job is cancelled on scan completion.
Polling server every 2 secondsReplace polling with push notifications (FCM) or a longer‑interval sync (e.g., 15 min) using WorkManager with setBackoffCriteria. If real‑time validity is required, open a WebSocket connection and let the server push updates only when they change.
Full‑size Bitmap allocation per frameConvert the Image to a YUV byte array and pass that directly to ZXing (which accepts PlanarYUVLuminanceSource). If a Bitmap is unavoidable, downscale first: Bitmap.createScaledBitmap(src, width, height, true). Reuse a single Bitmap buffer via BitmapPool to avoid GC churn.
GPU‑intensive animated overlayReduce shader complexity: use a simple line drawable animated via ValueTransformer on Canvas instead of a fragment shader. If you must use OpenGL, limit the overlay to a small region (e.g., 100 px high) and render at 15 fps using Choreographer.postFrameCallback.
Unregistered broadcast receivers keeping alarms aliveIn onDestroy() of your main activity or service, cancel any pending alarms: alarmManager.cancel(pendingIntent). Better yet, avoid BOOT_COMPLETED restarts; let the OS kill the service and rely on user‑initiated launches.

Prevention: how to catch battery drain before release

  1. Integrate battery‑profiling into CI – Add a step that runs the app on an emulator or real device farm (e.g., Firebase Test Lab) for a fixed scenario (scan 10 codes, idle 30

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