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