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