1 /*
2  * Copyright (C) 2024 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 android.server.wm.window;
18 
19 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
20 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
21 import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER;
22 import static android.view.WindowManager.SCREEN_RECORDING_STATE_NOT_VISIBLE;
23 import static android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE;
24 
25 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
26 
27 import static org.junit.Assert.assertEquals;
28 import static org.junit.Assert.assertTrue;
29 import static org.junit.Assume.assumeFalse;
30 
31 import android.app.ActivityOptions;
32 import android.app.ActivityOptions.LaunchCookie;
33 import android.content.ComponentName;
34 import android.content.Intent;
35 import android.media.projection.MediaProjection;
36 import android.platform.test.annotations.RequiresFlagsEnabled;
37 import android.platform.test.flag.junit.CheckFlagsRule;
38 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
39 import android.server.wm.MediaProjectionHelper;
40 import android.server.wm.WindowManagerTestBase;
41 import android.view.WindowManager;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 
46 import com.android.compatibility.common.util.ApiTest;
47 import com.android.compatibility.common.util.SystemUtil;
48 import com.android.window.flags.Flags;
49 
50 import org.junit.After;
51 import org.junit.Before;
52 import org.junit.Rule;
53 import org.junit.Test;
54 
55 import java.util.Objects;
56 import java.util.concurrent.BlockingQueue;
57 import java.util.concurrent.LinkedBlockingDeque;
58 import java.util.concurrent.TimeUnit;
59 import java.util.function.Consumer;
60 
61 /**
62  * CTS tests for {@link android.view.WindowManager#addScreenRecordingCallback}.
63  *
64  * Media Projection set up is handled by {@link android.server.wm.MediaProjectionHelper}. The
65  * helper handles Media Projection authorization and foreground service requirements. For each test,
66  * a new instance of MediaProjection is obtained through the Intent flow and a new instance of the
67  * required foreground service is started.
68  */
69 public class ScreenRecordingCallbackTests extends WindowManagerTestBase {
70 
71     @Rule
72     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
73 
74     private static class TestableScreenRecordingCallback implements Consumer<Integer> {
75         private final BlockingQueue<Integer> mQueue = new LinkedBlockingDeque<>();
76 
77         @Override
accept(Integer state)78         public void accept(Integer state) {
79             mQueue.add(state);
80         }
81 
getState()82         int getState() throws InterruptedException {
83             Integer value = mQueue.poll(5L * HW_TIMEOUT_MULTIPLIER, TimeUnit.SECONDS);
84             if (value == null) {
85                 throw new AssertionError("Callback not called");
86             }
87             return value;
88         }
89 
queueEmpty()90         boolean queueEmpty() {
91             return mQueue.isEmpty();
92         }
93     }
94 
95     public static class ScreenRecordingCallbackActivity extends FocusableActivity {
96 
97         final TestableScreenRecordingCallback mCallback = new TestableScreenRecordingCallback();
98 
99         @Override
onStart()100         public void onStart() {
101             super.onStart();
102             int initialState = getWindowManager().addScreenRecordingCallback(getMainExecutor(),
103                     mCallback);
104             mCallback.accept(initialState);
105         }
106 
107         @Override
onStop()108         public void onStop() {
109             super.onStop();
110             getWindowManager().removeScreenRecordingCallback(mCallback);
111         }
112     }
113 
114     private MediaProjectionHelper mMediaProjectionHelper = new MediaProjectionHelper();
115     private MediaProjection mMediaProjection = null;
116 
117     @Before
checkAssumptions()118     public void checkAssumptions() {
119         assumeFalse(isCar());
120     }
121 
122     @After
stopMediaProjection()123     public void stopMediaProjection() {
124         if (mMediaProjection != null) {
125             mMediaProjection.stop();
126         }
127     }
128 
startCallbackActivityWithLaunchCookie( @onNull LaunchCookie launchCookie)129     private ScreenRecordingCallbackActivity startCallbackActivityWithLaunchCookie(
130             @NonNull LaunchCookie launchCookie) {
131         Intent intent = new Intent(getInstrumentation().getTargetContext(),
132                 ScreenRecordingCallbackActivity.class).addFlags(FLAG_ACTIVITY_NEW_TASK);
133 
134         ActivityOptions activityOptions = ActivityOptions.makeBasic();
135         activityOptions.setLaunchCookie(launchCookie);
136         ScreenRecordingCallbackActivity[] activity = new ScreenRecordingCallbackActivity[1];
137         SystemUtil.runWithShellPermissionIdentity(() -> activity[0] =
138                 (ScreenRecordingCallbackActivity) getInstrumentation().startActivitySync(intent,
139                         activityOptions.toBundle()));
140         return activity[0];
141     }
142 
resumeCallbackActivity()143     private void resumeCallbackActivity() {
144         Intent intent = new Intent(mContext, ScreenRecordingCallbackActivity.class);
145         intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_SINGLE_TOP);
146         mContext.startActivity(intent);
147     }
148 
startExternalActivity(@ullable LaunchCookie launchCookie)149     private void startExternalActivity(@Nullable LaunchCookie launchCookie) {
150         ComponentName componentName = ComponentName.createRelative("android.server.wm",
151                 ".ExternalActivity");
152 
153         Intent intent = new Intent();
154         intent.setComponent(componentName);
155         intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
156 
157         ActivityOptions activityOptions = ActivityOptions.makeBasic();
158         if (launchCookie != null) {
159             activityOptions.setLaunchCookie(launchCookie);
160         }
161         mContext.startActivity(intent, activityOptions.toBundle());
162     }
163 
164     /**
165      * Test the screen recording callback is called correctly for an activity that starts before
166      * MediaProjection starts.
167      */
168     @RequiresFlagsEnabled(Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
169     @ApiTest(apis = {"android.view.WindowManager#addScreenRecordingCallback",
170             "android.view.WindowManager#removeScreenRecordingCallback"})
171     @Test
testFullDisplayMediaProjectionStartsAfterActivity()172     public void testFullDisplayMediaProjectionStartsAfterActivity() throws InterruptedException {
173         mMediaProjectionHelper.authorizeMediaProjection();
174         ScreenRecordingCallbackActivity activity = startActivity(
175                 ScreenRecordingCallbackActivity.class);
176 
177         assertEquals("Expected app to not be visible in screen recording before media projection"
178                 + " starts", SCREEN_RECORDING_STATE_NOT_VISIBLE, activity.mCallback.getState());
179 
180         mMediaProjection = mMediaProjectionHelper.startMediaProjection();
181         assertEquals("Expected app to be visible in screen recording after starting media"
182                 + " projection", SCREEN_RECORDING_STATE_VISIBLE, activity.mCallback.getState());
183 
184         mMediaProjection.stop();
185         assertEquals("Expected app to no longer be visible in screen recording after stopping media"
186                 + " projection", SCREEN_RECORDING_STATE_NOT_VISIBLE, activity.mCallback.getState());
187 
188         assertTrue(activity.mCallback.queueEmpty());
189     }
190 
191     /**
192      * Test the screen recording callback is called correctly for an activity that starts after
193      * MediaProjection starts.
194      */
195     @RequiresFlagsEnabled(Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
196     @ApiTest(apis = {"android.view.WindowManager#addScreenRecordingCallback",
197             "android.view.WindowManager#removeScreenRecordingCallback"})
198     @Test
testFullDisplayMediaProjectionStartsBeforeActivity()199     public void testFullDisplayMediaProjectionStartsBeforeActivity() throws InterruptedException {
200         mMediaProjectionHelper.authorizeMediaProjection();
201         mMediaProjection = mMediaProjectionHelper.startMediaProjection();
202 
203         ScreenRecordingCallbackActivity activity = startActivity(
204                 ScreenRecordingCallbackActivity.class);
205         assertEquals("Expected app to be visible in screen recording when the app starts",
206                 SCREEN_RECORDING_STATE_VISIBLE, activity.mCallback.getState());
207 
208         mMediaProjection.stop();
209         assertEquals("Expected app to no longer be visible in screen recording after stopping media"
210                 + " projection", SCREEN_RECORDING_STATE_NOT_VISIBLE, activity.mCallback.getState());
211 
212         assertTrue(activity.mCallback.queueEmpty());
213     }
214 
215     /**
216      * Test the screen recording callback is called correctly when it is not added/removed during an
217      * Activity's onStart/onStop methods.
218      */
219     @RequiresFlagsEnabled(Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
220     @ApiTest(apis = {"android.view.WindowManager#addScreenRecordingCallback",
221             "android.view.WindowManager#removeScreenRecordingCallback"})
222     @Test
testFullDisplayMediaProjectionAppCallback()223     public void testFullDisplayMediaProjectionAppCallback() throws InterruptedException {
224         mMediaProjectionHelper.authorizeMediaProjection();
225         mMediaProjection = mMediaProjectionHelper.startMediaProjection();
226 
227         TestableScreenRecordingCallback callback = new TestableScreenRecordingCallback();
228         WindowManager windowManager = mTargetContext.getSystemService(WindowManager.class);
229         int initialState = windowManager.addScreenRecordingCallback(
230                 mTargetContext.getMainExecutor(), callback);
231         assertEquals("Expected app to not be visible in screen recording before activity starts",
232                 SCREEN_RECORDING_STATE_NOT_VISIBLE, initialState);
233 
234         startActivity(ScreenRecordingCallbackActivity.class);
235         assertEquals("Expected app to be visible in screen recording after activity starts",
236                 SCREEN_RECORDING_STATE_VISIBLE, callback.getState());
237 
238         mMediaProjection.stop();
239         assertEquals(
240                 "Expected app to not be visible in screen recording after media projection stops",
241                 SCREEN_RECORDING_STATE_NOT_VISIBLE, callback.getState());
242 
243         assertTrue(callback.queueEmpty());
244     }
245 
246     /**
247      * Test the screen recording callback is called correctly when media projection records a
248      * specific task.
249      */
250     @RequiresFlagsEnabled(Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
251     @ApiTest(apis = {"android.view.WindowManager#addScreenRecordingCallback",
252             "android.view.WindowManager#removeScreenRecordingCallback"})
253     @Test
testPartialScreenSharingRecorded()254     public void testPartialScreenSharingRecorded() throws InterruptedException {
255         // The LaunchCookie is used to test partial screen sharing. In the typical Media
256         // Projection flow, when a user selects partial screen sharing, their selected app is
257         // launched into a new task with a specific launch cookie. Media Projection then records
258         // the window container corresponding to the task with that launch cookie. In this test,
259         // we specify the launch cookie to record and then directly start an activity with that
260         // launch cookie.
261         ActivityOptions.LaunchCookie launchCookie = new ActivityOptions.LaunchCookie();
262         mMediaProjectionHelper.authorizeMediaProjection(launchCookie);
263         startCallbackActivityWithLaunchCookie(launchCookie);
264 
265         TestableScreenRecordingCallback callback = new TestableScreenRecordingCallback();
266         WindowManager windowManager = mTargetContext.getSystemService(WindowManager.class);
267         Objects.requireNonNull(windowManager);
268         int initialState = windowManager.addScreenRecordingCallback(
269                 mTargetContext.getMainExecutor(), callback);
270         assertEquals("Expected app to not be visible in screen recording before activity starts",
271                 SCREEN_RECORDING_STATE_NOT_VISIBLE, initialState);
272 
273         mMediaProjection = mMediaProjectionHelper.startMediaProjection();
274         assertEquals("Expected app to be visible in screen recording after starting media"
275                 + " projection", SCREEN_RECORDING_STATE_VISIBLE, callback.getState());
276 
277         startExternalActivity(/*launchCookie=*/ null);
278 
279         assertEquals(
280                 "Expected app to not be visible in screen recording when another activity starts",
281                 SCREEN_RECORDING_STATE_NOT_VISIBLE, callback.getState());
282 
283         resumeCallbackActivity();
284         assertEquals("Expected app to be visible in screen recording when activity resumed",
285                 SCREEN_RECORDING_STATE_VISIBLE, callback.getState());
286 
287         mMediaProjection.stop();
288         assertEquals("Expected app to not be visible in screen recording after media projection"
289                 + " stops", SCREEN_RECORDING_STATE_NOT_VISIBLE, callback.getState());
290 
291         assertTrue(callback.queueEmpty());
292     }
293 
294     /**
295      * Test the screen recording callback is not called when media projection is recording a
296      * different task.
297      */
298     @RequiresFlagsEnabled(Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
299     @ApiTest(apis = {"android.view.WindowManager#addScreenRecordingCallback",
300             "android.view.WindowManager#removeScreenRecordingCallback"})
301     @Test
testPartialScreenSharingNotRecorded()302     public void testPartialScreenSharingNotRecorded() throws InterruptedException {
303         assumeFalse(isWatch());
304 
305         // The LaunchCookie is used to test partial screen sharing. In the typical Media
306         // Projection flow, when a user selects partial screen sharing, their selected app is
307         // launched into a new task with a specific launch cookie. Media Projection then records
308         // the window container corresponding to the task with that launch cookie. In this test,
309         // we specify the launch cookie to record and then directly start an activity with that
310         // launch cookie.
311         LaunchCookie launchCookie = new LaunchCookie();
312         mMediaProjectionHelper.authorizeMediaProjection(launchCookie);
313         startExternalActivity(launchCookie);
314         mMediaProjection = mMediaProjectionHelper.startMediaProjection();
315 
316         ScreenRecordingCallbackActivity activity = startActivity(
317                 ScreenRecordingCallbackActivity.class);
318         assertEquals(
319                 "Expected app to not be visible in screen recording when recording a different "
320                         + "task", SCREEN_RECORDING_STATE_NOT_VISIBLE,
321                 activity.mCallback.getState());
322 
323         mMediaProjection.stop();
324 
325         assertTrue(activity.mCallback.queueEmpty());
326     }
327 }
328