Tạo một dịch vụ hỗ trợ tiếp cận

Dịch vụ hỗ trợ tiếp cận là một ứng dụng giúp cải thiện giao diện người dùng để hỗ trợ người dùng bị khuyết tật hoặc người dùng tạm thời không thể tương tác đầy đủ với một thiết bị. Các dịch vụ này chạy ở chế độ nền và giao tiếp với hệ thống để kiểm tra nội dung trên màn hình và tương tác với các ứng dụng thay cho người dùng. Ví dụ: trình đọc màn hình (như TalkBack), công cụ Tiếp cận bằng công tắc và hệ thống điều khiển bằng giọng nói.

Hướng dẫn này trình bày những kiến thức cơ bản về cách tạo một dịch vụ hỗ trợ tiếp cận trên Android.

Vòng đời của dịch vụ hỗ trợ tiếp cận

Để tạo một dịch vụ hỗ trợ tiếp cận, bạn phải mở rộng lớp AccessibilityService và khai báo dịch vụ đó trong tệp kê khai của ứng dụng.

Tạo lớp dịch vụ

Tạo một lớp mở rộng AccessibilityService. Bạn phải ghi đè các phương thức sau:

  • onAccessibilityEvent: Được gọi khi hệ thống phát hiện thấy một sự kiện khớp với cấu hình của dịch vụ (ví dụ: thay đổi tiêu điểm hoặc một lượt nhấp vào nút). Đây là nơi dịch vụ của bạn diễn giải giao diện người dùng.
  • onInterrupt: Được gọi khi hệ thống làm gián đoạn ý kiến phản hồi của dịch vụ (ví dụ: để dừng đầu ra lời nói khi người dùng di chuyển tiêu điểm nhanh chóng).
package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.FingerprintGestureController
import android.accessibilityservice.AccessibilityButtonController
import android.accessibilityservice.GestureDescription
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.graphics.Path
import android.os.Build
import android.media.AudioManager
import android.content.Context

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Interpret the event and provide feedback to the user
    }

    override fun onInterrupt() {
        // Interrupt any ongoing feedback
    }

    override fun onServiceConnected() {
        // Perform initialization here
    }
}

Khai báo trong tệp kê khai

Đăng ký dịch vụ của bạn trong tệp AndroidManifest.xml. Bạn phải thực thi nghiêm ngặt quyền BIND_ACCESSIBILITY_SERVICE để chỉ hệ thống mới có thể liên kết với dịch vụ của bạn.

Để đảm bảo nút cài đặt hoạt động, hãy khai báo ServiceSettingsActivity.

<application>
  <service android:name=".accessibility.MyAccessibilityService"
      android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
      android:exported="true"
      android:label="@string/accessibility_service_label">
      <intent-filter>
          <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
          android:name="android.accessibilityservice"
          android:resource="@xml/accessibility_service_config" />
  </service>

  <activity android:name=".accessibility.ServiceSettingsActivity"
      android:exported="true"
      android:label="@string/accessibility_service_settings_label" />
</application>

Định cấu hình dịch vụ

Tạo tệp cấu hình trong res/xml/accessibility_service_config.xml. Tệp này xác định những sự kiện mà dịch vụ của bạn xử lý và những ý kiến phản hồi mà dịch vụ đó cung cấp. Hãy nhớ tham chiếu đến ServiceSettingsActivity mà bạn đã khai báo trong tệp kê khai:

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

Tệp cấu hình bao gồm các thuộc tính khoá sau:

  • android:accessibilityEventTypes: Các sự kiện mà bạn muốn nhận. Sử dụng typeAllMask cho dịch vụ đa năng.
  • android:canRetrieveWindowContent: Phải là true nếu dịch vụ của bạn cần kiểm tra hệ phân cấp giao diện người dùng (ví dụ: để đọc văn bản trên màn hình).
  • android:canPerformGestures: Phải là true nếu bạn dự định gửi các cử chỉ (chẳng hạn như cử chỉ vuốt hoặc nhấn) theo phương thức lập trình.
  • android:accessibilityFlags: Kết hợp các cờ để bật các tính năng. Bạn cần có flagRequestFingerprintGestures để dùng cử chỉ bằng vân tay. flagRequestAccessibilityButton là nút bắt buộc để hỗ trợ tiếp cận phần mềm.

Để xem danh sách đầy đủ các lựa chọn cấu hình, hãy xem AccessibilityServiceInfo.

Cấu hình thời gian chạy

Mặc dù cấu hình XML là tĩnh, nhưng bạn cũng có thể sửa đổi cấu hình dịch vụ của mình một cách linh hoạt trong thời gian chạy. Điều này hữu ích cho việc bật/tắt các tính năng dựa trên lựa chọn ưu tiên của người dùng.

Ghi đè onServiceConnected() để áp dụng các bản cập nhật thời gian chạy bằng setServiceInfo():

override fun onServiceConnected() {
    val info = AccessibilityServiceInfo()

    // Set the type of events that this service wants to listen to.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

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

    // Set flags at runtime.
    info.flags = AccessibilityServiceInfo.FLAG_DEFAULT or
            AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES

    this.setServiceInfo(info)
}

Diễn giải nội dung trên giao diện người dùng

Khi onAccessibilityEvent() kích hoạt, hệ thống sẽ cung cấp một AccessibilityEvent. Sự kiện này đóng vai trò là điểm truy cập vào cây hỗ trợ tiếp cận, một bản trình bày theo hệ phân cấp của nội dung trên màn hình.

Dịch vụ của bạn chủ yếu tương tác với các đối tượng AccessibilityNodeInfo. Các đối tượng này đại diện cho các phần tử trên giao diện người dùng như nút, danh sách và văn bản. Dữ liệu về các phần tử giao diện người dùng này được chuẩn hoá thành AccessibilityNodeInfo.

Ví dụ sau đây cho thấy cách truy xuất nguồn của một sự kiện và duyệt qua cây hỗ trợ tiếp cận để tìm thông tin.

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    // Get the source node of the event
    val sourceNode: AccessibilityNodeInfo? = event.source

    if (sourceNode == null) return

    // Inspect properties
    if (sourceNode.isCheckable) {
        val state = if (sourceNode.isChecked) "Checked" else "Unchecked"
        val label = sourceNode.text ?: sourceNode.contentDescription
        
        // Provide feedback (for example, speak to the user)
        speakToUser("$label is $state")
    }

    // Always recycle nodes to prevent memory leaks
    sourceNode.recycle()
}

private fun speakToUser(text: String) {
    // Your text-to-speech implementation goes here
}

Hành động thay cho người dùng

Các dịch vụ hỗ trợ tiếp cận có thể thực hiện các thao tác thay cho người dùng, chẳng hạn như nhấp vào nút hoặc cuộn danh sách.

Để thực hiện một thao tác, hãy gọi performAction() trên một đối tượng AccessibilityNodeInfo.

fun performClick(node: AccessibilityNodeInfo) {
    if (node.isClickable) {
        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }
}

Đối với các thao tác chung ảnh hưởng đến toàn bộ hệ thống (chẳng hạn như nhấn nút Quay lại hoặc mở bảng thông báo), hãy sử dụng performGlobalAction().

// Navigate back
fun navigateBack() {
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
}

Quản lý tiêu điểm

Android có 2 loại tiêu điểm riêng biệt: tiêu điểm nhập (nơi dữ liệu đầu vào từ bàn phím sẽ đi đến) và tiêu điểm hỗ trợ tiếp cận (nội dung mà dịch vụ hỗ trợ tiếp cận đang kiểm tra).

Đoạn mã sau đây cho biết cách tìm phần tử hiện có tiêu điểm hỗ trợ tiếp cận:

// Find the node that currently has accessibility focus
// Note: rootInActiveWindow can be null if the window is not available
val root = rootInActiveWindow
if (root != null) {
    val focusedNode = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)

    // Do something with focusedNode

    // Always recycle nodes
    focusedNode?.recycle()
    // rootInActiveWindow doesn't need to be recycled, but obtained nodes do.
}

Đoạn mã sau đây cho biết cách di chuyển tiêu điểm hỗ trợ tiếp cận đến một phần tử cụ thể:

// Request that the system give focus to a given node
fun focusNode(node: AccessibilityNodeInfo) {
    node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)
}

Khi tạo một dịch vụ hỗ trợ tiếp cận, hãy tôn trọng trạng thái tiêu điểm của người dùng và tránh chiếm lấy tiêu điểm trừ phi được kích hoạt rõ ràng bằng một hành động của người dùng.

Thực hiện cử chỉ

Dịch vụ của bạn có thể gửi các cử chỉ tuỳ chỉnh đến màn hình, chẳng hạn như cử chỉ vuốt, nhấn hoặc tương tác đa chạm. Để làm việc này, hãy khai báo android:canPerformGestures="true" trong cấu hình để bạn có thể sử dụng API dispatchGesture().

Cử chỉ đơn giản

Để thực hiện các cử chỉ đơn giản, hãy bắt đầu bằng cách tạo một đối tượng Path để biểu thị chuyển động liên kết với một cử chỉ nhất định. Sau đó, hãy gói Path trong một GestureDescription để mô tả nét vẽ. Cuối cùng, hãy gọi dispatchGesture để gửi cử chỉ.

fun swipeRight() {
    // Create a path for the swipe (from x=100 to x=500)
    val swipePath = Path()
    swipePath.moveTo(100f, 500f)
    swipePath.lineTo(500f, 500f)

    // Build the stroke description (0ms delay, 500ms duration)
    val stroke = GestureDescription.StrokeDescription(swipePath, 0, 500)

    // Build the gesture description
    val gestureBuilder = GestureDescription.Builder()
    gestureBuilder.addStroke(stroke)

    // Dispatch the gesture
    dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() {
        override fun onCompleted(gestureDescription: GestureDescription?) {
            super.onCompleted(gestureDescription)
            // Gesture finished successfully
        }
    }, null)
}

Cử chỉ liên tục

Đối với các hoạt động tương tác phức tạp (chẳng hạn như vẽ hình chữ L hoặc thực hiện thao tác kéo nhiều bước chính xác), bạn có thể xâu chuỗi các nét lại với nhau bằng cách sử dụng tham số willContinue.

fun performLShapedGesture() {
    val path1 = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }

    val path2 = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }

    // First stroke: willContinue = true
    val stroke1 = GestureDescription.StrokeDescription(path1, 0, 500, true)

    // Second stroke: continues immediately after stroke1
    val stroke2 = stroke1.continueStroke(path2, 0, 500, false)

    val builder = GestureDescription.Builder()
    builder.addStroke(stroke1)
    builder.addStroke(stroke2)

    dispatchGesture(builder.build(), null, null)
}

Quản lý âm thanh

Khi tạo một dịch vụ hỗ trợ tiếp cận (đặc biệt là trình đọc màn hình), hãy sử dụng luồng âm thanh STREAM_ACCESSIBILITY. Điều này cho phép người dùng kiểm soát âm lượng dịch vụ độc lập với âm lượng nội dung nghe nhìn của hệ thống.

fun increaseAccessibilityVolume() {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    audioManager.adjustStreamVolume(
        AudioManager.STREAM_ACCESSIBILITY,
        AudioManager.ADJUST_RAISE,
        0
    )
}

Hãy nhớ thêm cờ FLAG_ENABLE_ACCESSIBILITY_VOLUME vào cấu hình của bạn, có thể là trong XML hoặc thông qua setServiceInfo trong thời gian chạy.

Các tính năng nâng cao

Cử chỉ vân tay

Trên các thiết bị chạy Android 10 (cấp độ API 29) trở lên, dịch vụ của bạn có thể ghi lại các thao tác vuốt theo hướng trên cảm biến vân tay. Điều này hữu ích khi cung cấp các chế độ điều khiển điều hướng thay thế.

Thêm logic sau vào phương thức onServiceConnected() của bạn:

// Import: android.os.Build
// Import: android.accessibilityservice.FingerprintGestureController

private var gestureController: FingerprintGestureController? = null

override fun onServiceConnected() {
    // Check if the device is running Android 10 (Q) or higher
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        gestureController = fingerprintGestureController

        val callback = object : FingerprintGestureController.FingerprintGestureCallback() {
            override fun onGestureDetected(gesture: Int) {
                when (gesture) {
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN -> {
                        // Handle swipe down
                    }
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP -> {
                        // Handle swipe up
                    }
                }
            }
        }

        gestureController?.registerFingerprintGestureCallback(callback, null)
    }
}

Nút hỗ trợ tiếp cận

Trên các thiết bị sử dụng phím điều hướng bằng phần mềm, người dùng có thể gọi dịch vụ của bạn thông qua nút hỗ trợ tiếp cận trong thanh điều hướng.

Để sử dụng tính năng này, hãy thêm cờ FLAG_REQUEST_ACCESSIBILITY_BUTTON vào cấu hình dịch vụ của bạn. Sau đó, hãy thêm logic đăng ký vào phương thức onServiceConnected().

// Import: android.accessibilityservice.AccessibilityButtonController

override fun onServiceConnected() {
    // ... existing initialization code ...

    val controller = accessibilityButtonController

    controller.registerAccessibilityButtonCallback(
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                // Respond to button tap
            }
        }
    )
}

Chuyển văn bản sang lời nói đa ngôn ngữ

Một dịch vụ đọc to văn bản có thể tự động chuyển đổi ngôn ngữ nếu văn bản nguồn được gắn thẻ bằng LocaleSpan. Điều này giúp dịch vụ của bạn phát âm chính xác nội dung có nhiều ngôn ngữ mà không cần chuyển đổi thủ công.

import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.LocaleSpan
import java.util.Locale

// Wrap text in LocaleSpan to indicate language
val spannable = SpannableStringBuilder("Bonjour")
spannable.setSpan(
    LocaleSpan(Locale.FRANCE),
    0,
    spannable.length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

Khi dịch vụ của bạn xử lý AccessibilityNodeInfo, hãy kiểm tra thuộc tính text cho các đối tượng LocaleSpan để xác định ngôn ngữ chuyển văn bản sang lời nói phù hợp.

Tài nguyên khác

Để tìm hiểu thêm, hãy xem các tài nguyên sau:

Hướng dẫn

Lớp học lập trình

Xem nội dung