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