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