나만의 접근성 서비스 만들기

접근성 서비스는 장애가 있거나 일시적으로 기기와 완전히 상호작용할 수 없는 사용자를 지원하기 위해 사용자 인터페이스를 향상하는 앱입니다. 예를 들어 운전 중이거나, 어린 자녀를 돌보거나, 매우 시끄러운 파티에 참석하는 사용자에게는 추가 또는 대체 인터페이스 피드백이 필요할 수 있습니다.

Android는 TalkBack을 비롯한 표준 접근성 서비스를 제공하며 개발자는 자체 서비스를 만들고 배포할 수 있습니다. 이 문서에서는 접근성 서비스 빌드의 기본사항을 설명합니다.

접근성 서비스는 일반 앱과 함께 번들로 제공하거나 독립형 Android 프로젝트로 만들 수 있습니다. 서비스를 만드는 단계는 두 경우 모두 동일합니다.

접근성 서비스 만들기

프로젝트 내에서 AccessibilityService를 확장하는 클래스를 만듭니다.

Kotlin

package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent

class MyAccessibilityService : AccessibilityService() {
...
    override fun onInterrupt() {}

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
...
}

Java

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

...
}

Service에 대해 새 프로젝트를 만들고 여기에 연결된 앱을 포함할 계획이 없으면 소스에서 시작 Activity 클래스를 삭제할 수 있습니다.

매니페스트 선언 및 권한

접근성 서비스를 제공하는 앱은 Android 시스템에서 접근성 서비스로 취급할 앱 매니페스트에 특정 선언을 포함해야 합니다. 이 섹션에서는 접근성 서비스의 필수 및 선택적 설정에 관해 설명합니다.

접근성 서비스 선언

앱을 접근성 서비스로 처리하려면 매니페스트의 application 요소 내에 activity 요소가 아닌 service 요소를 포함합니다. 또한 service 요소 내에 접근성 서비스 인텐트 필터를 포함합니다. 또한 매니페스트는 시스템만 서비스에 결합할 수 있도록 BIND_ACCESSIBILITY_SERVICE 권한을 추가하여 이 서비스를 보호해야 합니다. 다음 예를 참조하세요.

  <application>
    <service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:label="@string/accessibility_service_label">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
    </service>
  </application>

접근성 서비스 구성

접근성 서비스는 서비스에서 처리하는 접근성 이벤트의 유형 및 서비스에 관한 추가 정보를 지정하는 구성을 제공해야 합니다. 접근성 서비스의 구성은 AccessibilityServiceInfo 클래스에 포함됩니다. 서비스는 이 클래스의 인스턴스와 런타임에 setServiceInfo()를 사용하여 구성을 빌드하고 설정할 수 있습니다. 그러나 이 메서드를 사용해도 일부 구성 옵션은 사용할 수 없습니다.

다음 예와 같이 구성 파일의 참조와 함께 매니페스트에 <meta-data> 요소를 포함할 수 있습니다. 이렇게 하면 접근성 서비스의 모든 옵션을 설정할 수 있습니다.

<service android:name=".MyAccessibilityService">
  ...
  <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility_service_config" />
</service>

<meta-data> 요소는 앱의 리소스 디렉터리에서 개발자가 만든 XML 파일(<project_dir>/res/xml/accessibility_service_config.xml>)을 참조합니다. 다음 코드는 서비스 구성 파일 콘텐츠의 예시를 보여줍니다.

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:packageNames="com.example.android.apis"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
/>

접근성 서비스 구성 파일에서 사용할 수 있는 XML 속성에 관한 자세한 내용은 다음 참조 문서를 확인하세요.

런타임 시 동적으로 설정될 수 있는 구성 설정에 관한 자세한 내용은 AccessibilityServiceInfo 참조 문서를 확인하세요.

접근성 서비스 구성

접근성 서비스의 구성 변수를 설정할 때 시스템에 실행 방법과 시기를 알리려면 다음 사항을 고려하세요.

  • 어떤 이벤트 유형에 응답하기를 원하시나요?
  • 서비스가 모든 앱에 대해 활성화되어야 하나요? 아니면 특정 패키지 이름에서만 활성화해야 하나요?
  • 서비스에서 사용하는 여러 피드백 유형 등을 알릴 수 있습니다.

이러한 변수를 설정하는 옵션에는 두 가지가 있습니다. 이전 버전과 호환되는 옵션은 setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo)를 사용하여 코드에서 설정하는 것입니다. 이렇게 하려면 다음 예와 같이 onServiceConnected() 메서드를 재정의하고 여기서 서비스를 구성합니다.

Kotlin

override fun onServiceConnected() {
    info.apply {
        // Set the type of events that this service wants to listen to. Others
        // aren't passed to this service.
        eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

        // If you only want this service to work with specific apps, set their
        // package names here. Otherwise, when the service is activated, it
        // listens to events from all apps.
        packageNames = arrayOf("com.example.android.myFirstApp", "com.example.android.mySecondApp")

        // Set the type of feedback your service provides.
        feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN

        // Default services are invoked only if no package-specific services are
        // present for the type of AccessibilityEvent generated. This service is
        // app-specific, so the flag isn't necessary. For a general-purpose
        // service, consider setting the DEFAULT flag.

        // flags = AccessibilityServiceInfo.DEFAULT;

        notificationTimeout = 100
    }

    this.serviceInfo = info

}

Java

@Override
public void onServiceConnected() {
    // Set the type of events that this service wants to listen to. Others
    // aren't passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

    // If you only want this service to work with specific apps, set their
    // package names here. Otherwise, when the service is activated, it listens
    // to events from all apps.
    info.packageNames = new String[]
            {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};

    // Set the type of feedback your service provides.
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;

    // Default services are invoked only if no package-specific services are
    // present for the type of AccessibilityEvent generated. This service is
    // app-specific, so the flag isn't necessary. For a general-purpose service,
    // consider setting the DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

두 번째 옵션은 XML 파일을 사용하여 서비스를 구성하는 것입니다. canRetrieveWindowContent와 같은 특정 구성 옵션은 XML을 사용하여 서비스를 구성하는 경우에만 사용할 수 있습니다. 이전 예의 구성 옵션은 XML을 사용하여 정의할 경우 다음과 같습니다.

<accessibility-service
     android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
     android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp"
     android:accessibilityFeedbackType="feedbackSpoken"
     android:notificationTimeout="100"
     android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity"
     android:canRetrieveWindowContent="true"
/>

XML을 사용하는 경우 XML 파일을 가리키는 <meta-data> 태그를 서비스 선언에 추가하여 매니페스트에서 이 태그를 참조합니다. XML 파일을 res/xml/serviceconfig.xml에 저장하면 새 태그는 다음과 같습니다.

<service android:name=".MyAccessibilityService">
     <intent-filter>
         <action android:name="android.accessibilityservice.AccessibilityService" />
     </intent-filter>
     <meta-data android:name="android.accessibilityservice"
     android:resource="@xml/serviceconfig" />
</service>

접근성 서비스 메서드

접근성 서비스는 AccessibilityService 클래스를 확장하고 이 클래스에서 다음 메서드를 재정의해야 합니다. 이러한 메서드는 서비스가 시작될 때(onServiceConnected()), 실행되는 동안(onAccessibilityEvent(), onInterrupt()), 종료될 때(onUnbind())까지 Android 시스템에서 호출하는 순서로 표시됩니다.

  • onServiceConnected(): (선택사항) 접근성 서비스에 연결할 때 시스템에서 이 메서드를 호출합니다. 오디오 관리자나 기기 진동기와 같은 사용자 피드백 시스템 서비스에 연결하는 등 서비스의 일회성 설정 단계를 실행하려면 이 메서드를 사용하세요. 런타임 시 서비스 구성을 설정하거나 한 번 조정하려는 경우 setServiceInfo()를 호출하기에 편리한 위치입니다.

  • onAccessibilityEvent(): (필수) 사용자가 접근성 서비스에서 의견을 제공하는 앱에서 버튼을 탭하거나 사용자 인터페이스 컨트롤에 포커스를 맞추는 경우와 같이 접근성 서비스에서 지정한 이벤트 필터링 매개변수와 일치하는 AccessibilityEvent를 감지하면 시스템이 이 메서드를 다시 호출합니다. 시스템에서 이 메서드를 호출하면 연결된 AccessibilityEvent를 서비스에서 전달하며, 서비스는 이를 해석하여 사용자에게 의견을 제공하는 데 사용할 수 있습니다. 이 메서드는 서비스 수명 주기 동안 여러 번 호출할 수 있습니다.

  • onInterrupt(): (필수) 일반적으로 포커스를 다른 컨트롤로 이동하는 등의 사용자 작업에 응답하여 시스템에서 서비스가 제공하는 피드백을 중단하려는 경우 시스템에서 이 메서드를 호출합니다. 이 메서드는 서비스 수명 주기 동안 여러 번 호출할 수 있습니다.

  • onUnbind(): (선택사항) 시스템에서 접근성 서비스를 종료하려고 할 때 시스템에서 이 메서드를 호출합니다. 오디오 관리자나 기기 진동기와 같은 사용자 피드백 시스템 서비스의 할당을 해제하는 등 모든 일회성 종료 절차를 실행하는 데 이 메서드를 사용합니다.

이러한 콜백 메서드는 접근성 서비스의 기본 구조를 제공합니다. Android 시스템에서 AccessibilityEvent 객체의 형태로 제공하는 데이터를 처리하고 사용자에게 의견을 제공하는 방법을 결정할 수 있습니다. 접근성 이벤트에서 정보를 가져오는 방법에 관한 자세한 내용은 이벤트 세부정보 가져오기를 참고하세요.

접근성 이벤트 등록

접근성 서비스 구성 매개변수의 가장 중요한 함수 중 하나는 서비스에서 처리할 수 있는 접근성 이벤트 유형을 지정할 수 있도록 하는 것입니다. 이 정보를 지정하면 접근성 서비스가 서로 협력할 수 있으며 특정 앱의 특정 이벤트 유형만 유연하게 처리할 수 있습니다. 이벤트 필터링에는 다음 기준이 포함될 수 있습니다.

  • 패키지 이름: 서비스에서 처리하도록 할 접근성 이벤트가 있는 앱의 패키지 이름을 지정합니다. 이 매개변수를 생략하면 접근성 서비스를 모든 앱의 서비스 접근성 이벤트에 사용할 수 있는 것으로 간주됩니다. 접근성 서비스 구성 파일에서 android:packageNames 속성을 쉼표로 구분된 목록으로 설정하거나 AccessibilityServiceInfo.packageNames 멤버를 사용할 수 있습니다.

  • 이벤트 유형: 서비스에서 처리하도록 할 접근성 이벤트의 유형을 지정합니다. 접근성 서비스 구성 파일에서 android:accessibilityEventTypes 속성을 사용하여 | 문자로 구분된 목록(예: accessibilityEventTypes="typeViewClicked|typeViewFocused")으로 이 매개변수를 설정할 수 있습니다. 또는 AccessibilityServiceInfo.eventTypes 구성원을 사용하여 설정할 수 있습니다.

접근성 서비스를 설정할 때 서비스에서 처리할 수 있는 이벤트를 신중하게 고려하고 이러한 이벤트만 등록하세요. 사용자는 한 번에 두 개 이상의 접근성 서비스를 활성화할 수 있으므로 서비스는 처리할 수 없는 이벤트를 사용해서는 안 됩니다. 다른 서비스에서 이러한 이벤트를 처리하여 사용자 환경을 개선할 수도 있습니다.

접근성 볼륨

Android 8.0 (API 수준 26) 이상을 실행하는 기기에는 기기의 다른 소리와 관계없이 접근성 서비스의 오디오 출력 볼륨을 제어할 수 있게 해주는 STREAM_ACCESSIBILITY 볼륨 카테고리가 포함되어 있습니다.

접근성 서비스는 FLAG_ENABLE_ACCESSIBILITY_VOLUME 옵션을 설정하여 이 스트림 유형을 사용할 수 있습니다. 그런 다음 기기의 AudioManager 인스턴스에서 adjustStreamVolume() 메서드를 호출하여 기기의 접근성 오디오 볼륨을 변경할 수 있습니다.

다음 코드 스니펫은 접근성 서비스에서 STREAM_ACCESSIBILITY 볼륨 카테고리를 사용하는 방법을 보여줍니다.

Kotlin

import android.media.AudioManager.*

class MyAccessibilityService : AccessibilityService() {

    private val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager

    override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent) {
        if (accessibilityEvent.source.text == "Increase volume") {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY, ADJUST_RAISE, 0)
        }
    }
}

Java

import static android.media.AudioManager.*;

public class MyAccessibilityService extends AccessibilityService {
    private AudioManager audioManager =
            (AudioManager) getSystemService(AUDIO_SERVICE);

    @Override
    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
        AccessibilityNodeInfo interactedNodeInfo =
                accessibilityEvent.getSource();
        if (interactedNodeInfo.getText().equals("Increase volume")) {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY,
                ADJUST_RAISE, 0);
        }
    }
}

자세한 내용은 Google I/O 2017의 Android 접근성의 새로운 기능 세션 동영상을 6분 35초부터 참고하세요.

접근성 단축키

Android 8.0 (API 수준 26) 이상을 실행하는 기기에서 사용자는 두 볼륨 키를 동시에 길게 눌러 어느 화면에서나 원하는 접근성 서비스를 사용 설정하거나 사용 중지할 수 있습니다. 이 단축키를 사용하면 기본적으로 TalkBack이 사용 설정 및 중지되지만 사용자는 버튼을 구성하여 기기에 설치된 모든 서비스를 사용 설정 또는 중지할 수 있습니다.

사용자가 접근성 바로가기에서 특정 접근성 서비스에 액세스하려면 서비스가 런타임에 기능을 요청해야 합니다.

자세한 내용은 Google I/O 2017의 Android 접근성의 새로운 기능 세션 동영상을 13분 25초부터 참고하세요.

접근성 버튼

소프트웨어에서 렌더링한 탐색 영역을 사용하고 Android 8.0(API 수준 26) 이상을 실행하는 기기의 경우 탐색 메뉴 오른쪽에 접근성 버튼이 있습니다. 사용자가 이 버튼을 누르면 현재 화면에 표시된 콘텐츠에 따라 사용 설정된 여러 접근성 기능 및 서비스 중 하나를 호출할 수 있습니다.

사용자가 접근성 버튼을 사용하여 특정 접근성 서비스를 호출할 수 있도록 하려면 서비스는 AccessibilityServiceInfo 객체의 android:accessibilityFlags 속성에 FLAG_REQUEST_ACCESSIBILITY_BUTTON 플래그를 추가해야 합니다. 그러면 서비스에서 registerAccessibilityButtonCallback()를 사용하여 콜백을 등록할 수 있습니다.

다음 코드 스니펫은 접근성 버튼을 누른 사용자에게 응답하도록 접근성 서비스를 구성하는 방법을 보여줍니다.

Kotlin

private var mAccessibilityButtonController: AccessibilityButtonController? = null
private var accessibilityButtonCallback:
        AccessibilityButtonController.AccessibilityButtonCallback? = null
private var mIsAccessibilityButtonAvailable: Boolean = false

override fun onServiceConnected() {
    mAccessibilityButtonController = accessibilityButtonController
    mIsAccessibilityButtonAvailable =
            mAccessibilityButtonController?.isAccessibilityButtonAvailable ?: false

    if (!mIsAccessibilityButtonAvailable) return

    serviceInfo = serviceInfo.apply {
        flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON
    }

    accessibilityButtonCallback =
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!")

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            override fun onAvailabilityChanged(
                    controller: AccessibilityButtonController,
                    available: Boolean
            ) {
                if (controller == mAccessibilityButtonController) {
                    mIsAccessibilityButtonAvailable = available
                }
            }
    }

    accessibilityButtonCallback?.also {
        mAccessibilityButtonController?.registerAccessibilityButtonCallback(it, null)
    }
}

Java

private AccessibilityButtonController accessibilityButtonController;
private AccessibilityButtonController
        .AccessibilityButtonCallback accessibilityButtonCallback;
private boolean mIsAccessibilityButtonAvailable;

@Override
protected void onServiceConnected() {
    accessibilityButtonController = getAccessibilityButtonController();
    mIsAccessibilityButtonAvailable =
            accessibilityButtonController.isAccessibilityButtonAvailable();

    if (!mIsAccessibilityButtonAvailable) {
        return;
    }

    AccessibilityServiceInfo serviceInfo = getServiceInfo();
    serviceInfo.flags
            |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
    setServiceInfo(serviceInfo);

    accessibilityButtonCallback =
        new AccessibilityButtonController.AccessibilityButtonCallback() {
            @Override
            public void onClicked(AccessibilityButtonController controller) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!");

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            @Override
            public void onAvailabilityChanged(
              AccessibilityButtonController controller, boolean available) {
                if (controller.equals(accessibilityButtonController)) {
                    mIsAccessibilityButtonAvailable = available;
                }
            }
        };

    if (accessibilityButtonCallback != null) {
        accessibilityButtonController.registerAccessibilityButtonCallback(
                accessibilityButtonCallback, null);
    }
}

자세한 내용은 Google I/O 2017의 Android 접근성의 새로운 기능 세션 동영상을 16분 28초부터 참조하세요.

지문 동작

Android 8.0 (API 수준 26) 이상을 실행하는 기기의 접근성 서비스는 기기의 지문 센서를 따라 방향 스와이프 (위, 아래, 왼쪽, 오른쪽)에 응답할 수 있습니다. 이러한 상호작용에 관한 콜백을 수신하도록 서비스를 구성하려면 다음 순서를 완료하세요.

  1. USE_BIOMETRIC 권한과 CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES 기능을 선언합니다.
  2. android:accessibilityFlags 속성 내에 FLAG_REQUEST_FINGERPRINT_GESTURES 플래그를 설정합니다.
  3. registerFingerprintGestureCallback()를 사용하여 콜백을 등록합니다.

지문 센서는 일부 기기에만 포함되어 있습니다. 기기에서 센서를 지원하는지 확인하려면 isHardwareDetected() 메서드를 사용하세요. 지문 센서가 포함된 기기에서도 서비스에서 인증 목적으로 사용 중일 때는 센서를 사용할 수 없습니다. 센서를 사용할 수 있는 시점을 확인하려면 isGestureDetectionAvailable() 메서드를 호출하고 onGestureDetectionAvailabilityChanged() 콜백을 구현하세요.

다음 코드 스니펫에서는 지문 동작을 사용하여 가상 게임 보드를 탐색하는 예를 보여줍니다.

// AndroidManifest.xml
<manifest ... >
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    ...
    <application>
        <service android:name="com.example.MyFingerprintGestureService" ... >
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/myfingerprintgestureservice" />
        </service>
    </application>
</manifest>
// myfingerprintgestureservice.xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:accessibilityFlags=" ... |flagRequestFingerprintGestures"
    android:canRequestFingerprintGestures="true"
    ... />

Kotlin

// MyFingerprintGestureService.kt
import android.accessibilityservice.FingerprintGestureController.*

class MyFingerprintGestureService : AccessibilityService() {

    private var gestureController: FingerprintGestureController? = null
    private var fingerprintGestureCallback:
            FingerprintGestureController.FingerprintGestureCallback? = null
    private var mIsGestureDetectionAvailable: Boolean = false

    override fun onCreate() {
        gestureController = fingerprintGestureController
        mIsGestureDetectionAvailable = gestureController?.isGestureDetectionAvailable ?: false
    }

    override fun onServiceConnected() {
        if (mFingerprintGestureCallback != null || !mIsGestureDetectionAvailable) return

        fingerprintGestureCallback =
                object : FingerprintGestureController.FingerprintGestureCallback() {
                    override fun onGestureDetected(gesture: Int) {
                        when (gesture) {
                            FINGERPRINT_GESTURE_SWIPE_DOWN -> moveGameCursorDown()
                            FINGERPRINT_GESTURE_SWIPE_LEFT -> moveGameCursorLeft()
                            FINGERPRINT_GESTURE_SWIPE_RIGHT -> moveGameCursorRight()
                            FINGERPRINT_GESTURE_SWIPE_UP -> moveGameCursorUp()
                            else -> Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!")
                        }
                    }

                    override fun onGestureDetectionAvailabilityChanged(available: Boolean) {
                        mIsGestureDetectionAvailable = available
                    }
                }

        fingerprintGestureCallback?.also {
            gestureController?.registerFingerprintGestureCallback(it, null)
        }
    }
}

Java

// MyFingerprintGestureService.java
import static android.accessibilityservice.FingerprintGestureController.*;

public class MyFingerprintGestureService extends AccessibilityService {
    private FingerprintGestureController gestureController;
    private FingerprintGestureController
            .FingerprintGestureCallback fingerprintGestureCallback;
    private boolean mIsGestureDetectionAvailable;

    @Override
    public void onCreate() {
        gestureController = getFingerprintGestureController();
        mIsGestureDetectionAvailable =
                gestureController.isGestureDetectionAvailable();
    }

    @Override
    protected void onServiceConnected() {
        if (fingerprintGestureCallback != null
                || !mIsGestureDetectionAvailable) {
            return;
        }

        fingerprintGestureCallback =
               new FingerprintGestureController.FingerprintGestureCallback() {
            @Override
            public void onGestureDetected(int gesture) {
                switch (gesture) {
                    case FINGERPRINT_GESTURE_SWIPE_DOWN:
                        moveGameCursorDown();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_LEFT:
                        moveGameCursorLeft();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_RIGHT:
                        moveGameCursorRight();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_UP:
                        moveGameCursorUp();
                        break;
                    default:
                        Log.e(MY_APP_TAG,
                                  "Error: Unknown gesture type detected!");
                        break;
                }
            }

            @Override
            public void onGestureDetectionAvailabilityChanged(boolean available) {
                mIsGestureDetectionAvailable = available;
            }
        };

        if (fingerprintGestureCallback != null) {
            gestureController.registerFingerprintGestureCallback(
                    fingerprintGestureCallback, null);
        }
    }
}

자세한 내용은 Google I/O 2017의 Android 접근성의 새로운 기능 세션 동영상(9분 3초부터)을 참고하세요.

다국어 TTS(텍스트 음성 변환)

Android 8.0 (API 수준 26)부터 Android의 텍스트 음성 변환 (TTS) 서비스는 단일 텍스트 블록 내에서 여러 언어로 된 구문을 식별하고 말할 수 있습니다. 접근성 서비스에서 자동 언어 전환 기능을 사용 설정하려면 다음 코드 스니펫과 같이 LocaleSpan 객체에 모든 문자열을 래핑합니다.

Kotlin

val localeWrappedTextView = findViewById<TextView>(R.id.my_french_greeting_text).apply {
    text = wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE)
}

private fun wrapTextInLocaleSpan(originalText: CharSequence, loc: Locale): SpannableStringBuilder {
    return SpannableStringBuilder(originalText).apply {
        setSpan(LocaleSpan(loc), 0, originalText.length - 1, 0)
    }
}

Java

TextView localeWrappedTextView = findViewById(R.id.my_french_greeting_text);
localeWrappedTextView.setText(wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE));

private SpannableStringBuilder wrapTextInLocaleSpan(
        CharSequence originalText, Locale loc) {
    SpannableStringBuilder myLocaleBuilder =
            new SpannableStringBuilder(originalText);
    myLocaleBuilder.setSpan(new LocaleSpan(loc), 0,
            originalText.length() - 1, 0);
    return myLocaleBuilder;
}

자세한 내용은 Google I/O 2017의 Android 접근성의 새로운 기능 세션 동영상을 10분 59초부터 참고하세요.

사용자 대신 행동하기

2011년부터 접근성 서비스는 입력 포커스 변경, 사용자 인터페이스 요소 선택 (활성화) 등 사용자를 대신하여 작동할 수 있습니다. 2012년에는 작업 범위가 목록 스크롤 및 텍스트 필드와의 상호작용을 포함하도록 확장되었습니다. 접근성 서비스는 홈 화면으로 이동, 뒤로 버튼 누르기, 알림 화면 및 최근 앱 목록 열기와 같은 전역 작업을 할 수도 있습니다. 2012년부터 Android에는 접근성 포커스가 포함되어 있어 접근성 서비스에서 표시되는 모든 요소를 선택할 수 있게 했습니다.

이러한 기능을 통해 접근성 서비스 개발자는 동작 탐색과 같은 대체 탐색 모드를 만들고 장애가 있는 사용자의 Android 지원 기기 제어 기능을 개선할 수 있습니다.

동작 수신 대기

접근성 서비스는 특정 동작을 수신 대기하고 사용자를 대신하여 응답할 수 있습니다. 이 기능을 사용하려면 접근성 서비스에서 터치하여 탐색 기능의 활성화를 요청해야 합니다. 다음 예와 같이 서비스에서 서비스 AccessibilityServiceInfo 인스턴스의 flags 멤버를 FLAG_REQUEST_TOUCH_EXPLORATION_MODE로 설정하여 이 활성화를 요청할 수 있습니다.

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onCreate() {
        serviceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {
    @Override
    public void onCreate() {
        getServiceInfo().flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
    }
    ...
}

서비스에서 터치하여 탐색의 활성화를 요청한 후, 아직 활성화되지 않은 경우 사용자는 기능이 사용 설정되도록 허용해야 합니다. 이 기능이 활성화되면 서비스에서 서비스의 onGesture() 콜백 메서드를 통해 접근성 동작 알림을 수신하고 사용자를 대신하여 응답할 수 있습니다.

이어지는 동작

Android 8.0 (API 수준 26)을 실행하는 기기는 이어지는 동작 또는 Path 객체를 둘 이상 포함하는 프로그래매틱 동작을 지원합니다.

획 시퀀스를 지정할 때 다음 코드 스니펫에서와 같이 GestureDescription.StrokeDescription 생성자에서 최종 인수 willContinue를 사용하여 획이 동일한 프로그래매틱 동작에 속하는지 지정할 수 있습니다.

Kotlin

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private fun doRightThenDownDrag() {
    val dragRightPath = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }
    val dragRightDuration = 500L // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    val dragDownPath = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }
    val dragDownDuration = 500L
    val rightThenDownDrag = GestureDescription.StrokeDescription(
            dragRightPath,
            0L,
            dragRightDuration,
            true
    ).apply {
        continueStroke(dragDownPath, dragRightDuration, dragDownDuration, false)
    }
}

Java

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private void doRightThenDownDrag() {
    Path dragRightPath = new Path();
    dragRightPath.moveTo(200, 200);
    dragRightPath.lineTo(400, 200);
    long dragRightDuration = 500L; // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    Path dragDownPath = new Path();
    dragDownPath.moveTo(400, 200);
    dragDownPath.lineTo(400, 400);
    long dragDownDuration = 500L;
    GestureDescription.StrokeDescription rightThenDownDrag =
            new GestureDescription.StrokeDescription(dragRightPath, 0L,
            dragRightDuration, true);
    rightThenDownDrag.continueStroke(dragDownPath, dragRightDuration,
            dragDownDuration, false);
}

자세한 내용은 Google I/O 2017의 Android 접근성의 새로운 기능 세션 동영상을 15분 47초부터 참고하세요.

접근성 작업 사용

접근성 서비스는 사용자 대신 작업을 수행하여 앱과의 상호작용을 간소화하고 생산성을 높일 수 있습니다. 접근성 서비스의 작업 수행 기능은 2011년에 추가되었고 2012년에 크게 확장되었습니다.

사용자를 대신하여 작업하려면 서비스 구성 파일에서 android:canRetrieveWindowContenttrue로 설정하여 접근성 서비스에서 앱의 이벤트를 수신하고 앱 콘텐츠를 볼 수 있는 권한을 요청하도록 등록해야 합니다. 서비스가 이벤트를 수신하면 getSource()를 사용하여 이벤트에서 AccessibilityNodeInfo 객체를 검색할 수 있습니다. 그런 다음 서비스는 AccessibilityNodeInfo 객체로 뷰 계층 구조를 탐색하여 실행할 작업을 결정하고 performAction()를 사용하여 사용자를 위해 조치를 취할 수 있습니다.

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Get the source node of the event.
        event.source?.apply {

            // Use the event and node information to determine what action to
            // take.

            // Act on behalf of the user.
            performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)

            // Recycle the nodeInfo object.
            recycle()
        }
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // Get the source node of the event.
        AccessibilityNodeInfo nodeInfo = event.getSource();

        // Use the event and node information to determine what action to take.

        // Act on behalf of the user.
        nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

        // Recycle the nodeInfo object.
        nodeInfo.recycle();
    }
    ...
}

performAction() 메서드를 사용하면 서비스가 앱 내에서 작업을 실행할 수 있습니다. 서비스가 홈 화면으로 이동, 뒤로 버튼 탭, 알림 화면 또는 최근 앱 목록 열기와 같은 전역 작업을 실행해야 하는 경우 performGlobalAction() 메서드를 사용합니다.

포커스 유형 사용

2012년에 Android에서는 접근성 포커스라는 사용자 인터페이스 포커스를 출시했습니다. 접근성 서비스는 이 포커스를 사용하여 표시되는 사용자 인터페이스 요소를 선택하고 작업할 수 있습니다. 이 포커스 유형은 사용자가 문자를 입력하거나 키보드의 Enter를 누르거나 D패드의 가운데 버튼을 누를 때 화면의 사용자 인터페이스 요소가 입력을 수신하는 것을 결정하는 입력 포커스와 다릅니다.

사용자 인터페이스의 한 요소에는 입력 포커스가 있고 다른 요소에는 접근성 포커스가 있을 수 있습니다. 접근성 포커스의 목적은 요소가 시스템 관점에서 입력에 포커스를 맞출 수 있는지 여부와 관계없이 화면에 표시되는 요소와 상호작용하는 메서드를 접근성 서비스에 제공하는 것입니다. 접근성 서비스가 앱의 입력 요소와 올바르게 상호작용하도록 하려면 앱의 접근성 테스트에 관한 가이드라인에 따라 일반적인 앱을 사용하는 동안 서비스를 테스트하세요.

접근성 서비스는 AccessibilityNodeInfo.findFocus() 메서드를 사용하여 어떤 사용자 인터페이스 요소에 입력 포커스 또는 접근성 포커스가 있는지 확인할 수 있습니다. focusSearch() 메서드를 사용하여 입력 포커스가 있는 요소를 선택할 수 있는 요소를 검색할 수도 있습니다. 마지막으로 접근성 서비스는 performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS) 메서드를 사용하여 접근성 포커스를 설정할 수 있습니다.

정보 수집하기

접근성 서비스에는 이벤트 세부정보, 텍스트, 숫자와 같은 사용자 제공 정보의 주요 단위를 수집하고 표현하는 표준 메서드가 있습니다.

창(window) 변경 세부정보 가져오기

Android 9 (API 수준 28) 이상에서는 앱이 여러 창을 동시에 다시 그릴 때 앱에서 창 업데이트를 추적할 수 있습니다. TYPE_WINDOWS_CHANGED 이벤트가 발생하면 getWindowChanges() API를 사용하여 창이 어떻게 변경되는지 확인합니다. 멀티 윈도우가 업데이트되는 동안 각 창은 자체 이벤트 집합을 생성합니다. getSource() 메서드는 각 이벤트와 연결된 창의 루트 뷰를 반환합니다.

앱에서 View 객체의 접근성 창 제목을 정의한다면 앱의 UI가 업데이트될 때 서비스에서 이를 인식할 수 있습니다. TYPE_WINDOW_STATE_CHANGED 이벤트가 발생하면 getContentChangeTypes()에서 반환한 유형을 사용하여 창이 어떻게 변경되는지 확인합니다. 예를 들어 프레임워크는 창(pane)에 새 제목이 있거나 창이 사라지면 이를 감지할 수 있습니다.

이벤트 세부정보 가져오기

Android는 AccessibilityEvent 객체를 통해 사용자 인터페이스 상호작용에 관한 정보를 접근성 서비스에 제공합니다. 이전 Android 버전에서는 접근성 이벤트에서 사용할 수 있는 정보가 사용자가 선택한 사용자 인터페이스 컨트롤에 관한 중요한 세부정보를 제공했지만 상황별 정보는 제한적으로 제공했습니다. 대부분의 경우 이렇게 누락된 컨텍스트 정보는 선택한 컨트롤의 의미를 이해하는 데 중요할 수도 있습니다.

컨텍스트가 중요한 인터페이스의 예로는 캘린더 또는 일정 플래너가 있습니다. 사용자가 월요일~금요일 목록에서 오후 4시 시간대를 선택하고 접근성 서비스에서 '오후 4시'는 알리지만 평일 이름, 날짜, 월 이름은 알리지 않으면 결과 피드백이 혼란스러울 수 있습니다. 이 경우 사용자 인터페이스 컨트롤의 컨텍스트는 회의를 예약하려는 사용자에게 매우 중요합니다.

2011년부터 Android는 뷰 계층 구조를 기반으로 접근성 이벤트를 구성하여 접근성 서비스가 사용자 인터페이스 상호작용에 관해 가져올 수 있는 정보의 양을 크게 확장했습니다. 뷰 계층 구조는 구성요소를 포함하는 사용자 인터페이스 구성요소 (상위 요소)와 이 구성요소에 포함될 수 있는 사용자 인터페이스 요소 (하위 요소)의 집합입니다. 이렇게 하면 Android에서 접근성 이벤트에 관한 더 자세한 세부정보를 제공할 수 있으므로 접근성 서비스에서 사용자에게 더 유용한 의견을 제공할 수 있습니다.

접근성 서비스는 시스템에서 서비스의 onAccessibilityEvent() 콜백 메서드로 전달한 AccessibilityEvent를 통해 사용자 인터페이스 이벤트에 관한 정보를 가져옵니다. 이 객체는 처리 중인 객체의 유형, 설명 텍스트, 기타 세부정보를 포함하여 이벤트에 대한 세부정보를 제공합니다.

  • AccessibilityEvent.getRecordCount()getRecord(int): 이러한 메서드를 사용하면 시스템에서 전달한 AccessibilityEvent에 기여하는 AccessibilityRecord 객체 집합을 검색할 수 있습니다. 이 세부정보 수준은 접근성 서비스를 트리거하는 이벤트에 관한 더 많은 컨텍스트를 제공합니다.

  • AccessibilityRecord.getSource(): 이 메서드는 AccessibilityNodeInfo 객체를 반환합니다. 이 객체를 사용하면 접근성 이벤트를 발생시키는 구성요소의 뷰 레이아웃 계층 구조 (상위 요소 및 하위 요소)를 요청할 수 있습니다. 이 기능을 사용하면 접근성 서비스에서 포함된 뷰 또는 하위 뷰의 콘텐츠와 상태를 비롯한 이벤트의 전체 컨텍스트를 조사할 수 있습니다.

Android 플랫폼에서는 AccessibilityService가 뷰 계층 구조를 쿼리하는 기능을 제공하여 이벤트, 상위 이벤트, 하위 이벤트를 생성하는 UI 구성요소에 관한 정보를 수집합니다. 이렇게 하려면 XML 구성에 다음 줄을 설정합니다.

android:canRetrieveWindowContent="true"

완료되면 getSource()를 사용하여 AccessibilityNodeInfo 객체를 가져옵니다. 이 호출은 이벤트가 발생한 창이 여전히 활성 창인 경우에만 객체를 반환합니다. 그렇지 않은 경우 null을 반환하므로 그에 맞게 동작해야 합니다.

다음 예에서 코드는 이벤트가 수신될 때 다음을 수행합니다.

  1. 이벤트가 발생한 뷰의 상위 요소를 즉시 가져옵니다.
  2. 이 뷰에서 라벨 및 체크박스를 하위 뷰로 찾습니다.
  3. 라벨을 찾으면 사용자에게 보고할 문자열을 만듭니다. 이 문자열은 라벨 및 라벨 선택 여부를 나타냅니다.

뷰 계층 구조를 순회하는 동안 언제든지 null 값이 반환되면 메서드는 아무 작업도 하지 않습니다.

Kotlin

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

override fun onAccessibilityEvent(event: AccessibilityEvent) {

    val source: AccessibilityNodeInfo = event.source ?: return

    // Grab the parent of the view that fires the event.
    val rowNode: AccessibilityNodeInfo = getListItemNodeInfo(source) ?: return

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    val taskLabel: CharSequence = rowNode.getChild(0)?.text ?: run {
        rowNode.recycle()
        return
    }

    val isComplete: Boolean = rowNode.getChild(1)?.isChecked ?: run {
        rowNode.recycle()
        return
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) {
        rowNode.recycle()
        return
    }

    val completeStr: String = if (isComplete) {
        getString(R.string.checked)
    } else {
        getString(R.string.not_checked)
    }
    val reportStr = "$taskLabel$completeStr"
    speakToUser(reportStr)
}

Java

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fires the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}

이제 완벽하게 작동하는 접근성 서비스가 완성되었습니다. Android의 텍스트 음성 변환 엔진을 추가하거나 Vibrator을 사용하여 햅틱 반응을 제공하여 사용자와 상호작용하는 방식을 구성해 보세요.

텍스트 처리

Android 8.0(API 수준 26) 이상을 실행하는 기기에는 화면에 표시되는 특정 텍스트 단위를 접근성 서비스에서 쉽게 식별하고 작동하게 하는 여러 텍스트 처리 기능이 포함되어 있습니다.

도움말

Android 9 (API 수준 28)에는 앱 UI의 도움말에 액세스할 수 있는 여러 가지 기능이 도입되었습니다. getTooltipText()를 사용하여 도움말의 텍스트를 읽고 ACTION_SHOW_TOOLTIPACTION_HIDE_TOOLTIP를 사용하여 View의 인스턴스에 도움말을 표시하거나 숨기도록 지시합니다.

힌트 텍스트

2017년부터 Android에는 텍스트 기반 객체의 힌트 텍스트와 상호작용하는 여러 메서드가 포함되어 있습니다.

  • isShowingHintText()setShowingHintText() 메서드는 각각 노드의 현재 텍스트 콘텐츠가 노드의 힌트 텍스트를 나타내는지 여부를 나타내고 설정합니다.
  • getHintText()은 힌트 텍스트 자체에 대한 액세스를 제공합니다. 객체가 힌트 텍스트를 표시하지 않는 경우에도 getHintText() 호출은 성공합니다.

화면에 표시되는 텍스트 문자의 위치

Android 8.0 (API 수준 26) 이상을 실행하는 기기에서 접근성 서비스는 TextView 위젯 내에 표시되는 각 문자의 경계 상자의 화면 좌표를 결정할 수 있습니다. 서비스에서는 refreshWithExtraData()를 호출하고 첫 번째 인수로 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY를, 두 번째 인수로 Bundle 객체를 전달하여 이러한 좌표를 찾습니다. 메서드가 실행되면 시스템은 Bundle 인수를 Rect 객체의 parcelable 배열로 채웁니다. 각 Rect 객체는 특정 문자의 경계 상자를 나타냅니다.

표준화된 단면 범위 값

일부 AccessibilityNodeInfo 객체는 AccessibilityNodeInfo.RangeInfo 인스턴스를 사용하여 UI 요소가 값 범위를 사용할 수 있음을 나타냅니다. RangeInfo.obtain()를 사용하여 범위를 만들거나 getMin()getMax()를 사용하여 범위의 극한 값을 가져올 때 Android 8.0 (API 수준 26) 이상을 실행하는 기기는 다음과 같이 표준화된 방식으로 단면 범위를 나타낸다는 점에 유의하세요.

접근성 이벤트에 응답

이제 서비스가 이벤트를 실행하고 수신 대기하도록 설정되었으므로 AccessibilityEvent가 도착하면 해야 할 작업을 알 수 있도록 코드를 작성합니다. 먼저 onAccessibilityEvent(AccessibilityEvent) 메서드를 재정의합니다. 이 메서드에서 getEventType()를 사용하여 이벤트 유형을 확인하고 getContentDescription()를 사용하여 이벤트를 실행하는 뷰와 연결된 라벨 텍스트를 추출합니다.

Kotlin

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    var eventText: String = when (event.eventType) {
        AccessibilityEvent.TYPE_VIEW_CLICKED -> "Clicked: "
        AccessibilityEvent.TYPE_VIEW_FOCUSED -> "Focused: "
        else -> ""
    }

    eventText += event.contentDescription

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText)
    ...
}

Java

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    final int eventType = event.getEventType();
    String eventText = null;
    switch(eventType) {
        case AccessibilityEvent.TYPE_VIEW_CLICKED:
            eventText = "Clicked: ";
            break;
        case AccessibilityEvent.TYPE_VIEW_FOCUSED:
            eventText = "Focused: ";
            break;
    }

    eventText = eventText + event.getContentDescription();

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText);
    ...
}

추가 리소스

자세한 내용은 다음 리소스를 참고하세요.

가이드

Codelab