• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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