• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = "UiAutomatorUtils2";
47 
48     // A flaky test can enable logging of ui dump when searching the item on screen.
49     public static final boolean DEBUG_UI_DUMP = false;
50 
51     /** Default swipe deadzone percentage. See {@link UiScrollable}. */
52     private static final double DEFAULT_SWIPE_DEADZONE_PCT_TV       = 0.1f;
53     private static final double DEFAULT_SWIPE_DEADZONE_PCT_ALL      = 0.25f;
54     /**
55      * On Wear, some cts tests like CtsPermissionUiTestCases that run on
56      * low performance device. Keep 0.05 to have better matching.
57      */
58     private static final double DEFAULT_SWIPE_DEADZONE_PCT_WEAR     = 0.05f;
59 
60     /** Minimum view height accepted (before needing to scroll more). */
61     private static final float MIN_VIEW_HEIGHT_DP = 8;
62 
63     private static Pattern sCollapsingToolbarResPattern =
64             Pattern.compile(".*:id/collapsing_toolbar");
65 
66     private static final UserHelper USER_HELPER = new UserHelper();
67 
getUiDevice()68     public static UiDevice getUiDevice() {
69         return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
70     }
71 
convertDpToPx(float dp)72     private static int convertDpToPx(float dp) {
73         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
74                 ApplicationProvider.getApplicationContext().getResources().getDisplayMetrics()));
75     }
76 
getSwipeDeadZonePct()77     private static double getSwipeDeadZonePct() {
78         if (FeatureUtil.isTV()) {
79             return DEFAULT_SWIPE_DEADZONE_PCT_TV;
80         } else if (FeatureUtil.isWatch()) {
81             return DEFAULT_SWIPE_DEADZONE_PCT_WEAR;
82         } else {
83             return DEFAULT_SWIPE_DEADZONE_PCT_ALL;
84         }
85     }
86 
waitUntilObjectGone(BySelector selector)87     public static void waitUntilObjectGone(BySelector selector) {
88         waitUntilObjectGone(selector, 20_000);
89     }
90 
waitUntilObjectGone(BySelector selector, long timeoutMs)91     public static void waitUntilObjectGone(BySelector selector, long timeoutMs) {
92         try {
93             if (getUiDevice().wait(Until.gone(selector), timeoutMs)) {
94                 return;
95             }
96         } catch (StaleObjectException exception) {
97             // UiDevice.wait() may cause StaleObjectException if the {@link View} attached to
98             // UiObject2 is no longer in the view tree.
99             return;
100         }
101 
102         throw new RuntimeException("view " + selector + " is still visible after " + timeoutMs
103                 + "ms");
104     }
105 
106     // Will wrap any asserting exceptions thrown by the parameter with a UI dump
assertWithUiDump(ThrowingRunnable assertion)107     public static void assertWithUiDump(ThrowingRunnable assertion) {
108         ExceptionUtils.wrappingExceptions(UiDumpUtils::wrapWithUiDump, assertion);
109     }
110 
waitFindObject(BySelector selector)111     public static UiObject2 waitFindObject(BySelector selector) throws UiObjectNotFoundException {
112         return waitFindObject(selector, 20_000);
113     }
114 
waitFindObject(BySelector selector, long timeoutMs)115     public static UiObject2 waitFindObject(BySelector selector, long timeoutMs)
116             throws UiObjectNotFoundException {
117         final UiObject2 view = waitFindObjectOrNull(selector, timeoutMs);
118         ExceptionUtils.wrappingExceptions(UiDumpUtils::wrapWithUiDump, () -> {
119             assertNotNull("View not found after waiting for " + timeoutMs + "ms: " + selector,
120                     view);
121         });
122         return view;
123     }
124 
waitFindObjectOrNull(BySelector selector)125     public static UiObject2 waitFindObjectOrNull(BySelector selector)
126             throws UiObjectNotFoundException {
127         return waitFindObjectOrNull(selector, 20_000);
128     }
129 
waitFindObjectOrNull(BySelector selector, long timeoutMs)130     public static UiObject2 waitFindObjectOrNull(BySelector selector, long timeoutMs)
131             throws UiObjectNotFoundException {
132         // If the target user is a visible background user, find the object on the main display
133         // assigned to the user. This is because UiScrollable does not support multi-display,
134         // so any scroll actions from UiScrollable will be performed on the default display,
135         // regardless of which display the test is running on.
136         // This is specifically for the tests that support secondary_user_on_secondary_display.
137         if (USER_HELPER.isVisibleBackgroundUser()) {
138             return waitFindObjectOrNullOnDisplay(
139                     selector, timeoutMs, USER_HELPER.getMainDisplayId());
140         }
141         UiObject2 view = null;
142         long start = System.currentTimeMillis();
143 
144         boolean isAtEnd = false;
145         boolean wasScrolledUpAlready = false;
146         boolean scrolledPastCollapsibleToolbar = false;
147 
148         final int minViewHeightPx = convertDpToPx(MIN_VIEW_HEIGHT_DP);
149 
150         int viewHeight = -1;
151         while (view == null && start + timeoutMs > System.currentTimeMillis()) {
152             try {
153                 view = getUiDevice().wait(Until.findObject(selector), 1000);
154                 if (view != null) {
155                     viewHeight = view.getVisibleBounds().height();
156                 }
157             } catch (StaleObjectException exception) {
158                 // UiDevice.wait() or view.getVisibleBounds() may cause StaleObjectException if
159                 // the {@link View} attached to UiObject2 is no longer in the view tree.
160                 Log.v(LOG_TAG, "UiObject2 view is no longer in the view tree.", exception);
161                 view = null;
162                 getUiDevice().waitForIdle();
163                 continue;
164             }
165             if (DEBUG_UI_DUMP) {
166                 StringBuilder sb = new StringBuilder();
167                 UiDumpUtils.dumpNodes(sb);
168                 Log.d(LOG_TAG, selector + " " + (view == null ? "not found" : "found")
169                         + " on the screen. \n" + sb);
170             }
171             if (view == null || viewHeight < minViewHeightPx) {
172                 if (view == null) {
173                     Log.v(LOG_TAG, selector + " not found on the screen, try scrolling.");
174                 }
175                 final double deadZone = getSwipeDeadZonePct();
176                 UiScrollable scrollable = new UiScrollable(new UiSelector().scrollable(true));
177                 scrollable.setSwipeDeadZonePercentage(deadZone);
178                 if (scrollable.exists()) {
179                     if (!scrolledPastCollapsibleToolbar) {
180                         scrollPastCollapsibleToolbar(scrollable, deadZone);
181                         scrolledPastCollapsibleToolbar = true;
182                         continue;
183                     }
184                     if (isAtEnd) {
185                         if (wasScrolledUpAlready) {
186                             return null;
187                         }
188                         scrollable.scrollToBeginning(Integer.MAX_VALUE);
189                         isAtEnd = false;
190                         wasScrolledUpAlready = true;
191                         scrolledPastCollapsibleToolbar = false;
192                     } else {
193                         Rect boundsBeforeScroll = scrollable.getBounds();
194                         boolean scrollAtStartOrEnd;
195                         boolean isWearCompose = FeatureUtil.isWatch() && Objects.equals(
196                                 scrollable.getPackageName(),
197                                 InstrumentationRegistry.getInstrumentation().getContext()
198                                         .getPackageManager().getPermissionControllerPackageName());
199                         if (isWearCompose) {
200                             // TODO(b/306483780): Removed the condition once the scrollForward is
201                             //  fixed.
202                             if (!wasScrolledUpAlready) {
203                                 // TODO(b/306483780): scrollForward() always returns false. Thus
204                                 // `isAtEnd` will never be false for Wear Compose, because
205                                 // `scrollAtStartOrEnd` is set to false, and the value of `isAtEnd`
206                                 // is an && combination of that value. To avoid skipping Views
207                                 // that exist above the start-point of the search, we will first
208                                 // scroll up before doing a downward search and scroll.
209                                 scrollable.scrollToBeginning(Integer.MAX_VALUE);
210                                 wasScrolledUpAlready = true;
211                                 continue;
212                             }
213                             scrollable.scrollForward();
214                             scrollAtStartOrEnd = false;
215                         } else {
216                             Log.v(LOG_TAG, "Scrolling boundsBeforeScroll: " + boundsBeforeScroll);
217                             scrollAtStartOrEnd = !scrollable.scrollForward();
218                         }
219                         // The scrollable view may no longer be scrollable after the toolbar is
220                         // collapsed.
221                         if (scrollable.exists()) {
222                             Rect boundsAfterScroll = scrollable.getBounds();
223                             isAtEnd = scrollAtStartOrEnd && boundsBeforeScroll.equals(
224                                     boundsAfterScroll);
225                             Log.v(LOG_TAG, "Scrolling done, boundsAfterScroll: "
226                                     + boundsAfterScroll);
227                         } else {
228                             isAtEnd = scrollAtStartOrEnd;
229                         }
230                         Log.v(LOG_TAG, "Scrolling done, scrollAtStartOrEnd=" + scrollAtStartOrEnd
231                                 + ", isAtEnd=" + isAtEnd);
232                     }
233                 } else {
234                     Log.v(LOG_TAG, "There might be a collapsing toolbar, but no scrollable view."
235                             + " Try to collapse");
236                     // There might be a collapsing toolbar, but no scrollable view. Try to collapse
237                     scrollPastCollapsibleToolbar(null, deadZone);
238                 }
239             }
240         }
241         return view;
242     }
243 
244     /**
245      * Finds the object on the given display.
246      *
247      * @param selector The selector to match.
248      * @param timeoutMs The timeout in milliseconds.
249      * @param displayId The display to search on.
250      * @return The object that matches the selector, or null if not found.
251      */
waitFindObjectOrNullOnDisplay( BySelector selector, long timeoutMs, int displayId)252     public static UiObject2 waitFindObjectOrNullOnDisplay(
253             BySelector selector, long timeoutMs, int displayId) throws UiObjectNotFoundException {
254         // Only supported in API level 30 or higher versions.
255         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
256             return null;
257         }
258 
259         UiObject2 view = null;
260         long start = System.currentTimeMillis();
261 
262         while (view == null && start + timeoutMs > System.currentTimeMillis()) {
263             view = getUiDevice().wait(Until.findObject(selector), 1000);
264             if (view != null) {
265                 break;
266             }
267 
268             List<UiObject2> scrollableViews = getUiDevice().findObjects(
269                     By.displayId(displayId).scrollable(true));
270             if (scrollableViews != null && !scrollableViews.isEmpty()) {
271                 for (int i = 0; i < scrollableViews.size(); i++) {
272                     UiObject2 scrollableView = scrollableViews.get(i);
273                     // Swipe far away from the edges to avoid triggering navigation gestures
274                     scrollableView.setGestureMarginPercentage((float) getSwipeDeadZonePct());
275                     // Scroll from the top to the bottom until the view object is found.
276                     scrollableView.scroll(Direction.UP, 1.0f);
277                     scrollableView.scrollUntil(Direction.DOWN, Until.findObject(selector));
278                     view = getUiDevice().findObject(selector);
279                     if (view != null) {
280                         break;
281                     }
282                 }
283             } else {
284                 // There might be a collapsing toolbar, but no scrollable view. Try to collapse
285                 final double deadZone = getSwipeDeadZonePct();
286                 scrollPastCollapsibleToolbar(null, deadZone);
287             }
288         }
289         return view;
290     }
291 
scrollPastCollapsibleToolbar(UiScrollable scrollable, double deadZone)292     private static void scrollPastCollapsibleToolbar(UiScrollable scrollable, double deadZone)
293             throws UiObjectNotFoundException {
294         final UiObject2 collapsingToolbar = getUiDevice().findObject(
295                 By.res(sCollapsingToolbarResPattern));
296         if (collapsingToolbar == null) {
297             Log.v(LOG_TAG, "collapsingToolbar is null, return");
298             return;
299         }
300 
301         final int steps = 55; // == UiScrollable.SCROLL_STEPS
302         if (scrollable != null && scrollable.exists()) {
303             final Rect scrollableBounds = scrollable.getVisibleBounds();
304             final int distanceToSwipe = collapsingToolbar.getVisibleBounds().height() / 2;
305             boolean result = getUiDevice().swipe(scrollableBounds.centerX(),
306                     scrollableBounds.centerY(),
307                     scrollableBounds.centerX(), scrollableBounds.centerY() - distanceToSwipe,
308                     steps);
309             Log.v(LOG_TAG, "scrollPastCollapsibleToolbar swipe successful = " + result);
310         } else {
311             // There might be a collapsing toolbar, but no scrollable view. Try to collapse
312             int maxY = getUiDevice().getDisplayHeight();
313             int minY = (int) (deadZone * maxY);
314             maxY -= minY;
315             boolean result = getUiDevice().drag(0, maxY, 0, minY, steps);
316             Log.v(LOG_TAG, "scrollPastCollapsibleToolbar drag successful = " + result);
317         }
318     }
319 }
320