1 /* 2 * Copyright (C) 2020 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.BroadcastOptions; 20 import android.app.Dialog; 21 import android.app.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.media.projection.StopReason; 27 import android.os.Bundle; 28 import android.os.CountDownTimer; 29 import android.os.Process; 30 import android.os.UserHandle; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.systemui.broadcast.BroadcastDispatcher; 37 import com.android.systemui.dagger.SysUISingleton; 38 import com.android.systemui.dagger.qualifiers.Main; 39 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; 40 import com.android.systemui.mediaprojection.SessionCreationSource; 41 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; 42 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate; 43 import com.android.systemui.settings.UserTracker; 44 import com.android.systemui.statusbar.policy.CallbackController; 45 46 import dagger.Lazy; 47 48 import java.util.concurrent.CopyOnWriteArrayList; 49 import java.util.concurrent.Executor; 50 51 import javax.inject.Inject; 52 53 /** 54 * Helper class to initiate a screen recording 55 */ 56 @SysUISingleton 57 public class RecordingController 58 implements CallbackController<RecordingController.RecordingStateChangeCallback> { 59 private boolean mIsStarting; 60 private boolean mIsRecording; 61 private PendingIntent mStopIntent; 62 private @StopReason int mStopReason = StopReason.STOP_UNKNOWN; 63 private final Bundle mInteractiveBroadcastOption; 64 private CountDownTimer mCountDownTimer = null; 65 private final Executor mMainExecutor; 66 private final BroadcastDispatcher mBroadcastDispatcher; 67 private final UserTracker mUserTracker; 68 private final RecordingControllerLogger mRecordingControllerLogger; 69 private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; 70 private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate; 71 private final ScreenRecordPermissionDialogDelegate.Factory 72 mScreenRecordPermissionDialogDelegateFactory; 73 private final ScreenRecordPermissionViewBinder.Factory 74 mScreenRecordPermissionViewBinderFactory; 75 76 protected static final String INTENT_UPDATE_STATE = 77 "com.android.systemui.screenrecord.UPDATE_STATE"; 78 protected static final String EXTRA_STATE = "extra_state"; 79 80 private final CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners = 81 new CopyOnWriteArrayList<>(); 82 83 private final Lazy<ScreenCaptureDevicePolicyResolver> mDevicePolicyResolver; 84 85 @VisibleForTesting 86 final UserTracker.Callback mUserChangedCallback = 87 new UserTracker.Callback() { 88 @Override 89 public void onUserChanged(int newUser, @NonNull Context userContext) { 90 stopRecording(StopReason.STOP_USER_SWITCH); 91 } 92 }; 93 94 @VisibleForTesting 95 protected final BroadcastReceiver mStateChangeReceiver = new BroadcastReceiver() { 96 @Override 97 public void onReceive(Context context, Intent intent) { 98 if (intent != null && INTENT_UPDATE_STATE.equals(intent.getAction())) { 99 if (intent.hasExtra(EXTRA_STATE)) { 100 boolean state = intent.getBooleanExtra(EXTRA_STATE, false); 101 mRecordingControllerLogger.logIntentStateUpdated(state); 102 updateState(state); 103 } else { 104 mRecordingControllerLogger.logIntentMissingState(); 105 } 106 } 107 } 108 }; 109 110 /** 111 * Create a new RecordingController 112 */ 113 @Inject RecordingController( @ain Executor mainExecutor, BroadcastDispatcher broadcastDispatcher, Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver, UserTracker userTracker, RecordingControllerLogger recordingControllerLogger, MediaProjectionMetricsLogger mediaProjectionMetricsLogger, ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate, ScreenRecordPermissionDialogDelegate.Factory screenRecordPermissionDialogDelegateFactory, ScreenRecordPermissionViewBinder.Factory screenRecordPermissionViewBinderFactory)114 public RecordingController( 115 @Main Executor mainExecutor, 116 BroadcastDispatcher broadcastDispatcher, 117 Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver, 118 UserTracker userTracker, 119 RecordingControllerLogger recordingControllerLogger, 120 MediaProjectionMetricsLogger mediaProjectionMetricsLogger, 121 ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate, 122 ScreenRecordPermissionDialogDelegate.Factory 123 screenRecordPermissionDialogDelegateFactory, 124 ScreenRecordPermissionViewBinder.Factory screenRecordPermissionViewBinderFactory) { 125 mMainExecutor = mainExecutor; 126 mDevicePolicyResolver = devicePolicyResolver; 127 mBroadcastDispatcher = broadcastDispatcher; 128 mUserTracker = userTracker; 129 mRecordingControllerLogger = recordingControllerLogger; 130 mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger; 131 mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate; 132 mScreenRecordPermissionDialogDelegateFactory = screenRecordPermissionDialogDelegateFactory; 133 mScreenRecordPermissionViewBinderFactory = screenRecordPermissionViewBinderFactory; 134 135 BroadcastOptions options = BroadcastOptions.makeBasic(); 136 options.setInteractive(true); 137 mInteractiveBroadcastOption = options.toBundle(); 138 } 139 140 /** 141 * MediaProjection host is SystemUI for the screen recorder, so return 'my user handle' 142 */ getHostUserHandle()143 private UserHandle getHostUserHandle() { 144 return UserHandle.of(UserHandle.myUserId()); 145 } 146 147 /** 148 * MediaProjection host is SystemUI for the screen recorder, so return 'my process uid' 149 */ getHostUid()150 private int getHostUid() { 151 return Process.myUid(); 152 } 153 154 /** Create a dialog to show screen recording options to the user. 155 * If screen capturing is currently not allowed it will return a dialog 156 * that warns users about it. */ createScreenRecordDialog(@ullable Runnable onStartRecordingClicked)157 public Dialog createScreenRecordDialog(@Nullable Runnable onStartRecordingClicked) { 158 if (isScreenCaptureDisabled()) { 159 return mScreenCaptureDisabledDialogDelegate.createSysUIDialog(); 160 } 161 162 mMediaProjectionMetricsLogger.notifyProjectionInitiated( 163 getHostUid(), SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER); 164 165 return mScreenRecordPermissionDialogDelegateFactory 166 .create(this, getHostUserHandle(), getHostUid(), onStartRecordingClicked) 167 .createDialog(); 168 } 169 170 /** 171 * Create a view binder that controls the logic of views inside the screen record permission 172 * view. 173 * @param onStartRecordingClicked the callback that is run when the start button is clicked. 174 */ createScreenRecordPermissionViewBinder( @ullable Runnable onStartRecordingClicked )175 public ScreenRecordPermissionViewBinder createScreenRecordPermissionViewBinder( 176 @Nullable Runnable onStartRecordingClicked 177 ) { 178 return mScreenRecordPermissionViewBinderFactory 179 .create(getHostUserHandle(), getHostUid(), this, 180 onStartRecordingClicked); 181 } 182 183 /** 184 * Check if screen capture is currently disabled for this device and user. 185 */ isScreenCaptureDisabled()186 public boolean isScreenCaptureDisabled() { 187 return mDevicePolicyResolver.get() 188 .isScreenCaptureCompletelyDisabled(getHostUserHandle()); 189 } 190 191 /** 192 * Start counting down in preparation to start a recording 193 * @param ms Total time in ms to wait before starting 194 * @param interval Time in ms per countdown step 195 * @param startIntent Intent to start a recording 196 * @param stopIntent Intent to stop a recording 197 */ startCountdown(long ms, long interval, PendingIntent startIntent, PendingIntent stopIntent)198 public void startCountdown(long ms, long interval, PendingIntent startIntent, 199 PendingIntent stopIntent) { 200 mIsStarting = true; 201 mStopIntent = stopIntent; 202 203 mCountDownTimer = new CountDownTimer(ms, interval) { 204 @Override 205 public void onTick(long millisUntilFinished) { 206 for (RecordingStateChangeCallback cb : mListeners) { 207 cb.onCountdown(millisUntilFinished); 208 } 209 } 210 211 @Override 212 public void onFinish() { 213 mIsStarting = false; 214 mIsRecording = true; 215 for (RecordingStateChangeCallback cb : mListeners) { 216 cb.onCountdownEnd(); 217 } 218 try { 219 startIntent.send(mInteractiveBroadcastOption); 220 mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); 221 222 IntentFilter stateFilter = new IntentFilter(INTENT_UPDATE_STATE); 223 mBroadcastDispatcher.registerReceiver(mStateChangeReceiver, stateFilter, null, 224 UserHandle.ALL); 225 mRecordingControllerLogger.logSentStartIntent(); 226 } catch (PendingIntent.CanceledException e) { 227 mRecordingControllerLogger.logPendingIntentCancelled(e); 228 } 229 } 230 }; 231 232 mCountDownTimer.start(); 233 } 234 235 /** 236 * Cancel a countdown in progress. This will not stop the recording if it already started. 237 */ cancelCountdown()238 public void cancelCountdown() { 239 if (mCountDownTimer != null) { 240 mRecordingControllerLogger.logCountdownCancelled(); 241 mCountDownTimer.cancel(); 242 } else { 243 mRecordingControllerLogger.logCountdownCancelErrorNoTimer(); 244 } 245 mIsStarting = false; 246 247 for (RecordingStateChangeCallback cb : mListeners) { 248 cb.onCountdownEnd(); 249 } 250 } 251 252 /** 253 * Check if the recording is currently counting down to begin 254 * @return 255 */ isStarting()256 public boolean isStarting() { 257 return mIsStarting; 258 } 259 260 /** 261 * Check if the recording is ongoing 262 * @return 263 */ isRecording()264 public synchronized boolean isRecording() { 265 return mIsRecording; 266 } 267 268 /** 269 * Stop the recording and sets the stop reason to be used by the RecordingService 270 * @param stopReason the method of the recording stopped (i.e. QS tile, status bar chip, etc.) 271 */ stopRecording(@topReason int stopReason)272 public void stopRecording(@StopReason int stopReason) { 273 mStopReason = stopReason; 274 try { 275 if (mStopIntent != null) { 276 mRecordingControllerLogger.logRecordingStopped(); 277 mStopIntent.send(mInteractiveBroadcastOption); 278 } else { 279 mRecordingControllerLogger.logRecordingStopErrorNoStopIntent(); 280 } 281 updateState(false); 282 } catch (PendingIntent.CanceledException e) { 283 mRecordingControllerLogger.logRecordingStopError(e); 284 } 285 } 286 287 /** 288 * Update the current status 289 * @param isRecording 290 */ updateState(boolean isRecording)291 public synchronized void updateState(boolean isRecording) { 292 mRecordingControllerLogger.logStateUpdated(isRecording); 293 if (!isRecording && mIsRecording) { 294 // Unregister receivers if we have stopped recording 295 mUserTracker.removeCallback(mUserChangedCallback); 296 mBroadcastDispatcher.unregisterReceiver(mStateChangeReceiver); 297 } 298 mIsRecording = isRecording; 299 for (RecordingStateChangeCallback cb : mListeners) { 300 if (isRecording) { 301 cb.onRecordingStart(); 302 } else { 303 cb.onRecordingEnd(); 304 } 305 } 306 } 307 getStopReason()308 public @StopReason int getStopReason() { 309 return mStopReason; 310 } 311 312 @Override addCallback(@onNull RecordingStateChangeCallback listener)313 public void addCallback(@NonNull RecordingStateChangeCallback listener) { 314 mListeners.add(listener); 315 } 316 317 @Override removeCallback(@onNull RecordingStateChangeCallback listener)318 public void removeCallback(@NonNull RecordingStateChangeCallback listener) { 319 mListeners.remove(listener); 320 } 321 322 /** 323 * A callback for changes in the screen recording state 324 */ 325 public interface RecordingStateChangeCallback { 326 /** 327 * Called when a countdown to recording has updated 328 * 329 * @param millisUntilFinished Time in ms remaining in the countdown 330 */ onCountdown(long millisUntilFinished)331 default void onCountdown(long millisUntilFinished) {} 332 333 /** 334 * Called when a countdown to recording has ended. This is a separate method so that if 335 * needed, listeners can handle cases where recording fails to start 336 */ onCountdownEnd()337 default void onCountdownEnd() {} 338 339 /** 340 * Called when a screen recording has started 341 */ onRecordingStart()342 default void onRecordingStart() {} 343 344 /** 345 * Called when a screen recording has ended 346 */ onRecordingEnd()347 default void onRecordingEnd() {} 348 } 349 } 350