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.PendingIntent; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.os.CountDownTimer; 25 import android.os.UserHandle; 26 import android.util.Log; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.systemui.broadcast.BroadcastDispatcher; 33 import com.android.systemui.dagger.SysUISingleton; 34 import com.android.systemui.settings.UserContextProvider; 35 import com.android.systemui.statusbar.policy.CallbackController; 36 37 import java.util.concurrent.CopyOnWriteArrayList; 38 39 import javax.inject.Inject; 40 41 /** 42 * Helper class to initiate a screen recording 43 */ 44 @SysUISingleton 45 public class RecordingController 46 implements CallbackController<RecordingController.RecordingStateChangeCallback> { 47 private static final String TAG = "RecordingController"; 48 49 private boolean mIsStarting; 50 private boolean mIsRecording; 51 private PendingIntent mStopIntent; 52 private CountDownTimer mCountDownTimer = null; 53 private BroadcastDispatcher mBroadcastDispatcher; 54 private UserContextProvider mUserContextProvider; 55 56 protected static final String INTENT_UPDATE_STATE = 57 "com.android.systemui.screenrecord.UPDATE_STATE"; 58 protected static final String EXTRA_STATE = "extra_state"; 59 60 private CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners = 61 new CopyOnWriteArrayList<>(); 62 63 @VisibleForTesting 64 protected final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { 65 @Override 66 public void onReceive(Context context, Intent intent) { 67 stopRecording(); 68 } 69 }; 70 71 @VisibleForTesting 72 protected final BroadcastReceiver mStateChangeReceiver = new BroadcastReceiver() { 73 @Override 74 public void onReceive(Context context, Intent intent) { 75 if (intent != null && INTENT_UPDATE_STATE.equals(intent.getAction())) { 76 if (intent.hasExtra(EXTRA_STATE)) { 77 boolean state = intent.getBooleanExtra(EXTRA_STATE, false); 78 updateState(state); 79 } else { 80 Log.e(TAG, "Received update intent with no state"); 81 } 82 } 83 } 84 }; 85 86 /** 87 * Create a new RecordingController 88 */ 89 @Inject RecordingController(BroadcastDispatcher broadcastDispatcher, UserContextProvider userContextProvider)90 public RecordingController(BroadcastDispatcher broadcastDispatcher, 91 UserContextProvider userContextProvider) { 92 mBroadcastDispatcher = broadcastDispatcher; 93 mUserContextProvider = userContextProvider; 94 } 95 96 /** Create a dialog to show screen recording options to the user. */ createScreenRecordDialog(Context context, @Nullable Runnable onStartRecordingClicked)97 public ScreenRecordDialog createScreenRecordDialog(Context context, 98 @Nullable Runnable onStartRecordingClicked) { 99 return new ScreenRecordDialog(context, this, mUserContextProvider, onStartRecordingClicked); 100 } 101 102 /** 103 * Start counting down in preparation to start a recording 104 * @param ms Total time in ms to wait before starting 105 * @param interval Time in ms per countdown step 106 * @param startIntent Intent to start a recording 107 * @param stopIntent Intent to stop a recording 108 */ startCountdown(long ms, long interval, PendingIntent startIntent, PendingIntent stopIntent)109 public void startCountdown(long ms, long interval, PendingIntent startIntent, 110 PendingIntent stopIntent) { 111 mIsStarting = true; 112 mStopIntent = stopIntent; 113 114 mCountDownTimer = new CountDownTimer(ms, interval) { 115 @Override 116 public void onTick(long millisUntilFinished) { 117 for (RecordingStateChangeCallback cb : mListeners) { 118 cb.onCountdown(millisUntilFinished); 119 } 120 } 121 122 @Override 123 public void onFinish() { 124 mIsStarting = false; 125 mIsRecording = true; 126 for (RecordingStateChangeCallback cb : mListeners) { 127 cb.onCountdownEnd(); 128 } 129 try { 130 startIntent.send(); 131 IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_SWITCHED); 132 mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userFilter, null, 133 UserHandle.ALL); 134 135 IntentFilter stateFilter = new IntentFilter(INTENT_UPDATE_STATE); 136 mBroadcastDispatcher.registerReceiver(mStateChangeReceiver, stateFilter, null, 137 UserHandle.ALL); 138 Log.d(TAG, "sent start intent"); 139 } catch (PendingIntent.CanceledException e) { 140 Log.e(TAG, "Pending intent was cancelled: " + e.getMessage()); 141 } 142 } 143 }; 144 145 mCountDownTimer.start(); 146 } 147 148 /** 149 * Cancel a countdown in progress. This will not stop the recording if it already started. 150 */ cancelCountdown()151 public void cancelCountdown() { 152 if (mCountDownTimer != null) { 153 mCountDownTimer.cancel(); 154 } else { 155 Log.e(TAG, "Timer was null"); 156 } 157 mIsStarting = false; 158 159 for (RecordingStateChangeCallback cb : mListeners) { 160 cb.onCountdownEnd(); 161 } 162 } 163 164 /** 165 * Check if the recording is currently counting down to begin 166 * @return 167 */ isStarting()168 public boolean isStarting() { 169 return mIsStarting; 170 } 171 172 /** 173 * Check if the recording is ongoing 174 * @return 175 */ isRecording()176 public synchronized boolean isRecording() { 177 return mIsRecording; 178 } 179 180 /** 181 * Stop the recording 182 */ stopRecording()183 public void stopRecording() { 184 try { 185 if (mStopIntent != null) { 186 mStopIntent.send(); 187 } else { 188 Log.e(TAG, "Stop intent was null"); 189 } 190 updateState(false); 191 } catch (PendingIntent.CanceledException e) { 192 Log.e(TAG, "Error stopping: " + e.getMessage()); 193 } 194 } 195 196 /** 197 * Update the current status 198 * @param isRecording 199 */ updateState(boolean isRecording)200 public synchronized void updateState(boolean isRecording) { 201 if (!isRecording && mIsRecording) { 202 // Unregister receivers if we have stopped recording 203 mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); 204 mBroadcastDispatcher.unregisterReceiver(mStateChangeReceiver); 205 } 206 mIsRecording = isRecording; 207 for (RecordingStateChangeCallback cb : mListeners) { 208 if (isRecording) { 209 cb.onRecordingStart(); 210 } else { 211 cb.onRecordingEnd(); 212 } 213 } 214 } 215 216 @Override addCallback(@onNull RecordingStateChangeCallback listener)217 public void addCallback(@NonNull RecordingStateChangeCallback listener) { 218 mListeners.add(listener); 219 } 220 221 @Override removeCallback(@onNull RecordingStateChangeCallback listener)222 public void removeCallback(@NonNull RecordingStateChangeCallback listener) { 223 mListeners.remove(listener); 224 } 225 226 /** 227 * A callback for changes in the screen recording state 228 */ 229 public interface RecordingStateChangeCallback { 230 /** 231 * Called when a countdown to recording has updated 232 * 233 * @param millisUntilFinished Time in ms remaining in the countdown 234 */ onCountdown(long millisUntilFinished)235 default void onCountdown(long millisUntilFinished) {} 236 237 /** 238 * Called when a countdown to recording has ended. This is a separate method so that if 239 * needed, listeners can handle cases where recording fails to start 240 */ onCountdownEnd()241 default void onCountdownEnd() {} 242 243 /** 244 * Called when a screen recording has started 245 */ onRecordingStart()246 default void onRecordingStart() {} 247 248 /** 249 * Called when a screen recording has ended 250 */ onRecordingEnd()251 default void onRecordingEnd() {} 252 } 253 } 254