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