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