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 package android.support.test.launcherhelper; 17 18 import android.graphics.Rect; 19 import android.os.RemoteException; 20 import android.os.SystemClock; 21 import android.support.test.uiautomator.By; 22 import android.support.test.uiautomator.BySelector; 23 import android.support.test.uiautomator.Direction; 24 import android.support.test.uiautomator.UiDevice; 25 import android.support.test.uiautomator.UiObject2; 26 import android.support.test.uiautomator.Until; 27 import android.util.Log; 28 29 /** 30 * A helper class for generic launcher interactions that can be abstracted across different types 31 * of launchers. 32 * 33 */ 34 public class CommonLauncherHelper { 35 36 private static final String LOG_TAG = CommonLauncherHelper.class.getSimpleName(); 37 private static final int MAX_SCROLL_ATTEMPTS = 40; 38 private static final int MIN_INTERACT_SIZE = 100; 39 private static final int APP_LAUNCH_TIMEOUT = 10000; 40 private static CommonLauncherHelper sInstance; 41 private UiDevice mDevice; 42 43 private CommonLauncherHelper(UiDevice uiDevice) { 44 mDevice = uiDevice; 45 } 46 47 /** 48 * Retrieves the singleton instance of {@link CommonLauncherHelper} 49 * @param uiDevice 50 * @return 51 */ 52 public static CommonLauncherHelper getInstance(UiDevice uiDevice) { 53 if (sInstance == null) { 54 sInstance = new CommonLauncherHelper(uiDevice); 55 } 56 return sInstance; 57 } 58 59 /** 60 * Scrolls a container back to the beginning 61 * @param container 62 * @param backDirection 63 */ 64 public void scrollBackToBeginning(UiObject2 container, Direction backDirection) { 65 scrollBackToBeginning(container, backDirection, MAX_SCROLL_ATTEMPTS); 66 } 67 68 /** 69 * Scrolls a container back to the beginning 70 * @param container 71 * @param backDirection 72 * @param maxAttempts 73 */ 74 public void scrollBackToBeginning(UiObject2 container, Direction backDirection, int maxAttempts) { 75 int attempts = 0; 76 while (container.scroll(backDirection, 0.25f)) { 77 attempts++; 78 if (attempts > maxAttempts) { 79 throw new RuntimeException( 80 "scrollBackToBeginning: exceeded max attempts: " + maxAttempts); 81 } 82 } 83 } 84 85 /** 86 * Ensures that the described widget has enough visible portion by scrolling its container if 87 * necessary 88 * @param app 89 * @param container 90 * @param dir 91 */ 92 private void ensureIconVisible(BySelector app, UiObject2 container, Direction dir) { 93 UiObject2 appIcon = mDevice.findObject(app); 94 Rect appR = appIcon.getVisibleBounds(); 95 Rect containerR = container.getVisibleBounds(); 96 int size = 0; 97 int containerSize = 0; 98 if (Direction.DOWN.equals(dir) || Direction.UP.equals(dir)) { 99 size = appR.height(); 100 containerSize = containerR.height(); 101 } else { 102 size = appR.width(); 103 containerSize = containerR.width(); 104 } 105 if (size < MIN_INTERACT_SIZE) { 106 // try to figure out how much percentage of the container needs to be scrolled in order 107 // to reveal the app icon to have the MIN_INTERACT_SIZE 108 float pct = ((float)(MIN_INTERACT_SIZE - size)) / containerSize; 109 if (pct < 0.2f) { 110 pct = 0.2f; 111 } 112 container.scroll(dir, pct); 113 } 114 } 115 116 /** 117 * Triggers app launch by interacting with its launcher icon as described, optionally verify 118 * that the frontend UI has the expected app package name 119 * @param launcherStrategy 120 * @param app 121 * @param packageName 122 * @return 123 */ 124 public long launchApp(ILauncherStrategy launcherStrategy, BySelector app, 125 String packageName) { 126 return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS); 127 } 128 129 /** 130 * Triggers app launch by interacting with its launcher icon as described, optionally verify 131 * that the frontend UI has the expected app package name 132 * @param launcherStrategy 133 * @param app 134 * @param packageName 135 * @param maxScrollAttempts 136 * @return the SystemClock#uptimeMillis timestamp just before launching the application. 137 */ 138 public long launchApp(ILauncherStrategy launcherStrategy, BySelector app, 139 String packageName, int maxScrollAttempts) { 140 unlockDeviceIfAsleep(); 141 142 if (isAppOpen(packageName)) { 143 // Application is already open 144 return 0; 145 } 146 147 // Go to the home page 148 launcherStrategy.open(); 149 Direction dir = launcherStrategy.getAllAppsScrollDirection(); 150 // attempt to find the app icon if it's not already on the screen 151 if (!mDevice.hasObject(app)) { 152 UiObject2 container = launcherStrategy.openAllApps(false); 153 154 if (!mDevice.hasObject(app)) { 155 scrollBackToBeginning(container, Direction.reverse(dir)); 156 int attempts = 0; 157 while (!mDevice.hasObject(app) && container.scroll(dir, 0.8f)) { 158 attempts++; 159 if (attempts > maxScrollAttempts) { 160 throw new RuntimeException( 161 "launchApp: exceeded max attempts to locate app icon: " 162 + maxScrollAttempts); 163 } 164 } 165 } 166 // HACK-ish: ensure icon has enough parts revealed for it to be clicked on 167 ensureIconVisible(app, container, dir); 168 } 169 170 long ready = SystemClock.uptimeMillis(); 171 if (!mDevice.findObject(app).clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT)) { 172 Log.w(LOG_TAG, "no new window detected after app launch attempt."); 173 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP; 174 } 175 mDevice.waitForIdle(); 176 if (packageName != null) { 177 Log.w(LOG_TAG, String.format( 178 "No UI element with package name %s detected.", packageName)); 179 boolean success = mDevice.wait(Until.hasObject( 180 By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT); 181 if (success) { 182 return ready; 183 } else { 184 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP; 185 } 186 } else { 187 return ready; 188 } 189 } 190 191 private boolean isAppOpen (String appPackage) { 192 return mDevice.hasObject(By.pkg(appPackage).depth(0)); 193 } 194 195 private void unlockDeviceIfAsleep () { 196 // Turn screen on if necessary 197 try { 198 if (!mDevice.isScreenOn()) { 199 mDevice.wakeUp(); 200 } 201 } catch (RemoteException e) { 202 Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e); 203 } 204 // Check for lock screen element 205 if (mDevice.hasObject(By.res("com.android.systemui", "keyguard_bottom_area"))) { 206 mDevice.pressMenu(); 207 } 208 } 209 } 210