Added: Root automator, custom theme color, some helps
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
package com.stardust.view.accessibility;
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/3/6.
|
||||
*/
|
||||
|
||||
|
||||
import android.os.Environment;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.Xml;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class AccessibilityNodeInfoDumper {
|
||||
|
||||
private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
|
||||
private static final String[] NAF_EXCLUDED_CLASSES = new String[]{
|
||||
android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
|
||||
android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
|
||||
};
|
||||
|
||||
/**
|
||||
* Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
|
||||
* and generates an xml dump into the /data/local/window_dump.xml
|
||||
*
|
||||
* @param root The root accessibility node.
|
||||
* @param rotation The rotaion of current display
|
||||
* @param width The pixel width of current display
|
||||
* @param height The pixel height of current display
|
||||
*/
|
||||
public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation,
|
||||
int width, int height) {
|
||||
File baseDir = new File(Environment.getDataDirectory(), "local");
|
||||
if (!baseDir.exists()) {
|
||||
baseDir.mkdir();
|
||||
baseDir.setExecutable(true, false);
|
||||
baseDir.setWritable(true, false);
|
||||
baseDir.setReadable(true, false);
|
||||
}
|
||||
dumpWindowToFile(root,
|
||||
new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"),
|
||||
rotation, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
|
||||
* and generates an xml dump to the location specified by <code>dumpFile</code>
|
||||
*
|
||||
* @param root The root accessibility node.
|
||||
* @param dumpFile The file to dump to.
|
||||
* @param rotation The rotaion of current display
|
||||
* @param width The pixel width of current display
|
||||
* @param height The pixel height of current display
|
||||
*/
|
||||
public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
|
||||
int width, int height) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
final long startTime = SystemClock.uptimeMillis();
|
||||
try {
|
||||
FileWriter writer = new FileWriter(dumpFile);
|
||||
XmlSerializer serializer = Xml.newSerializer();
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
serializer.setOutput(stringWriter);
|
||||
serializer.startDocument("UTF-8", true);
|
||||
serializer.startTag("", "hierarchy");
|
||||
serializer.attribute("", "rotation", Integer.toString(rotation));
|
||||
dumpNodeRec(root, serializer, 0, width, height);
|
||||
serializer.endTag("", "hierarchy");
|
||||
serializer.endDocument();
|
||||
writer.write(stringWriter.toString());
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(LOGTAG, "failed to dump window to file", e);
|
||||
}
|
||||
final long endTime = SystemClock.uptimeMillis();
|
||||
Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
|
||||
}
|
||||
|
||||
private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer, int index,
|
||||
int width, int height) throws IOException {
|
||||
serializer.startTag("", "node");
|
||||
if (!nafExcludedClass(node) && !nafCheck(node))
|
||||
serializer.attribute("", "NAF", Boolean.toString(true));
|
||||
serializer.attribute("", "index", Integer.toString(index));
|
||||
serializer.attribute("", "text", safeCharSeqToString(node.getText()));
|
||||
serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
|
||||
serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
|
||||
serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
|
||||
serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
|
||||
serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
|
||||
serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
|
||||
serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
|
||||
serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
|
||||
serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
|
||||
serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
|
||||
serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
|
||||
serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
|
||||
serializer.attribute("", "password", Boolean.toString(node.isPassword()));
|
||||
serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
|
||||
serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
|
||||
node, width, height).toShortString());
|
||||
int count = node.getChildCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
AccessibilityNodeInfo child = node.getChild(i);
|
||||
if (child != null) {
|
||||
if (child.isVisibleToUser()) {
|
||||
dumpNodeRec(child, serializer, i, width, height);
|
||||
child.recycle();
|
||||
} else {
|
||||
Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
|
||||
}
|
||||
} else {
|
||||
Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
|
||||
i, count, node.toString()));
|
||||
}
|
||||
}
|
||||
serializer.endTag("", "node");
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of classes to exclude my not be complete. We're attempting to
|
||||
* only reduce noise from standard layout classes that may be falsely
|
||||
* configured to accept clicks and are also enabled.
|
||||
*
|
||||
* @param node
|
||||
* @return true if node is excluded.
|
||||
*/
|
||||
private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
|
||||
String className = safeCharSeqToString(node.getClassName());
|
||||
for (String excludedClassName : NAF_EXCLUDED_CLASSES) {
|
||||
if (className.endsWith(excludedClassName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* We're looking for UI controls that are enabled, clickable but have no
|
||||
* text nor content-description. Such controls configuration indicate an
|
||||
* interactive control is present in the UI and is most likely not
|
||||
* accessibility friendly. We refer to such controls here as NAF controls
|
||||
* (Not Accessibility Friendly)
|
||||
*
|
||||
* @param node
|
||||
* @return false if a node fails the check, true if all is OK
|
||||
*/
|
||||
private static boolean nafCheck(AccessibilityNodeInfo node) {
|
||||
boolean isNaf = node.isClickable() && node.isEnabled()
|
||||
&& safeCharSeqToString(node.getContentDescription()).isEmpty()
|
||||
&& safeCharSeqToString(node.getText()).isEmpty();
|
||||
|
||||
if (!isNaf)
|
||||
return true;
|
||||
|
||||
// check children since sometimes the containing element is clickable
|
||||
// and NAF but a child's text or description is available. Will assume
|
||||
// such layout as fine.
|
||||
return childNafCheck(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* This should be used when it's already determined that the node is NAF and
|
||||
* a further check of its children is in order. A node maybe a container
|
||||
* such as LinerLayout and may be set to be clickable but have no text or
|
||||
* content description but it is counting on one of its children to fulfill
|
||||
* the requirement for being accessibility friendly by having one or more of
|
||||
* its children fill the text or content-description. Such a combination is
|
||||
* considered by this dumper as acceptable for accessibility.
|
||||
*
|
||||
* @param node
|
||||
* @return false if node fails the check.
|
||||
*/
|
||||
private static boolean childNafCheck(AccessibilityNodeInfo node) {
|
||||
int childCount = node.getChildCount();
|
||||
for (int x = 0; x < childCount; x++) {
|
||||
AccessibilityNodeInfo childNode = node.getChild(x);
|
||||
|
||||
if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
|
||||
|| !safeCharSeqToString(childNode.getText()).isEmpty())
|
||||
return true;
|
||||
|
||||
if (childNafCheck(childNode))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String safeCharSeqToString(CharSequence cs) {
|
||||
if (cs == null)
|
||||
return "";
|
||||
else {
|
||||
return stripInvalidXMLChars(cs);
|
||||
}
|
||||
}
|
||||
|
||||
private static String stripInvalidXMLChars(CharSequence cs) {
|
||||
StringBuffer ret = new StringBuffer();
|
||||
char ch;
|
||||
/* http://www.w3.org/TR/xml11/#charsets
|
||||
[#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
|
||||
[#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
|
||||
[#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
|
||||
[#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
|
||||
[#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
|
||||
[#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
|
||||
[#x10FFFE-#x10FFFF].
|
||||
*/
|
||||
for (int i = 0; i < cs.length(); i++) {
|
||||
ch = cs.charAt(i);
|
||||
|
||||
if ((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
|
||||
(ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
|
||||
(ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
|
||||
(ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
|
||||
(ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
|
||||
(ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
|
||||
(ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
|
||||
(ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
|
||||
(ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
|
||||
(ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
|
||||
(ch >= 0x10FFFE && ch <= 0x10FFFF))
|
||||
ret.append(".");
|
||||
else
|
||||
ret.append(ch);
|
||||
}
|
||||
return ret.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.stardust.view.accessibility;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/3/6.
|
||||
*/
|
||||
|
||||
public class AccessibilityNodeInfoHelper {
|
||||
|
||||
/**
|
||||
* Returns the node's bounds clipped to the size of the display
|
||||
*
|
||||
* @param node
|
||||
* @param width pixel width of the display
|
||||
* @param height pixel height of the display
|
||||
* @return null if node is null, else a Rect containing visible bounds
|
||||
*/
|
||||
public static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
// targeted node's bounds
|
||||
Rect nodeRect = new Rect();
|
||||
node.getBoundsInScreen(nodeRect);
|
||||
|
||||
Rect displayRect = new Rect();
|
||||
displayRect.top = 0;
|
||||
displayRect.left = 0;
|
||||
displayRect.right = width;
|
||||
displayRect.bottom = height;
|
||||
|
||||
boolean intersect = nodeRect.intersect(displayRect);
|
||||
return nodeRect;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.stardust.view.accessibility;
|
||||
|
||||
import android.accessibilityservice.AccessibilityService;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -7,8 +8,11 @@ import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.stardust.scriptdroid.App;
|
||||
import com.stardust.scriptdroid.Pref;
|
||||
import com.stardust.scriptdroid.R;
|
||||
import com.stardust.scriptdroid.service.AccessibilityWatchDogService;
|
||||
import com.stardust.scriptdroid.tool.Shell;
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/1/26.
|
||||
@@ -23,7 +27,7 @@ public class AccessibilityServiceUtils {
|
||||
context.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}
|
||||
|
||||
public static boolean isAccessibilityServiceEnabled(Context context, Class<?> accessibilityService) {
|
||||
public static boolean isAccessibilityServiceEnabled(Context context, Class<? extends AccessibilityService> accessibilityService) {
|
||||
ComponentName expectedComponentName = new ComponentName(context, accessibilityService);
|
||||
|
||||
String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
|
||||
@@ -43,4 +47,23 @@ public class AccessibilityServiceUtils {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean enableAccessibilityServiceByRootAndWaitFor(Context context, Class<? extends AccessibilityService> accessibilityService, long timeout) {
|
||||
Shell.execCommand("settings put secure enabled_accessibility_services %accessibility:"
|
||||
+ context.getPackageName() + "/" + accessibilityService.getName(), true);
|
||||
long millis = System.currentTimeMillis();
|
||||
while (true) {
|
||||
if (isAccessibilityServiceEnabled(context, accessibilityService))
|
||||
return true;
|
||||
try {
|
||||
Thread.sleep(300);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (System.currentTimeMillis() - millis >= timeout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user