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