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