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