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