Android Kotlin

CameraX Silent Photo Capture:
Stealth Services in Kotlin

By Ankit Kumar, Founder of Kyvronix Technologies · 11 min read

In PhoneGuard, when an unauthorized user attempts to unlock the device and fails 3 times, the app silently captures a photo using the front camera and uploads it to Firebase. To do this, we cannot open an Activity or show a camera preview. It must happen entirely in the background.

Historically, the android.hardware.Camera API made this somewhat easy (though deprecated). Camera2 made it notoriously complex. Today, CameraX provides the perfect balance, but it's designed around having a LifecycleOwner (like an Activity). Here's how to hack CameraX to work in a headless Foreground Service.

1. The Challenge: LifecycleOwner in a Service

CameraX requires a LifecycleOwner to know when to open and close the camera. A standard Android Service is not a LifecycleOwner. To fix this, we implement LifecycleOwner manually using a LifecycleRegistry.

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import android.app.Service

class IntrusionCameraService : Service(), LifecycleOwner {
    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate() {
        super.onCreate()
        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.currentState = Lifecycle.State.CREATED
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        lifecycleRegistry.currentState = Lifecycle.State.STARTED
        
        // Ensure this is a Foreground Service! Camera won't start otherwise on Android 9+
        startForeground(1, createNotification())
        
        takePhoto()
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
    }

    override val lifecycle: Lifecycle
        get() = lifecycleRegistry
}
  

2. Configuring CameraX Without a Preview

Usually, you bind an ImageCapture use case and a Preview use case. For stealth capture, we simply omit the Preview use case entirely.

import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat

private fun takePhoto() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({
        val cameraProvider = cameraProviderFuture.get()

        val imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
            .build()

        // Select front camera
        val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

        try {
            // Unbind use cases before rebinding
            cameraProvider.unbindAll()

            // Bind use cases to lifecycle. Notice there is NO Preview object here!
            cameraProvider.bindToLifecycle(
                this, cameraSelector, imageCapture
            )
            
            // Trigger the capture
            captureImage(imageCapture)

        } catch (exc: Exception) {
            Log.e("Camera", "Use case binding failed", exc)
        }
    }, ContextCompat.getMainExecutor(this))
}
  

3. Silencing the Shutter Sound

In many regions (like Japan and South Korea), it is hardcoded into the firmware that the shutter sound cannot be muted. However, globally, if the device is in Silent or Vibrate mode, the shutter sound won't play. We can programmatically request the AudioManager to mute streams temporarily.

import android.media.AudioManager

private fun captureImage(imageCapture: ImageCapture) {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    
    // Mute system sounds temporarily
    audioManager.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_MUTE, 0)

    val outputFileOptions = ImageCapture.OutputFileOptions.Builder(getFile()).build()

    imageCapture.takePicture(
        outputFileOptions, 
        ContextCompat.getMainExecutor(this),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                // Restore volume
                audioManager.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_UNMUTE, 0)
                stopSelf()
            }

            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                // Restore volume
                audioManager.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_UNMUTE, 0)
                
                // Upload photo to Firebase...
                uploadToCloud(output.savedUri)
                stopSelf() // Stop the service when done
            }
        }
    )
}
  

Conclusion

CameraX drastically simplifies camera hardware interaction. By implementing a custom LifecycleOwner inside a Foreground Service and omitting the Preview use case, you can create a highly effective stealth camera system for security and anti-theft applications.

— Ankit Kumar