Skip to content

proof of concept showing how one Android app can display a camera feed that originates in another app

License

Notifications You must be signed in to change notification settings

glennhartmann/camera-delegation-poc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Android Camera Delegation Proof of Concept

This project is a proof of concept consisting of 2 small Android apps. It shows how one app can display a camera feed that originates in another app.

The "Client" app, which displays the video, requires no user permissions and doesn't get recorded by the system as having accessed the camera.

The "Server" app requires the user to grant it permissions, and gets recorded as having accessed the camera.

Note that this branch is the "full" (though still far from productionized) version of the apps. There's also a minimized branch that cuts a lot more corners and has easier-to-follow code, focusing more directly on camera delegation and less on synchronizing, avoiding crashes, and correctness in general.

Building and Installing

This is an Android Studio project (actually 2 projects - one for each app). Follow the Android Studio documentation to open the projects, build, and install in the typical Android Studio way. Alternatively, you can install the Android command-line tools and install pre-built APKs (from the Releases page, for example) with adb install.

Using

Server

The Server app is very straightforward to run directly, but is also unexciting and pretty useless. It just connects directly to the camera (after requesting permission, if necessary), and displays it on-screen. It does not attempt to connect to the Client app.

The Server app's real purpose is not to be run directly, but to be invoked from the Client app to delegate the permissions and video feed.

Client

First make sure both the Client and Server apps are installed on the device, then launch the Client app.

Client app initial view

Tap the "BIND SERVICE" button to bind to the Server app's exposed Camera Service. When or if the connection succeeds, the rest of the buttons will be enabled. You can also follow the logs in Android's logcat to follow the sequence of events.

Client app after binding to the
Server app's service

Tap the "REQUEST PERMISSIONS" button to get prompts for Notification and Camera permissions from the Server app (assuming you haven't already granted them). Allow them both.

Notification
permission prompt from Server app

Camera permission
prompt from Server app

Tap the "START FOREGROUND SERVICE" button (note that it's actually possible to do this step before even binding to the service - but only if the Server app has already been granted permissions).

Optionally - you can wait for a few seconds and note that the Server displays a notification indicating that it's running a foreground service.

Server's
foreground service notification

Finally, tap the "DELEGATE CAMERA" button. The black box in the Client app should start showing a video feed from your camera.

Notice at this point that there's an icon in the notification area that indicates that the camera is in use.

Notification area shows
"camera in use" icon

Tapping on the icon reveals that Server is using the camera.

Camera access is
attributed to Server app

Digging a bit deeper, the device's Camera usage history page shows that only Server (not Client) accessed the camera.

Camera usage history

How it works

  1. Server declares a foreground service with type="camera", and also declares all necessary permissions. The exposed servicecan also be bound to via its AIDL API. Additionally, the Client declares that it wants to call into the Server.

  2. Upon "BIND SERVICE" button press, the client binds to the Server's CameraService.

  3. Upon "REQUEST PERMISSIONS" button press, the Client fetches a PendingIntent to PermissionRequestActivity from the Server via the bound service. This PendingIntent is then sent along with another PendingIntent back to Client's MainActivity.

    Note that on Android 14+, this PendingIntent only works because we opted in to background activity start (more details)

  4. In the Server code, PermissionRequestActivity starts up invisibly (thanks to its style), and immediately requests the desired permission (Notification permission in this case).

    Note that the permission request is run in the Server, so the user grants or denies permissions for the Server app, not for the Client app. This follows the delegated permission request pattern established in the WebAPK Shell's WebApkServiceImplWrapper and NotificationPermissionRequestActivity.

    Also note that Notification permission is only required since Android 13.

  5. Once the permission dialog closes, we finish the PermissionRequestActivity and send the callback PendingIntent back to the Client.

  6. Back on the Client, we open a new MainActivity instance and immediately call into the registered callback function, which in this case will be a second permission request (this time for Camera permission), and then finishes.

  7. The permission request flow works the same as last time, but this time we don't register another callback - once the Camera permission dialog closes, the user is back in the original Client MainActivity instance, awaiting user input.

  8. Upon "START FOREGROUND SERVICE" button press, the Client asks the server to start its foreground service, and the Server, of course, complies.

    Note that the foreground service can actually be started before binding to it, but the Server app must already have permissions or the foreground service will crash on startup.

    Also note that since Android 11, we are required to declare the foreground service type.

    Finally, note that the reason we need a foreground service in the first place is that camera access is not allowed in background services. Trying to delegate the camera without a foreground service running will result in connect(): camera access exception: android.hardware.camera2.CameraAccessException: CAMERA_DISABLED (1): connectHelper:2059: Camera "0" disabled by policy

  9. Upon "DELEGATE CAMERA" button press, the Client sends its Surface (embedded in MainActivity's VideoView widget) to the Server via the bound service.

  10. On the Server side, we select a camera (in this case, just the first camera in the list - in production there are of course better ways), start a CameraCaptureSession, and set a repeating request using the Client's Surface. For more details on the camera setup, session, and capture process, see the camera2 documentation.

Notes

These are not meant to be a production quality apps. They are proof-of-concept quality and cut numerous corners.

For example, the following situations are not handled properly in this demo:

  • camera errors and disconnects (when switching between apps you may see Session 0: Exception while unconfiguring outputs: android.hardware.camera2.CameraAccessException: CAMERA_DISCONNECTED (2): checkPidStatus:2157: The camera device has been disconnected or Session 0: Exception while stopping repeating: android.hardware.camera2.CameraAccessException: CAMERA_ERROR (3): cancelRequest:609: Camera 0: Error clearing streaming request: Function not implemented (-38) or similar errors in logs)
  • onConfigureFailed is not handled in the Server's CameraManager
  • proper selection of which camera to use is not implemented
  • we don't check if notification permission was actually granted before starting the foreground service (foreground services are supposed to be required to show a notification when they're running, but it still seems to work without - this may be a Play Policy violation?)
  • switching between apps may result in the Client ending up with a null mServerCameraServiceManager pointer in MainActivity

In addition, the following clean-up code is not properly implemented:

  • camera access (if you switch back and forth between Client and Server apps, particularly if you didn't start the foreground service before attempting to attach to the camera, you may start seeing connect(): camera access exception: android.hardware.camera2.CameraAccessException: CAMERA_IN_USE (4): connectHelper:2049: Camera "0" is already open in logs in some cases)
  • surfaces (you may see [SurfaceView[dev.hartmanng.client/dev.hartmanng.client.MainActivity] . . . queueBuffer: BufferQueue has been abandoned in logs)
  • foreground service (it keeps running after both apps are closed)

Other misc notes:

  • when connecting the Client to the camera, logs show Recent tasks don't include camera client package name: dev.hartmanng.server. It seems to work anyway, but it doesn't sound like Android's happy about it
  • I'm not that happy about how the server callback is stored as global state in the Client's MainActivity
  • requesting permissions from the Client app may leave an extra useless all-black activity in Android's "recent apps" stack (presumably left over from PermissionRequestActivity)
  • if you see connect(): camera access exception: android.hardware.camera2.CameraAccessException: CAMERA_DISABLED (1): connectHelper:2059: Camera "0" disabled by policy in the logs, this probably means you tried to connect the camera when the foreground service wasn't running

About

proof of concept showing how one Android app can display a camera feed that originates in another app

Resources

License

Stars

Watchers

Forks

Packages

No packages published