1 /*
2  * Copyright (C) 2018 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.systemui.screenrecord;
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.res.Resources;
27 import android.graphics.Bitmap;
28 import android.graphics.drawable.Icon;
29 import android.media.MediaRecorder;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.os.IBinder;
33 import android.os.RemoteException;
34 import android.os.UserHandle;
35 import android.util.Log;
36 import android.widget.Toast;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.logging.UiEventLogger;
40 import com.android.systemui.R;
41 import com.android.systemui.dagger.qualifiers.LongRunning;
42 import com.android.systemui.settings.UserContextProvider;
43 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
44 
45 import java.io.IOException;
46 import java.util.concurrent.Executor;
47 
48 import javax.inject.Inject;
49 
50 /**
51  * A service which records the device screen and optionally microphone input.
52  */
53 public class RecordingService extends Service implements MediaRecorder.OnInfoListener {
54     public static final int REQUEST_CODE = 2;
55 
56     private static final int NOTIFICATION_RECORDING_ID = 4274;
57     private static final int NOTIFICATION_PROCESSING_ID = 4275;
58     private static final int NOTIFICATION_VIEW_ID = 4273;
59     private static final String TAG = "RecordingService";
60     private static final String CHANNEL_ID = "screen_record";
61     private static final String EXTRA_RESULT_CODE = "extra_resultCode";
62     private static final String EXTRA_PATH = "extra_path";
63     private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio";
64 
65     private static final String ACTION_START = "com.android.systemui.screenrecord.START";
66     private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
67     private static final String ACTION_STOP_NOTIF =
68             "com.android.systemui.screenrecord.STOP_FROM_NOTIF";
69     private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE";
70     private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
71 
72     private final RecordingController mController;
73     private final KeyguardDismissUtil mKeyguardDismissUtil;
74     private ScreenRecordingAudioSource mAudioSource;
75     private ScreenMediaRecorder mRecorder;
76     private final Executor mLongExecutor;
77     private final UiEventLogger mUiEventLogger;
78     private final NotificationManager mNotificationManager;
79     private final UserContextProvider mUserContextTracker;
80 
81     @Inject
RecordingService(RecordingController controller, @LongRunning Executor executor, UiEventLogger uiEventLogger, NotificationManager notificationManager, UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil)82     public RecordingService(RecordingController controller, @LongRunning Executor executor,
83             UiEventLogger uiEventLogger, NotificationManager notificationManager,
84             UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
85         mController = controller;
86         mLongExecutor = executor;
87         mUiEventLogger = uiEventLogger;
88         mNotificationManager = notificationManager;
89         mUserContextTracker = userContextTracker;
90         mKeyguardDismissUtil = keyguardDismissUtil;
91     }
92 
93     /**
94      * Get an intent to start the recording service.
95      *
96      * @param context    Context from the requesting activity
97      * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int,
98      *                   android.content.Intent)}
99      * @param audioSource   The ordinal value of the audio source
100      *                      {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource}
101      */
getStartIntent(Context context, int resultCode, int audioSource)102     public static Intent getStartIntent(Context context, int resultCode, int audioSource) {
103         return new Intent(context, RecordingService.class)
104                 .setAction(ACTION_START)
105                 .putExtra(EXTRA_RESULT_CODE, resultCode)
106                 .putExtra(EXTRA_AUDIO_SOURCE, audioSource);
107     }
108 
109     @Override
onStartCommand(Intent intent, int flags, int startId)110     public int onStartCommand(Intent intent, int flags, int startId) {
111         if (intent == null) {
112             return Service.START_NOT_STICKY;
113         }
114         String action = intent.getAction();
115         Log.d(TAG, "onStartCommand " + action);
116 
117         int currentUserId = mUserContextTracker.getUserContext().getUserId();
118         UserHandle currentUser = new UserHandle(currentUserId);
119         switch (action) {
120             case ACTION_START:
121                 mAudioSource = ScreenRecordingAudioSource
122                         .values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)];
123                 Log.d(TAG, "recording with audio source" + mAudioSource);
124 
125                 mRecorder = new ScreenMediaRecorder(
126                         mUserContextTracker.getUserContext(),
127                         currentUserId,
128                         mAudioSource,
129                         this
130                 );
131 
132                 if (startRecording()) {
133                     updateState(true);
134                     createRecordingNotification();
135                     mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
136                 } else {
137                     updateState(false);
138                     createErrorNotification();
139                     stopForeground(true);
140                     stopSelf();
141                     return Service.START_NOT_STICKY;
142                 }
143                 break;
144 
145             case ACTION_STOP_NOTIF:
146             case ACTION_STOP:
147                 // only difference for actions is the log event
148                 if (ACTION_STOP_NOTIF.equals(action)) {
149                     mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_NOTIFICATION);
150                 } else {
151                     mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE);
152                 }
153                 // Check user ID - we may be getting a stop intent after user switch, in which case
154                 // we want to post the notifications for that user, which is NOT current user
155                 int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
156                 if (userId == -1) {
157                     userId = mUserContextTracker.getUserContext().getUserId();
158                 }
159                 Log.d(TAG, "notifying for user " + userId);
160                 stopRecording(userId);
161                 mNotificationManager.cancel(NOTIFICATION_RECORDING_ID);
162                 stopSelf();
163                 break;
164 
165             case ACTION_SHARE:
166                 Uri shareUri = Uri.parse(intent.getStringExtra(EXTRA_PATH));
167 
168                 Intent shareIntent = new Intent(Intent.ACTION_SEND)
169                         .setType("video/mp4")
170                         .putExtra(Intent.EXTRA_STREAM, shareUri);
171                 mKeyguardDismissUtil.executeWhenUnlocked(() -> {
172                     String shareLabel = getResources().getString(R.string.screenrecord_share_label);
173                     startActivity(Intent.createChooser(shareIntent, shareLabel)
174                             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
175                     // Remove notification
176                     mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser);
177                     return false;
178                 }, false, false);
179 
180                 // Close quick shade
181                 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
182                 break;
183         }
184         return Service.START_STICKY;
185     }
186 
187     @Override
onBind(Intent intent)188     public IBinder onBind(Intent intent) {
189         return null;
190     }
191 
192     @Override
onCreate()193     public void onCreate() {
194         super.onCreate();
195     }
196 
197     @VisibleForTesting
getRecorder()198     protected ScreenMediaRecorder getRecorder() {
199         return mRecorder;
200     }
201 
updateState(boolean state)202     private void updateState(boolean state) {
203         int userId = mUserContextTracker.getUserContext().getUserId();
204         if (userId == UserHandle.USER_SYSTEM) {
205             // Main user has a reference to the correct controller, so no need to use a broadcast
206             mController.updateState(state);
207         } else {
208             Intent intent = new Intent(RecordingController.INTENT_UPDATE_STATE);
209             intent.putExtra(RecordingController.EXTRA_STATE, state);
210             intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
211             sendBroadcast(intent, PERMISSION_SELF);
212         }
213     }
214 
215     /**
216      * Begin the recording session
217      * @return true if successful, false if something went wrong
218      */
startRecording()219     private boolean startRecording() {
220         try {
221             getRecorder().start();
222             return true;
223         } catch (IOException | RemoteException | RuntimeException e) {
224             showErrorToast(R.string.screenrecord_start_error);
225             e.printStackTrace();
226         }
227         return false;
228     }
229 
230     /**
231      * Simple error notification, needed since startForeground must be called to avoid errors
232      */
233     @VisibleForTesting
createErrorNotification()234     protected void createErrorNotification() {
235         Resources res = getResources();
236         NotificationChannel channel = new NotificationChannel(
237                 CHANNEL_ID,
238                 getString(R.string.screenrecord_name),
239                 NotificationManager.IMPORTANCE_DEFAULT);
240         channel.setDescription(getString(R.string.screenrecord_channel_description));
241         channel.enableVibration(true);
242         mNotificationManager.createNotificationChannel(channel);
243 
244         Bundle extras = new Bundle();
245         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
246                 res.getString(R.string.screenrecord_name));
247         String notificationTitle = res.getString(R.string.screenrecord_start_error);
248 
249         Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
250                 .setSmallIcon(R.drawable.ic_screenrecord)
251                 .setContentTitle(notificationTitle)
252                 .addExtras(extras);
253         startForeground(NOTIFICATION_RECORDING_ID, builder.build());
254     }
255 
256     @VisibleForTesting
showErrorToast(int stringId)257     protected void showErrorToast(int stringId) {
258         Toast.makeText(this, stringId, Toast.LENGTH_LONG).show();
259     }
260 
261     @VisibleForTesting
createRecordingNotification()262     protected void createRecordingNotification() {
263         Resources res = getResources();
264         NotificationChannel channel = new NotificationChannel(
265                 CHANNEL_ID,
266                 getString(R.string.screenrecord_name),
267                 NotificationManager.IMPORTANCE_DEFAULT);
268         channel.setDescription(getString(R.string.screenrecord_channel_description));
269         channel.enableVibration(true);
270         mNotificationManager.createNotificationChannel(channel);
271 
272         Bundle extras = new Bundle();
273         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
274                 res.getString(R.string.screenrecord_name));
275 
276         String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
277                 ? res.getString(R.string.screenrecord_ongoing_screen_only)
278                 : res.getString(R.string.screenrecord_ongoing_screen_and_audio);
279 
280         PendingIntent pendingIntent = PendingIntent.getService(
281                 this,
282                 REQUEST_CODE,
283                 getNotificationIntent(this),
284                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
285         Notification.Action stopAction = new Notification.Action.Builder(
286                 Icon.createWithResource(this, R.drawable.ic_android),
287                 getResources().getString(R.string.screenrecord_stop_label),
288                 pendingIntent).build();
289         Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
290                 .setSmallIcon(R.drawable.ic_screenrecord)
291                 .setContentTitle(notificationTitle)
292                 .setUsesChronometer(true)
293                 .setColorized(true)
294                 .setColor(getResources().getColor(R.color.GM2_red_700))
295                 .setOngoing(true)
296                 .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
297                 .addAction(stopAction)
298                 .addExtras(extras);
299         startForeground(NOTIFICATION_RECORDING_ID, builder.build());
300     }
301 
302     @VisibleForTesting
createProcessingNotification()303     protected Notification createProcessingNotification() {
304         Resources res = getApplicationContext().getResources();
305         String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
306                 ? res.getString(R.string.screenrecord_ongoing_screen_only)
307                 : res.getString(R.string.screenrecord_ongoing_screen_and_audio);
308 
309         Bundle extras = new Bundle();
310         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
311                 res.getString(R.string.screenrecord_name));
312 
313         Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
314                 .setContentTitle(notificationTitle)
315                 .setContentText(
316                         getResources().getString(R.string.screenrecord_background_processing_label))
317                 .setSmallIcon(R.drawable.ic_screenrecord)
318                 .addExtras(extras);
319         return builder.build();
320     }
321 
322     @VisibleForTesting
createSaveNotification(ScreenMediaRecorder.SavedRecording recording)323     protected Notification createSaveNotification(ScreenMediaRecorder.SavedRecording recording) {
324         Uri uri = recording.getUri();
325         Intent viewIntent = new Intent(Intent.ACTION_VIEW)
326                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION)
327                 .setDataAndType(uri, "video/mp4");
328 
329         Notification.Action shareAction = new Notification.Action.Builder(
330                 Icon.createWithResource(this, R.drawable.ic_screenrecord),
331                 getResources().getString(R.string.screenrecord_share_label),
332                 PendingIntent.getService(
333                         this,
334                         REQUEST_CODE,
335                         getShareIntent(this, uri.toString()),
336                         PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
337                 .build();
338 
339         Bundle extras = new Bundle();
340         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
341                 getResources().getString(R.string.screenrecord_name));
342 
343         Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
344                 .setSmallIcon(R.drawable.ic_screenrecord)
345                 .setContentTitle(getResources().getString(R.string.screenrecord_save_title))
346                 .setContentText(getResources().getString(R.string.screenrecord_save_text))
347                 .setContentIntent(PendingIntent.getActivity(
348                         this,
349                         REQUEST_CODE,
350                         viewIntent,
351                         PendingIntent.FLAG_IMMUTABLE))
352                 .addAction(shareAction)
353                 .setAutoCancel(true)
354                 .addExtras(extras);
355 
356         // Add thumbnail if available
357         Bitmap thumbnailBitmap = recording.getThumbnail();
358         if (thumbnailBitmap != null) {
359             Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle()
360                     .bigPicture(thumbnailBitmap)
361                     .showBigPictureWhenCollapsed(true);
362             builder.setStyle(pictureStyle);
363         }
364         return builder.build();
365     }
366 
stopRecording(int userId)367     private void stopRecording(int userId) {
368         if (getRecorder() != null) {
369             getRecorder().end();
370             saveRecording(userId);
371         } else {
372             Log.e(TAG, "stopRecording called, but recorder was null");
373         }
374         updateState(false);
375     }
376 
saveRecording(int userId)377     private void saveRecording(int userId) {
378         UserHandle currentUser = new UserHandle(userId);
379         mNotificationManager.notifyAsUser(null, NOTIFICATION_PROCESSING_ID,
380                 createProcessingNotification(), currentUser);
381 
382         mLongExecutor.execute(() -> {
383             try {
384                 Log.d(TAG, "saving recording");
385                 Notification notification = createSaveNotification(getRecorder().save());
386                 if (!mController.isRecording()) {
387                     mNotificationManager.notifyAsUser(null, NOTIFICATION_VIEW_ID, notification,
388                             currentUser);
389                 }
390             } catch (IOException e) {
391                 Log.e(TAG, "Error saving screen recording: " + e.getMessage());
392                 showErrorToast(R.string.screenrecord_delete_error);
393             } finally {
394                 mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser);
395             }
396         });
397     }
398 
399     /**
400      * Get an intent to stop the recording service.
401      * @param context Context from the requesting activity
402      * @return
403      */
getStopIntent(Context context)404     public static Intent getStopIntent(Context context) {
405         return new Intent(context, RecordingService.class)
406                 .setAction(ACTION_STOP)
407                 .putExtra(Intent.EXTRA_USER_HANDLE, context.getUserId());
408     }
409 
410     /**
411      * Get the recording notification content intent
412      * @param context
413      * @return
414      */
getNotificationIntent(Context context)415     protected static Intent getNotificationIntent(Context context) {
416         return new Intent(context, RecordingService.class).setAction(ACTION_STOP_NOTIF);
417     }
418 
getShareIntent(Context context, String path)419     private static Intent getShareIntent(Context context, String path) {
420         return new Intent(context, RecordingService.class).setAction(ACTION_SHARE)
421                 .putExtra(EXTRA_PATH, path);
422     }
423 
424     @Override
onInfo(MediaRecorder mr, int what, int extra)425     public void onInfo(MediaRecorder mr, int what, int extra) {
426         Log.d(TAG, "Media recorder info: " + what);
427         onStartCommand(getStopIntent(this), 0, 0);
428     }
429 }
430