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