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