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