Inclusive Android Interfaces with Custom Accessibility Services

Valdio Veliu
Share

This article was peer reviewed by Adrian Sandu. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Spending time to develop an app that is accessible to all is an important task to empower potential users.

Android has built in accessibility features to help users who interact with their devices in different ways. These include autocomplete, a screen reader (TalkBack) and Text-to-speech output. So why create your own custom accessibility service?

Making your own accessibility service allows you to customize an app for a specific group of users that suits your app.

Handling Accessibility Services

The Android framework builds a event stream of everything on the screen and accessibility services subscribe to this stream, paying attention to different elements.

In this tutorial I will cover creating an accessibility service, which filters actions users make and based on those actions the service will use TextToSpeech to give feedback.

Let’s start building, you can find the code for the project on GitHub. Open Android Studio and create a new Blank Activity project.

Create a class with a file name of CustomAccessibilityService.java that extends AccessibilityService and implements the override methods (onAccessibilityEvent and onInterrupt):

public class CustomAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }

    @Override
    public void onInterrupt() {

    }
}

Declare the AccessibilityService in the Manifest File

Accessibility services must be explicitly declared in the application’s manifest file:

<application>
  ...
  <!--Declare AccessibilityService-->
  <service
      android:name=".CustomAccessibilityService"
      android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
      <intent-filter>
          <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
          android:name="android.accessibilityservice"
          android:resource="@xml/service_configuration" />
  </service>
  ...
</application>

The declaration specifies the name of the AccessibilityService class with the permission BIND_ACCESSIBILITY_SERVICE that distinguishes this service as an accessibility service. The meta data contains the service configuration resource file.

Configure the Accessibility Service

Configuring the accessibility service involves specifying the types of accessibility events that the service handles.

Create service_configuration.xml inside res/xml/ and add the following:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeViewClicked|typeViewScrolled"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="100"
    android:packageNames="valdioveliu.accessibilityservice" />

The most important setting here is the accessibilityEventTypes which represent the event streams that this service subscribes to.

An important setting is the packageNames declaration. If you don’t specify a package name for the accessibility service it will operate system wide. You can declare more than one packageName for an accessibility service.

Implementing the Accessibility Service Class

The Accessibility service class is where all the magic happens. Add the following to CustomAccessibilityService.java:

public class CustomAccessibilityService extends AccessibilityService {

    private TextToSpeech textToSpeech;

    //Configure the Accessibility Service
    @Override
    protected void onServiceConnected() {
        Toast.makeText(getApplication(), "onServiceConnected", Toast.LENGTH_SHORT).show();

        //Init TextToSpeech
        textToSpeech = new TextToSpeech(getApplication(), new TextToSpeech.OnInitListener() {
            @Override
            public void onInit(int status) {
                if (status == TextToSpeech.SUCCESS) {
                    int result = textToSpeech.setLanguage(Locale.US);
                    if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                        Log.e("TextToSpeech", "Language not supported");
                    }
                } else {
                    Log.e("TextToSpeech", "Initialization Failed! :( ");
                }
            }
        });
    }

    //Respond to AccessibilityEvents
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
            Toast.makeText(getApplication(), event.getText().toString(), Toast.LENGTH_SHORT).show();

            //TextToSpeech
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                textToSpeech.speak(event.getText().toString(), TextToSpeech.QUEUE_FLUSH, null, "TextToSpeech_ID");
            } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                textToSpeech.speak(event.getText().toString(), TextToSpeech.QUEUE_FLUSH, null);
            }

        } else if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {

            //RecyclerView Scrolled
            //TextToSpeech
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                textToSpeech.speak("Scrolling", TextToSpeech.QUEUE_FLUSH, null, "TextToSpeech_ID");
            } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                textToSpeech.speak("Scrolling", TextToSpeech.QUEUE_FLUSH, null);
            }
        }

    }

    @Override
    public void onInterrupt() {
        //Interrupt the Accessibility service
        //Stop TextToSpeech
        if (textToSpeech != null) {
            textToSpeech.stop();
            textToSpeech.shutdown();
        }
    }


}

The onAccessibilityEvent method responds to incoming events, jumping in when an accessibility event that matches one of the accessibility events subscribed to is triggered by the user.

The onInterrupt method is called when the Android framework wants to stop what the service is doing.

The onServiceConnected sets up TextToSpeech after the system has successfully bound to the service.

This accessibility service only works for clickable views. This means that it won’t work if a user tries to click an un-clickable view, for example a textView. To use the service in this case you need to add the following attribute to the view.

<TextView
...
   android:clickable="true"
.../>

Note: To use an accessibility service on a device your user will have to enable the accessibility service in their device settings.

Conclusion

In this article I covered all that’s needed to implement an accessibility service. To truly make your app accessible I recommend you follow the Android guidelines, the detailed description of AccessibilityEvents and their methods and SitePoint’s own guide to Android accessibility features.

CSS Master, 3rd Edition