1 /*
2  * Copyright 2023 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 androidx.camera.testing.impl;
18 
19 import static org.junit.Assume.assumeFalse;
20 import static org.junit.Assume.assumeTrue;
21 
22 import android.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.app.Instrumentation;
25 import android.app.KeyguardManager;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.hardware.camera2.CameraAccessException;
29 import android.hardware.camera2.CameraCharacteristics;
30 import android.os.Build;
31 import android.os.RemoteException;
32 import android.util.Log;
33 
34 import androidx.camera.core.Logger;
35 import androidx.camera.testing.impl.activity.ForegroundTestActivity;
36 import androidx.test.espresso.Espresso;
37 import androidx.test.espresso.IdlingRegistry;
38 import androidx.test.espresso.idling.CountingIdlingResource;
39 import androidx.test.platform.app.InstrumentationRegistry;
40 import androidx.test.uiautomator.UiDevice;
41 
42 import org.jspecify.annotations.NonNull;
43 import org.jspecify.annotations.Nullable;
44 import org.junit.AssumptionViolatedException;
45 
46 import java.io.IOException;
47 
48 /** Utility functions of tests on CoreTestApp. */
49 public final class CoreAppTestUtil {
50 
51     private static final String TAG = "CoreAppTestUtil";
52 
53     /** ADB shell input key code for dismissing keyguard for device with API level <= 22. */
54     private static final int DISMISS_LOCK_SCREEN_CODE = 82;
55     /** ADB shell command for dismissing keyguard for device with API level >= 23. */
56     private static final String ADB_SHELL_DISMISS_KEYGUARD_API23_AND_ABOVE = "wm dismiss-keyguard";
57     /** ADB shell command to set the screen always on when usb is connected. */
58     private static final String ADB_SHELL_SCREEN_ALWAYS_ON = "svc power stayon true";
59 
60     private static final int MAX_TIMEOUT_MS = 3000;
61 
CoreAppTestUtil()62     private CoreAppTestUtil() {
63     }
64 
65     /**
66      * Check if this is compatible device for test.
67      *
68      * <p> Most devices should be compatible except devices with compatible issues.
69      *
70      */
assumeCompatibleDevice()71     public static void assumeCompatibleDevice() {
72         // TODO(b/134894604) This will be removed once the issue is fixed.
73         if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
74                 && Build.MODEL.contains("Nexus 5")) {
75             throw new AssumptionViolatedException("Known issue, b/134894604.");
76         }
77 
78         assumeFalse("See b/152082918, Wembley Api30 has a libjpeg issue which causes"
79                         + " the test failure.",
80                 Build.MODEL.equalsIgnoreCase("wembley") && Build.VERSION.SDK_INT <= 30);
81     }
82 
83     /**
84      * Throws the Exception for the devices which is not compatible to the testing.
85      */
assumeCanTestCameraDisconnect()86     public static void assumeCanTestCameraDisconnect() {
87         // TODO(b/141656413) Remove this when the issue is fixed.
88         if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M
89                 && (Build.MODEL.contains("Nexus 5") || Build.MODEL.contains("Pixel C"))) {
90             throw new AssumptionViolatedException("Known issue, b/141656413.");
91         }
92     }
93 
94     /**
95      * Throws the Exception for devices whose front camera is not testable.
96      *
97      * Vivo 1805 will popup dialog to ask permission for accessing its front camera and then break
98      * the test. This function is used for switching camera tests that the tests should be
99      * skipped no matter the tests are started from the back or front camera.
100      */
assumeCanTestFrontCamera()101     public static void assumeCanTestFrontCamera() throws CameraAccessException {
102         if ("vivo 1805".equals(Build.MODEL)) {
103             throw new AssumptionViolatedException("Vivo 1805 will popup dialog to ask permission "
104                     + "to access the front camera.");
105         }
106     }
107 
108     /**
109      * Throws the Exception for devices which the specified camera id is front camera and is not
110      * testable.
111      *
112      * Vivo 1805 will popup dialog to ask permission for accessing its front camera and then break
113      * the test.
114      */
assumeNotUntestableFrontCamera(@onNull String cameraId)115     public static void assumeNotUntestableFrontCamera(@NonNull String cameraId)
116             throws CameraAccessException {
117         CameraCharacteristics characteristics =
118                 CameraUtil.getCameraManager().getCameraCharacteristics(cameraId);
119         if ("vivo 1805".equals(Build.MODEL) && characteristics.get(
120                 CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) {
121             throw new AssumptionViolatedException("Vivo 1805 will popup dialog to ask permission "
122                     + "to access the front camera.");
123         }
124     }
125 
126     /**
127      * Clean up the device UI and back to the home screen for test.
128      * @param instrumentation the instrumentation used to run the test
129      */
130     @SuppressLint("MissingPermission") // Permission needed for action_close_system_dialogs in S
131     @SuppressWarnings("deprecation")
clearDeviceUI(@onNull Instrumentation instrumentation)132     public static void clearDeviceUI(@NonNull Instrumentation instrumentation) {
133         UiDevice device = UiDevice.getInstance(instrumentation);
134         // On some devices, its necessary to wake up the device before attempting unlock, otherwise
135         // unlock attempt will not unlock.
136         try {
137             device.wakeUp();
138         } catch (RemoteException remoteException) {
139         }
140 
141         // In case the lock screen on top, the action to dismiss it.
142         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
143             device.pressKeyCode(DISMISS_LOCK_SCREEN_CODE);
144         } else {
145             try {
146                 device.executeShellCommand(ADB_SHELL_DISMISS_KEYGUARD_API23_AND_ABOVE);
147             } catch (IOException e) {
148             }
149         }
150 
151         try {
152             device.executeShellCommand(ADB_SHELL_SCREEN_ALWAYS_ON);
153         } catch (IOException e) {
154         }
155 
156         device.pressHome();
157         try {
158             device.waitForIdle(MAX_TIMEOUT_MS);
159         } catch (IllegalStateException e) {
160             Logger.d(TAG, "Fail to waitForIdle", e);
161         }
162         // Close system dialogs first to avoid interrupt.
163         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
164             instrumentation.getTargetContext().sendBroadcast(
165                     new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
166         }
167     }
168 
169     /**
170      * Checks whether keyguard is locked.
171      *
172      * Keyguard is locked if the screen is off or the device is currently locked and requires a
173      * PIN, pattern or password to unlock.
174      *
175      * @throws IllegalStateException if keyguard is locked.
176      */
checkKeyguard(@onNull Context context)177     public static void checkKeyguard(@NonNull Context context) {
178         KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(
179                 Context.KEYGUARD_SERVICE);
180 
181         if (keyguardManager != null && keyguardManager.isKeyguardLocked()) {
182             throw new IllegalStateException("<KEYGUARD_STATE_ERROR> Keyguard is locked!");
183         }
184     }
185 
186     /**
187      * Try to clear the UI and then check if there is any dialog or lock screen on the top of the
188      * window that might cause the activity related test fail.
189      *
190      * @param instrumentation The instrumentation instance.
191      * @throws ForegroundOccupiedError throw the exception when the test app cannot get
192      *                                 foreground of the device window in CameraX lab.
193      */
prepareDeviceUI(@onNull Instrumentation instrumentation)194     public static void prepareDeviceUI(@NonNull Instrumentation instrumentation)
195             throws ForegroundOccupiedError {
196         clearDeviceUI(instrumentation);
197 
198         ForegroundTestActivity activityRef = null;
199         try {
200             Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
201 
202             Intent startIntent = new Intent(Intent.ACTION_MAIN);
203             startIntent.setClassName(context.getPackageName(),
204                     ForegroundTestActivity.class.getName());
205             startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
206 
207             activityRef = ForegroundTestActivity.class.cast(
208                     instrumentation.startActivitySync(startIntent));
209             instrumentation.waitForIdleSync();
210 
211             if (activityRef == null) {
212                 Logger.d(TAG, String.format("Activity %s, failed to launch",
213                         startIntent.getComponent()) + ", ignore the foreground checking");
214                 return;
215             }
216             IdlingRegistry.getInstance().register(activityRef.getViewReadyIdlingResource());
217 
218             // The {@link Espresso#onIdle()} throws timeout exception if the
219             // ForegroundTestActivity cannot get focus. The default timeout in espresso is 26 sec.
220             Espresso.onIdle();
221             return;
222         } catch (Exception e) {
223             Logger.d(TAG, "Fail to get foreground", e);
224         } finally {
225             if (activityRef != null) {
226                 IdlingRegistry.getInstance().unregister(activityRef.getViewReadyIdlingResource());
227                 final Activity act = activityRef;
228                 instrumentation.runOnMainSync(() -> act.finish());
229                 instrumentation.waitForIdleSync();
230             }
231         }
232 
233         // Throw AssumptionViolatedException to skip the test if not in the CameraX lab
234         // environment. The loggable tag will be set when running the CameraX daily testing.
235         assumeTrue(Log.isLoggable("MH", Log.DEBUG));
236 
237         throw new ForegroundOccupiedError("CameraX_fail_to_start_foreground, model:" + Build.MODEL);
238     }
239 
240     /** The display foreground of the device is occupied that cannot execute UI related test. */
241     public static class ForegroundOccupiedError extends Exception {
ForegroundOccupiedError(@onNull String message)242         public ForegroundOccupiedError(@NonNull String message) {
243             super(message);
244         }
245     }
246 
247     /**
248      * Launch activity and return the activity instance for testing.
249      *
250      * @param instrumentation The instrumentation used to run the test
251      * @param activityClass   The activity under test. This must be a class in the instrumentation
252      *                        targetPackage specified in the AndroidManifest.xml
253      * @param startIntent     The Intent that will be used to start the Activity under test. If
254      *                        {@code startIntent} is null, a default launch Intent for the
255      *                        {@code activityClass} is used.
256      * @param <T>             The Activity class under test
257      * @return Returns the reference to the activity for test.
258      */
launchActivity( @onNull Instrumentation instrumentation, @NonNull Class<T> activityClass, @Nullable Intent startIntent)259     public static <T extends Activity> @Nullable T launchActivity(
260             @NonNull Instrumentation instrumentation, @NonNull Class<T> activityClass,
261             @Nullable Intent startIntent) {
262         Context context = instrumentation.getTargetContext();
263 
264         // inject custom intent, if provided
265         if (null == startIntent) {
266             startIntent = new Intent(Intent.ACTION_MAIN);
267         }
268 
269         // Set target component if not set Intent
270         if (null == startIntent.getComponent()) {
271             startIntent.setClassName(context.getPackageName(), activityClass.getName());
272         }
273 
274         // Set launch flags where if not set Intent
275         if (0 /* No flags set */ == startIntent.getFlags()) {
276             startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
277         }
278 
279         T activityRef = activityClass.cast(instrumentation.startActivitySync(startIntent));
280         instrumentation.waitForIdleSync();
281 
282         return activityRef;
283     }
284 
285     /**
286      * Launch a auto-closed {@link ForegroundTestActivity}.
287      *
288      * <p>The launched activity will make the callee activity enter PAUSE state and return to
289      * RESUME state after the activity is closed.
290      *
291      * <p>If the <code>countingIdlingResource</code> is specified, the activity will wait for it
292      * becoming idle to close the activity. This can be used to control the activity close speed to
293      * make it work more like the real user behavior.
294      */
launchAutoClosedForegroundActivity( @onNull Context context, @NonNull Instrumentation instrumentation, @Nullable CountingIdlingResource countingIdlingResource )295     public static void launchAutoClosedForegroundActivity(
296             @NonNull Context context,
297             @NonNull Instrumentation instrumentation,
298             @Nullable CountingIdlingResource countingIdlingResource
299     ) {
300         ForegroundTestActivity foregroundTestActivity = launchActivity(
301                 instrumentation,
302                 ForegroundTestActivity.class,
303                 new Intent(context, ForegroundTestActivity.class)
304         );
305         try {
306             if (countingIdlingResource != null) {
307                 IdlingRegistry.getInstance().register(countingIdlingResource);
308                 Espresso.onIdle();
309                 IdlingRegistry.getInstance().unregister(countingIdlingResource);
310             }
311         } finally {
312             foregroundTestActivity.finish();
313         }
314     }
315 }
316