1 /* 2 * Copyright (C) 2009 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 package android.media.cts; 17 18 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 19 20 import static org.junit.Assert.assertTrue; 21 22 import android.annotation.NonNull; 23 import android.app.Activity; 24 import android.app.ActivityOptions; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.res.Resources; 31 import android.media.projection.MediaProjection; 32 import android.media.projection.MediaProjectionConfig; 33 import android.media.projection.MediaProjectionManager; 34 import android.os.Bundle; 35 import android.util.Log; 36 import android.view.WindowManager; 37 38 import androidx.annotation.Nullable; 39 import androidx.test.uiautomator.By; 40 import androidx.test.uiautomator.BySelector; 41 import androidx.test.uiautomator.UiDevice; 42 import androidx.test.uiautomator.UiObject2; 43 import androidx.test.uiautomator.UiObjectNotFoundException; 44 import androidx.test.uiautomator.UiScrollable; 45 import androidx.test.uiautomator.UiSelector; 46 import androidx.test.uiautomator.Until; 47 48 import com.android.compatibility.common.util.UiAutomatorUtils2; 49 50 import java.util.concurrent.CountDownLatch; 51 import java.util.concurrent.TimeUnit; 52 import java.util.regex.Pattern; 53 54 // This is a partial copy of android.view.cts.surfacevalidator.CapturedActivity. 55 // Common code should be move in a shared library 56 57 /** Start this activity to retrieve a MediaProjection through waitForMediaProjection() */ 58 public class MediaProjectionActivity extends Activity { 59 private static final int PERMISSION_CODE = 1; 60 private static final int PERMISSION_DIALOG_WAIT_MS = 1000; 61 private static final String TAG = "MediaProjectionActivity"; 62 private static final String SYSTEM_UI_PACKAGE = "com.android.systemui"; 63 64 // Builds from 24Q3 and earlier will have screen_share_mode_spinner, while builds from 65 // 24Q4 onwards will have screen_share_mode_options, so need to check both options here 66 private static final String SCREEN_SHARE_OPTIONS_REGEX = 67 SYSTEM_UI_PACKAGE + ":id/screen_share_mode_(options|spinner)"; 68 69 // Extra used to specify a foreground service to use for the MediaProjection session. 70 // If unset MediaProjection will default to the LocalMediaProjectionService implementation. 71 public static final String EXTRA_FOREGROUND_SERVICE_CLASS = "extra_foreground_service_class"; 72 public static final String EXTRA_MP_CONFIG = "extra_mp_config"; 73 public static final String EXTRA_LAUNCH_COOKIE = "extra_launch_cookie"; 74 public static final String ACCEPT_RESOURCE_ID = "android:id/button1"; 75 public static final String CANCEL_RESOURCE_ID = "android:id/button2"; 76 public static final Pattern SCREEN_SHARE_OPTIONS_RES_PATTERN = 77 Pattern.compile(SCREEN_SHARE_OPTIONS_REGEX); 78 public static final String ENTIRE_SCREEN_STRING_RES_NAME = 79 "screen_share_permission_dialog_option_entire_screen"; 80 public static final String SINGLE_APP_STRING_RES_NAME = 81 "screen_share_permission_dialog_option_single_app"; 82 83 private boolean mHandleActivityResult = false; 84 85 private MediaProjectionManager mProjectionManager; 86 private MediaProjection mMediaProjection; 87 private CountDownLatch mCountDownLatch; 88 private boolean mProjectionServiceBound; 89 90 private int mResultCode; 91 private Intent mResultData; 92 93 @Override onCreate(Bundle savedInstanceState)94 protected void onCreate(Bundle savedInstanceState) { 95 super.onCreate(savedInstanceState); 96 // UI automator need the screen ON in dismissPermissionDialog() 97 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 98 mProjectionManager = getSystemService(MediaProjectionManager.class); 99 mCountDownLatch = new CountDownLatch(1); 100 101 startActivityForResult(createRequestIntent(), PERMISSION_CODE); 102 } 103 104 @Override onDestroy()105 protected void onDestroy() { 106 super.onDestroy(); 107 if (mProjectionServiceBound) { 108 mProjectionServiceBound = false; 109 } 110 } 111 getScreenCaptureIntent()112 protected Intent getScreenCaptureIntent() { 113 return mProjectionManager.createScreenCaptureIntent(); 114 } 115 createRequestIntent()116 private Intent createRequestIntent() { 117 if (getIntent().hasExtra(EXTRA_MP_CONFIG)) { 118 MediaProjectionConfig config = 119 getIntent().getParcelableExtra(EXTRA_MP_CONFIG, MediaProjectionConfig.class); 120 return mProjectionManager.createScreenCaptureIntent(config); 121 } 122 if (getIntent().hasExtra(EXTRA_LAUNCH_COOKIE)) { 123 ActivityOptions.LaunchCookie launchCookie = 124 getIntent() 125 .getParcelableExtra( 126 EXTRA_LAUNCH_COOKIE, ActivityOptions.LaunchCookie.class); 127 return mProjectionManager.createScreenCaptureIntent(launchCookie); 128 } 129 130 return mProjectionManager.createScreenCaptureIntent(); 131 } 132 133 /** 134 * Request to start a foreground service with type "mediaProjection", it's free to run in either 135 * the same process or a different process in the package; passing a messenger object to send 136 * signal back when the foreground service is up. 137 */ startMediaProjectionService()138 private void startMediaProjectionService() { 139 ForegroundServiceUtil.requestStartForegroundService( 140 this, getForegroundServiceComponentName(), this::createMediaProjection, null); 141 } 142 143 /** 144 * @return the Intent result from navigating the consent dialogs 145 */ getResultData()146 public Intent getResultData() { 147 return mResultData; 148 } 149 150 /** 151 * @return The component name of the foreground service for this test. 152 */ getForegroundServiceComponentName()153 private ComponentName getForegroundServiceComponentName() { 154 if (getIntent().hasExtra(EXTRA_FOREGROUND_SERVICE_CLASS)) { 155 String fgsClass = getIntent().getStringExtra(EXTRA_FOREGROUND_SERVICE_CLASS); 156 return new ComponentName(this, fgsClass); 157 } 158 159 return new ComponentName(this, android.media.cts.LocalMediaProjectionService.class); 160 } 161 162 @Override onActivityResult(int requestCode, int resultCode, Intent data)163 public void onActivityResult(int requestCode, int resultCode, Intent data) { 164 // Only handle onActivityResult if the caller actually tries to start 165 if (!mHandleActivityResult) { 166 return; 167 } 168 169 if (requestCode != PERMISSION_CODE) { 170 throw new IllegalStateException("Unknown request code: " + requestCode); 171 } 172 if (resultCode != RESULT_OK) { 173 throw new IllegalStateException("User denied screen sharing permission"); 174 } 175 Log.d(TAG, "onActivityResult"); 176 mResultCode = resultCode; 177 mResultData = data; 178 startMediaProjectionService(); 179 } 180 createMediaProjection()181 private void createMediaProjection() { 182 mMediaProjection = mProjectionManager.getMediaProjection(mResultCode, mResultData); 183 mCountDownLatch.countDown(); 184 } 185 186 /** Perform the steps required to pass the MediaProjection consent flow */ waitForMediaProjection()187 public MediaProjection waitForMediaProjection() throws InterruptedException { 188 mHandleActivityResult = true; 189 final long timeOutMs = 10000; 190 final int retryCount = 5; 191 int count = 0; 192 // Sometimes system decides to rotate the permission activity to another orientation 193 // right after showing it. This results in: uiautomation thinks that accept button appears, 194 // we successfully click it in terms of uiautomation, but nothing happens, 195 // because permission activity is already recreated. 196 // Thus, we try to click that button multiple times. 197 do { 198 assertTrue("Can't get the permission", count <= retryCount); 199 dismissPermissionDialog( 200 /* isWatch= */ getPackageManager() 201 .hasSystemFeature(PackageManager.FEATURE_WATCH), 202 getResourceString(this, ENTIRE_SCREEN_STRING_RES_NAME)); 203 count++; 204 } while (!mCountDownLatch.await(timeOutMs, TimeUnit.MILLISECONDS)); 205 return mMediaProjection; 206 } 207 208 /** The permission dialog will be auto-opened by the activity - find it and accept */ dismissPermissionDialog( boolean isWatch, @Nullable String entireScreenString)209 public static void dismissPermissionDialog( 210 boolean isWatch, @Nullable String entireScreenString) { 211 // Ensure the device is initialized before interacting with any UI elements. 212 UiDevice.getInstance(getInstrumentation()); 213 if (entireScreenString != null && !isWatch) { 214 // if not testing on a watch device, then we need to select the entire screen option 215 // before pressing "Start recording" button. This is because single app capture is 216 // not supported on watches. 217 if (!selectEntireScreenOption(entireScreenString)) { 218 Log.e(TAG, "Couldn't select entire screen option"); 219 } 220 } 221 pressStartRecording(); 222 } 223 224 @Nullable findUiObject(BySelector selector, UiSelector uiSelector)225 private static UiObject2 findUiObject(BySelector selector, UiSelector uiSelector) { 226 // Check if the View can be found on the current screen. 227 UiObject2 obj = waitForObject(selector); 228 229 // If the View is not found on the current screen. Try scrolling around to find it. 230 if (obj == null) { 231 Log.w(TAG, "Couldn't find " + selector + ", now scrolling to it."); 232 scrollToGivenResource(uiSelector); 233 obj = waitForObject(selector); 234 } 235 if (obj == null) { 236 Log.w(TAG, "Still couldn't find " + selector + ", now scrolling screen height."); 237 try { 238 obj = UiAutomatorUtils2.waitFindObjectOrNull(selector); 239 } catch (UiObjectNotFoundException e) { 240 Log.e(TAG, "Error in looking for " + selector, e); 241 } 242 } 243 244 if (obj == null) { 245 Log.e(TAG, "Unable to find " + selector); 246 } 247 248 return obj; 249 } 250 selectEntireScreenOption(String entireScreenString)251 private static boolean selectEntireScreenOption(String entireScreenString) { 252 UiObject2 optionSelector = 253 findUiObject( 254 By.res(SCREEN_SHARE_OPTIONS_RES_PATTERN), 255 new UiSelector().resourceIdMatches(SCREEN_SHARE_OPTIONS_REGEX)); 256 if (optionSelector == null) { 257 Log.e( 258 TAG, 259 "Couldn't find option selector to select projection mode, " 260 + "even after scrolling"); 261 return false; 262 } 263 optionSelector.click(); 264 265 UiDevice.getInstance(getInstrumentation()) 266 .waitForWindowUpdate(null, PERMISSION_DIALOG_WAIT_MS); 267 UiObject2 entireScreenOption = waitForObject(By.text(entireScreenString)); 268 if (entireScreenOption == null) { 269 Log.e(TAG, "Couldn't find entire screen option"); 270 return false; 271 } 272 entireScreenOption.click(); 273 return true; 274 } 275 276 /** Returns the string for the drop down option to capture the entire screen. */ 277 @Nullable getResourceString(@onNull Context context, String resName)278 public static String getResourceString(@NonNull Context context, String resName) { 279 Resources sysUiResources; 280 try { 281 sysUiResources = 282 context.getPackageManager().getResourcesForApplication(SYSTEM_UI_PACKAGE); 283 } catch (NameNotFoundException e) { 284 return null; 285 } 286 int resourceId = 287 sysUiResources.getIdentifier(resName, /* defType= */ "string", SYSTEM_UI_PACKAGE); 288 if (resourceId == 0) { 289 // Resource id not found 290 return null; 291 } 292 return sysUiResources.getString(resourceId); 293 } 294 pressStartRecording()295 private static void pressStartRecording() { 296 // May need to scroll down to the start button on small screen devices. 297 UiObject2 startRecordingButton = 298 findUiObject( 299 By.res(ACCEPT_RESOURCE_ID), 300 new UiSelector().resourceId(ACCEPT_RESOURCE_ID)); 301 if (startRecordingButton != null) { 302 startRecordingButton.click(); 303 } 304 } 305 306 /** When testing on a small screen device, scrolls to a given UI element. */ scrollToGivenResource(UiSelector uiSelector)307 private static void scrollToGivenResource(UiSelector uiSelector) { 308 // Scroll down the dialog; on a device with a small screen the elements may not be visible. 309 final UiScrollable scrollable = new UiScrollable(new UiSelector().scrollable(true)); 310 try { 311 if (!scrollable.scrollIntoView(uiSelector)) { 312 Log.e(TAG, "Didn't find " + uiSelector + " when scrolling"); 313 return; 314 } 315 Log.d(TAG, "We finished scrolling down to the ui element " + uiSelector); 316 } catch (UiObjectNotFoundException e) { 317 Log.d(TAG, "There was no scrolling (UI may not be scrollable"); 318 } 319 } 320 waitForObject(BySelector selector)321 private static UiObject2 waitForObject(BySelector selector) { 322 UiDevice uiDevice = UiDevice.getInstance(getInstrumentation()); 323 return uiDevice.wait(Until.findObject(selector), PERMISSION_DIALOG_WAIT_MS); 324 } 325 326 @Override onResume()327 protected void onResume() { 328 Log.i(TAG, "onResume"); 329 super.onResume(); 330 } 331 332 @Override onPause()333 protected void onPause() { 334 Log.i(TAG, "onPause"); 335 super.onPause(); 336 } 337 } 338