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.util.Log; 36 import android.widget.Toast; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.logging.UiEventLogger; 40 import com.android.systemui.R; 41 import com.android.systemui.dagger.qualifiers.LongRunning; 42 import com.android.systemui.settings.UserContextProvider; 43 import com.android.systemui.statusbar.phone.KeyguardDismissUtil; 44 45 import java.io.IOException; 46 import java.util.concurrent.Executor; 47 48 import javax.inject.Inject; 49 50 /** 51 * A service which records the device screen and optionally microphone input. 52 */ 53 public class RecordingService extends Service implements MediaRecorder.OnInfoListener { 54 public static final int REQUEST_CODE = 2; 55 56 private static final int NOTIFICATION_RECORDING_ID = 4274; 57 private static final int NOTIFICATION_PROCESSING_ID = 4275; 58 private static final int NOTIFICATION_VIEW_ID = 4273; 59 private static final String TAG = "RecordingService"; 60 private static final String CHANNEL_ID = "screen_record"; 61 private static final String EXTRA_RESULT_CODE = "extra_resultCode"; 62 private static final String EXTRA_PATH = "extra_path"; 63 private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; 64 65 private static final String ACTION_START = "com.android.systemui.screenrecord.START"; 66 private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP"; 67 private static final String ACTION_STOP_NOTIF = 68 "com.android.systemui.screenrecord.STOP_FROM_NOTIF"; 69 private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; 70 private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; 71 72 private final RecordingController mController; 73 private final KeyguardDismissUtil mKeyguardDismissUtil; 74 private ScreenRecordingAudioSource mAudioSource; 75 private ScreenMediaRecorder mRecorder; 76 private final Executor mLongExecutor; 77 private final UiEventLogger mUiEventLogger; 78 private final NotificationManager mNotificationManager; 79 private final UserContextProvider mUserContextTracker; 80 81 @Inject RecordingService(RecordingController controller, @LongRunning Executor executor, UiEventLogger uiEventLogger, NotificationManager notificationManager, UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil)82 public RecordingService(RecordingController controller, @LongRunning Executor executor, 83 UiEventLogger uiEventLogger, NotificationManager notificationManager, 84 UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) { 85 mController = controller; 86 mLongExecutor = executor; 87 mUiEventLogger = uiEventLogger; 88 mNotificationManager = notificationManager; 89 mUserContextTracker = userContextTracker; 90 mKeyguardDismissUtil = keyguardDismissUtil; 91 } 92 93 /** 94 * Get an intent to start the recording service. 95 * 96 * @param context Context from the requesting activity 97 * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int, 98 * android.content.Intent)} 99 * @param audioSource The ordinal value of the audio source 100 * {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource} 101 */ getStartIntent(Context context, int resultCode, int audioSource)102 public static Intent getStartIntent(Context context, int resultCode, int audioSource) { 103 return new Intent(context, RecordingService.class) 104 .setAction(ACTION_START) 105 .putExtra(EXTRA_RESULT_CODE, resultCode) 106 .putExtra(EXTRA_AUDIO_SOURCE, audioSource); 107 } 108 109 @Override onStartCommand(Intent intent, int flags, int startId)110 public int onStartCommand(Intent intent, int flags, int startId) { 111 if (intent == null) { 112 return Service.START_NOT_STICKY; 113 } 114 String action = intent.getAction(); 115 Log.d(TAG, "onStartCommand " + action); 116 117 int currentUserId = mUserContextTracker.getUserContext().getUserId(); 118 UserHandle currentUser = new UserHandle(currentUserId); 119 switch (action) { 120 case ACTION_START: 121 mAudioSource = ScreenRecordingAudioSource 122 .values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)]; 123 Log.d(TAG, "recording with audio source" + mAudioSource); 124 125 mRecorder = new ScreenMediaRecorder( 126 mUserContextTracker.getUserContext(), 127 currentUserId, 128 mAudioSource, 129 this 130 ); 131 132 if (startRecording()) { 133 updateState(true); 134 createRecordingNotification(); 135 mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); 136 } else { 137 updateState(false); 138 createErrorNotification(); 139 stopForeground(true); 140 stopSelf(); 141 return Service.START_NOT_STICKY; 142 } 143 break; 144 145 case ACTION_STOP_NOTIF: 146 case ACTION_STOP: 147 // only difference for actions is the log event 148 if (ACTION_STOP_NOTIF.equals(action)) { 149 mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_NOTIFICATION); 150 } else { 151 mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE); 152 } 153 // Check user ID - we may be getting a stop intent after user switch, in which case 154 // we want to post the notifications for that user, which is NOT current user 155 int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); 156 if (userId == -1) { 157 userId = mUserContextTracker.getUserContext().getUserId(); 158 } 159 Log.d(TAG, "notifying for user " + userId); 160 stopRecording(userId); 161 mNotificationManager.cancel(NOTIFICATION_RECORDING_ID); 162 stopSelf(); 163 break; 164 165 case ACTION_SHARE: 166 Uri shareUri = Uri.parse(intent.getStringExtra(EXTRA_PATH)); 167 168 Intent shareIntent = new Intent(Intent.ACTION_SEND) 169 .setType("video/mp4") 170 .putExtra(Intent.EXTRA_STREAM, shareUri); 171 mKeyguardDismissUtil.executeWhenUnlocked(() -> { 172 String shareLabel = getResources().getString(R.string.screenrecord_share_label); 173 startActivity(Intent.createChooser(shareIntent, shareLabel) 174 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 175 // Remove notification 176 mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); 177 return false; 178 }, false, false); 179 180 // Close quick shade 181 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 182 break; 183 } 184 return Service.START_STICKY; 185 } 186 187 @Override onBind(Intent intent)188 public IBinder onBind(Intent intent) { 189 return null; 190 } 191 192 @Override onCreate()193 public void onCreate() { 194 super.onCreate(); 195 } 196 197 @VisibleForTesting getRecorder()198 protected ScreenMediaRecorder getRecorder() { 199 return mRecorder; 200 } 201 updateState(boolean state)202 private void updateState(boolean state) { 203 int userId = mUserContextTracker.getUserContext().getUserId(); 204 if (userId == UserHandle.USER_SYSTEM) { 205 // Main user has a reference to the correct controller, so no need to use a broadcast 206 mController.updateState(state); 207 } else { 208 Intent intent = new Intent(RecordingController.INTENT_UPDATE_STATE); 209 intent.putExtra(RecordingController.EXTRA_STATE, state); 210 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); 211 sendBroadcast(intent, PERMISSION_SELF); 212 } 213 } 214 215 /** 216 * Begin the recording session 217 * @return true if successful, false if something went wrong 218 */ startRecording()219 private boolean startRecording() { 220 try { 221 getRecorder().start(); 222 return true; 223 } catch (IOException | RemoteException | RuntimeException e) { 224 showErrorToast(R.string.screenrecord_start_error); 225 e.printStackTrace(); 226 } 227 return false; 228 } 229 230 /** 231 * Simple error notification, needed since startForeground must be called to avoid errors 232 */ 233 @VisibleForTesting createErrorNotification()234 protected void createErrorNotification() { 235 Resources res = getResources(); 236 NotificationChannel channel = new NotificationChannel( 237 CHANNEL_ID, 238 getString(R.string.screenrecord_name), 239 NotificationManager.IMPORTANCE_DEFAULT); 240 channel.setDescription(getString(R.string.screenrecord_channel_description)); 241 channel.enableVibration(true); 242 mNotificationManager.createNotificationChannel(channel); 243 244 Bundle extras = new Bundle(); 245 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 246 res.getString(R.string.screenrecord_name)); 247 String notificationTitle = res.getString(R.string.screenrecord_start_error); 248 249 Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) 250 .setSmallIcon(R.drawable.ic_screenrecord) 251 .setContentTitle(notificationTitle) 252 .addExtras(extras); 253 startForeground(NOTIFICATION_RECORDING_ID, builder.build()); 254 } 255 256 @VisibleForTesting showErrorToast(int stringId)257 protected void showErrorToast(int stringId) { 258 Toast.makeText(this, stringId, Toast.LENGTH_LONG).show(); 259 } 260 261 @VisibleForTesting createRecordingNotification()262 protected void createRecordingNotification() { 263 Resources res = getResources(); 264 NotificationChannel channel = new NotificationChannel( 265 CHANNEL_ID, 266 getString(R.string.screenrecord_name), 267 NotificationManager.IMPORTANCE_DEFAULT); 268 channel.setDescription(getString(R.string.screenrecord_channel_description)); 269 channel.enableVibration(true); 270 mNotificationManager.createNotificationChannel(channel); 271 272 Bundle extras = new Bundle(); 273 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 274 res.getString(R.string.screenrecord_name)); 275 276 String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE 277 ? res.getString(R.string.screenrecord_ongoing_screen_only) 278 : res.getString(R.string.screenrecord_ongoing_screen_and_audio); 279 280 PendingIntent pendingIntent = PendingIntent.getService( 281 this, 282 REQUEST_CODE, 283 getNotificationIntent(this), 284 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 285 Notification.Action stopAction = new Notification.Action.Builder( 286 Icon.createWithResource(this, R.drawable.ic_android), 287 getResources().getString(R.string.screenrecord_stop_label), 288 pendingIntent).build(); 289 Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) 290 .setSmallIcon(R.drawable.ic_screenrecord) 291 .setContentTitle(notificationTitle) 292 .setUsesChronometer(true) 293 .setColorized(true) 294 .setColor(getResources().getColor(R.color.GM2_red_700)) 295 .setOngoing(true) 296 .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) 297 .addAction(stopAction) 298 .addExtras(extras); 299 startForeground(NOTIFICATION_RECORDING_ID, builder.build()); 300 } 301 302 @VisibleForTesting createProcessingNotification()303 protected Notification createProcessingNotification() { 304 Resources res = getApplicationContext().getResources(); 305 String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE 306 ? res.getString(R.string.screenrecord_ongoing_screen_only) 307 : res.getString(R.string.screenrecord_ongoing_screen_and_audio); 308 309 Bundle extras = new Bundle(); 310 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 311 res.getString(R.string.screenrecord_name)); 312 313 Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID) 314 .setContentTitle(notificationTitle) 315 .setContentText( 316 getResources().getString(R.string.screenrecord_background_processing_label)) 317 .setSmallIcon(R.drawable.ic_screenrecord) 318 .addExtras(extras); 319 return builder.build(); 320 } 321 322 @VisibleForTesting createSaveNotification(ScreenMediaRecorder.SavedRecording recording)323 protected Notification createSaveNotification(ScreenMediaRecorder.SavedRecording recording) { 324 Uri uri = recording.getUri(); 325 Intent viewIntent = new Intent(Intent.ACTION_VIEW) 326 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION) 327 .setDataAndType(uri, "video/mp4"); 328 329 Notification.Action shareAction = new Notification.Action.Builder( 330 Icon.createWithResource(this, R.drawable.ic_screenrecord), 331 getResources().getString(R.string.screenrecord_share_label), 332 PendingIntent.getService( 333 this, 334 REQUEST_CODE, 335 getShareIntent(this, uri.toString()), 336 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) 337 .build(); 338 339 Bundle extras = new Bundle(); 340 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 341 getResources().getString(R.string.screenrecord_name)); 342 343 Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) 344 .setSmallIcon(R.drawable.ic_screenrecord) 345 .setContentTitle(getResources().getString(R.string.screenrecord_save_title)) 346 .setContentText(getResources().getString(R.string.screenrecord_save_text)) 347 .setContentIntent(PendingIntent.getActivity( 348 this, 349 REQUEST_CODE, 350 viewIntent, 351 PendingIntent.FLAG_IMMUTABLE)) 352 .addAction(shareAction) 353 .setAutoCancel(true) 354 .addExtras(extras); 355 356 // Add thumbnail if available 357 Bitmap thumbnailBitmap = recording.getThumbnail(); 358 if (thumbnailBitmap != null) { 359 Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle() 360 .bigPicture(thumbnailBitmap) 361 .showBigPictureWhenCollapsed(true); 362 builder.setStyle(pictureStyle); 363 } 364 return builder.build(); 365 } 366 stopRecording(int userId)367 private void stopRecording(int userId) { 368 if (getRecorder() != null) { 369 getRecorder().end(); 370 saveRecording(userId); 371 } else { 372 Log.e(TAG, "stopRecording called, but recorder was null"); 373 } 374 updateState(false); 375 } 376 saveRecording(int userId)377 private void saveRecording(int userId) { 378 UserHandle currentUser = new UserHandle(userId); 379 mNotificationManager.notifyAsUser(null, NOTIFICATION_PROCESSING_ID, 380 createProcessingNotification(), currentUser); 381 382 mLongExecutor.execute(() -> { 383 try { 384 Log.d(TAG, "saving recording"); 385 Notification notification = createSaveNotification(getRecorder().save()); 386 if (!mController.isRecording()) { 387 mNotificationManager.notifyAsUser(null, NOTIFICATION_VIEW_ID, notification, 388 currentUser); 389 } 390 } catch (IOException e) { 391 Log.e(TAG, "Error saving screen recording: " + e.getMessage()); 392 showErrorToast(R.string.screenrecord_delete_error); 393 } finally { 394 mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser); 395 } 396 }); 397 } 398 399 /** 400 * Get an intent to stop the recording service. 401 * @param context Context from the requesting activity 402 * @return 403 */ getStopIntent(Context context)404 public static Intent getStopIntent(Context context) { 405 return new Intent(context, RecordingService.class) 406 .setAction(ACTION_STOP) 407 .putExtra(Intent.EXTRA_USER_HANDLE, context.getUserId()); 408 } 409 410 /** 411 * Get the recording notification content intent 412 * @param context 413 * @return 414 */ getNotificationIntent(Context context)415 protected static Intent getNotificationIntent(Context context) { 416 return new Intent(context, RecordingService.class).setAction(ACTION_STOP_NOTIF); 417 } 418 getShareIntent(Context context, String path)419 private static Intent getShareIntent(Context context, String path) { 420 return new Intent(context, RecordingService.class).setAction(ACTION_SHARE) 421 .putExtra(EXTRA_PATH, path); 422 } 423 424 @Override onInfo(MediaRecorder mr, int what, int extra)425 public void onInfo(MediaRecorder mr, int what, int extra) { 426 Log.d(TAG, "Media recorder info: " + what); 427 onStartCommand(getStopIntent(this), 0, 0); 428 } 429 } 430