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