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