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