1 /*
2  * Copyright (C) 2019 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.accessibility.cts.common;
18 
19 import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
20 import static android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES;
21 
22 import static androidx.test.InstrumentationRegistry.getContext;
23 import static androidx.test.InstrumentationRegistry.getInstrumentation;
24 
25 import static org.junit.Assert.assertFalse;
26 
27 import android.accessibilityservice.AccessibilityServiceInfo;
28 import android.app.UiAutomation;
29 import android.graphics.Bitmap;
30 import android.graphics.Rect;
31 import android.os.Environment;
32 import android.support.test.uiautomator.Configurator;
33 import android.support.test.uiautomator.UiDevice;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.accessibility.AccessibilityNodeInfo;
37 import android.view.accessibility.AccessibilityWindowInfo;
38 
39 import com.android.compatibility.common.util.BitmapUtils;
40 
41 import java.io.ByteArrayOutputStream;
42 import java.io.File;
43 import java.time.LocalTime;
44 import java.util.HashSet;
45 import java.util.Set;
46 
47 /**
48  * Helper class to dump data for accessibility test cases.
49  *
50  * It can dump {@code dumpsys accessibility}, accessibility node tree to logcat and/or
51  * screenshot for inspect later.
52  */
53 public class AccessibilityDumper {
54     private static final String TAG = "AccessibilityDumper";
55 
56     /** Dump flag to write the output of {@code dumpsys accessibility} to logcat. */
57     public static final int FLAG_DUMPSYS = 0x1;
58 
59     /** Dump flag to write the output of {@code uiautomator dump} to logcat. */
60     public static final int FLAG_HIERARCHY = 0x2;
61 
62     /** Dump flag to save the screenshot to external storage. */
63     public static final int FLAG_SCREENSHOT = 0x4;
64 
65     /** Dump flag to write the tree of accessility node info to logcat. */
66     public static final int FLAG_NODETREE = 0x8;
67 
68     /** Default dump flag */
69     public static final int FLAG_DUMP_ALL = FLAG_DUMPSYS | FLAG_HIERARCHY | FLAG_SCREENSHOT;
70 
71     private static AccessibilityDumper sDumper;
72 
73     private int mFlag;
74 
75     /** Screenshot filename */
76     private String mName;
77 
78     /** Root directory matching the directory-key of collector in AndroidTest.xml */
79     private File mRoot;
80 
getInstance()81     public static synchronized AccessibilityDumper getInstance() {
82         if (sDumper == null) {
83             sDumper = new AccessibilityDumper(FLAG_DUMP_ALL);
84         }
85         return sDumper;
86     }
87 
88     /**
89      * Define the directory to dump/clean and initial dump options
90      *
91      * @param flag control what to dump
92      */
AccessibilityDumper(int flag)93     private AccessibilityDumper(int flag) {
94         mRoot = getDumpRoot(getContext().getPackageName());
95         mFlag = flag;
96     }
97 
dump(int flag)98     public void dump(int flag) {
99         final UiAutomation automation = getUiAutomation();
100 
101         if ((flag & FLAG_DUMPSYS) != 0) {
102             dumpsysOnLogcat(automation);
103         }
104         if ((flag & FLAG_HIERARCHY) != 0) {
105             dumpHierarchyOnLogcat();
106         }
107         if ((flag & FLAG_SCREENSHOT) != 0) {
108             dumpScreen(automation);
109         }
110         if ((flag & FLAG_NODETREE) != 0) {
111             dumpAccessibilityNodeTreeOnLogcat(automation);
112         }
113     }
114 
dump()115     void dump() {
116         dump(mFlag);
117     }
118 
setName(String name)119     void setName(String name) {
120         assertNotEmpty(name);
121         mName = name;
122     }
123 
getDumpRoot(String directory)124     private File getDumpRoot(String directory) {
125         return new File(Environment.getExternalStorageDirectory(), directory);
126     }
127 
dumpsysOnLogcat(UiAutomation automation)128     private void dumpsysOnLogcat(UiAutomation automation) {
129         ShellCommandBuilder.create(automation)
130             .addCommandPrintOnLogCat("dumpsys accessibility")
131             .run();
132     }
133 
dumpHierarchyOnLogcat()134     private void dumpHierarchyOnLogcat() {
135         try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {
136             UiDevice.getInstance(getInstrumentation()).dumpWindowHierarchy(os);
137             Log.w(TAG, "Window hierarchy:");
138             for (String line : os.toString("UTF-8").split("\\n")) {
139                 Log.w(TAG, line);
140             }
141         } catch (Exception e) {
142             Log.e(TAG, "ERROR: unable to dumping hierarchy on logcat", e);
143         }
144     }
145 
dumpScreen(UiAutomation automation)146     private void dumpScreen(UiAutomation automation) {
147         assertNotEmpty(mName);
148         final Bitmap screenshot = automation.takeScreenshot();
149         final String filename = String.format("%s_%s__screenshot.png", mName, LocalTime.now());
150         BitmapUtils.saveBitmap(screenshot, mRoot.toString(), filename);
151     }
152 
153     /** Dump hierarchy compactly and include nodes not visible to user */
dumpAccessibilityNodeTreeOnLogcat(UiAutomation automation)154     private void dumpAccessibilityNodeTreeOnLogcat(UiAutomation automation) {
155         final Set<AccessibilityNodeInfo> roots = new HashSet<>();
156         for (AccessibilityWindowInfo window : automation.getWindows()) {
157             AccessibilityNodeInfo root = window.getRoot();
158             if (root == null) {
159                 Log.w(TAG, String.format("Skipping null root node for window: %s",
160                         window.toString()));
161             } else {
162                 roots.add(root);
163             }
164         }
165         if (roots.isEmpty()) {
166             Log.w(TAG, "No node of windows to dump");
167         } else {
168             Log.w(TAG, "Accessibility nodes hierarchy:");
169             for (AccessibilityNodeInfo root : roots) {
170                 dumpTreeWithPrefix(root, "");
171             }
172         }
173     }
174 
dumpTreeWithPrefix(AccessibilityNodeInfo node, String prefix)175     private static void dumpTreeWithPrefix(AccessibilityNodeInfo node, String prefix) {
176         final StringBuilder nodeText = new StringBuilder(prefix);
177         appendNodeText(nodeText, node);
178         Log.v(TAG, nodeText.toString());
179         final int count = node.getChildCount();
180         for (int i = 0; i < count; i++) {
181             AccessibilityNodeInfo child = node.getChild(i);
182             if (child != null) {
183                 dumpTreeWithPrefix(child, "-" + prefix);
184             } else {
185                 Log.i(TAG, String.format("%sNull child %d/%d", prefix, i, count));
186             }
187         }
188     }
189 
appendNodeText(StringBuilder out, AccessibilityNodeInfo node)190     private static void appendNodeText(StringBuilder out, AccessibilityNodeInfo node) {
191         final CharSequence txt = node.getText();
192         final CharSequence description = node.getContentDescription();
193         final String viewId = node.getViewIdResourceName();
194 
195         if (!TextUtils.isEmpty(description)) {
196             out.append(escape(description));
197         } else if (!TextUtils.isEmpty(txt)) {
198             out.append('"').append(escape(txt)).append('"');
199         }
200         if (!TextUtils.isEmpty(viewId)) {
201             out.append("(").append(viewId).append(")");
202         }
203         out.append("+").append(node.getClassName());
204         out.append("+ \t<");
205         out.append(node.isCheckable()       ? "C" : ".");
206         out.append(node.isChecked()         ? "c" : ".");
207         out.append(node.isClickable()       ? "K" : ".");
208         out.append(node.isEnabled()         ? "E" : ".");
209         out.append(node.isFocusable()       ? "F" : ".");
210         out.append(node.isFocused()         ? "f" : ".");
211         out.append(node.isLongClickable()   ? "L" : ".");
212         out.append(node.isPassword()        ? "P" : ".");
213         out.append(node.isScrollable()      ? "S" : ".");
214         out.append(node.isSelected()        ? "s" : ".");
215         out.append(node.isVisibleToUser()   ? "V" : ".");
216         out.append("> ");
217         final Rect bounds = new Rect();
218         node.getBoundsInScreen(bounds);
219         out.append(bounds.toShortString());
220     }
221 
222     /**
223      * Produce a displayable string from a CharSequence
224      */
escape(CharSequence s)225     private static String escape(CharSequence s) {
226         final StringBuilder out = new StringBuilder();
227         for (int i = 0; i < s.length(); i++) {
228             char c = s.charAt(i);
229             if ((c < 127) || (c == 0xa0) || ((c >= 0x2000) && (c < 0x2070))) {
230                 out.append(c);
231             } else {
232                 out.append("\\u").append(Integer.toHexString(c));
233             }
234         }
235         return out.toString();
236     }
237 
assertNotEmpty(String name)238     private void assertNotEmpty(String name) {
239         assertFalse("Expected non empty name.", TextUtils.isEmpty(name));
240     }
241 
getUiAutomation()242     private UiAutomation getUiAutomation() {
243         // Reuse UiAutomation from UiAutomator with the same flag
244         Configurator.getInstance().setUiAutomationFlags(
245                 FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
246         final UiAutomation automation = getInstrumentation().getUiAutomation(
247                 FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
248         // Dump window info & node tree
249         final AccessibilityServiceInfo info = automation.getServiceInfo();
250         if (info != null && ((info.flags & FLAG_RETRIEVE_INTERACTIVE_WINDOWS) == 0)) {
251             info.flags |= FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
252             automation.setServiceInfo(info);
253         }
254         return automation;
255     }
256 }
257