• 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.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