1 /* 2 * Copyright (C) 2024 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 android.platform.systemui_tapl.ui; 18 19 import static android.platform.systemui_tapl.utils.DeviceUtils.LONG_WAIT; 20 import static android.platform.systemui_tapl.utils.DeviceUtils.SHORT_WAIT; 21 import static android.platform.systemui_tapl.utils.DeviceUtils.sysuiResSelector; 22 import static android.platform.test.util.HealthTestingUtils.waitForValueCatchingStaleObjectExceptions; 23 import static android.platform.uiautomatorhelpers.DeviceHelpers.getUiDevice; 24 import static android.platform.uiautomatorhelpers.WaitUtils.ensureThat; 25 26 import static androidx.test.uiautomator.Until.findObject; 27 28 import static com.android.settingslib.flags.Flags.newStatusBarIcons; 29 import static com.android.systemui.Flags.statusBarChipsModernization; 30 31 import static com.google.common.truth.Truth.assertThat; 32 import static com.google.common.truth.Truth.assertWithMessage; 33 34 import static org.junit.Assume.assumeFalse; 35 36 import android.app.Flags; 37 import android.graphics.Point; 38 import android.graphics.Rect; 39 import android.platform.helpers.foldable.UnfoldAnimationTestingUtils; 40 import android.platform.uiautomatorhelpers.DeviceHelpers; 41 42 import androidx.test.uiautomator.By; 43 import androidx.test.uiautomator.BySelector; 44 import androidx.test.uiautomator.SearchCondition; 45 import androidx.test.uiautomator.UiObject2; 46 import androidx.test.uiautomator.Until; 47 48 import java.util.Collections; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.Set; 52 import java.util.regex.Pattern; 53 54 /** System UI test automation object representing status bar. */ 55 public class StatusBar { 56 57 private static final BySelector PERCENTAGE_SELECTOR = By.text(Pattern.compile("\\d+%")); 58 private static final String BATTERY_ID = "battery"; 59 static final String BATTERY_LEVEL_TEXT_ID = "battery_percentage_view"; 60 static final String CLOCK_ID = "clock"; 61 private static final String NOTIFICATION_ICON_CONTAINER_ID = "notificationIcons"; 62 private static final BySelector NOTIFICATION_LIGHTS_OUT_DOT_SELECTOR = 63 sysuiResSelector("notification_lights_out"); 64 private static final String UI_SYSTEM_ICONS_ID = "system_icons"; 65 private static final String DATE_ID = "date"; 66 static final String STATUS_ICON_CONTAINER_ID = "statusIcons"; 67 private static final String ALARM_ICON_DESC_PREFIX_STRING = "Alarm"; 68 private static final String AIRPLANE_MODE_ICON_DESC = "Airplane mode."; 69 private static final String DATA_SAVER_ICON_DESC = "Data Saver is on"; 70 // https://hsv.googleplex.com/6227911158792192?node=18 71 static final String DOCK_DEFEND_ICON_SUFFIX_STRING = "charging paused for battery protection"; 72 static final String DND_ICON_DESC = Flags.modesUi() ? "Do Not Disturb is on" : "Do Not Disturb"; 73 private static final String WIFI_ICON_ID = "wifi_combo"; 74 private static final String ONGOING_ACTIVITY_CHIP_ICON_ID = "ongoing_activity_chip_primary"; 75 // Corresponds to ScreenRecordChipViewModel.KEY 76 private static final String SCREEN_RECORDING_CHIP_ID = "ScreenRecord"; 77 static final String SCREEN_RECORD_DESC_STRING = "Recording screen"; 78 static final String SILENT_ICON_DESC_PREFIX_STRING = "Ringer silent"; 79 static final String VIBRATE_ICON_DESC_PREFIX_STRING = "Ringer vibrate"; 80 private final List<String> mStatusBarViewIds = 81 List.of(DATE_ID, CLOCK_ID, WIFI_ICON_ID, BATTERY_ID, BATTERY_LEVEL_TEXT_ID); 82 StatusBar()83 StatusBar() { 84 verifyClockIsVisible(); 85 } 86 getNotificationIconsObjects()87 private static List<UiObject2> getNotificationIconsObjects() { 88 // As the container for notifications can change between the moment we get it and we get its 89 // children, we retry several times in case of failure. This aims at reducing flakiness. 90 return waitForValueCatchingStaleObjectExceptions( 91 () -> "Failed to get status bar icons.", 92 () -> { 93 UiObject2 container = 94 getUiDevice() 95 .wait( 96 Until.findObject( 97 sysuiResSelector( 98 NOTIFICATION_ICON_CONTAINER_ID)), 99 10000); 100 return container != null ? container.getChildren() : Collections.emptyList(); 101 }); 102 } 103 104 /** Returns the number of notification icons visible on the status bar. */ getNotificationIconCount()105 public int getNotificationIconCount() { 106 List<UiObject2> icons = getNotificationIconsObjects(); 107 108 // 2 icons are never ellipsized, let's return them. 109 if (icons.size() <= 2) { 110 return icons.size(); 111 } 112 113 // Notification icons don't change visibility when collapsed. 114 // The same icon can represent either the notification and the ellipsis. 115 // 116 // For this reason, UiAutomator thinks the icons are always visible, when we're 117 // actually stacking them up on top of each other and drawing transparent. 118 // 119 // Let's check for overlap instead of view visibility. 120 int iconPadding = 121 icons.get(1).getVisibleBounds().left - icons.get(0).getVisibleBounds().left; 122 int lastIcon = icons.size() - 1; 123 for (int i = 1; i < icons.size(); i++) { 124 lastIcon = i; 125 int padding = 126 icons.get(i).getVisibleBounds().left - icons.get(i - 1).getVisibleBounds().left; 127 if (padding != iconPadding) { 128 break; 129 } 130 } 131 return lastIcon; 132 } 133 134 /** Verifies visibility of the battery percentage indicator. */ verifyBatteryPercentageVisibility(boolean expectedVisible)135 public void verifyBatteryPercentageVisibility(boolean expectedVisible) { 136 assumeFalse(newStatusBarIcons()); 137 UiObject2 batteryIndication = getBatteryIndication(); 138 assertThat(batteryIndication).isNotNull(); 139 140 if (expectedVisible) { 141 assertThat(batteryIndication.wait(Until.hasObject(PERCENTAGE_SELECTOR), 10000)) 142 .isTrue(); 143 } else { 144 assertThat(batteryIndication.wait(Until.gone(PERCENTAGE_SELECTOR), 10000)).isTrue(); 145 } 146 } 147 getBatteryIndication()148 private static UiObject2 getBatteryIndication() { 149 return getUiDevice().wait(findObject(sysuiResSelector(BATTERY_ID)), 1000); 150 } 151 152 /** Assert that clock indicator is visible. */ verifyClockIsVisible()153 private void verifyClockIsVisible() { 154 // Matches 12h or 24h time format 155 Pattern timePattern = Pattern.compile("^(?:[01]?\\d|2[0-3]):[0-5]\\d"); 156 DeviceHelpers.waitForObj( 157 By.pkg("com.android.systemui").text(timePattern), 158 SHORT_WAIT, 159 () -> "Clock should be visible."); 160 } 161 162 /** Assert that clock indicator is NOT visible. */ verifyClockIsNotVisible()163 public void verifyClockIsNotVisible() { 164 assertWithMessage("StatusBar clock is visible") 165 .that( 166 getUiDevice() 167 .wait(Until.gone(sysuiResSelector(CLOCK_ID)), LONG_WAIT.toMillis())) 168 .isTrue(); 169 } 170 171 /** Assert that the lights out notification dot is visible. */ verifyLightsOutDotIsVisible()172 public void verifyLightsOutDotIsVisible() { 173 SearchCondition<Boolean> searchCondition = 174 Until.hasObject(NOTIFICATION_LIGHTS_OUT_DOT_SELECTOR); 175 assertThat(getUiDevice().wait(searchCondition, LONG_WAIT.toMillis())).isTrue(); 176 } 177 178 /** Assert that the lights out notification dot is NOT visible. */ verifyLightsOutDotIsNotVisible()179 public void verifyLightsOutDotIsNotVisible() { 180 SearchCondition<Boolean> searchCondition = Until.gone(NOTIFICATION_LIGHTS_OUT_DOT_SELECTOR); 181 assertThat(getUiDevice().wait(searchCondition, LONG_WAIT.toMillis())).isTrue(); 182 } 183 184 /** Returns the visible user switcher chip, or fails if it's not visible. */ getUserSwitcherChip()185 public UserSwitcherChip getUserSwitcherChip() { 186 return new UserSwitcherChip(); 187 } 188 189 /** Gets the icons object on the right hand side StatusBar. This is for screenshot test. */ getBoundsOfRightHandSideStatusBarIconsForScreenshot()190 public Rect getBoundsOfRightHandSideStatusBarIconsForScreenshot() { 191 return DeviceHelpers.INSTANCE 192 .waitForObj( 193 /* UiDevice= */ getUiDevice(), 194 /* selector= */ sysuiResSelector(STATUS_ICON_CONTAINER_ID), 195 /* timeout= */ SHORT_WAIT, 196 /* errorProvider= */ () -> 197 "StatusBar icons are not found on the right hand side.") 198 .getVisibleBounds(); 199 } 200 201 /** Assert that alarm icon is visible. */ verifyAlarmIconIsVisible()202 public void verifyAlarmIconIsVisible() { 203 DeviceHelpers.waitForObj( 204 By.pkg("com.android.systemui") 205 .clazz("android.widget.ImageView") 206 .descContains(ALARM_ICON_DESC_PREFIX_STRING), 207 SHORT_WAIT, 208 () -> "Alarm icon should be visible."); 209 } 210 211 /** Assert that airplane mode icon is visible. */ verifyAirplaneModeIconIsVisible()212 public void verifyAirplaneModeIconIsVisible() { 213 assertThat( 214 getUiDevice() 215 .wait( 216 Until.hasObject( 217 sysuiResSelector(STATUS_ICON_CONTAINER_ID) 218 .hasChild( 219 By.desc(AIRPLANE_MODE_ICON_DESC))), 220 SHORT_WAIT.toMillis())) 221 .isTrue(); 222 } 223 224 /** Assert that data saver icon is visible. */ verifyDataSaverIconIsVisible()225 public void verifyDataSaverIconIsVisible() { 226 assertThat( 227 getUiDevice() 228 .wait( 229 Until.hasObject( 230 sysuiResSelector(STATUS_ICON_CONTAINER_ID) 231 .hasChild(By.desc(DATA_SAVER_ICON_DESC))), 232 SHORT_WAIT.toMillis())) 233 .isTrue(); 234 } 235 236 /** Assert that dock defend icon is visible. */ verifyDockDefendIconIsVisible()237 public void verifyDockDefendIconIsVisible() { 238 assertWithMessage("Dock Defend icon should be visible in status bar.") 239 .that( 240 getUiDevice() 241 .wait( 242 Until.hasObject( 243 sysuiResSelector(UI_SYSTEM_ICONS_ID) 244 .hasDescendant( 245 By.descContains( 246 DOCK_DEFEND_ICON_SUFFIX_STRING))), 247 SHORT_WAIT.toMillis())) 248 .isTrue(); 249 } 250 251 /** Asserts that user switcher chip is invisible. */ assertUserSwitcherChipIsInvisible()252 public void assertUserSwitcherChipIsInvisible() { 253 DeviceHelpers.INSTANCE.assertInvisible( 254 sysuiResSelector(UserSwitcherChip.USER_SWITCHER_CONTAINER_ID), 255 SHORT_WAIT, 256 () -> "User switcher chip should be invisible in status bar."); 257 } 258 259 /** Returns the clock time value on StatusBar. Experimental. */ getClockTime()260 public String getClockTime() { 261 UiObject2 clockTime = 262 DeviceHelpers.INSTANCE.waitForObj( 263 /* UiDevice= */ getUiDevice(), 264 /* selector= */ sysuiResSelector(CLOCK_ID), 265 /* timeout= */ SHORT_WAIT, 266 /* errorProvider= */ () -> "Clock not found."); 267 return clockTime.getText(); 268 } 269 270 /** Returns the position of views in StatusBar. */ getStatusBarViewPositions()271 public Set<UnfoldAnimationTestingUtils.Icon> getStatusBarViewPositions() { 272 Set<UnfoldAnimationTestingUtils.Icon> statusBarViewPositions = new HashSet<>(); 273 mStatusBarViewIds.forEach( 274 viewId -> { 275 UiObject2 viewUiObject = 276 DeviceHelpers.INSTANCE.waitForNullableObj( 277 sysuiResSelector(viewId), SHORT_WAIT); 278 if (viewUiObject != null) { 279 Rect iconPosition = viewUiObject.getVisibleBounds(); 280 statusBarViewPositions.add( 281 new UnfoldAnimationTestingUtils.Icon( 282 viewId, 283 new Point(iconPosition.centerX(), iconPosition.centerY()))); 284 } 285 }); 286 return statusBarViewPositions; 287 } 288 289 /** Assert that DND icon is visible. */ verifyDndIconIsVisible()290 public void verifyDndIconIsVisible() { 291 assertWithMessage("DND icon should be visible in status bar.") 292 .that( 293 getUiDevice() 294 .wait( 295 Until.hasObject( 296 sysuiResSelector(STATUS_ICON_CONTAINER_ID) 297 .hasChild(By.descContains(DND_ICON_DESC))), 298 SHORT_WAIT.toMillis())) 299 .isTrue(); 300 } 301 302 /** Returns the value of the battery level on StatusBar. Experimental. */ getBatteryLevel()303 public String getBatteryLevel() { 304 UiObject2 batteryPercentage = 305 DeviceHelpers.INSTANCE.waitForObj( 306 /* UiDevice= */ getUiDevice(), 307 /* selector= */ sysuiResSelector(BATTERY_LEVEL_TEXT_ID), 308 /* timeout= */ LONG_WAIT, 309 /* errorProvider= */ () -> "Battery percentage not found."); 310 return batteryPercentage.getText(); 311 } 312 313 /** Assert that WiFi icon is visible. Experimental. */ verifyWifiIconIsVisible()314 public void verifyWifiIconIsVisible() { 315 DeviceHelpers.INSTANCE.assertVisible( 316 sysuiResSelector(UI_SYSTEM_ICONS_ID).hasDescendant(sysuiResSelector(WIFI_ICON_ID)), 317 LONG_WAIT, 318 () -> "WiFi icon should be visible in status bar."); 319 } 320 321 /** Assert that silent icon is visible. */ verifySilentIconIsVisible()322 public void verifySilentIconIsVisible() { 323 DeviceHelpers.INSTANCE.assertVisible( 324 sysuiResSelector(STATUS_ICON_CONTAINER_ID) 325 .hasChild(By.descContains(SILENT_ICON_DESC_PREFIX_STRING)), 326 LONG_WAIT, 327 () -> "Silent icon should be visible in status bar."); 328 } 329 330 /** Assert that the screen record chip is visible. */ verifyScreenRecordChipIsVisible()331 public void verifyScreenRecordChipIsVisible() { 332 String resSelector; 333 if (statusBarChipsModernization()) { 334 resSelector = SCREEN_RECORDING_CHIP_ID; 335 } else { 336 resSelector = ONGOING_ACTIVITY_CHIP_ICON_ID; 337 } 338 DeviceHelpers.INSTANCE.assertVisible( 339 sysuiResSelector(resSelector) 340 .hasDescendant(By.descContains(SCREEN_RECORD_DESC_STRING)), 341 LONG_WAIT, 342 () -> "Recording chip should be visible in status bar."); 343 } 344 345 /** Assert there is at least one status icon visible. */ verifyAtLeastOneStatusIconIsVisible()346 public void verifyAtLeastOneStatusIconIsVisible() { 347 UiObject2 statusBar = DeviceHelpers.waitForObj(sysuiResSelector(STATUS_ICON_CONTAINER_ID)); 348 assertWithMessage("Status bar should have at least one icon visible") 349 .that(statusBar.getChildCount()) 350 .isGreaterThan(0); 351 } 352 353 /** Verifies visibility of the vibrate icon. */ assertVibrateIconVisibility(boolean visible)354 public void assertVibrateIconVisibility(boolean visible) { 355 DeviceHelpers.INSTANCE.assertVisibility( 356 /* UiDevice= */ getUiDevice(), 357 /* selector= */ sysuiResSelector(STATUS_ICON_CONTAINER_ID) 358 .hasChild(By.descContains(VIBRATE_ICON_DESC_PREFIX_STRING)), 359 /* visible= */ visible, 360 /* timeout= */ LONG_WAIT); 361 } 362 363 /** 364 * Fails when {@link #getNotificationIconCount()} doesn't become the expected value within a 365 * timeout. 366 */ assertNotificationIconCount(int expected)367 public void assertNotificationIconCount(int expected) { 368 ensureThat( 369 "Visible StatusBar icon count should be " + expected, 370 () -> getNotificationIconCount() == expected); 371 } 372 } 373