1 /* 2 * Copyright (C) 2015 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 com.android.shell; 18 19 import android.app.Instrumentation; 20 import android.app.StatusBarManager; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.os.SystemClock; 24 import android.text.format.DateUtils; 25 import android.util.Log; 26 import android.util.PluralsMessageFormatter; 27 28 import androidx.test.uiautomator.By; 29 import androidx.test.uiautomator.UiDevice; 30 import androidx.test.uiautomator.UiObject; 31 import androidx.test.uiautomator.UiObject2; 32 import androidx.test.uiautomator.UiObjectNotFoundException; 33 import androidx.test.uiautomator.UiSelector; 34 import androidx.test.uiautomator.Until; 35 36 import static junit.framework.Assert.assertFalse; 37 import static junit.framework.Assert.assertNotNull; 38 import static junit.framework.Assert.assertTrue; 39 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 44 /** 45 * A helper class for UI-related testing tasks. 46 */ 47 final class UiBot { 48 49 private static final String TAG = "UiBot"; 50 private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; 51 private static final String ANDROID_PACKAGE = "android"; 52 53 private static final long SHORT_UI_TIMEOUT_MS = (3 * DateUtils.SECOND_IN_MILLIS); 54 55 private final Instrumentation mInstrumentation; 56 private final UiDevice mDevice; 57 private final int mTimeout; 58 UiBot(Instrumentation instrumentation, int timeout)59 public UiBot(Instrumentation instrumentation, int timeout) { 60 mInstrumentation = instrumentation; 61 mDevice = UiDevice.getInstance(instrumentation); 62 mTimeout = timeout; 63 } 64 65 /** 66 * Opens the system notification and gets a UiObject with the text. 67 * 68 * @param text Notification's text as displayed by the UI. 69 * @return notification object. 70 */ getNotification(String text)71 public UiObject getNotification(String text) { 72 boolean opened = mDevice.openNotification(); 73 Log.v(TAG, "openNotification(): " + opened); 74 boolean gotIt = mDevice.wait(Until.hasObject(By.pkg(SYSTEMUI_PACKAGE)), mTimeout); 75 assertTrue("could not get system ui (" + SYSTEMUI_PACKAGE + ")", gotIt); 76 77 return getObject(text); 78 } 79 80 /** 81 * Opens the system notification and gets a notification containing the text. 82 * 83 * @param text Notification's text as displayed by the UI. 84 * @return notification object. 85 */ getNotification2(String text)86 public UiObject2 getNotification2(String text) { 87 boolean opened = mDevice.openNotification(); 88 Log.v(TAG, "openNotification(): " + opened); 89 final UiObject2 notificationScroller = mDevice.wait(Until.findObject( 90 By.res(SYSTEMUI_PACKAGE, "notification_stack_scroller")), mTimeout); 91 assertNotNull("could not get notification stack scroller", notificationScroller); 92 final List<UiObject2> notificationList = notificationScroller.getChildren(); 93 for (UiObject2 notification: notificationList) { 94 final UiObject2 notificationText = notification.findObject(By.textContains(text)); 95 if (notificationText != null) { 96 return notification; 97 } 98 } 99 return null; 100 } 101 102 /** 103 * Expands the notification. 104 * 105 * @param notification The notification object returned by {@link #getNotification2(String)}. 106 */ expandNotification(UiObject2 notification)107 public void expandNotification(UiObject2 notification) { 108 final UiObject2 expandBtn = notification.findObject( 109 By.res(ANDROID_PACKAGE, "expand_button")); 110 if (expandBtn.getContentDescription().equals("Collapse")) { 111 return; 112 } 113 expandBtn.click(); 114 mDevice.waitForIdle(); 115 } 116 collapseStatusBar()117 public void collapseStatusBar() throws Exception { 118 // TODO: mDevice should provide such method.. 119 StatusBarManager sbm = 120 (StatusBarManager) mInstrumentation.getContext().getSystemService("statusbar"); 121 sbm.collapsePanels(); 122 } 123 124 /** 125 * Opens the system notification and clicks a given notification. 126 * 127 * @param text Notificaton's text as displayed by the UI. 128 */ clickOnNotification(String text)129 public void clickOnNotification(String text) { 130 UiObject notification = getNotification(text); 131 click(notification, "bug report notification"); 132 } 133 134 /** 135 * Gets an object that might not yet be available in current UI. 136 * 137 * @param text Object's text as displayed by the UI. 138 */ getObject(String text)139 public UiObject getObject(String text) { 140 boolean gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout); 141 assertTrue("object with text '(" + text + "') not visible yet", gotIt); 142 return getVisibleObject(text); 143 } 144 145 /** 146 * Gets an object that might not yet be available in current UI. 147 * 148 * @param id Object's fully-qualified resource id (like {@code android:id/button1}) 149 */ getObjectById(String id)150 public UiObject getObjectById(String id) { 151 boolean gotIt = mDevice.wait(Until.hasObject(By.res(id)), mTimeout); 152 assertTrue("object with id '(" + id + "') not visible yet", gotIt); 153 return getVisibleObjectById(id); 154 } 155 156 /** 157 * Gets an object which is guaranteed to be present in the current UI. 158 * 159 * @param text Object's text as displayed by the UI. 160 */ getVisibleObject(String text)161 public UiObject getVisibleObject(String text) { 162 UiObject uiObject = mDevice.findObject(new UiSelector().text(text)); 163 assertTrue("could not find object with text '" + text + "'", uiObject.exists()); 164 return uiObject; 165 } 166 167 /** 168 * Gets an object which is guaranteed to be present in the current UI. 169 * 170 * @param text Object's text as displayed by the UI. 171 */ getVisibleObjectById(String id)172 public UiObject getVisibleObjectById(String id) { 173 UiObject uiObject = mDevice.findObject(new UiSelector().resourceId(id)); 174 assertTrue("could not find object with id '" + id+ "'", uiObject.exists()); 175 return uiObject; 176 } 177 178 /** 179 * Asserts an object is not visible. 180 */ assertNotVisibleById(String id)181 public void assertNotVisibleById(String id) { 182 // TODO: not working when the bugreport dialog is shown, it hangs until the dialog is 183 // dismissed and hence always work. 184 boolean hasIt = mDevice.hasObject(By.res(id)); 185 assertFalse("should not have found object with id '" + id+ "'", hasIt); 186 } 187 188 189 /** 190 * Clicks on a UI element. 191 * 192 * @param uiObject UI element to be clicked. 193 * @param description Elements's description used on logging statements. 194 */ click(UiObject uiObject, String description)195 public void click(UiObject uiObject, String description) { 196 try { 197 boolean clicked = uiObject.click(); 198 // TODO: assertion below fails sometimes, even though the click succeeded, 199 // (specially when clicking the "Just Once" button), so it's currently just logged. 200 // assertTrue("could not click on object '" + description + "'", clicked); 201 202 Log.v(TAG, "onClick for " + description + ": " + clicked); 203 } catch (UiObjectNotFoundException e) { 204 throw new IllegalStateException("exception when clicking on object '" + description 205 + "'", e); 206 } 207 } 208 209 /** 210 * Chooses a given activity to handle an Intent. 211 * 212 * @param name name of the activity as displayed in the UI (typically the value set by 213 * {@code android:label} in the manifest). 214 * @param context Context of the target application 215 * @param count Number of files to be shared 216 */ chooseActivity(String name, Context context, int count)217 public void chooseActivity(String name, Context context, int count) { 218 // It uses an intent chooser now, so just getting the activity by text is enough... 219 Resources res = null; 220 try { 221 res = context.getPackageManager() 222 .getResourcesForApplication("com.android.intentresolver"); 223 } catch (Exception e) { 224 assertNotNull("could not get resources for com.android.intentresolver", res); 225 } 226 /* Resource read is defined as a string which contains a plural 227 * which needs some formatting */ 228 Map<String, Object> arguments = new HashMap<>(); 229 arguments.put("count", count); 230 final String share = PluralsMessageFormatter.format( 231 res, 232 arguments, 233 res.getIdentifier("sharing_files", "string", "com.android.intentresolver")); 234 boolean gotIt = mDevice.wait(Until.hasObject(By.text(share)), mTimeout); 235 assertTrue("could not get share activity (" + share + ")", gotIt); 236 swipeUp(); 237 SystemClock.sleep(SHORT_UI_TIMEOUT_MS); 238 UiObject activity = getObject(name); 239 click(activity, name); 240 } 241 pressBack()242 public void pressBack() { 243 mDevice.pressBack(); 244 } 245 turnScreenOn()246 public void turnScreenOn() throws Exception { 247 mDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP"); 248 mDevice.executeShellCommand("wm dismiss-keyguard"); 249 mDevice.waitForIdle(); 250 } 251 swipeUp()252 public void swipeUp() { 253 mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() * 3 / 4, 254 mDevice.getDisplayWidth() / 2, 0, 30); 255 } 256 } 257