• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.DeviceAsWebcam;
18 
19 import android.app.Notification;
20 import android.app.NotificationChannel;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.app.Service;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.ServiceInfo;
27 import android.graphics.SurfaceTexture;
28 import android.hardware.HardwareBuffer;
29 import android.os.Binder;
30 import android.os.IBinder;
31 import android.util.Log;
32 import android.util.Size;
33 
34 import androidx.annotation.Nullable;
35 import androidx.core.app.NotificationCompat;
36 import androidx.core.app.NotificationManagerCompat;
37 
38 import com.android.DeviceAsWebcam.annotations.UsedByNative;
39 import com.android.DeviceAsWebcam.utils.IgnoredV4L2Nodes;
40 
41 import java.lang.ref.WeakReference;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.function.Consumer;
45 
46 public class DeviceAsWebcamFgService extends Service {
47     private static final String TAG = "DeviceAsWebcamFgService";
48     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
49     private static final String NOTIF_CHANNEL_ID = "WebcamService";
50     private static final int NOTIF_ID = 1;
51 
52     static {
53         System.loadLibrary("jni_deviceAsWebcam");
54     }
55 
56     // Guards all methods in the service to ensure a consistent state while executing a method
57     private final Object mServiceLock = new Object();
58     private final IBinder mBinder = new LocalBinder();
59     private Context mContext;
60     private CameraController mCameraController;
61     private Runnable mDestroyActivityCallback = null;
62     private boolean mServiceRunning = false;
63 
64     private NotificationCompat.Builder mNotificationBuilder;
65     private int mNotificationIcon;
66     private int mNextNotificationIcon;
67     private boolean mNotificationUpdatePending;
68 
69     @Override
onBind(Intent intent)70     public IBinder onBind(Intent intent) {
71         return mBinder;
72     }
73 
74     @Override
onCreate()75     public void onCreate() {
76         super.onCreate();
77     }
78 
79     @Override
onStartCommand(Intent intent, int flags, int startId)80     public int onStartCommand(Intent intent, int flags, int startId) {
81         super.onStartCommand(intent, flags, startId);
82         synchronized (mServiceLock) {
83             mContext = getApplicationContext();
84             if (mContext == null) {
85                 Log.e(TAG, "Application context is null!, something is going to go wrong");
86             }
87             mCameraController = new CameraController(mContext, new WeakReference<>(this));
88             int res = setupServicesAndStartListening();
89             startForegroundWithNotification();
90             // If `setupServiceAndStartListening` fails, we don't want to start the foreground
91             // service. However, Android expects a call to `startForegroundWithNotification` in
92             // `onStartCommand` and throws an exception if it isn't called. So, if the foreground
93             // service should not be running, we call `startForegroundWithNotification` which starts
94             // the service, and immediately call `stopSelf` which causes the service to be
95             // torn down once `onStartCommand` returns.
96             if (res != 0) {
97                 stopSelf();
98             }
99             mServiceRunning = true;
100             return START_NOT_STICKY;
101         }
102     }
103 
createNotificationChannel()104     private String createNotificationChannel() {
105         NotificationChannel channel = new NotificationChannel(NOTIF_CHANNEL_ID,
106                 getString(R.string.notif_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
107         NotificationManager notMan = getSystemService(NotificationManager.class);
108         Objects.requireNonNull(notMan).createNotificationChannel(channel);
109         return NOTIF_CHANNEL_ID;
110     }
111 
startForegroundWithNotification()112     private void startForegroundWithNotification() {
113         Intent notificationIntent = new Intent(mContext, DeviceAsWebcamPreview.class);
114         PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, notificationIntent,
115                 PendingIntent.FLAG_MUTABLE);
116         String channelId = createNotificationChannel();
117         mNextNotificationIcon = mNotificationIcon = R.drawable.ic_notif_line;
118         mNotificationBuilder = new NotificationCompat.Builder(this, channelId)
119                 .setCategory(Notification.CATEGORY_SERVICE)
120                 .setContentIntent(pendingIntent)
121                 .setContentText(getString(R.string.notif_desc))
122                 .setContentTitle(getString(R.string.notif_title))
123                 .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
124                 .setOngoing(true)
125                 .setPriority(NotificationManager.IMPORTANCE_DEFAULT)
126                 .setShowWhen(false)
127                 .setSmallIcon(mNotificationIcon)
128                 .setTicker(getString(R.string.notif_ticker))
129                 .setVisibility(Notification.VISIBILITY_PUBLIC);
130         Notification notif = mNotificationBuilder.build();
131         startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA);
132     }
133 
setupServicesAndStartListening()134     private int setupServicesAndStartListening() {
135         String[] ignoredNodes = IgnoredV4L2Nodes.getIgnoredNodes(getApplicationContext());
136         return setupServicesAndStartListeningNative(ignoredNodes);
137     }
138 
139     @Override
onDestroy()140     public void onDestroy() {
141         synchronized (mServiceLock) {
142             if (!mServiceRunning) {
143                 return;
144             }
145             mServiceRunning = false;
146             if (mDestroyActivityCallback != null) {
147                 mDestroyActivityCallback.run();
148             }
149             nativeOnDestroy();
150             if (VERBOSE) {
151                 Log.v(TAG, "Destroyed fg service");
152             }
153             // Ensure that the service notification is removed.
154             NotificationManagerCompat.from(mContext).cancelAll();
155         }
156         super.onDestroy();
157     }
158 
159     /**
160      * Returns the best suitable output size for preview.
161      *
162      * <p>If the webcam stream doesn't exist, find the largest 16:9 supported output size which is
163      * not larger than 1080p. If the webcam stream exists, find the largest supported output size
164      * which matches the aspect ratio of the webcam stream size and is not larger than the webcam
165      * stream size.
166      */
getSuitablePreviewSize()167     public Size getSuitablePreviewSize() {
168         synchronized (mServiceLock) {
169             if (!mServiceRunning) {
170                 Log.e(TAG, "getSuitablePreviewSize called after Service was destroyed.");
171                 return null;
172             }
173             return mCameraController.getSuitablePreviewSize();
174         }
175     }
176 
177     /**
178      * Method to set a preview surface texture that camera will stream to. Should be of the size
179      * returned by {@link #getSuitablePreviewSize}.
180      *
181      * @param surfaceTexture surfaceTexture to stream preview frames to
182      * @param previewSize the preview size
183      * @param previewSizeChangeListener a listener to monitor the preview size change events.
184      */
setPreviewSurfaceTexture(SurfaceTexture surfaceTexture, Size previewSize, Consumer<Size> previewSizeChangeListener)185     public void setPreviewSurfaceTexture(SurfaceTexture surfaceTexture, Size previewSize,
186             Consumer<Size> previewSizeChangeListener) {
187         synchronized (mServiceLock) {
188             if (!mServiceRunning) {
189                 Log.e(TAG, "setPreviewSurfaceTexture called after Service was destroyed.");
190                 return;
191             }
192             mCameraController.startPreviewStreaming(surfaceTexture, previewSize,
193                     previewSizeChangeListener);
194         }
195     }
196 
197     /**
198      * Method to remove any preview SurfaceTexture set by {@link #setPreviewSurfaceTexture}.
199      */
removePreviewSurfaceTexture()200     public void removePreviewSurfaceTexture() {
201         synchronized (mServiceLock) {
202             if (!mServiceRunning) {
203                 Log.e(TAG, "removePreviewSurfaceTexture was called after Service was destroyed.");
204                 return;
205             }
206             mCameraController.stopPreviewStreaming();
207         }
208     }
209 
210     /**
211      * Method to setOnDestroyedCallback. This callback will be called when immediately before the
212      * foreground service is destroyed. Intended to give and bound context a change to clean up
213      * before the Service is destroyed. {@code setOnDestroyedCallback(null)} must be called to unset
214      * the callback when a bound context finishes to prevent Context leak.
215      * <p>
216      * This callback must not call {@code setOnDestroyedCallback} from within the callback.
217      *
218      * @param callback callback to be called when the service is destroyed. {@code null} unsets
219      *                 the callback
220      */
setOnDestroyedCallback(@ullable Runnable callback)221     public void setOnDestroyedCallback(@Nullable Runnable callback) {
222         synchronized (mServiceLock) {
223             if (!mServiceRunning) {
224                 Log.e(TAG, "setOnDestroyedCallback was called after Service was destroyed");
225                 return;
226             }
227             mDestroyActivityCallback = callback;
228         }
229     }
230 
231     /**
232      * Returns the {@link CameraInfo} of the working camera.
233      */
getCameraInfo()234     public CameraInfo getCameraInfo() {
235         synchronized (mServiceLock) {
236             if (!mServiceRunning) {
237                 Log.e(TAG, "getCameraInfo called after Service was destroyed.");
238                 return null;
239             }
240             return mCameraController.getCameraInfo();
241         }
242     }
243 
244     /**
245      * Returns the available {@link CameraId} list.
246      */
247     @Nullable
getAvailableCameraIds()248     public List<CameraId> getAvailableCameraIds() {
249         synchronized (mServiceLock) {
250             if (!mServiceRunning) {
251                 Log.e(TAG, "getAvailableCameraIds called after Service was destroyed.");
252                 return null;
253             }
254             return mCameraController.getAvailableCameraIds();
255         }
256     }
257 
258     /**
259      * Returns the {@link CameraInfo} for the specified camera id.
260      */
261     @Nullable
getOrCreateCameraInfo(CameraId cameraId)262     public CameraInfo getOrCreateCameraInfo(CameraId cameraId) {
263         synchronized (mServiceLock) {
264             if (!mServiceRunning) {
265                 Log.e(TAG, "getCameraInfo called after Service was destroyed.");
266                 return null;
267             }
268             return mCameraController.getOrCreateCameraInfo(cameraId);
269         }
270     }
271 
272     /**
273      * Sets the new zoom ratio setting to the working camera.
274      */
setZoomRatio(float zoomRatio)275     public void setZoomRatio(float zoomRatio) {
276         synchronized (mServiceLock) {
277             if (!mServiceRunning) {
278                 Log.e(TAG, "setZoomRatio called after Service was destroyed.");
279                 return;
280             }
281             mCameraController.setZoomRatio(zoomRatio);
282         }
283     }
284 
285     /**
286      * Returns current zoom ratio setting.
287      */
getZoomRatio()288     public float getZoomRatio() {
289         synchronized (mServiceLock) {
290             if (!mServiceRunning) {
291                 Log.e(TAG, "getZoomRatio called after Service was destroyed.");
292                 return 1.0f;
293             }
294             return mCameraController.getZoomRatio();
295         }
296     }
297 
298     /**
299      * Toggles camera between the back and front cameras.
300      */
toggleCamera()301     public void toggleCamera() {
302         synchronized (mServiceLock) {
303             if (!mServiceRunning) {
304                 Log.e(TAG, "toggleCamera called after Service was destroyed.");
305                 return;
306             }
307             mCameraController.toggleCamera();
308         }
309     }
310 
311     /**
312      * Switches current working camera to specific one.
313      */
switchCamera(CameraId cameraId)314     public void switchCamera(CameraId cameraId) {
315         synchronized (mServiceLock) {
316             if (!mServiceRunning) {
317                 Log.e(TAG, "switchCamera called after Service was destroyed.");
318                 return;
319             }
320             mCameraController.switchCamera(cameraId);
321         }
322     }
323 
324     /**
325      * Sets a {@link CameraController.RotationUpdateListener} to monitor the device rotation
326      * changes.
327      */
setRotationUpdateListener(CameraController.RotationUpdateListener listener)328     public void setRotationUpdateListener(CameraController.RotationUpdateListener listener) {
329         synchronized (mServiceLock) {
330             if (!mServiceRunning) {
331                 Log.e(TAG, "setRotationUpdateListener called after Service was destroyed.");
332                 return;
333             }
334             mCameraController.setRotationUpdateListener(listener);
335         }
336     }
337 
338     /**
339      * Returns current rotation degrees value.
340      */
getCurrentRotation()341     public int getCurrentRotation() {
342         synchronized (mServiceLock) {
343             if (!mServiceRunning) {
344                 Log.e(TAG, "getCurrentRotation was called after Service was destroyed");
345                 return 0;
346             }
347             return mCameraController.getCurrentRotation();
348         }
349     }
350 
updateNotification(boolean isStreaming)351     private void updateNotification(boolean isStreaming) {
352         int transitionIcon; // animated icon
353         int finalIcon; // static icon
354         if (isStreaming) {
355             transitionIcon = R.drawable.ic_notif_streaming;
356             // last frame of ic_notif_streaming
357             finalIcon = R.drawable.ic_notif_filled;
358         } else {
359             transitionIcon = R.drawable.ic_notif_idle;
360             // last frame of ic_notif_idle
361             finalIcon = R.drawable.ic_notif_line;
362         }
363 
364         synchronized (mServiceLock) {
365             if (finalIcon == mNotificationIcon) {
366                 // Notification already is desired state.
367                 return;
368             }
369             if (transitionIcon == mNotificationIcon) {
370                 // Notification currently animating to finalIcon.
371                 // Set next state to desired steady state icon.
372                 mNextNotificationIcon = finalIcon;
373                 return;
374             }
375 
376             if (mNotificationUpdatePending) {
377                 // Notification animating to some other icon. Set the next icon to the new
378                 // transition icon and let the update runnable handle the actual updates.
379                 mNextNotificationIcon = transitionIcon;
380                 return;
381             }
382 
383             // Notification is in a steady state. Update notification to the new icon.
384             mNextNotificationIcon = transitionIcon;
385             updateNotificationToNextIcon();
386         }
387     }
388 
updateNotificationToNextIcon()389     private void updateNotificationToNextIcon() {
390         synchronized (mServiceLock) {
391             if (!mServiceRunning) {
392                 return;
393             }
394 
395             mNotificationBuilder.setSmallIcon(mNextNotificationIcon);
396             NotificationManagerCompat.from(mContext).notify(NOTIF_ID, mNotificationBuilder.build());
397             mNotificationIcon = mNextNotificationIcon;
398 
399             boolean notifNeedsUpdate = false;
400             if (mNotificationIcon == R.drawable.ic_notif_streaming) {
401                 // last frame of ic_notif_streaming
402                 mNextNotificationIcon = R.drawable.ic_notif_filled;
403                 notifNeedsUpdate = true;
404             } else if (mNotificationIcon == R.drawable.ic_notif_idle) {
405                 // last frame of ic_notif_idle
406                 mNextNotificationIcon = R.drawable.ic_notif_line;
407                 notifNeedsUpdate = true;
408             }
409             mNotificationUpdatePending = notifNeedsUpdate;
410             if (notifNeedsUpdate) {
411                 // Run this method again after 500ms to update the notification to steady
412                 // state icon
413                 getMainThreadHandler().postDelayed(this::updateNotificationToNextIcon, 500);
414             }
415         }
416     }
417 
418     @UsedByNative("DeviceAsWebcamNative.cpp")
startStreaming()419     private void startStreaming() {
420         synchronized (mServiceLock) {
421             if (!mServiceRunning) {
422                 Log.e(TAG, "startStreaming was called after Service was destroyed");
423                 return;
424             }
425             mCameraController.startWebcamStreaming();
426             updateNotification(/*isStreaming*/ true);
427         }
428     }
429 
430     @UsedByNative("DeviceAsWebcamNative.cpp")
stopService()431     private void stopService() {
432         synchronized (mServiceLock) {
433             if (!mServiceRunning) {
434                 Log.e(TAG, "stopService was called after Service was destroyed");
435                 return;
436             }
437             stopSelf();
438         }
439     }
440 
441     @UsedByNative("DeviceAsWebcamNative.cpp")
stopStreaming()442     private void stopStreaming() {
443         synchronized (mServiceLock) {
444             if (!mServiceRunning) {
445                 Log.e(TAG, "stopStreaming was called after Service was destroyed");
446                 return;
447             }
448             mCameraController.stopWebcamStreaming();
449             updateNotification(/*isStreaming*/ false);
450         }
451     }
452 
453     @UsedByNative("DeviceAsWebcamNative.cpp")
returnImage(long timestamp)454     private void returnImage(long timestamp) {
455         synchronized (mServiceLock) {
456             if (!mServiceRunning) {
457                 Log.e(TAG, "returnImage was called after Service was destroyed");
458                 return;
459             }
460             mCameraController.returnImage(timestamp);
461         }
462     }
463 
464     @UsedByNative("DeviceAsWebcamNative.cpp")
setStreamConfig(boolean mjpeg, int width, int height, int fps)465     private void setStreamConfig(boolean mjpeg, int width, int height, int fps) {
466         synchronized (mServiceLock) {
467             if (!mServiceRunning) {
468                 Log.e(TAG, "setStreamConfig was called after Service was destroyed");
469                 return;
470             }
471             mCameraController.setWebcamStreamConfig(mjpeg, width, height, fps);
472         }
473     }
474 
475     /**
476      * Trigger tap-to-focus operation for the specified normalized points mapping to the FOV.
477      *
478      * <p>The specified normalized points will be used to calculate the corresponding metering
479      * rectangles that will be applied for AF, AE and AWB.
480      */
tapToFocus(float[] normalizedPoint)481     public void tapToFocus(float[] normalizedPoint) {
482         synchronized (mServiceLock) {
483             if (!mServiceRunning) {
484                 Log.e(TAG, "tapToFocus was called after Service was destroyed");
485                 return;
486             }
487             mCameraController.tapToFocus(normalizedPoint);
488         }
489     }
490 
491     /**
492      * Retrieves current tap-to-focus points.
493      *
494      * @return the normalized points or {@code null} if it is auto-focus mode currently.
495      */
getTapToFocusPoints()496     public float[] getTapToFocusPoints() {
497         synchronized (mServiceLock) {
498             if (!mServiceRunning) {
499                 Log.e(TAG, "getTapToFocusPoints was called after Service was destroyed");
500                 return null;
501             }
502             return mCameraController.getTapToFocusPoints();
503         }
504     }
505 
506     /**
507      * Resets to the auto-focus mode.
508      */
resetToAutoFocus()509     public void resetToAutoFocus() {
510         synchronized (mServiceLock) {
511             if (!mServiceRunning) {
512                 Log.e(TAG, "resetToAutoFocus was called after Service was destroyed");
513                 return;
514             }
515             mCameraController.resetToAutoFocus();
516         }
517     }
518 
519     /**
520      * Called by {@link DeviceAsWebcamReceiver} to check if the service should be started.
521      * @param ignoredNodes V4L2 nodes to ignore
522      * @return {@code true} if the foreground service should be started,
523      *         {@code false} if the service is already running or should not be started
524      */
shouldStartServiceNative(String[] ignoredNodes)525     public static native boolean shouldStartServiceNative(String[] ignoredNodes);
526 
527     /**
528      * Called during {@link #onStartCommand} to initialize the native side of the service.
529      * @param ignoredNodes V4L2 nodes to ignore
530      * @return 0 if native side code was successfully initialized,
531      *         non-0 otherwise
532      */
setupServicesAndStartListeningNative(String[] ignoredNodes)533     private native int setupServicesAndStartListeningNative(String[] ignoredNodes);
534 
535     /**
536      * Called by {@link CameraController} to queue frames for encoding. The frames are encoded
537      * asynchronously. When encoding is done, the native code call {@link #returnImage} with the
538      * {@code timestamp} passed here.
539      * @param buffer buffer containing the frame to be encoded
540      * @param timestamp timestamp associated with the buffer which uniquely identifies the buffer
541      * @return 0 if buffer was successfully queued for encoding. non-0 otherwise.
542      */
nativeEncodeImage(HardwareBuffer buffer, long timestamp, int rotation)543     public native int nativeEncodeImage(HardwareBuffer buffer, long timestamp, int rotation);
544 
545     /**
546      * Called by {@link #onDestroy} to give the JNI code a chance to clean up before the service
547      * goes out of scope.
548      */
nativeOnDestroy()549     private native void nativeOnDestroy();
550 
551 
552     /**
553      * Simple class to hold a reference to {@link DeviceAsWebcamFgService} instance and have it be
554      * accessible from {@link android.content.ServiceConnection#onServiceConnected} callback.
555      */
556     public class LocalBinder extends Binder {
getService()557         DeviceAsWebcamFgService getService() {
558             return DeviceAsWebcamFgService.this;
559         }
560     }
561 }
562