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