blob: a9896f51369cc2e1ab596e23bc6399c5cf916d26 [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;
25import android.content.Context;
26import android.content.Intent;
27import android.graphics.Bitmap;
28import android.graphics.drawable.Icon;
29import android.hardware.display.DisplayManager;
30import android.hardware.display.VirtualDisplay;
31import android.media.MediaRecorder;
32import android.media.ThumbnailUtils;
33import android.media.projection.MediaProjection;
34import android.media.projection.MediaProjectionManager;
35import android.net.Uri;
36import android.os.Environment;
37import android.os.IBinder;
38import android.provider.MediaStore;
39import android.provider.Settings;
40import android.util.DisplayMetrics;
41import android.util.Log;
42import android.view.Surface;
43import android.widget.Toast;
44
45import androidx.core.content.FileProvider;
46
47import com.android.systemui.R;
48
49import java.io.File;
50import java.io.IOException;
51import java.nio.file.Files;
52import java.nio.file.Path;
53import 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;
79 private static final String RECORD_DIR = "Captures"; // TODO: use a translatable string
80 private static final int VIDEO_BIT_RATE = 6000000;
81 private static final int VIDEO_FRAME_RATE = 30;
82 private static final int AUDIO_BIT_RATE = 16;
83 private static final int AUDIO_SAMPLE_RATE = 44100;
84 private static final String FILE_PROVIDER = "com.android.systemui.fileprovider";
85
86 private MediaProjectionManager mMediaProjectionManager;
87 private MediaProjection mMediaProjection;
88 private Surface mInputSurface;
89 private VirtualDisplay mVirtualDisplay;
90 private MediaRecorder mMediaRecorder;
91 private Notification.Builder mRecordingNotificationBuilder;
92
93 private boolean mUseAudio;
94 private boolean mShowTaps;
95 private File mTempFile;
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 data The data from {@link android.app.Activity#onActivityResult(int, int,
104 * android.content.Intent)}
105 * @param useAudio True to enable microphone input while recording
106 * @param showTaps True to make touches visible while recording
107 */
108 public static Intent getStartIntent(Context context, int resultCode, Intent data,
109 boolean useAudio, boolean showTaps) {
110 return new Intent(context, RecordingService.class)
111 .setAction(ACTION_START)
112 .putExtra(EXTRA_RESULT_CODE, resultCode)
113 .putExtra(EXTRA_DATA, data)
114 .putExtra(EXTRA_USE_AUDIO, useAudio)
115 .putExtra(EXTRA_SHOW_TAPS, showTaps);
116 }
117
118 @Override
119 public int onStartCommand(Intent intent, int flags, int startId) {
120 Log.d(TAG, "RecordingService is starting");
121 if (intent == null) {
122 return Service.START_NOT_STICKY;
123 }
124 String action = intent.getAction();
125
126 NotificationManager notificationManager =
127 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
128
129 switch (action) {
130 case ACTION_START:
131 int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED);
132 mUseAudio = intent.getBooleanExtra(EXTRA_USE_AUDIO, false);
133 mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
134 Intent data = intent.getParcelableExtra(EXTRA_DATA);
135 if (data != null) {
136 mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
137 startRecording();
138 }
139 break;
140
141 case ACTION_CANCEL:
142 stopRecording();
143
144 // Delete temp file
145 if (!mTempFile.delete()) {
146 Log.e(TAG, "Error canceling screen recording!");
147 Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
148 .show();
149 } else {
150 Toast.makeText(this, R.string.screenrecord_cancel_success, Toast.LENGTH_LONG)
151 .show();
152 }
153
154 // Close quick shade
155 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
156 break;
157
158 case ACTION_STOP:
159 stopRecording();
160
161 // Move temp file to user directory
162 File recordDir = new File(
163 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
164 RECORD_DIR);
165 recordDir.mkdirs();
166
167 String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
168 .format(new Date());
169 Path path = new File(recordDir, fileName).toPath();
170
171 try {
172 Files.move(mTempFile.toPath(), path);
173 Notification notification = createSaveNotification(path);
174 notificationManager.notify(NOTIFICATION_ID, notification);
175 } catch (IOException e) {
176 e.printStackTrace();
177 Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
178 .show();
179 }
180 break;
181
182 case ACTION_PAUSE:
183 mMediaRecorder.pause();
184 setNotificationActions(true, notificationManager);
185 break;
186
187 case ACTION_RESUME:
188 mMediaRecorder.resume();
189 setNotificationActions(false, notificationManager);
190 break;
191
192 case ACTION_SHARE:
193 File shareFile = new File(intent.getStringExtra(EXTRA_PATH));
194 Uri shareUri = FileProvider.getUriForFile(this, FILE_PROVIDER, shareFile);
195
196 Intent shareIntent = new Intent(Intent.ACTION_SEND)
197 .setType("video/mp4")
198 .putExtra(Intent.EXTRA_STREAM, shareUri);
199 String shareLabel = getResources().getString(R.string.screenrecord_share_label);
200
201 // Close quick shade
202 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
203
204 // Remove notification
205 notificationManager.cancel(NOTIFICATION_ID);
206
207 startActivity(Intent.createChooser(shareIntent, shareLabel)
208 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
209 break;
210 case ACTION_DELETE:
211 // Close quick shade
212 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
213
214 File file = new File(intent.getStringExtra(EXTRA_PATH));
215 if (file.delete()) {
216 Toast.makeText(
217 this,
218 R.string.screenrecord_delete_description,
219 Toast.LENGTH_LONG).show();
220
221 // Remove notification
222 notificationManager.cancel(NOTIFICATION_ID);
223 } else {
224 Log.e(TAG, "Error deleting screen recording!");
225 Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
226 .show();
227 }
228 break;
229 }
230 return Service.START_STICKY;
231 }
232
233 @Override
234 public IBinder onBind(Intent intent) {
235 return null;
236 }
237
238 @Override
239 public void onCreate() {
240 super.onCreate();
241
242 mMediaProjectionManager =
243 (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
244 }
245
246 /**
247 * Begin the recording session
248 */
249 private void startRecording() {
250 try {
251 mTempFile = File.createTempFile("temp", ".mp4");
252 Log.d(TAG, "Writing video output to: " + mTempFile.getAbsolutePath());
253
254 setTapsVisible(mShowTaps);
255
256 // Set up media recorder
257 mMediaRecorder = new MediaRecorder();
258 if (mUseAudio) {
259 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
260 }
261 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
262 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
263
264 // Set up video
265 DisplayMetrics metrics = getResources().getDisplayMetrics();
266 int screenWidth = metrics.widthPixels;
267 int screenHeight = metrics.heightPixels;
268 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
269 mMediaRecorder.setVideoSize(screenWidth, screenHeight);
270 mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE);
271 mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE);
272
273 // Set up audio
274 if (mUseAudio) {
275 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
276 mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS);
277 mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE);
278 mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE);
279 }
280
281 mMediaRecorder.setOutputFile(mTempFile);
282 mMediaRecorder.prepare();
283
284 // Create surface
285 mInputSurface = mMediaRecorder.getSurface();
286 mVirtualDisplay = mMediaProjection.createVirtualDisplay(
287 "Recording Display",
288 screenWidth,
289 screenHeight,
290 metrics.densityDpi,
291 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
292 mInputSurface,
293 null,
294 null);
295
296 mMediaRecorder.start();
297 } catch (IOException e) {
298 e.printStackTrace();
299 throw new RuntimeException(e);
300 }
301
302 createRecordingNotification();
303 }
304
305 private void createRecordingNotification() {
306 NotificationChannel channel = new NotificationChannel(
307 CHANNEL_ID,
308 getString(R.string.screenrecord_name),
309 NotificationManager.IMPORTANCE_HIGH);
310 channel.setDescription(getString(R.string.screenrecord_channel_description));
311 channel.enableVibration(true);
312 NotificationManager notificationManager =
313 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
314 notificationManager.createNotificationChannel(channel);
315
316 mRecordingNotificationBuilder = new Notification.Builder(this, CHANNEL_ID)
317 .setSmallIcon(R.drawable.ic_android)
318 .setContentTitle(getResources().getString(R.string.screenrecord_name))
319 .setUsesChronometer(true)
320 .setOngoing(true);
321 setNotificationActions(false, notificationManager);
322 Notification notification = mRecordingNotificationBuilder.build();
323 startForeground(NOTIFICATION_ID, notification);
324 }
325
326 private void setNotificationActions(boolean isPaused, NotificationManager notificationManager) {
327 String pauseString = getResources()
328 .getString(isPaused ? R.string.screenrecord_resume_label
329 : R.string.screenrecord_pause_label);
330 Intent pauseIntent = isPaused ? getResumeIntent(this) : getPauseIntent(this);
331
332 mRecordingNotificationBuilder.setActions(
333 new Notification.Action.Builder(
334 Icon.createWithResource(this, R.drawable.ic_android),
335 getResources().getString(R.string.screenrecord_stop_label),
336 PendingIntent
337 .getService(this, REQUEST_CODE, getStopIntent(this),
338 PendingIntent.FLAG_UPDATE_CURRENT))
339 .build(),
340 new Notification.Action.Builder(
341 Icon.createWithResource(this, R.drawable.ic_android), pauseString,
342 PendingIntent.getService(this, REQUEST_CODE, pauseIntent,
343 PendingIntent.FLAG_UPDATE_CURRENT))
344 .build(),
345 new Notification.Action.Builder(
346 Icon.createWithResource(this, R.drawable.ic_android),
347 getResources().getString(R.string.screenrecord_cancel_label),
348 PendingIntent
349 .getService(this, REQUEST_CODE, getCancelIntent(this),
350 PendingIntent.FLAG_UPDATE_CURRENT))
351 .build());
352 notificationManager.notify(NOTIFICATION_ID, mRecordingNotificationBuilder.build());
353 }
354
355 private Notification createSaveNotification(Path path) {
356 Uri saveUri = FileProvider.getUriForFile(this, FILE_PROVIDER, path.toFile());
357 Log.d(TAG, "Screen recording saved to " + path.toString());
358
359 Intent viewIntent = new Intent(Intent.ACTION_VIEW)
360 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION)
361 .setDataAndType(saveUri, "video/mp4");
362
363 Notification.Action shareAction = new Notification.Action.Builder(
364 Icon.createWithResource(this, R.drawable.ic_android),
365 getResources().getString(R.string.screenrecord_share_label),
366 PendingIntent.getService(
367 this,
368 REQUEST_CODE,
369 getShareIntent(this, path.toString()),
370 PendingIntent.FLAG_UPDATE_CURRENT))
371 .build();
372
373 Notification.Action deleteAction = new Notification.Action.Builder(
374 Icon.createWithResource(this, R.drawable.ic_android),
375 getResources().getString(R.string.screenrecord_delete_label),
376 PendingIntent.getService(
377 this,
378 REQUEST_CODE,
379 getDeleteIntent(this, path.toString()),
380 PendingIntent.FLAG_UPDATE_CURRENT))
381 .build();
382
383 Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
384 .setSmallIcon(R.drawable.ic_android)
385 .setContentTitle(getResources().getString(R.string.screenrecord_name))
386 .setContentText(getResources().getString(R.string.screenrecord_save_message))
387 .setContentIntent(PendingIntent.getActivity(
388 this,
389 REQUEST_CODE,
390 viewIntent,
391 Intent.FLAG_GRANT_READ_URI_PERMISSION))
392 .addAction(shareAction)
393 .addAction(deleteAction)
394 .setAutoCancel(true);
395
396 // Add thumbnail if available
397 Bitmap thumbnailBitmap = ThumbnailUtils.createVideoThumbnail(path.toString(),
398 MediaStore.Video.Thumbnails.MINI_KIND);
399 if (thumbnailBitmap != null) {
400 Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle()
401 .bigPicture(thumbnailBitmap)
402 .bigLargeIcon((Bitmap) null);
403 builder.setLargeIcon(thumbnailBitmap).setStyle(pictureStyle);
404 }
405 return builder.build();
406 }
407
408 private void stopRecording() {
409 setTapsVisible(false);
410 mMediaRecorder.stop();
411 mMediaRecorder.release();
412 mMediaRecorder = null;
413 mMediaProjection.stop();
414 mMediaProjection = null;
415 mInputSurface.release();
416 mVirtualDisplay.release();
417 stopSelf();
418 }
419
420 private void setTapsVisible(boolean turnOn) {
421 int value = turnOn ? 1 : 0;
422 Settings.System.putInt(getApplicationContext().getContentResolver(),
423 Settings.System.SHOW_TOUCHES, value);
424 }
425
426 private static Intent getStopIntent(Context context) {
427 return new Intent(context, RecordingService.class).setAction(ACTION_STOP);
428 }
429
430 private static Intent getPauseIntent(Context context) {
431 return new Intent(context, RecordingService.class).setAction(ACTION_PAUSE);
432 }
433
434 private static Intent getResumeIntent(Context context) {
435 return new Intent(context, RecordingService.class).setAction(ACTION_RESUME);
436 }
437
438 private static Intent getCancelIntent(Context context) {
439 return new Intent(context, RecordingService.class).setAction(ACTION_CANCEL);
440 }
441
442 private static Intent getShareIntent(Context context, String path) {
443 return new Intent(context, RecordingService.class).setAction(ACTION_SHARE)
444 .putExtra(EXTRA_PATH, path);
445 }
446
447 private static Intent getDeleteIntent(Context context, String path) {
448 return new Intent(context, RecordingService.class).setAction(ACTION_DELETE)
449 .putExtra(EXTRA_PATH, path);
450 }
451}