1 /* 2 * Copyright (C) 2017 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.system.helpers; 18 19 import android.app.Instrumentation; 20 import android.content.ComponentName; 21 import android.content.Intent; 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.support.test.uiautomator.By; 25 import android.support.test.uiautomator.UiDevice; 26 import android.support.test.uiautomator.UiObject2; 27 import android.support.test.uiautomator.UiObjectNotFoundException; 28 import android.support.test.uiautomator.UiScrollable; 29 import android.support.test.uiautomator.UiSelector; 30 import android.support.test.uiautomator.Until; 31 import android.util.Log; 32 33 import java.util.List; 34 35 /** 36 * Implement common helper functions for Accessibility scanner. 37 */ 38 public class AccessibilityScannerHelper { 39 public static final String ACCESSIBILITY_SCANNER_PACKAGE 40 = "com.google.android.apps.accessibility.auditor"; 41 public static final String MAIN_ACTIVITY_CLASS = "%s.ui.MainActivity"; 42 public static final String CHECK_BUTTON_RES_ID = "accessibilibutton"; 43 private static final int SCANNER_WAIT_TIME = 5000; 44 private static final int SHORT_TIMEOUT = 2000; 45 private static final String LOG_TAG = AccessibilityScannerHelper.class.getSimpleName(); 46 private static final String RESULT_TAG = "A11Y_SCANNER_RESULT"; 47 public static AccessibilityScannerHelper sInstance = null; 48 private UiDevice mDevice = null; 49 private ActivityHelper mActivityHelper = null; 50 private PackageHelper mPackageHelper = null; 51 private AccessibilityHelper mAccessibilityHelper = null; 52 AccessibilityScannerHelper(Instrumentation instr)53 private AccessibilityScannerHelper(Instrumentation instr) { 54 mDevice = UiDevice.getInstance(instr); 55 mActivityHelper = ActivityHelper.getInstance(); 56 mPackageHelper = PackageHelper.getInstance(instr); 57 mAccessibilityHelper = AccessibilityHelper.getInstance(instr); 58 } 59 getInstance(Instrumentation instr)60 public static AccessibilityScannerHelper getInstance(Instrumentation instr) { 61 if (sInstance == null) { 62 sInstance = new AccessibilityScannerHelper(instr); 63 } 64 return sInstance; 65 } 66 67 /** 68 * If accessibility scanner installed. 69 * 70 * @return true/false 71 */ scannerInstalled()72 public boolean scannerInstalled() { 73 return mPackageHelper.isPackageInstalled(ACCESSIBILITY_SCANNER_PACKAGE); 74 } 75 76 /** 77 * Click scanner check button and parse and log results. 78 * 79 * @param resultPrefix 80 * @throws Exception 81 */ runScanner(String resultPrefix)82 public void runScanner(String resultPrefix) throws Exception { 83 int tries = 3; // retries 84 while (tries-- > 0) { 85 try { 86 clickScannerCheck(); 87 logScannerResult(resultPrefix); 88 break; 89 } catch (UiObjectNotFoundException e) { 90 continue; 91 } catch (Exception e) { 92 throw e; 93 } 94 } 95 } 96 97 /** 98 * Click scanner check button and open share app in the share menu. 99 * 100 * @param resultPrefix 101 * @param shareAppTag 102 * @throws Exception 103 */ runScannerAndOpenShareApp(String resultPrefix, String shareAppTag)104 public void runScannerAndOpenShareApp(String resultPrefix, String shareAppTag) 105 throws Exception { 106 runScanner(resultPrefix); 107 UiObject2 shareApp = getShareApp(shareAppTag); 108 if (shareApp != null) { 109 shareApp.click(); 110 } 111 } 112 113 /** 114 * Set Accessibility Scanner setting ON/OFF. 115 * 116 * @throws Exception 117 */ setAccessibilityScannerSetting(AccessibilityHelper.SwitchStatus value)118 public void setAccessibilityScannerSetting(AccessibilityHelper.SwitchStatus value) 119 throws Exception { 120 if (!scannerInstalled()) { 121 throw new Exception("Accessibility Scanner not installed."); 122 } 123 mAccessibilityHelper.launchSpecificAccessibilitySetting("Accessibility Scanner"); 124 for (int tries = 0; tries < 2; tries++) { 125 UiObject2 swt = mDevice.wait(Until.findObject( 126 By.res(AccessibilityHelper.SETTINGS_PACKAGE, "switch_widget")), 127 SHORT_TIMEOUT * 2); 128 if (swt.getText().equals(value.toString())) { 129 break; 130 } else if (tries == 1) { 131 throw new Exception(String.format("Fail to set scanner to: %s.", value.toString())); 132 } else { 133 swt.click(); 134 UiObject2 okBtn = mDevice.wait(Until.findObject(By.text("OK")), SHORT_TIMEOUT); 135 if (okBtn != null) { 136 okBtn.click(); 137 } 138 if (initialSetups()) { 139 mDevice.pressBack(); 140 } 141 grantPermissions(); 142 } 143 } 144 } 145 146 /** 147 * Click through all permission pop ups for scanner. Grant all necessary permissions. 148 */ grantPermissions()149 private void grantPermissions() { 150 UiObject2 auth1 = mDevice.wait(Until.findObject( 151 By.text("BEGIN AUTHORIZATION")), SHORT_TIMEOUT); 152 if (auth1 != null) { 153 auth1.click(); 154 } 155 UiObject2 chk = mDevice.wait(Until.findObject( 156 By.clazz(AccessibilityHelper.CHECK_BOX)), SHORT_TIMEOUT); 157 if (chk != null) { 158 chk.click(); 159 mDevice.findObject(By.text("START NOW")).click(); 160 } 161 UiObject2 auth2 = mDevice.wait(Until.findObject( 162 By.text("BEGIN AUTHORIZATION")), SHORT_TIMEOUT); 163 if (auth2 != null) { 164 auth2.click(); 165 } 166 UiObject2 tapOk = mDevice.wait(Until.findObject( 167 By.pkg(ACCESSIBILITY_SCANNER_PACKAGE).text("OK")), SHORT_TIMEOUT); 168 if (tapOk != null) { 169 tapOk.click(); 170 } 171 } 172 173 /** 174 * Launch accessibility scanner. 175 * 176 * @throws UiObjectNotFoundException 177 */ launchScannerApp()178 public void launchScannerApp() throws Exception { 179 Intent intent = new Intent(Intent.ACTION_MAIN); 180 ComponentName settingComponent = new ComponentName(ACCESSIBILITY_SCANNER_PACKAGE, 181 String.format(MAIN_ACTIVITY_CLASS, ACCESSIBILITY_SCANNER_PACKAGE)); 182 intent.setComponent(settingComponent); 183 mActivityHelper.launchIntent(intent); 184 initialSetups(); 185 } 186 187 /** 188 * Steps for first time launching scanner app. 189 * 190 * @return true/false return false immediately, if initial setup screen doesn't show up. 191 * @throws Exception 192 */ initialSetups()193 private boolean initialSetups() throws Exception { 194 UiObject2 getStartBtn = mDevice.wait( 195 Until.findObject(By.text("GET STARTED")), SHORT_TIMEOUT); 196 if (getStartBtn != null) { 197 getStartBtn.click(); 198 UiObject2 msg = mDevice.wait(Until.findObject( 199 By.text("Turn on Accessibility Scanner")), SHORT_TIMEOUT); 200 if (msg != null) { 201 mDevice.findObject(By.text("OK")).click(); 202 setAccessibilityScannerSetting(AccessibilityHelper.SwitchStatus.ON); 203 } 204 mDevice.wait(Until.findObject(By.text("OK, GOT IT")), SCANNER_WAIT_TIME).click(); 205 mDevice.wait(Until.findObject(By.text("DISMISS")), SHORT_TIMEOUT).click(); 206 return true; 207 } else { 208 return false; 209 } 210 } 211 212 /** 213 * Clear history of accessibility scanner. 214 * 215 * @throws InterruptedException 216 */ clearHistory()217 public void clearHistory() throws Exception { 218 launchScannerApp(); 219 int maxTry = 20; 220 while (maxTry > 0) { 221 List<UiObject2> historyItemList = mDevice.findObjects( 222 By.res(ACCESSIBILITY_SCANNER_PACKAGE, "history_item_row")); 223 if (historyItemList.size() == 0) { 224 break; 225 } 226 historyItemList.get(0).click(); 227 Thread.sleep(SHORT_TIMEOUT); 228 deleteHistory(); 229 Thread.sleep(SHORT_TIMEOUT); 230 maxTry--; 231 } 232 } 233 234 /** 235 * Log results of accessibility scanner. 236 * 237 * @param pageName 238 * @throws Exception 239 */ logScannerResult(String pageName)240 public void logScannerResult(String pageName) throws Exception { 241 int res = getNumberOfSuggestions(); 242 if (res > 0) { 243 Log.i(RESULT_TAG, String.format("%s: %s suggestions!", pageName, res)); 244 } else if (res == 0) { 245 Log.i(RESULT_TAG, String.format("%s: Pass.", pageName)); 246 } else { 247 throw new UiObjectNotFoundException("Fail to get number of suggestions."); 248 } 249 } 250 251 /** 252 * Move scanner button to avoid blocking the object. 253 * 254 * @param avoidObj object to move the check button away from 255 */ adjustScannerButton(UiObject2 avoidObj)256 public void adjustScannerButton(UiObject2 avoidObj) 257 throws UiObjectNotFoundException, InterruptedException { 258 Rect origBounds = getScannerCheckBtn().getVisibleBounds(); 259 Rect avoidBounds = avoidObj.getVisibleBounds(); 260 if (origBounds.intersect(avoidBounds)) { 261 Point dest = calculateDest(origBounds, avoidBounds); 262 moveScannerCheckButton(dest.x, dest.y); 263 } 264 } 265 266 /** 267 * Move scanner check button back to the middle of the screen. 268 */ resetScannerCheckButton()269 public void resetScannerCheckButton() throws UiObjectNotFoundException, InterruptedException { 270 int midY = (int) Math.ceil(mDevice.getDisplayHeight() * 0.5); 271 int midX = (int) Math.ceil(mDevice.getDisplayWidth() * 0.5); 272 moveScannerCheckButton(midX, midY); 273 } 274 275 /** 276 * Move scanner check button to a target location. 277 * 278 * @param locX target location x-axis 279 * @param locY target location y-axis 280 * @throws UiObjectNotFoundException 281 */ moveScannerCheckButton(int locX, int locY)282 public void moveScannerCheckButton(int locX, int locY) 283 throws UiObjectNotFoundException, InterruptedException { 284 int tries = 2; 285 while (tries-- > 0) { 286 UiObject2 btn = getScannerCheckBtn(); 287 Rect bounds = btn.getVisibleBounds(); 288 int origX = bounds.centerX(); 289 int origY = bounds.centerY(); 290 int buttonWidth = bounds.width(); 291 int buttonHeight = bounds.height(); 292 if (Math.abs(locX - origX) > buttonWidth || Math.abs(locY - origY) > buttonHeight) { 293 btn.drag(new Point(locX, locY)); 294 } 295 Thread.sleep(SCANNER_WAIT_TIME); 296 // drag cause a click on the scanner button, bring the UI into scanner app 297 if (getScannerCheckBtn() == null 298 && mDevice.findObject(By.pkg(ACCESSIBILITY_SCANNER_PACKAGE)) != null) { 299 mDevice.pressBack(); 300 } else { 301 break; 302 } 303 } 304 } 305 306 /** 307 * Calculate the moving destination of check button. 308 * 309 * @param origRect original bounds of the check button 310 * @param avoidRect bounds to move away from 311 * @return destination of check button center point. 312 */ calculateDest(Rect origRect, Rect avoidRect)313 private Point calculateDest(Rect origRect, Rect avoidRect) { 314 int bufferY = (int)Math.ceil(mDevice.getDisplayHeight() * 0.1); 315 int destY = avoidRect.bottom + bufferY + origRect.height()/2; 316 if (destY >= mDevice.getDisplayHeight()) { 317 destY = avoidRect.top - bufferY - origRect.height()/2; 318 } 319 return new Point(origRect.centerX(), destY); 320 } 321 322 /** 323 * Return scanner check button. 324 * 325 * @return UiObject2 326 */ getScannerCheckBtn()327 private UiObject2 getScannerCheckBtn() { 328 return mDevice.findObject(By.res(ACCESSIBILITY_SCANNER_PACKAGE, CHECK_BUTTON_RES_ID)); 329 } 330 clickScannerCheck()331 private void clickScannerCheck() throws UiObjectNotFoundException, InterruptedException { 332 UiObject2 accessibilityScannerButton = getScannerCheckBtn(); 333 if (accessibilityScannerButton != null) { 334 accessibilityScannerButton.click(); 335 } else { 336 // TODO: check if app crash error, restart scanner service 337 Log.i(LOG_TAG, "Fail to find accessibility scanner check button."); 338 throw new UiObjectNotFoundException( 339 "Fail to find accessibility scanner check button."); 340 } 341 Thread.sleep(SCANNER_WAIT_TIME); 342 } 343 344 /** 345 * Check if no suggestion. 346 * @deprecated Use {@link #getNumberOfSuggestions} instead 347 */ 348 @Deprecated testPass()349 private Boolean testPass() throws UiObjectNotFoundException { 350 UiObject2 txtView = getToolBarTextView(); 351 return txtView.getText().equals("No suggestions"); 352 } 353 354 /** 355 * Return accessibility scanner tool bar text view. 356 * 357 * @return UiObject2 358 * @throws UiObjectNotFoundException 359 */ getToolBarTextView()360 private UiObject2 getToolBarTextView() throws UiObjectNotFoundException { 361 UiObject2 toolBar = mDevice.wait(Until.findObject( 362 By.res(ACCESSIBILITY_SCANNER_PACKAGE, "toolbar")), SHORT_TIMEOUT); 363 if (toolBar != null) { 364 return toolBar.findObject(By.clazz(AccessibilityHelper.TEXT_VIEW)); 365 } else { 366 throw new UiObjectNotFoundException( 367 "Failed to find Scanner tool bar. Scanner app might not be active."); 368 } 369 } 370 371 /** 372 * Delete active scanner history. 373 */ deleteHistory()374 private void deleteHistory() { 375 UiObject2 moreBtn = mDevice.wait(Until.findObject(By.desc("More options")), SHORT_TIMEOUT); 376 if (moreBtn != null) { 377 moreBtn.click(); 378 mDevice.wait(Until.findObject( 379 By.clazz(AccessibilityHelper.TEXT_VIEW).text("Delete")), SHORT_TIMEOUT).click(); 380 } 381 } 382 383 /** 384 * Return number suggestions. 385 * 386 * @return number of suggestions 387 * @throws UiObjectNotFoundException 388 */ getNumberOfSuggestions()389 private int getNumberOfSuggestions() throws UiObjectNotFoundException { 390 int tries = 2; // retries 391 while (tries-- > 0) { 392 UiObject2 txtView = getToolBarTextView(); 393 if (txtView != null) { 394 String result = txtView.getText(); 395 if (result.equals("No suggestions")) { 396 return 0; 397 } else { 398 String str = result.split("\\s+")[0]; 399 return Integer.parseInt(str); 400 } 401 } 402 } 403 Log.i(LOG_TAG, String.format("Error in getting number of suggestions.")); 404 return -1; 405 } 406 407 /** 408 * Return share app UiObject2 409 * 410 * @param appName 411 * @return 412 */ getShareApp(String appName)413 private UiObject2 getShareApp(String appName) throws UiObjectNotFoundException { 414 UiObject2 shareBtn = mDevice.wait(Until.findObject(By.res(ACCESSIBILITY_SCANNER_PACKAGE, 415 "action_share_results")), SHORT_TIMEOUT); 416 if (shareBtn != null) { 417 shareBtn.click(); 418 mDevice.wait(Until.hasObject(By.res("android:id/resolver_list")), SHORT_TIMEOUT * 3); 419 UiScrollable scrollable = new UiScrollable( 420 new UiSelector().className("android.widget.ScrollView")); 421 int tries = 3; 422 while (!mDevice.hasObject(By.text(appName)) && tries-- > 0) { 423 scrollable.scrollForward(); 424 } 425 return mDevice.findObject(By.text(appName)); 426 } 427 return null; 428 } 429 430 /** 431 * Return if scanner enabled by check if home screen has check button. 432 * 433 * @return true/false 434 */ ifScannerEnabled()435 public boolean ifScannerEnabled() throws InterruptedException { 436 mDevice.pressHome(); 437 Thread.sleep(SHORT_TIMEOUT); 438 mDevice.waitForIdle(); 439 return getScannerCheckBtn() != null; 440 } 441 } 442