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