xref: /aosp_15_r20/cts/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils2.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 package com.android.compatibility.common.util;
2 /*
3  * Copyright (C) 2022 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 
19 import static org.junit.Assert.assertNotNull;
20 
21 import android.graphics.Rect;
22 import android.os.Build;
23 import android.util.Log;
24 import android.util.TypedValue;
25 
26 import androidx.test.core.app.ApplicationProvider;
27 import androidx.test.platform.app.InstrumentationRegistry;
28 import androidx.test.uiautomator.By;
29 import androidx.test.uiautomator.BySelector;
30 import androidx.test.uiautomator.Direction;
31 import androidx.test.uiautomator.StaleObjectException;
32 import androidx.test.uiautomator.UiDevice;
33 import androidx.test.uiautomator.UiObject2;
34 import androidx.test.uiautomator.UiObjectNotFoundException;
35 import androidx.test.uiautomator.UiScrollable;
36 import androidx.test.uiautomator.UiSelector;
37 import androidx.test.uiautomator.Until;
38 
39 import java.util.List;
40 import java.util.Objects;
41 import java.util.regex.Pattern;
42 
43 public class UiAutomatorUtils2 {
UiAutomatorUtils2()44     private UiAutomatorUtils2() {}
45 
46     private static final String LOG_TAG = "UiAutomatorUtils";
47 
48     /** Default swipe deadzone percentage. See {@link UiScrollable}. */
49     private static final double DEFAULT_SWIPE_DEADZONE_PCT_TV       = 0.1f;
50     private static final double DEFAULT_SWIPE_DEADZONE_PCT_ALL      = 0.25f;
51     /**
52      * On Wear, some cts tests like CtsPermissionUiTestCases that run on
53      * low performance device. Keep 0.05 to have better matching.
54      */
55     private static final double DEFAULT_SWIPE_DEADZONE_PCT_WEAR     = 0.05f;
56 
57     /** Minimum view height accepted (before needing to scroll more). */
58     private static final float MIN_VIEW_HEIGHT_DP = 8;
59 
60     private static Pattern sCollapsingToolbarResPattern =
61             Pattern.compile(".*:id/collapsing_toolbar");
62 
63     private static final UserHelper USER_HELPER = new UserHelper();
64 
getUiDevice()65     public static UiDevice getUiDevice() {
66         return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
67     }
68 
convertDpToPx(float dp)69     private static int convertDpToPx(float dp) {
70         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
71                 ApplicationProvider.getApplicationContext().getResources().getDisplayMetrics()));
72     }
73 
getSwipeDeadZonePct()74     private static double getSwipeDeadZonePct() {
75         if (FeatureUtil.isTV()) {
76             return DEFAULT_SWIPE_DEADZONE_PCT_TV;
77         } else if (FeatureUtil.isWatch()) {
78             return DEFAULT_SWIPE_DEADZONE_PCT_WEAR;
79         } else {
80             return DEFAULT_SWIPE_DEADZONE_PCT_ALL;
81         }
82     }
83 
waitUntilObjectGone(BySelector selector)84     public static void waitUntilObjectGone(BySelector selector) {
85         waitUntilObjectGone(selector, 20_000);
86     }
87 
waitUntilObjectGone(BySelector selector, long timeoutMs)88     public static void waitUntilObjectGone(BySelector selector, long timeoutMs) {
89         try {
90             if (getUiDevice().wait(Until.gone(selector), timeoutMs)) {
91                 return;
92             }
93         } catch (StaleObjectException exception) {
94             // UiDevice.wait() may cause StaleObjectException if the {@link View} attached to
95             // UiObject2 is no longer in the view tree.
96             return;
97         }
98 
99         throw new RuntimeException("view " + selector + " is still visible after " + timeoutMs
100                 + "ms");
101     }
102 
103     // Will wrap any asserting exceptions thrown by the parameter with a UI dump
assertWithUiDump(ThrowingRunnable assertion)104     public static void assertWithUiDump(ThrowingRunnable assertion) {
105         ExceptionUtils.wrappingExceptions(UiDumpUtils::wrapWithUiDump, assertion);
106     }
107 
waitFindObject(BySelector selector)108     public static UiObject2 waitFindObject(BySelector selector) throws UiObjectNotFoundException {
109         return waitFindObject(selector, 20_000);
110     }
111 
waitFindObject(BySelector selector, long timeoutMs)112     public static UiObject2 waitFindObject(BySelector selector, long timeoutMs)
113             throws UiObjectNotFoundException {
114         final UiObject2 view = waitFindObjectOrNull(selector, timeoutMs);
115         ExceptionUtils.wrappingExceptions(UiDumpUtils::wrapWithUiDump, () -> {
116             assertNotNull("View not found after waiting for " + timeoutMs + "ms: " + selector,
117                     view);
118         });
119         return view;
120     }
121 
waitFindObjectOrNull(BySelector selector)122     public static UiObject2 waitFindObjectOrNull(BySelector selector)
123             throws UiObjectNotFoundException {
124         return waitFindObjectOrNull(selector, 20_000);
125     }
126 
waitFindObjectOrNull(BySelector selector, long timeoutMs)127     public static UiObject2 waitFindObjectOrNull(BySelector selector, long timeoutMs)
128             throws UiObjectNotFoundException {
129         // If the target user is a visible background user, find the object on the main display
130         // assigned to the user. This is because UiScrollable does not support multi-display,
131         // so any scroll actions from UiScrollable will be performed on the default display,
132         // regardless of which display the test is running on.
133         // This is specifically for the tests that support secondary_user_on_secondary_display.
134         if (USER_HELPER.isVisibleBackgroundUser()) {
135             return waitFindObjectOrNullOnDisplay(
136                     selector, timeoutMs, USER_HELPER.getMainDisplayId());
137         }
138         UiObject2 view = null;
139         long start = System.currentTimeMillis();
140 
141         boolean isAtEnd = false;
142         boolean wasScrolledUpAlready = false;
143         boolean scrolledPastCollapsibleToolbar = false;
144 
145         final int minViewHeightPx = convertDpToPx(MIN_VIEW_HEIGHT_DP);
146 
147         int viewHeight = -1;
148         while (view == null && start + timeoutMs > System.currentTimeMillis()) {
149             try {
150                 view = getUiDevice().wait(Until.findObject(selector), 1000);
151                 if (view != null) {
152                     viewHeight = view.getVisibleBounds().height();
153                 }
154             } catch (StaleObjectException exception) {
155                 // UiDevice.wait() or view.getVisibleBounds() may cause StaleObjectException if
156                 // the {@link View} attached to UiObject2 is no longer in the view tree.
157                 Log.v(LOG_TAG, "UiObject2 view is no longer in the view tree.", exception);
158                 view = null;
159                 getUiDevice().waitForIdle();
160                 continue;
161             }
162 
163             if (view == null || viewHeight < minViewHeightPx) {
164                 final double deadZone = getSwipeDeadZonePct();
165                 UiScrollable scrollable = new UiScrollable(new UiSelector().scrollable(true));
166                 scrollable.setSwipeDeadZonePercentage(deadZone);
167                 if (scrollable.exists()) {
168                     if (!scrolledPastCollapsibleToolbar) {
169                         scrollPastCollapsibleToolbar(scrollable, deadZone);
170                         scrolledPastCollapsibleToolbar = true;
171                         continue;
172                     }
173                     if (isAtEnd) {
174                         if (wasScrolledUpAlready) {
175                             return null;
176                         }
177                         scrollable.scrollToBeginning(Integer.MAX_VALUE);
178                         isAtEnd = false;
179                         wasScrolledUpAlready = true;
180                         scrolledPastCollapsibleToolbar = false;
181                     } else {
182                         Rect boundsBeforeScroll = scrollable.getBounds();
183                         boolean scrollAtStartOrEnd;
184                         boolean isWearCompose = FeatureUtil.isWatch() && Objects.equals(
185                                 scrollable.getPackageName(),
186                                 InstrumentationRegistry.getInstrumentation().getContext()
187                                         .getPackageManager().getPermissionControllerPackageName());
188                         if (isWearCompose) {
189                             // TODO(b/306483780): Removed the condition once the scrollForward is
190                             //  fixed.
191                             if (!wasScrolledUpAlready) {
192                                 // TODO(b/306483780): scrollForward() always returns false. Thus
193                                 // `isAtEnd` will never be false for Wear Compose, because
194                                 // `scrollAtStartOrEnd` is set to false, and the value of `isAtEnd`
195                                 // is an && combination of that value. To avoid skipping Views
196                                 // that exist above the start-point of the search, we will first
197                                 // scroll up before doing a downward search and scroll.
198                                 scrollable.scrollToBeginning(Integer.MAX_VALUE);
199                                 wasScrolledUpAlready = true;
200                                 continue;
201                             }
202                             scrollable.scrollForward();
203                             scrollAtStartOrEnd = false;
204                         } else {
205                             scrollAtStartOrEnd = !scrollable.scrollForward();
206                         }
207                         // The scrollable view may no longer be scrollable after the toolbar is
208                         // collapsed.
209                         if (scrollable.exists()) {
210                             Rect boundsAfterScroll = scrollable.getBounds();
211                             isAtEnd = scrollAtStartOrEnd && boundsBeforeScroll.equals(
212                                     boundsAfterScroll);
213                         } else {
214                             isAtEnd = scrollAtStartOrEnd;
215                         }
216                     }
217                 } else {
218                     // There might be a collapsing toolbar, but no scrollable view. Try to collapse
219                     scrollPastCollapsibleToolbar(null, deadZone);
220                 }
221             }
222         }
223         return view;
224     }
225 
226     /**
227      * Finds the object on the given display.
228      *
229      * @param selector The selector to match.
230      * @param timeoutMs The timeout in milliseconds.
231      * @param displayId The display to search on.
232      * @return The object that matches the selector, or null if not found.
233      */
waitFindObjectOrNullOnDisplay( BySelector selector, long timeoutMs, int displayId)234     public static UiObject2 waitFindObjectOrNullOnDisplay(
235             BySelector selector, long timeoutMs, int displayId) throws UiObjectNotFoundException {
236         // Only supported in API level 30 or higher versions.
237         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
238             return null;
239         }
240 
241         UiObject2 view = null;
242         long start = System.currentTimeMillis();
243 
244         while (view == null && start + timeoutMs > System.currentTimeMillis()) {
245             view = getUiDevice().wait(Until.findObject(selector), 1000);
246             if (view != null) {
247                 break;
248             }
249 
250             List<UiObject2> scrollableViews = getUiDevice().findObjects(
251                     By.displayId(displayId).scrollable(true));
252             if (scrollableViews != null && !scrollableViews.isEmpty()) {
253                 for (int i = 0; i < scrollableViews.size(); i++) {
254                     UiObject2 scrollableView = scrollableViews.get(i);
255                     // Swipe far away from the edges to avoid triggering navigation gestures
256                     scrollableView.setGestureMarginPercentage((float) getSwipeDeadZonePct());
257                     // Scroll from the top to the bottom until the view object is found.
258                     scrollableView.scroll(Direction.UP, 1.0f);
259                     scrollableView.scrollUntil(Direction.DOWN, Until.findObject(selector));
260                     view = getUiDevice().findObject(selector);
261                     if (view != null) {
262                         break;
263                     }
264                 }
265             } else {
266                 // There might be a collapsing toolbar, but no scrollable view. Try to collapse
267                 final double deadZone = getSwipeDeadZonePct();
268                 scrollPastCollapsibleToolbar(null, deadZone);
269             }
270         }
271         return view;
272     }
273 
scrollPastCollapsibleToolbar(UiScrollable scrollable, double deadZone)274     private static void scrollPastCollapsibleToolbar(UiScrollable scrollable, double deadZone)
275             throws UiObjectNotFoundException {
276         final UiObject2 collapsingToolbar = getUiDevice().findObject(
277                 By.res(sCollapsingToolbarResPattern));
278         if (collapsingToolbar == null) {
279             return;
280         }
281 
282         final int steps = 55; // == UiScrollable.SCROLL_STEPS
283         if (scrollable != null && scrollable.exists()) {
284             final Rect scrollableBounds = scrollable.getVisibleBounds();
285             final int distanceToSwipe = collapsingToolbar.getVisibleBounds().height() / 2;
286             getUiDevice().swipe(scrollableBounds.centerX(), scrollableBounds.centerY(),
287                     scrollableBounds.centerX(), scrollableBounds.centerY() - distanceToSwipe,
288                     steps);
289         } else {
290             // There might be a collapsing toolbar, but no scrollable view. Try to collapse
291             int maxY = getUiDevice().getDisplayHeight();
292             int minY = (int) (deadZone * maxY);
293             maxY -= minY;
294             getUiDevice().drag(0, maxY, 0, minY, steps);
295         }
296     }
297 }
298