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