본 포스팅은 DroidKaigi 2017 ~ AccessibilityServiceを使ってアプリの可能性を広げよう 을 기본으로 번역하여 작성했습니다
제 일본어 실력으로 인하여 오역이나 오타가 발생할 수 있습니다.
門田福男 @litmon
첫 컨퍼런스 발표로 엄청 긴장
하고 있습니다. 상냥한 눈으로
봐주세요
contentDescription
을 읽어준다hint
를 읽어준다이거 스스로 만들 수 있다는 거 알고 계시는가요?
저는 몰랐습니다
이것으로 여러가지가 될 것 같지 않나요?
Hello Accessibility Service
필요한 작업은 아래와 같다
이것뿐!
MyAccessibilityService.java
public class MyAccessibilityService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent e) {
// accessibility service 이벤트를 얻었을 때 호출
}
@Override
public void onInterrupt() {
// accessibility servic이 예외로 멈췄을 때 호출
}
}
AndroidManifest.xml
<application>
<service android:name=".MyAccessibilityService">
<intent-filter>
<!-- action 설정 -->
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<!-- AccessibilityService 설정을 xml에 작성 -->
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
res/xml/accessibility_service_config.xml
<accessibility-service
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackAllMask"
android:accessibilityFlags="flagDefault"
android:notificationTimeout="100"
android:description="My Accessibility Service"
/>
<accessibility-service
android:accessibilityEventTypes="typeWindowStateChanged"
typeAllMask
로 해둔다<accessibility-service
android:accessibilityFeedbackType="feedbackAllMask"
feedbackAllMask
`<accessibility-service
android:accessibilityFlags="flagDefault"
MyAccessibilityService.java
@Override
protected void onServiceConnected() {
// AccessibilityService 를 ON으로 한 타이밍에 호출된다
AccessibilityServiceInfo serviceInfo = AccessibilityServiceInfo();
// ... some settings
serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
setServiceInfo(serviceInfo);
}
(앱을 설치한 상태에서)
설정 앱 > System > Accessibility > Services > 자신의 앱에서 Accessibility Service를 켠다
MyAccessibilityService.java
@Override
protected void onAccessibilityEvent(AccessibilityEvent event) {
// 사용자 조작 이벤트가 온다
// 조작 대상의 View의 정보가 담긴 객체 취득
AccessibilityNodeInfo nodeInfo = event.getSource();
// 반드시 null 체크를 할 것! (화면 이동 등의 타이밍에 null이 오는 경우가 있다)
if (nodeInfo == null) {
return;
}
// 뒤는 원하는 대로...
}
MyAccessibilityService.java
@Override
protected void onAccessibilityEvent(AccessibilityEvent event) {
// 사용자 조작 이벤트가 온다
// 조작 대상의 View의 정보가 담긴 객체 취득
AccessibilityNodeInfo nodeInfo = event.getSource();
// 뒤는 원하는 대로...
String id = nodeInfo.getViewIdResourceName(); // View의 ID를 취득
String className = nodeInfo.getClassName(); // Class명을 취득
}
<package-name>:id/<id>
형태로 되어있다
com.litmon.app:id/text_view
android:accessibilityFlags="flagReportViewIds"
, android:canRetrieveWindowContent="true"
가 필요여기에서 예상하지 못한 함정이…
event.getSource()
가 얻은 NodeInfo에는 ID가 없는 것 같다(?!)event.getSource().getChildAt(0).getParent()
로 하면 ID가 들어있는 NodeInfo가 얻을 수 있다
event.getSource().getChildAt(0)
로 해도 OKgetChildAt
, getParent
로 얻은 View는 계층이 하나의 Depth이라고는 할 수 없다
flagNotImportantViews
로 안되어 있어서?)public static String getNodeInfoTree(AccessibilityNodeInfo node) {
if (node != null && node.getParent() != null) {
AccessibilityNodeInfo parentNode = node.getParent();
// 재귀적으로 같은 Nodeinfo를 찾는다
node = findNodeInfoTree(parentNode, node);
}
if (node != null && node.getChildCount() > 0) {
AccessibilityNodeInfo childNode = node.getChild(0);
if (childNode != null) {
AccessibilityNodeInfo parentNode = childNode.getParent();
// 거슬러 올라가 동일한 parentNode를 찾는다
while (parentNode.hasCode() != node.hasCode()) {
parentNode = parentNode.getParent();
}
node = parentNode;
}
}
return getNodeInfoTree(node, 0);
}
MyAccessibilityService.java
@Override
protected void onAccessibilityEvent(AccessibilityEvent event) {
// 사용자 조작 이벤트가 온다
// 조작 대상의 View의 정보가 담긴 객체 취득
AccessibilityNodeInfo nodeInfo = event.getSource();
// 뒤는 원하는 대로...
Rect rect = new Rect();
nodeInfo.getBoundsInParent(rect);
int w = rect.width(); // View 사이즈(px)를 취득
int h = rect.height();
}
MyAccessibilityService.java
@Override
protected void onAccessibilityEvent(AccessibilityEvent event) {
// 사용자 조작 이벤트가 온다
// 조작 대상의 View의 정보가 담긴 객체 취득
AccessibilityNodeInfo nodeInfo = event.getSource();
// 뒤는 원하는 대로...
// 부모 View의 NodeInfo를 취득
AccessibilityNodeInfo parentNodeInfo = nodeInfo.getParent();
for(int i = 0; i < nodeInfo.getChildCount(); i++) {
// 자식 View의 NodeInfo를 취득
AccessibilityNodeInfo childNodeInfo = nodeInfo.getChildAt(i);
}
}
MyAccessibilityService.java
@Override
protected void onAccessibilityEvent(AccessibilityEvent event) {
// 사용자 조작 이벤트가 온다
// 조작 대상의 View의 정보가 담긴 객체 취득
AccessibilityNodeInfo nodeInfo = event.getSource();
// 뒤는 원하는 대로...
// 이벤트명을 String으로 취득
String eventType = AccessibilityEvent.eventTypeToString(event.getEventType());
}
MyAccessibilityService.java
@Override
protected void onAccessibilityEvent(AccessibilityEvent event) {
// 사용자 조작 이벤트가 온다
// 조작 대상의 View의 정보가 담긴 객체 취득
AccessibilityNodeInfo nodeInfo = event.getSource();
// 뒤는 원하는 대로...
// 클릭 이벤트를 발생시킨다
nodeInfo.performAction(AccessibilityEvent.TYPE_VIEW_CLICKED);
// 홈 버튼을 누르는 것도 가능
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
}
왠지 이것저것 가능하다!
※ 전부 테스트해보지 않아서 틀릴 수도 있습니다
https://developer.android.com/guide/topics/ui/accessibility/services.html#methods
Accessibility Service를 활성화한다
Accessibility Service를 활성화한다
W: ClassLoader reference unknown path: /data/app/com.litmon
D: onCreate: called
D: onServiceConnected: called
D: onAccessibilityEvent: TYPE_WINDOWS_CHANGED
Accessibility Service를 비활성화한다
Accessibility Service 내에서 예외가 발생
Multi Window 에서의 움직임
AccessibilityService#getRootInActiveWindow()
AccessibilityService#getWindows()
`windowInfo.getRoot()
를 하면 각각의 Window의 Root NodeInfo 가 취득된다id/content
가 포함되는 Window가 Multi Window 의 아래/위의 View 가 되어있다어떻게 도움이 되는가
솔직히, 아이디어 따름이다
그러면 어떻게 할까
디버그용 앱으로서 만들면 되지 않을까?
재미있고 도움이 된다
(사내에서도 그럭저럭 감촉이 좋다)<?xml version="1.0" encoding="UTF-8" ?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:canRetrieveWindowContent="true"
android:accessibilityFeedbackType="feedbackAllMask"
android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews/flagReportViewIds"
android:notificationTimeout="100"
android:description="@string/accessibility_description" />
방침
public AccessibilityNodeInfo findNodeInfoByPoint(AccessibilityNodeInfo nodeInfo, int x, int y) {
if (nodeInfo == null) {
return null;
}
AccessibilityService resultNodeInfo = nodeInfo;
for (int i = 0; i < nodeInfo.getChildCount(); i++) {
AccessibilityNodeInfo childInfo = nodeInfo.getChild(i);
if (childInfo != null && isPointInNodeInfo(childInfo, x, y)) {
AccessibilityNodeInfo foundNodeInfo = findNodeInfoByPoint(childInfo, x, y);
resultNodeInfo = getSmallerNodeInfo(resultNodeInfo, foundNodeInfo);
}
}
return resultNodeInfo;
}
Rect rect = new Rect();
// 화면에 표시된 부분의 좌표를 취득
nodeInfo.getBoundsInScreen(rect);
// 부모 View로부터 본 부분의 좌표를 취득
nodeInfo.getBoundsInParent(rect);
public static boolean isPointInNodeInfo(AccessibilityNodeInfo nodeInfo, int x, int y) {
Rect rect = new Rect();
nodeInfo.getBoundsInScreen(rect);
// 좌표가 View에 포함되어있는가
return rect.contains(x, y);
}
public static AccessibilityNodeInfo getSmallerNodeInfo(AccessibilityNodeInfo leftInfo, AccessibilityNodeInfo rightInfo) {
Rect leftInfoRect = new Rect();
leftInfo.getBoundsInScreen(leftInfoRect);
Rect rightInfoRect = new Rect();
rightInfo.getBoundsInScreen(rightInfoRect);
// 사이즈가 작은 NodeInfo를 취득
return leftInfoRect.width() * leftInfoRect.height() < rightInfoRect.width() * rightInfoRect.height() ? leftInfo : rightInfo;
}
int id = getResources().getIdentifier("status_bar_height", "dimen", "android");
getResources().getDimensionPixelSize(id); // statisBar의 높이
new AccessibilityNodeInfoCompat(nodeInfo)
comments powered by Disqus
Subscribe to this blog via RSS.