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