blob: 0383dee4f9c358398561153072baeb8c05f456a4 [file] [log] [blame]
Beth Thibodeau5898ac42018-10-26 13:00:09 -04001/*
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
17package com.android.systemui.screenrecord;
18
19import android.app.Activity;
20import android.app.Notification;
21import android.app.NotificationChannel;
22import android.app.NotificationManager;
23import android.app.PendingIntent;
24import android.app.Service;
Beth Thibodeaube08b6b2019-07-24 15:23:54 -040025import android.content.ContentResolver;
26import android.content.ContentValues;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040027import android.content.Context;
28import android.content.Intent;
29import android.graphics.Bitmap;
Beth Thibodeaucf0ff862019-08-08 14:52:24 -040030import android.graphics.Point;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040031import android.graphics.drawable.Icon;
32import android.hardware.display.DisplayManager;
33import android.hardware.display.VirtualDisplay;
34import android.media.MediaRecorder;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040035import android.media.projection.MediaProjection;
36import android.media.projection.MediaProjectionManager;
37import android.net.Uri;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040038import android.os.IBinder;
39import android.provider.MediaStore;
40import android.provider.Settings;
41import android.util.DisplayMetrics;
42import android.util.Log;
Beth Thibodeaucf0ff862019-08-08 14:52:24 -040043import android.util.Size;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040044import android.view.Surface;
45import android.widget.Toast;
46
Beth Thibodeau5898ac42018-10-26 13:00:09 -040047import com.android.systemui.R;
48
49import java.io.File;
50import java.io.IOException;
Beth Thibodeaube08b6b2019-07-24 15:23:54 -040051import java.io.OutputStream;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040052import java.nio.file.Files;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040053import java.text.SimpleDateFormat;
54import java.util.Date;
55
56/**
57 * A service which records the device screen and optionally microphone input.
58 */
59public class RecordingService extends Service {
60 private static final int NOTIFICATION_ID = 1;
61 private static final String TAG = "RecordingService";
62 private static final String CHANNEL_ID = "screen_record";
63 private static final String EXTRA_RESULT_CODE = "extra_resultCode";
64 private static final String EXTRA_DATA = "extra_data";
65 private static final String EXTRA_PATH = "extra_path";
66 private static final String EXTRA_USE_AUDIO = "extra_useAudio";
67 private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
68 private static final int REQUEST_CODE = 2;
69
70 private static final String ACTION_START = "com.android.systemui.screenrecord.START";
71 private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
72 private static final String ACTION_PAUSE = "com.android.systemui.screenrecord.PAUSE";
73 private static final String ACTION_RESUME = "com.android.systemui.screenrecord.RESUME";
74 private static final String ACTION_CANCEL = "com.android.systemui.screenrecord.CANCEL";
75 private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE";
76 private static final String ACTION_DELETE = "com.android.systemui.screenrecord.DELETE";
77
78 private static final int TOTAL_NUM_TRACKS = 1;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040079 private static final int VIDEO_BIT_RATE = 6000000;
80 private static final int VIDEO_FRAME_RATE = 30;
81 private static final int AUDIO_BIT_RATE = 16;
82 private static final int AUDIO_SAMPLE_RATE = 44100;
Beth Thibodeau5898ac42018-10-26 13:00:09 -040083
84 private MediaProjectionManager mMediaProjectionManager;
85 private MediaProjection mMediaProjection;
86 private Surface mInputSurface;
87 private VirtualDisplay mVirtualDisplay;
88 private MediaRecorder mMediaRecorder;
89 private Notification.Builder mRecordingNotificationBuilder;
90
91 private boolean mUseAudio;
92 private boolean mShowTaps;
93 private File mTempFile;
94
95 /**
96 * Get an intent to start the recording service.
97 *
98 * @param context Context from the requesting activity
99 * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int,
100 * android.content.Intent)}
101 * @param data The data from {@link android.app.Activity#onActivityResult(int, int,
102 * android.content.Intent)}
103 * @param useAudio True to enable microphone input while recording
104 * @param showTaps True to make touches visible while recording
105 */
106 public static Intent getStartIntent(Context context, int resultCode, Intent data,
107 boolean useAudio, boolean showTaps) {
108 return new Intent(context, RecordingService.class)
109 .setAction(ACTION_START)
110 .putExtra(EXTRA_RESULT_CODE, resultCode)
111 .putExtra(EXTRA_DATA, data)
112 .putExtra(EXTRA_USE_AUDIO, useAudio)
113 .putExtra(EXTRA_SHOW_TAPS, showTaps);
114 }
115
116 @Override
117 public int onStartCommand(Intent intent, int flags, int startId) {
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400118 if (intent == null) {
119 return Service.START_NOT_STICKY;
120 }
121 String action = intent.getAction();
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400122 Log.d(TAG, "onStartCommand " + action);
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400123
124 NotificationManager notificationManager =
125 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
126
127 switch (action) {
128 case ACTION_START:
129 int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED);
130 mUseAudio = intent.getBooleanExtra(EXTRA_USE_AUDIO, false);
131 mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
132 Intent data = intent.getParcelableExtra(EXTRA_DATA);
133 if (data != null) {
134 mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
135 startRecording();
136 }
137 break;
138
139 case ACTION_CANCEL:
140 stopRecording();
141
142 // Delete temp file
143 if (!mTempFile.delete()) {
144 Log.e(TAG, "Error canceling screen recording!");
145 Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
146 .show();
147 } else {
148 Toast.makeText(this, R.string.screenrecord_cancel_success, Toast.LENGTH_LONG)
149 .show();
150 }
151
152 // Close quick shade
153 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
154 break;
155
156 case ACTION_STOP:
157 stopRecording();
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400158 saveRecording(notificationManager);
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400159 break;
160
161 case ACTION_PAUSE:
162 mMediaRecorder.pause();
163 setNotificationActions(true, notificationManager);
164 break;
165
166 case ACTION_RESUME:
167 mMediaRecorder.resume();
168 setNotificationActions(false, notificationManager);
169 break;
170
171 case ACTION_SHARE:
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400172 Uri shareUri = Uri.parse(intent.getStringExtra(EXTRA_PATH));
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400173
174 Intent shareIntent = new Intent(Intent.ACTION_SEND)
175 .setType("video/mp4")
176 .putExtra(Intent.EXTRA_STREAM, shareUri);
177 String shareLabel = getResources().getString(R.string.screenrecord_share_label);
178
179 // Close quick shade
180 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
181
182 // Remove notification
183 notificationManager.cancel(NOTIFICATION_ID);
184
185 startActivity(Intent.createChooser(shareIntent, shareLabel)
186 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
187 break;
188 case ACTION_DELETE:
189 // Close quick shade
190 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
191
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400192 ContentResolver resolver = getContentResolver();
193 Uri uri = Uri.parse(intent.getStringExtra(EXTRA_PATH));
194 resolver.delete(uri, null, null);
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400195
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400196 Toast.makeText(
197 this,
198 R.string.screenrecord_delete_description,
199 Toast.LENGTH_LONG).show();
200
201 // Remove notification
202 notificationManager.cancel(NOTIFICATION_ID);
203 Log.d(TAG, "Deleted recording " + uri);
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400204 break;
205 }
206 return Service.START_STICKY;
207 }
208
209 @Override
210 public IBinder onBind(Intent intent) {
211 return null;
212 }
213
214 @Override
215 public void onCreate() {
216 super.onCreate();
217
218 mMediaProjectionManager =
219 (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
220 }
221
222 /**
223 * Begin the recording session
224 */
225 private void startRecording() {
226 try {
227 mTempFile = File.createTempFile("temp", ".mp4");
228 Log.d(TAG, "Writing video output to: " + mTempFile.getAbsolutePath());
229
230 setTapsVisible(mShowTaps);
231
232 // Set up media recorder
233 mMediaRecorder = new MediaRecorder();
234 if (mUseAudio) {
235 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
236 }
237 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
238 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
239
240 // Set up video
241 DisplayMetrics metrics = getResources().getDisplayMetrics();
242 int screenWidth = metrics.widthPixels;
243 int screenHeight = metrics.heightPixels;
244 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
245 mMediaRecorder.setVideoSize(screenWidth, screenHeight);
246 mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE);
247 mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE);
248
249 // Set up audio
250 if (mUseAudio) {
251 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
252 mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS);
253 mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE);
254 mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE);
255 }
256
257 mMediaRecorder.setOutputFile(mTempFile);
258 mMediaRecorder.prepare();
259
260 // Create surface
261 mInputSurface = mMediaRecorder.getSurface();
262 mVirtualDisplay = mMediaProjection.createVirtualDisplay(
263 "Recording Display",
264 screenWidth,
265 screenHeight,
266 metrics.densityDpi,
267 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
268 mInputSurface,
269 null,
270 null);
271
272 mMediaRecorder.start();
273 } catch (IOException e) {
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400274 Log.e(TAG, "Error starting screen recording: " + e.getMessage());
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400275 e.printStackTrace();
276 throw new RuntimeException(e);
277 }
278
279 createRecordingNotification();
280 }
281
282 private void createRecordingNotification() {
283 NotificationChannel channel = new NotificationChannel(
284 CHANNEL_ID,
285 getString(R.string.screenrecord_name),
286 NotificationManager.IMPORTANCE_HIGH);
287 channel.setDescription(getString(R.string.screenrecord_channel_description));
288 channel.enableVibration(true);
289 NotificationManager notificationManager =
290 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
291 notificationManager.createNotificationChannel(channel);
292
293 mRecordingNotificationBuilder = new Notification.Builder(this, CHANNEL_ID)
294 .setSmallIcon(R.drawable.ic_android)
295 .setContentTitle(getResources().getString(R.string.screenrecord_name))
296 .setUsesChronometer(true)
297 .setOngoing(true);
298 setNotificationActions(false, notificationManager);
299 Notification notification = mRecordingNotificationBuilder.build();
300 startForeground(NOTIFICATION_ID, notification);
301 }
302
303 private void setNotificationActions(boolean isPaused, NotificationManager notificationManager) {
304 String pauseString = getResources()
305 .getString(isPaused ? R.string.screenrecord_resume_label
306 : R.string.screenrecord_pause_label);
307 Intent pauseIntent = isPaused ? getResumeIntent(this) : getPauseIntent(this);
308
309 mRecordingNotificationBuilder.setActions(
310 new Notification.Action.Builder(
311 Icon.createWithResource(this, R.drawable.ic_android),
312 getResources().getString(R.string.screenrecord_stop_label),
313 PendingIntent
314 .getService(this, REQUEST_CODE, getStopIntent(this),
315 PendingIntent.FLAG_UPDATE_CURRENT))
316 .build(),
317 new Notification.Action.Builder(
318 Icon.createWithResource(this, R.drawable.ic_android), pauseString,
319 PendingIntent.getService(this, REQUEST_CODE, pauseIntent,
320 PendingIntent.FLAG_UPDATE_CURRENT))
321 .build(),
322 new Notification.Action.Builder(
323 Icon.createWithResource(this, R.drawable.ic_android),
324 getResources().getString(R.string.screenrecord_cancel_label),
325 PendingIntent
326 .getService(this, REQUEST_CODE, getCancelIntent(this),
327 PendingIntent.FLAG_UPDATE_CURRENT))
328 .build());
329 notificationManager.notify(NOTIFICATION_ID, mRecordingNotificationBuilder.build());
330 }
331
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400332 private Notification createSaveNotification(Uri uri) {
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400333 Intent viewIntent = new Intent(Intent.ACTION_VIEW)
334 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION)
Beth Thibodeaube08b6b2019-07-24 15:23:54 -0400335 .setDataAndType(uri, "video/mp4");
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400336
337 Notification.Action shareAction = new Notification.Action.Builder(
338 Icon.createWithResource(this, R.drawable.ic_android),
339 getResources().getString(R.string.screenrecord_share_label),
340 PendingIntent.getService(
341 this,
342 REQUEST_CODE,
Beth Thibodeaube08b6b2019-07-24 15:23:54 -0400343 getShareIntent(this, uri.toString()),
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400344 PendingIntent.FLAG_UPDATE_CURRENT))
345 .build();
346
347 Notification.Action deleteAction = new Notification.Action.Builder(
348 Icon.createWithResource(this, R.drawable.ic_android),
349 getResources().getString(R.string.screenrecord_delete_label),
350 PendingIntent.getService(
351 this,
352 REQUEST_CODE,
Beth Thibodeaube08b6b2019-07-24 15:23:54 -0400353 getDeleteIntent(this, uri.toString()),
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400354 PendingIntent.FLAG_UPDATE_CURRENT))
355 .build();
356
357 Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
358 .setSmallIcon(R.drawable.ic_android)
359 .setContentTitle(getResources().getString(R.string.screenrecord_name))
360 .setContentText(getResources().getString(R.string.screenrecord_save_message))
361 .setContentIntent(PendingIntent.getActivity(
362 this,
363 REQUEST_CODE,
364 viewIntent,
365 Intent.FLAG_GRANT_READ_URI_PERMISSION))
366 .addAction(shareAction)
367 .addAction(deleteAction)
368 .setAutoCancel(true);
369
370 // Add thumbnail if available
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400371 Bitmap thumbnailBitmap = null;
372 try {
373 ContentResolver resolver = getContentResolver();
374 Size size = Point.convert(MediaStore.ThumbnailConstants.MINI_SIZE);
375 thumbnailBitmap = resolver.loadThumbnail(uri, size, null);
376 } catch (IOException e) {
377 Log.e(TAG, "Error creating thumbnail: " + e.getMessage());
378 e.printStackTrace();
379 }
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400380 if (thumbnailBitmap != null) {
381 Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle()
382 .bigPicture(thumbnailBitmap)
383 .bigLargeIcon((Bitmap) null);
384 builder.setLargeIcon(thumbnailBitmap).setStyle(pictureStyle);
385 }
386 return builder.build();
387 }
388
389 private void stopRecording() {
390 setTapsVisible(false);
391 mMediaRecorder.stop();
392 mMediaRecorder.release();
393 mMediaRecorder = null;
394 mMediaProjection.stop();
395 mMediaProjection = null;
396 mInputSurface.release();
397 mVirtualDisplay.release();
398 stopSelf();
399 }
400
Beth Thibodeaucf0ff862019-08-08 14:52:24 -0400401 private void saveRecording(NotificationManager notificationManager) {
402 String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
403 .format(new Date());
404
405 ContentValues values = new ContentValues();
406 values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
407 values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
408 values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis());
409 values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
410
411 ContentResolver resolver = getContentResolver();
412 Uri collectionUri = MediaStore.Video.Media.getContentUri(
413 MediaStore.VOLUME_EXTERNAL_PRIMARY);
414 Uri itemUri = resolver.insert(collectionUri, values);
415
416 try {
417 // Add to the mediastore
418 OutputStream os = resolver.openOutputStream(itemUri, "w");
419 Files.copy(mTempFile.toPath(), os);
420 os.close();
421
422 Notification notification = createSaveNotification(itemUri);
423 notificationManager.notify(NOTIFICATION_ID, notification);
424
425 mTempFile.delete();
426 } catch (IOException e) {
427 Log.e(TAG, "Error saving screen recording: " + e.getMessage());
428 Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
429 .show();
430 }
431 }
432
Beth Thibodeau5898ac42018-10-26 13:00:09 -0400433 private void setTapsVisible(boolean turnOn) {
434 int value = turnOn ? 1 : 0;
435 Settings.System.putInt(getApplicationContext().getContentResolver(),
436 Settings.System.SHOW_TOUCHES, value);
437 }
438
439 private static Intent getStopIntent(Context context) {
440 return new Intent(context, RecordingService.class).setAction(ACTION_STOP);
441 }
442
443 private static Intent getPauseIntent(Context context) {
444 return new Intent(context, RecordingService.class).setAction(ACTION_PAUSE);
445 }
446
447 private static Intent getResumeIntent(Context context) {
448 return new Intent(context, RecordingService.class).setAction(ACTION_RESUME);
449 }
450
451 private static Intent getCancelIntent(Context context) {
452 return new Intent(context, RecordingService.class).setAction(ACTION_CANCEL);
453 }
454
455 private static Intent getShareIntent(Context context, String path) {
456 return new Intent(context, RecordingService.class).setAction(ACTION_SHARE)
457 .putExtra(EXTRA_PATH, path);
458 }
459
460 private static Intent getDeleteIntent(Context context, String path) {
461 return new Intent(context, RecordingService.class).setAction(ACTION_DELETE)
462 .putExtra(EXTRA_PATH, path);
463 }
464}