본문 바로가기
안드로이드 자바

[Android][JAVA] 접근성 서비스 (Accessibility Service) : 앱에서 이벤트 추적과 자동화 구현하기

by teamnova 2025. 1. 13.
728x90

사용하는 이유?

접근성 서비스는 시각장애인이나 다른 신체적 제약을 가진 사용자들에게 모바일 앱을 사용할 수 있도록 돕는 기능입니다. 하지만 이 서비스는 단순히 보조 기술에 그치지 않고, 앱 내에서 발생하는 다양한 이벤트를 추적하고 자동화하는 데도 활용될 수 있습니다.

 

  • 뷰 클릭: 버튼 클릭, 화면 내 특정 영역 클릭 시 이벤트 추적
  • 뷰 포커스: 포커스를 받은 UI 요소 추적
  • 텍스트 변경: 사용자가 텍스트를 입력하거나 수정할 때 이벤트 추적
  • 화면 상태 변경: 화면 전환이나 UI 업데이트와 같은 상태 변경 추적

어디에 사용하면 좋을까요?

  1. 보조 기술을 위한 앱 개발:
    • 시각 장애인을 위한 화면 읽기 기능을 지원하는 앱에서 필수적인 역할을 합니다. 접근성 서비스를 통해 사용자가 버튼을 클릭하거나 텍스트를 수정할 때 그 내용을 읽어줄 수 있습니다.
  2. 자동화된 UI 테스트:
    • 접근성 서비스를 사용하면 앱 내 UI의 상호작용을 자동으로 추적하고, 각 이벤트가 정상적으로 작동하는지 테스트할 수 있습니다. 예를 들어, UI 테스트 자동화 도구에서 유용하게 사용할 수 있습니다.
  3. 사용자 행동 분석:
    • 사용자가 앱을 어떻게 사용하는지 파악하려면 이벤트 추적이 필요합니다. 접근성 서비스를 통해 버튼 클릭, 화면 이동, 텍스트 입력 등을 실시간으로 로그로 기록하고 분석할 수 있습니다.
  4. 앱의 상태 변경 알림:
    • 앱 내에서 화면 상태가 변경될 때, 예를 들어 화면 전환이나 뷰 업데이트가 있을 때, 이를 추적하고 알림을 받을 수 있습니다. 이는 앱의 상태 관리알림 시스템에서 유용하게 활용될 수 있습니다.

 

 

manifests

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Test7745"
        tools:targetApi="31">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".MyAccessibilityService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:exported="false">  <!-- 서비스 외부 접근 방지 -->
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>

    </application>
</manifest>

 

mainActivity

import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 테스트 버튼 클릭 이벤트
        Button testButton = findViewById(R.id.button_test);
        testButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("MainActivity", "테스트 버튼이 클릭되었습니다!");
            }
        });

        // EditText 입력 감지
        EditText editText = findViewById(R.id.edit_text);
        editText.setOnFocusChangeListener((v, hasFocus) -> {
            if (hasFocus) {
                Log.d("MainActivity", "텍스트 필드가 포커스를 받았습니다.");
            }
        });

        // 접근성 설정 열기 버튼 클릭 이벤트
        Button openAccessibilityButton = findViewById(R.id.button_open_accessibility);
        openAccessibilityButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
                startActivity(intent);
            }
        });
    }
}
MyAccessibilityService
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;


public class MyAccessibilityService extends AccessibilityService {

    private static final String TAG = "MyAccessibilityService";

    // 접근성 이벤트 처리
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        String eventTypeString = "";

        // 이벤트 유형 확인 및 로그 출력
        switch (eventType) {
            case AccessibilityEvent.TYPE_VIEW_CLICKED:
                eventTypeString = "버튼 클릭됨";
                Log.d(TAG, eventTypeString + ": " + event.getText());
                break;

            case AccessibilityEvent.TYPE_VIEW_FOCUSED:
                eventTypeString = "뷰에 포커스가 이동됨";
                Log.d(TAG, eventTypeString + ": " + event.getText());
                break;

            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                eventTypeString = "화면 상태 변경됨";
                Log.d(TAG, eventTypeString + ": " + event.getClassName());
                break;

            case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
                eventTypeString = "텍스트 변경됨";
                Log.d(TAG, eventTypeString + ": " + event.getText());
                break;

            default:
                eventTypeString = "기타 이벤트";
                Log.d(TAG, eventTypeString);
                break;
        }
    }

    // 접근성 서비스가 중단될 때 호출
    @Override
    public void onInterrupt() {
        Log.d(TAG, "서비스가 중단되었습니다.");
    }

    // 서비스 연결 시 초기 설정
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        Log.d(TAG, "접근성 서비스 연결됨");

        // 서비스 설정
        AccessibilityServiceInfo info = new AccessibilityServiceInfo();
        info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
                AccessibilityEvent.TYPE_VIEW_FOCUSED |
                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED |
                AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED;

        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN; // 피드백 타입 설정
        info.flags = AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; // 중요하지 않은 뷰 포함
        info.notificationTimeout = 100; // 이벤트 처리 시간 간격 (밀리초)
        setServiceInfo(info);
    }
}

res/layout/actvity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="16dp">

    <Button
        android:id="@+id/button_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="테스트 버튼" />

    <EditText
        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="텍스트를 입력하세요"
        android:layout_marginTop="16dp" />

    <Button
        android:id="@+id/button_open_accessibility"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="접근성 설정 열기"
        android:layout_marginTop="16dp" />
</LinearLayout>

res/xml/accessibility_service_config.xml

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeViewClicked|typeViewTextChanged|typeViewFocused"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:packageNames="com.example.targetapp"
    android:accessibilityFlags="flagDefault"
    android:settingsActivity="com.example.MySettingsActivity"/>

 

시연영상