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 
351     /**
352      * Returns true if high quality mode is enabled, false otherwise
353      */
isHighQualityModeEnabled()354     public boolean isHighQualityModeEnabled() {
355         synchronized (mServiceLock) {
356             if (!mServiceRunning) {
357                 Log.e(TAG, "isHighQualityModeEnabled was called after Service was destroyed");
358                 return false;
359             }
360             return mCameraController.isHighQualityModeEnabled();
361         }
362     }
363 
364     /**
365      * Enables/Disables high quality mode. See {@link CameraController#setHighQualityModeEnabled}
366      * for more info.
367      */
setHighQualityModeEnabled(boolean enabled, Runnable callback)368     public void setHighQualityModeEnabled(boolean enabled, Runnable callback) {
369         synchronized (mServiceLock) {
370             if (!mServiceRunning) {
371                 Log.e(TAG, "switchHighQualityMode was called after Service was destroyed");
372                 return;
373             }
374             mCameraController.setHighQualityModeEnabled(enabled, callback);
375         }
376     }
377 
updateNotification(boolean isStreaming)378     private void updateNotification(boolean isStreaming) {
379         int transitionIcon; // animated icon
380         int finalIcon; // static icon
381         if (isStreaming) {
382             transitionIcon = R.drawable.ic_notif_streaming;
383             // last frame of ic_notif_streaming
384             finalIcon = R.drawable.ic_notif_filled;
385         } else {
386             transitionIcon = R.drawable.ic_notif_idle;
387             // last frame of ic_notif_idle
388             finalIcon = R.drawable.ic_notif_line;
389         }
390 
391         synchronized (mServiceLock) {
392             if (finalIcon == mNotificationIcon) {
393                 // Notification already is desired state.
394                 return;
395             }
396             if (transitionIcon == mNotificationIcon) {
397                 // Notification currently animating to finalIcon.
398                 // Set next state to desired steady state icon.
399                 mNextNotificationIcon = finalIcon;
400                 return;
401             }
402 
403             if (mNotificationUpdatePending) {
404                 // Notification animating to some other icon. Set the next icon to the new
405                 // transition icon and let the update runnable handle the actual updates.
406                 mNextNotificationIcon = transitionIcon;
407                 return;
408             }
409 
410             // Notification is in a steady state. Update notification to the new icon.
411             mNextNotificationIcon = transitionIcon;
412             updateNotificationToNextIcon();
413         }
414     }
415 
updateNotificationToNextIcon()416     private void updateNotificationToNextIcon() {
417         synchronized (mServiceLock) {
418             if (!mServiceRunning) {
419                 return;
420             }
421 
422             mNotificationBuilder.setSmallIcon(mNextNotificationIcon);
423             NotificationManagerCompat.from(mContext).notify(NOTIF_ID, mNotificationBuilder.build());
424             mNotificationIcon = mNextNotificationIcon;
425 
426             boolean notifNeedsUpdate = false;
427             if (mNotificationIcon == R.drawable.ic_notif_streaming) {
428                 // last frame of ic_notif_streaming
429                 mNextNotificationIcon = R.drawable.ic_notif_filled;
430                 notifNeedsUpdate = true;
431             } else if (mNotificationIcon == R.drawable.ic_notif_idle) {
432                 // last frame of ic_notif_idle
433                 mNextNotificationIcon = R.drawable.ic_notif_line;
434                 notifNeedsUpdate = true;
435             }
436             mNotificationUpdatePending = notifNeedsUpdate;
437             if (notifNeedsUpdate) {
438                 // Run this method again after 500ms to update the notification to steady
439                 // state icon
440                 getMainThreadHandler().postDelayed(this::updateNotificationToNextIcon, 500);
441             }
442         }
443     }
444 
445     @UsedByNative("DeviceAsWebcamNative.cpp")
startStreaming()446     private void startStreaming() {
447         synchronized (mServiceLock) {
448             if (!mServiceRunning) {
449                 Log.e(TAG, "startStreaming was called after Service was destroyed");
450                 return;
451             }
452             mCameraController.startWebcamStreaming();
453             updateNotification(/*isStreaming*/ true);
454         }
455     }
456 
457     @UsedByNative("DeviceAsWebcamNative.cpp")
stopService()458     private void stopService() {
459         synchronized (mServiceLock) {
460             if (!mServiceRunning) {
461                 Log.e(TAG, "stopService was called after Service was destroyed");
462                 return;
463             }
464             stopSelf();
465         }
466     }
467 
468     @UsedByNative("DeviceAsWebcamNative.cpp")
stopStreaming()469     private void stopStreaming() {
470         synchronized (mServiceLock) {
471             if (!mServiceRunning) {
472                 Log.e(TAG, "stopStreaming was called after Service was destroyed");
473                 return;
474             }
475             mCameraController.stopWebcamStreaming();
476             updateNotification(/*isStreaming*/ false);
477         }
478     }
479 
480     @UsedByNative("DeviceAsWebcamNative.cpp")
returnImage(long timestamp)481     private void returnImage(long timestamp) {
482         synchronized (mServiceLock) {
483             if (!mServiceRunning) {
484                 Log.e(TAG, "returnImage was called after Service was destroyed");
485                 return;
486             }
487             mCameraController.returnImage(timestamp);
488         }
489     }
490 
491     @UsedByNative("DeviceAsWebcamNative.cpp")
setStreamConfig(boolean mjpeg, int width, int height, int fps)492     private void setStreamConfig(boolean mjpeg, int width, int height, int fps) {
493         synchronized (mServiceLock) {
494             if (!mServiceRunning) {
495                 Log.e(TAG, "setStreamConfig was called after Service was destroyed");
496                 return;
497             }
498             mCameraController.setWebcamStreamConfig(mjpeg, width, height, fps);
499         }
500     }
501 
502     /**
503      * Trigger tap-to-focus operation for the specified normalized points mapping to the FOV.
504      *
505      * <p>The specified normalized points will be used to calculate the corresponding metering
506      * rectangles that will be applied for AF, AE and AWB.
507      */
tapToFocus(float[] normalizedPoint)508     public void tapToFocus(float[] normalizedPoint) {
509         synchronized (mServiceLock) {
510             if (!mServiceRunning) {
511                 Log.e(TAG, "tapToFocus was called after Service was destroyed");
512                 return;
513             }
514             mCameraController.tapToFocus(normalizedPoint);
515         }
516     }
517 
518     /**
519      * Retrieves current tap-to-focus points.
520      *
521      * @return the normalized points or {@code null} if it is auto-focus mode currently.
522      */
getTapToFocusPoints()523     public float[] getTapToFocusPoints() {
524         synchronized (mServiceLock) {
525             if (!mServiceRunning) {
526                 Log.e(TAG, "getTapToFocusPoints was called after Service was destroyed");
527                 return null;
528             }
529             return mCameraController.getTapToFocusPoints();
530         }
531     }
532 
533     /**
534      * Resets to the auto-focus mode.
535      */
resetToAutoFocus()536     public void resetToAutoFocus() {
537         synchronized (mServiceLock) {
538             if (!mServiceRunning) {
539                 Log.e(TAG, "resetToAutoFocus was called after Service was destroyed");
540                 return;
541             }
542             mCameraController.resetToAutoFocus();
543         }
544     }
545 
546     /**
547      * Called by {@link DeviceAsWebcamReceiver} to check if the service should be started.
548      * @param ignoredNodes V4L2 nodes to ignore
549      * @return {@code true} if the foreground service should be started,
550      *         {@code false} if the service is already running or should not be started
551      */
shouldStartServiceNative(String[] ignoredNodes)552     public static native boolean shouldStartServiceNative(String[] ignoredNodes);
553 
554     /**
555      * Called during {@link #onStartCommand} to initialize the native side of the service.
556      * @param ignoredNodes V4L2 nodes to ignore
557      * @return 0 if native side code was successfully initialized,
558      *         non-0 otherwise
559      */
setupServicesAndStartListeningNative(String[] ignoredNodes)560     private native int setupServicesAndStartListeningNative(String[] ignoredNodes);
561 
562     /**
563      * Called by {@link CameraController} to queue frames for encoding. The frames are encoded
564      * asynchronously. When encoding is done, the native code call {@link #returnImage} with the
565      * {@code timestamp} passed here.
566      * @param buffer buffer containing the frame to be encoded
567      * @param timestamp timestamp associated with the buffer which uniquely identifies the buffer
568      * @return 0 if buffer was successfully queued for encoding. non-0 otherwise.
569      */
nativeEncodeImage(HardwareBuffer buffer, long timestamp, int rotation)570     public native int nativeEncodeImage(HardwareBuffer buffer, long timestamp, int rotation);
571 
572     /**
573      * Called by {@link #onDestroy} to give the JNI code a chance to clean up before the service
574      * goes out of scope.
575      */
nativeOnDestroy()576     private native void nativeOnDestroy();
577 
578 
579     /**
580      * Simple class to hold a reference to {@link DeviceAsWebcamFgService} instance and have it be
581      * accessible from {@link android.content.ServiceConnection#onServiceConnected} callback.
582      */
583     public class LocalBinder extends Binder {
getService()584         DeviceAsWebcamFgService getService() {
585             return DeviceAsWebcamFgService.this;
586         }
587     }
588 }
589