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