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