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