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.annotation.NonNull; 20 import android.app.Notification; 21 import android.app.NotificationChannel; 22 import android.app.NotificationManager; 23 import android.app.PendingIntent; 24 import android.app.Service; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.ServiceInfo; 28 import android.hardware.HardwareBuffer; 29 import android.util.Log; 30 import android.util.Size; 31 32 import androidx.core.app.NotificationCompat; 33 import androidx.core.app.NotificationManagerCompat; 34 35 import com.android.DeviceAsWebcam.R; 36 import com.android.deviceaswebcam.annotations.UsedByNative; 37 import com.android.deviceaswebcam.utils.IgnoredV4L2Nodes; 38 39 import java.util.Objects; 40 41 /** 42 * Base abstract class which implements necessary foreground service functionality to talk to the 43 * native layer and handles service lifecycle. 44 */ 45 public abstract class DeviceAsWebcamFgService extends Service { 46 private static final String TAG = "DeviceAsWebcamFgService"; 47 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 48 private static final String NOTIF_CHANNEL_ID = "WebcamService"; 49 private static final int NOTIF_ID = 1; 50 51 static { 52 System.loadLibrary("jni_deviceAsWebcam"); 53 } 54 55 // Guards all methods in the service to ensure a consistent state while executing a method 56 private final Object mServiceLock = new Object(); 57 private Context mContext; 58 private WebcamController mWebcamController; 59 private boolean mServiceRunning = false; 60 61 private NotificationCompat.Builder mNotificationBuilder; 62 private int mNotificationIcon; 63 private int mNextNotificationIcon; 64 private boolean mNotificationUpdatePending; 65 66 /** 67 * Returns the concrete implementation of the WebcamController interface that handles webcam 68 * functionality. This method will be called during the service's {@link #onStartCommand} and 69 * instance returned by this method will be used until the service is stopped, i.e. when 70 * {@link WebcamController#onDestroy} is called. 71 * 72 * @param context context to access service resources 73 * @return The WebcamController that controls the basic webcam functionality. 74 */ getWebcamController(@onNull Context context)75 protected abstract WebcamController getWebcamController(@NonNull Context context); 76 77 /** 78 * The Intent to start the preview activity. This Intent will be called when the user taps the 79 * webcam notification. 80 * 81 * @param context context to access service resources 82 * @return {@link Intent} to start the user facing activity. 83 */ getPreviewActivityIntent(@onNull Context context)84 protected abstract Intent getPreviewActivityIntent(@NonNull Context context); 85 86 @Override onCreate()87 public void onCreate() { 88 super.onCreate(); 89 } 90 91 @Override onStartCommand(Intent intent, int flags, int startId)92 public final int onStartCommand(Intent intent, int flags, int startId) { 93 super.onStartCommand(intent, flags, startId); 94 synchronized (mServiceLock) { 95 mContext = getApplicationContext(); 96 if (mContext == null) { 97 Log.e(TAG, "Application context is null!, something is going to go wrong"); 98 } 99 100 mWebcamController = getWebcamController(mContext); 101 mWebcamController.registerServiceInstance(this); 102 103 int res = setupServicesAndStartListening(); 104 startForegroundWithNotification(); 105 // If `setupServicesAndStartListening` fails, we don't want to start the foreground 106 // service. However, Android expects a call to `startForegroundWithNotification` in 107 // `onStartCommand` and throws an exception if it isn't called. So, if the foreground 108 // service should not be running, we call `startForegroundWithNotification` which starts 109 // the service, and immediately call `stopSelf` which causes the service to be 110 // torn down once `onStartCommand` returns. 111 if (res != 0) { 112 stopSelf(); 113 } 114 mServiceRunning = true; 115 return START_NOT_STICKY; 116 } 117 } 118 createNotificationChannel()119 private String createNotificationChannel() { 120 NotificationChannel channel = 121 new NotificationChannel( 122 NOTIF_CHANNEL_ID, 123 getString(R.string.notif_channel_name), 124 NotificationManager.IMPORTANCE_DEFAULT); 125 NotificationManager notMan = getSystemService(NotificationManager.class); 126 Objects.requireNonNull(notMan).createNotificationChannel(channel); 127 return NOTIF_CHANNEL_ID; 128 } 129 startForegroundWithNotification()130 private void startForegroundWithNotification() { 131 Intent notificationIntent = getPreviewActivityIntent(mContext); 132 PendingIntent pendingIntent = 133 PendingIntent.getActivity( 134 mContext, 0, notificationIntent, PendingIntent.FLAG_MUTABLE); 135 String channelId = createNotificationChannel(); 136 mNextNotificationIcon = mNotificationIcon = R.drawable.ic_notif_line; 137 mNotificationBuilder = 138 new NotificationCompat.Builder(this, channelId) 139 .setCategory(Notification.CATEGORY_SERVICE) 140 .setContentIntent(pendingIntent) 141 .setContentText(getString(R.string.notif_desc)) 142 .setContentTitle(getString(R.string.notif_title)) 143 .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) 144 .setOngoing(true) 145 .setPriority(NotificationManager.IMPORTANCE_DEFAULT) 146 .setShowWhen(false) 147 .setSmallIcon(mNotificationIcon) 148 .setTicker(getString(R.string.notif_ticker)) 149 .setVisibility(Notification.VISIBILITY_PUBLIC); 150 Notification notif = mNotificationBuilder.build(); 151 startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA); 152 } 153 setupServicesAndStartListening()154 private int setupServicesAndStartListening() { 155 String[] ignoredNodes = IgnoredV4L2Nodes.getIgnoredNodes(getApplicationContext()); 156 return setupServicesAndStartListeningNative(ignoredNodes); 157 } 158 159 @Override onDestroy()160 public void onDestroy() { 161 synchronized (mServiceLock) { 162 if (!mServiceRunning) { 163 return; 164 } 165 mServiceRunning = false; 166 if (mWebcamController != null) { 167 mWebcamController.onDestroy(); 168 } 169 nativeOnDestroy(); 170 if (VERBOSE) { 171 Log.v(TAG, "Destroyed fg service"); 172 } 173 // Ensure that the service notification is removed. 174 NotificationManagerCompat.from(mContext).cancelAll(); 175 } 176 super.onDestroy(); 177 } 178 updateNotification(boolean isStreaming)179 private void updateNotification(boolean isStreaming) { 180 int transitionIcon; // animated icon 181 int finalIcon; // static icon 182 if (isStreaming) { 183 transitionIcon = R.drawable.ic_notif_streaming; 184 // last frame of ic_notif_streaming 185 finalIcon = R.drawable.ic_notif_filled; 186 } else { 187 transitionIcon = R.drawable.ic_notif_idle; 188 // last frame of ic_notif_idle 189 finalIcon = R.drawable.ic_notif_line; 190 } 191 192 synchronized (mServiceLock) { 193 if (finalIcon == mNotificationIcon) { 194 // Notification already is desired state. 195 return; 196 } 197 if (transitionIcon == mNotificationIcon) { 198 // Notification currently animating to finalIcon. 199 // Set next state to desired steady state icon. 200 mNextNotificationIcon = finalIcon; 201 return; 202 } 203 204 if (mNotificationUpdatePending) { 205 // Notification animating to some other icon. Set the next icon to the new 206 // transition icon and let the update runnable handle the actual updates. 207 mNextNotificationIcon = transitionIcon; 208 return; 209 } 210 211 // Notification is in a steady state. Update notification to the new icon. 212 mNextNotificationIcon = transitionIcon; 213 updateNotificationToNextIcon(); 214 } 215 } 216 updateNotificationToNextIcon()217 private void updateNotificationToNextIcon() { 218 synchronized (mServiceLock) { 219 if (!mServiceRunning) { 220 return; 221 } 222 223 mNotificationBuilder.setSmallIcon(mNextNotificationIcon); 224 NotificationManagerCompat.from(mContext).notify(NOTIF_ID, mNotificationBuilder.build()); 225 mNotificationIcon = mNextNotificationIcon; 226 227 boolean notifNeedsUpdate = false; 228 if (mNotificationIcon == R.drawable.ic_notif_streaming) { 229 // last frame of ic_notif_streaming 230 mNextNotificationIcon = R.drawable.ic_notif_filled; 231 notifNeedsUpdate = true; 232 } else if (mNotificationIcon == R.drawable.ic_notif_idle) { 233 // last frame of ic_notif_idle 234 mNextNotificationIcon = R.drawable.ic_notif_line; 235 notifNeedsUpdate = true; 236 } 237 mNotificationUpdatePending = notifNeedsUpdate; 238 if (notifNeedsUpdate) { 239 // Run this method again after 500ms to update the notification to steady 240 // state icon 241 getMainThreadHandler().postDelayed(this::updateNotificationToNextIcon, 500); 242 } 243 } 244 } 245 246 @UsedByNative("DeviceAsWebcamNative.cpp") startStreaming()247 private void startStreaming() { 248 synchronized (mServiceLock) { 249 if (!mServiceRunning) { 250 Log.e(TAG, "startStreaming was called after Service was destroyed"); 251 return; 252 } 253 mWebcamController.startStream(); 254 updateNotification(/*isStreaming*/ true); 255 } 256 } 257 258 @UsedByNative("DeviceAsWebcamNative.cpp") stopService()259 private void stopService() { 260 synchronized (mServiceLock) { 261 if (!mServiceRunning) { 262 Log.e(TAG, "stopService was called after Service was destroyed"); 263 return; 264 } 265 stopSelf(); 266 } 267 } 268 269 @UsedByNative("DeviceAsWebcamNative.cpp") stopStreaming()270 private void stopStreaming() { 271 synchronized (mServiceLock) { 272 if (!mServiceRunning) { 273 Log.e(TAG, "stopStreaming was called after Service was destroyed"); 274 return; 275 } 276 mWebcamController.stopStream(); 277 updateNotification(/*isStreaming*/ false); 278 } 279 } 280 281 @UsedByNative("DeviceAsWebcamNative.cpp") returnImage(long timestamp)282 private void returnImage(long timestamp) { 283 synchronized (mServiceLock) { 284 if (!mServiceRunning) { 285 Log.e(TAG, "returnImage was called after Service was destroyed"); 286 return; 287 } 288 mWebcamController.onImageReturned(timestamp); 289 } 290 } 291 292 @UsedByNative("DeviceAsWebcamNative.cpp") setStreamConfig(boolean mjpeg, int width, int height, int fps)293 private void setStreamConfig(boolean mjpeg, int width, int height, int fps) { 294 synchronized (mServiceLock) { 295 if (!mServiceRunning) { 296 Log.e(TAG, "setStreamConfig was called after Service was destroyed"); 297 return; 298 } 299 mWebcamController.setStreamConfig(new Size(width, height), fps); 300 } 301 } 302 303 /** 304 * Called by {@link DeviceAsWebcamReceiver} to check if the service should be started. 305 * 306 * @param ignoredNodes V4L2 nodes to ignore 307 * @return {@code true} if the foreground service should be started, {@code false} if the 308 * service is already running or should not be started 309 */ shouldStartServiceNative(String[] ignoredNodes)310 public static native boolean shouldStartServiceNative(String[] ignoredNodes); 311 312 /** 313 * Called during {@link #onStartCommand} to initialize the native side of the service. 314 * 315 * @param ignoredNodes V4L2 nodes to ignore 316 * @return 0 if native side code was successfully initialized, non-0 otherwise 317 */ setupServicesAndStartListeningNative(String[] ignoredNodes)318 private native int setupServicesAndStartListeningNative(String[] ignoredNodes); 319 320 /** 321 * Called by {@link CameraController} to queue frames for encoding. The frames are encoded 322 * asynchronously. When encoding is done, the native code call {@link #returnImage} with the 323 * {@code timestamp} passed here. 324 * 325 * @param buffer buffer containing the frame to be encoded 326 * @param timestamp timestamp associated with the buffer which uniquely identifies the buffer 327 * @return 0 if buffer was successfully queued for encoding. non-0 otherwise. 328 */ nativeEncodeImage(HardwareBuffer buffer, long timestamp, int rotation)329 public native int nativeEncodeImage(HardwareBuffer buffer, long timestamp, int rotation); 330 331 /** 332 * Called by {@link #onDestroy} to give the JNI code a chance to clean up before the service 333 * goes out of scope. 334 */ nativeOnDestroy()335 private native void nativeOnDestroy(); 336 } 337