1 /* 2 * Copyright (C) 2016 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.test.helpers; 18 19 import android.app.Instrumentation; 20 import android.content.pm.PackageManager.NameNotFoundException; 21 import android.os.SystemClock; 22 import android.platform.test.helpers.exceptions.UnknownUiException; 23 import android.support.test.uiautomator.By; 24 import android.support.test.uiautomator.BySelector; 25 import android.support.test.uiautomator.Direction; 26 import android.support.test.uiautomator.Until; 27 import android.support.test.uiautomator.UiDevice; 28 import android.support.test.uiautomator.UiObject2; 29 import android.util.Log; 30 31 import java.util.List; 32 import java.util.regex.Pattern; 33 34 import junit.framework.Assert; 35 36 37 import android.os.Environment; 38 import java.io.File; 39 import java.io.IOException; 40 41 public class PhotosHelperImpl extends AbstractPhotosHelper { 42 private static final String LOG_TAG = PhotosHelperImpl.class.getSimpleName(); 43 44 private static final long APP_LOAD_WAIT = 7500; 45 private static final long HACKY_WAIT = 2500; 46 private static final long PICTURE_LOAD_WAIT = 20000; 47 private static final long UI_NAVIGATION_WAIT = 5000; 48 49 private static final Pattern UI_PHOTO_DESC = Pattern.compile("^Photo.*"); 50 51 private static final String UI_DONE_BUTTON_ID = "done_button"; 52 private static final String UI_GET_STARTED_CONTAINER = "get_started_container"; 53 private static final String UI_GET_STARTED_ID = "get_started"; 54 private static final String UI_LOADING_ICON_ID = "list_empty_progress_bar"; 55 private static final String UI_NEXT_BUTTON_ID = "next_button"; 56 private static final String UI_PACKAGE_NAME = "com.google.android.apps.photos"; 57 private static final String UI_PHOTO_TAB_ID = "tab_photos"; 58 private static final String UI_DEVICE_FOLDER_TEXT = "Device folders"; 59 private static final String UI_PHOTO_VIEW_PAGER_ID = "photo_view_pager"; 60 private static final String UI_PHOTO_SCROLL_VIEW_ID = "recycler_view"; 61 private static final String UI_NAVIGATION_LIST_ID = "navigation_list"; 62 private static final int MAX_UI_SCROLL_COUNT = 20; 63 private static final int MAX_DISMISS_INIT_DIALOG_RETRY = 20; 64 65 public PhotosHelperImpl(Instrumentation instr) { 66 super(instr); 67 } 68 69 /** 70 * {@inheritDoc} 71 */ 72 @Override 73 public String getPackage() { 74 return "com.google.android.apps.photos"; 75 } 76 77 78 /** 79 * {@inheritDoc} 80 */ 81 @Override 82 public String getLauncherName() { 83 return "Photos"; 84 } 85 86 /** 87 * {@inheritDoc} 88 */ 89 @Override 90 public void dismissInitialDialogs() { 91 // Target Photos version 1.18.0.119671374 92 SystemClock.sleep(APP_LOAD_WAIT); 93 94 if (isOnInitialDialogScreen()) { 95 UiObject2 getStartedButton = mDevice.wait( 96 Until.findObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_ID)), APP_LOAD_WAIT); 97 int retryCount = 0; 98 while ((retryCount < MAX_DISMISS_INIT_DIALOG_RETRY) && 99 (getStartedButton == null)) { 100 /* 101 The UiAutomator sometimes cannot find GET STARTED button even though 102 it is seen on the screen. 103 The reason is because the initial "spinner" animation screen updates 104 views too quickly for UiAutomator to catch the change. 105 106 The following hack is used to reload the init dialog for UiAutomator to 107 retry catching the GET STARTED button. 108 */ 109 110 mDevice.pressBack(); 111 mDevice.waitForIdle(); 112 mDevice.pressHome(); 113 mDevice.waitForIdle(); 114 open(); 115 116 getStartedButton = mDevice.wait( 117 Until.findObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_ID)), 118 APP_LOAD_WAIT); 119 retryCount += 1; 120 121 if (!isOnInitialDialogScreen()) { 122 break; 123 } 124 } 125 126 if (isOnInitialDialogScreen() && (getStartedButton == null)) { 127 throw new IllegalStateException("UiAutomator cannot catch GET STARTED button"); 128 } 129 else { 130 if (getStartedButton != null) { 131 getStartedButton.click(); 132 } 133 } 134 } 135 else { 136 Log.e(LOG_TAG, "Didn't find GET STARTED button."); 137 } 138 139 // Address dialogs with an account vs. without an account 140 Pattern signInWords = Pattern.compile("Sign in", Pattern.CASE_INSENSITIVE); 141 boolean hasAccount = !mDevice.hasObject(By.text(signInWords)); 142 if (!hasAccount) { 143 // Select 'NO THANKS' if no account exists 144 Pattern noThanksWords = Pattern.compile("No thanks", Pattern.CASE_INSENSITIVE); 145 UiObject2 noThanksButton = mDevice.findObject(By.text(noThanksWords)); 146 if (noThanksButton != null) { 147 noThanksButton.click(); 148 mDevice.waitForIdle(); 149 } else { 150 Log.e(LOG_TAG, "Unable to find NO THANKS button."); 151 } 152 } else { 153 UiObject2 doneButton = mDevice.wait(Until.findObject( 154 By.res(UI_PACKAGE_NAME, UI_DONE_BUTTON_ID)), 5000); 155 if (doneButton != null) { 156 doneButton.click(); 157 mDevice.waitForIdle(); 158 } 159 else { 160 Log.e(LOG_TAG, "Didn't find DONE button."); 161 } 162 163 // Press the next button (arrow and check mark) four consecutive times 164 for (int repeat = 0; repeat < 4; repeat++) { 165 UiObject2 nextButton = mDevice.findObject( 166 By.res(UI_PACKAGE_NAME, UI_NEXT_BUTTON_ID)); 167 if (nextButton != null) { 168 nextButton.click(); 169 mDevice.waitForIdle(); 170 } else { 171 Log.e(LOG_TAG, "Unable to find arrow or check mark buttons."); 172 } 173 } 174 175 mDevice.wait(Until.gone( 176 By.res(UI_PACKAGE_NAME, UI_LOADING_ICON_ID)), PICTURE_LOAD_WAIT); 177 } 178 179 mDevice.waitForIdle(); 180 } 181 182 /** 183 * {@inheritDoc} 184 */ 185 @Override 186 public void openFirstClip() { 187 if (searchForVideoClip()) { 188 UiObject2 clip = getFirstClip(); 189 if (clip != null) { 190 clip.click(); 191 mDevice.wait(Until.findObject( 192 By.res(UI_PACKAGE_NAME, "photos_videoplayer_play_button_holder")), 2000); 193 } 194 else { 195 throw new IllegalStateException("Cannot play a video after finding video clips"); 196 } 197 } 198 else { 199 throw new UnsupportedOperationException("Cannot find a video clip"); 200 } 201 } 202 203 /** 204 * {@inheritDoc} 205 */ 206 @Override 207 public void pauseClip() { 208 UiObject2 holder = mDevice.findObject( 209 By.res(UI_PACKAGE_NAME, "photos_videoplayer_play_button_holder")); 210 if (holder != null) { 211 holder.click(); 212 } else { 213 throw new UnknownUiException("Unable to find pause button holder."); 214 } 215 216 UiObject2 pause = mDevice.wait(Until.findObject( 217 By.res(UI_PACKAGE_NAME, "photos_videoplayer_pause_button")), 2500); 218 if (pause != null) { 219 pause.click(); 220 mDevice.wait(Until.findObject(By.desc("Play video")), 2500); 221 } else { 222 throw new UnknownUiException("Unable to find pause button."); 223 } 224 } 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override 230 public void playClip() { 231 UiObject2 play = mDevice.findObject(By.desc("Play video")); 232 if (play != null) { 233 play.click(); 234 mDevice.wait(Until.findObject( 235 By.res(UI_PACKAGE_NAME, "photos_videoplayer_pause_button")), 2500); 236 } else { 237 throw new UnknownUiException("Unable to find play button"); 238 } 239 } 240 241 /** 242 * {@inheritDoc} 243 */ 244 @Override 245 public void goToMainScreen() { 246 for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnMainScreen(); 247 --retriesRemaining) { 248 // check if we see the Photos tab at the bottom of the screen 249 // If we do, clicking on the tab should go to home screen. 250 UiObject2 photosButton = mDevice.findObject( 251 By.res(UI_PACKAGE_NAME, UI_PHOTO_TAB_ID)); 252 if (photosButton != null) { 253 photosButton.click(); 254 } 255 else { 256 mDevice.pressBack(); 257 } 258 mDevice.waitForIdle(); 259 } 260 261 if (!isOnMainScreen()) { 262 throw new IllegalStateException("Cannot go to main screen"); 263 } 264 } 265 266 /** 267 * {@inheritDoc} 268 */ 269 @Override 270 public void openPicture(int index) { 271 272 mDevice.waitForIdle(); 273 List<UiObject2> photos = mDevice.findObjects(By.pkg(UI_PACKAGE_NAME).desc(UI_PHOTO_DESC)); 274 275 if (photos == null) { 276 throw new IllegalStateException("Cannot find photos on current view screen"); 277 } 278 279 if ((index < 0) || (index >= photos.size())) { 280 String errMsg = String.format("Photo index (%d) out of bound (0..%d)", 281 index, photos.size()); 282 throw new IllegalArgumentException(errMsg); 283 } 284 285 UiObject2 photo = photos.get(index); 286 photo.click(); 287 if (!mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID)), 288 UI_NAVIGATION_WAIT)) { 289 throw new IllegalStateException("Cannot display photo on screen"); 290 } 291 } 292 293 /** 294 * {@inheritDoc} 295 */ 296 @Override 297 public void scrollAlbum(Direction direction) { 298 if (!(Direction.LEFT.equals(direction) || Direction.RIGHT.equals(direction))) { 299 throw new IllegalArgumentException("Scroll direction must be LEFT or RIGHT"); 300 } 301 302 UiObject2 scrollContainer = mDevice.findObject( 303 By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID)); 304 305 if (scrollContainer == null) { 306 throw new UnknownUiException("Cannot find scroll container"); 307 } 308 309 scrollContainer.scroll(direction, 1.0f); 310 } 311 312 /** 313 * {@inheritDoc} 314 */ 315 @Override 316 public void goToDeviceFolderScreen() { 317 if (!isOnDeviceFolderScreen()) { 318 319 if (!isOnMainScreen()) { 320 goToMainScreen(); 321 } 322 323 openNavigationDrawer(); 324 325 UiObject2 deviceFolderButton = mDevice.wait(Until.findObject( 326 By.text(UI_DEVICE_FOLDER_TEXT)), UI_NAVIGATION_WAIT); 327 if (deviceFolderButton != null) { 328 deviceFolderButton.click(); 329 } 330 else { 331 UiObject2 photosButton = mDevice.wait(Until.findObject(By.text("Photos")), 332 UI_NAVIGATION_WAIT); 333 if (photosButton != null) { 334 photosButton.click(); 335 } 336 else { 337 throw new IllegalStateException("No device folder in navigation drawer"); 338 } 339 } 340 } 341 342 if (!isOnDeviceFolderScreen()) { 343 throw new UnknownUiException("Can not go to device folder screen"); 344 } 345 } 346 347 /** 348 * {@inheritDoc} 349 */ 350 @Override 351 public boolean searchForDeviceFolder(String folderName) { 352 boolean foundFolder = false; 353 int scrollCount = 0; 354 while (!foundFolder && (scrollCount < MAX_UI_SCROLL_COUNT)) { 355 foundFolder = mDevice.wait(Until.hasObject(By.text(folderName)), 2000); 356 if (!foundFolder) { 357 if (!scrollView(Direction.DOWN)) { 358 break; 359 } 360 } 361 scrollCount += 1; 362 } 363 364 if (!foundFolder) { 365 foundFolder = mDevice.wait(Until.hasObject(By.text(folderName)), 2000); 366 } 367 368 return foundFolder; 369 } 370 371 /** 372 * {@inheritDoc} 373 */ 374 @Override 375 public boolean searchForVideoClip() { 376 boolean foundVideoClip = false; 377 int scrollCount = 0; 378 while (!foundVideoClip && (scrollCount < MAX_UI_SCROLL_COUNT)) { 379 foundVideoClip = (getFirstClip() != null); 380 if (!foundVideoClip) { 381 if (!scrollView(Direction.DOWN)) { 382 break; 383 } 384 } 385 scrollCount += 1; 386 } 387 return foundVideoClip; 388 } 389 390 /** 391 * {@inheritDoc} 392 */ 393 @Override 394 public boolean searchForPicture() { 395 boolean foundPicture = false; 396 int scrollCount = 0; 397 while (!foundPicture && (scrollCount < MAX_UI_SCROLL_COUNT)) { 398 foundPicture = mDevice.wait(Until.hasObject(By.descStartsWith("Photo")), 2000); 399 if (!foundPicture) { 400 if (!scrollView(Direction.DOWN)) { 401 break; 402 } 403 } 404 scrollCount += 1; 405 } 406 return foundPicture; 407 } 408 409 /** 410 * {@inheritDoc} 411 */ 412 @Override 413 public void openDeviceFolder(String folderName) { 414 UiObject2 deviceFolder = mDevice.wait(Until.findObject(By.text(folderName)), 415 UI_NAVIGATION_WAIT); 416 if (deviceFolder != null) { 417 deviceFolder.click(); 418 } 419 else { 420 throw new IllegalArgumentException(String.format("Cannot open device folder %s", 421 folderName)); 422 } 423 } 424 425 private UiObject2 getFirstClip() { 426 return mDevice.wait(Until.findObject(By.descStartsWith("Video")), 2000); 427 } 428 429 /** 430 * This function returns true if Photos is currently on the first-use 431 * initial dialog screen, with "Get Started" button displayed on screen 432 * 433 * @return Returns true if app is on the initial dialog screen, false otherwise 434 */ 435 private boolean isOnInitialDialogScreen() { 436 return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_CONTAINER)); 437 } 438 439 private boolean isOnMainScreen() { 440 return mDevice.hasObject(By.descContains("Photos, selected")); 441 } 442 443 /** 444 * This function returns true if Photos is currently in the 445 * photo-viewing screen, displaying either one photo 446 * or video on the screen. 447 * 448 * @return Returns true if one photo or video is displayed on the screen, 449 * false otherwise. 450 */ 451 private boolean isOnPhotoViewingScreen() { 452 return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID)); 453 } 454 455 private boolean isOnDeviceFolderScreen() { 456 457 if (mDevice.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_DEVICE_FOLDER_TEXT))) { 458 return true; 459 } 460 461 // sometimes the "Device Folder" tab is hidden. 462 // scroll down once to make sure the tab is visible 463 UiObject2 scrollContainer = mDevice.findObject( 464 By.res(UI_PACKAGE_NAME, UI_PHOTO_SCROLL_VIEW_ID)); 465 if (scrollContainer != null) { 466 scrollContainer.scroll(Direction.DOWN, 1.0f); 467 return mDevice.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_DEVICE_FOLDER_TEXT)); 468 } 469 else { 470 return false; 471 } 472 } 473 474 /** 475 * This function performs one scroll on the current screen, in the direction 476 * specified by input argument. 477 * 478 * @param dir The direction of the scroll 479 * @return Returns whether the object can still scroll in the given direction 480 */ 481 private boolean scrollView(Direction dir) { 482 UiObject2 scrollContainer = mDevice.findObject(By.res(UI_PACKAGE_NAME, 483 UI_PHOTO_SCROLL_VIEW_ID)); 484 if (scrollContainer == null) { 485 return false; 486 } 487 488 return scrollContainer.scroll(dir, 1.0f); 489 } 490 491 private void openNavigationDrawer() { 492 UiObject2 navigationDrawer = mDevice.findObject(By.desc("Show Navigation Drawer")); 493 if (navigationDrawer == null) { 494 mDevice.pressBack(); 495 navigationDrawer = mDevice.wait(Until.findObject(By.desc("Show Navigation Drawer")), 496 UI_NAVIGATION_WAIT); 497 } 498 499 if (navigationDrawer == null) { 500 throw new UnknownUiException("Cannot find navigation drawer"); 501 } 502 503 navigationDrawer.click(); 504 505 if (!mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_NAVIGATION_LIST_ID))) { 506 throw new UnknownUiException("Cannot open navigation drawer"); 507 } 508 } 509 } 510