19

Background

Back a few years ago, I asked how TeamViewer allows the user to control the device without normal interaction with the device. I was told it's a special "backdoor" that manufacturers allow specifically for this app, and only possible using root priviledge for other apps.

Seeing that an app like "Airplane Mode Shortcut" allows to toggle airplane mode, by automatic navigation to its screen and toggling the switch, it made me realize this situation has changed.

The problem

It is said in the docs:

Starting with Android 4.0 (API Level 14), accessibility services can act on behalf of users, including changing the input focus and selecting (activating) user interface elements. In Android 4.1 (API Level 16) the range of actions has been expanded to include scrolling lists and interacting with text fields. Accessibility services can also take global actions, such as navigating to the Home screen, pressing the Back button, opening the notifications screen and recent applications list. Android 4.1 also includes a new type of focus, Accessibilty Focus, which makes all visible elements selectable by an accessibility service.

These new capabilities make it possible for developers of accessibility services to create alternative navigation modes such as gesture navigation, and give users with disabilities improved control of their Android devices.

But there is no more information about how to use it. Only samples I've found are at the bottom, but those are very old and a part of the apiDemos bundle.

The question

How do I make a service that can query, focus, click, enter text, and perform other UI related operations?

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270

1 Answers1

19

By implementing AccessibilityService (https://developer.android.com/training/accessibility/service.html) you get access to that features.

You can either inspect or perform action on the element lastly interacted by user or inspect whole application which currently active.

Intercept user events by implementing onAccessibilityEvent(AccessibilityEvent event), here you can retrieve virtual view (representing original view) with event.getSource() and then inspect it with getClassName() or getText() or anything you find in the documentation.

Inspect whole application by calling getRootInActiveWindow() and iterate throught tree of virtaul views with getRootInActiveWindow().getChild(index).

Both getRootInActiveWindow() and event.getSource() return AccessibilityNodeInfo, on which you can invoke performAction(action) and do something like Click, Set Text, etc..

Example: Play Store

Search for 'facebook' app and open it's page on play store, once you opened the play store app.

    @Override
    public void onAccessibilityEvent(final AccessibilityEvent event) {

        AccessibilityNodeInfo rootInActiveWindow = getRootInActiveWindow();
        //Inspect app elements if ready
        if (rootInActiveWindow != null) {
            //Search bar is covered with textview which need to be clicked
            List<AccessibilityNodeInfo> searchBarIdle = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.android.vending:id/search_box_idle_text");
            if (searchBarIdle.size() > 0) {
                AccessibilityNodeInfo searchBar = searchBarIdle.get(0);
                searchBar.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
            //Check is search bar is visible
            List<AccessibilityNodeInfo> searchBars = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.android.vending:id/search_box_text_input");
            if (searchBars.size() > 0) {
                AccessibilityNodeInfo searchBar = searchBars.get(0);
                //Check is searchbar have the required text, if not set the text
                if (searchBar.getText() == null || !searchBar.getText().toString().equalsIgnoreCase("facebook")) {
                    Bundle args = new Bundle();
                    args.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "facebook");
                    searchBar.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
                } else {
                    //There is no way to press Enter to perform search, so find corresponding suggestion and click
                    List<AccessibilityNodeInfo> searchSuggestions = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.android.vending:id/suggest_text");
                    for (AccessibilityNodeInfo suggestion : searchSuggestions) {
                        if(suggestion.getText().toString().equals("Facebook")) {
                            //We found textview, but its not clickable, so we should perform the click on the parent
                            AccessibilityNodeInfo clickableParent = suggestion.getParent();
                            clickableParent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                        }
                    }
                }
            }


        }
   }

EDIT: full code below:

MyAccessibilityService

public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("MyAccessibilityService", "onCreate");
    }

    @Override
    public void onAccessibilityEvent(final AccessibilityEvent event) {
        Log.d("MyAccessibilityService", "onAccessibilityEvent");
        AccessibilityNodeInfo rootInActiveWindow = getRootInActiveWindow();
        //Inspect app elements if ready
        if (rootInActiveWindow != null) {
            //Search bar is covered with textview which need to be clicked
            List<AccessibilityNodeInfo> searchBarIdle = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.android.vending:id/search_box_idle_text");
            if (searchBarIdle.size() > 0) {
                AccessibilityNodeInfo searchBar = searchBarIdle.get(0);
                searchBar.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
            //Check is search bar is visible
            List<AccessibilityNodeInfo> searchBars = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.android.vending:id/search_box_text_input");
            if (searchBars.size() > 0) {
                AccessibilityNodeInfo searchBar = searchBars.get(0);
                //Check is searchbar have the required text, if not set the text
                if (searchBar.getText() == null || !searchBar.getText().toString().equalsIgnoreCase("facebook")) {
                    Bundle args = new Bundle();
                    args.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "facebook");
                    searchBar.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
                } else {
                    //There is no way to press Enter to perform search, so find corresponding suggestion and click
                    List<AccessibilityNodeInfo> searchSuggestions = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.android.vending:id/suggest_text");
                    for (AccessibilityNodeInfo suggestion : searchSuggestions) {
                        if (suggestion.getText().toString().equals("Facebook")) {
                            //We found textview, but its not clickable, so we should perform the click on the parent
                            AccessibilityNodeInfo clickableParent = suggestion.getParent();
                            clickableParent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                        }
                    }
                }
            }
        }
    }

    @Override
    public void onInterrupt() {
    }
}

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.findfacebookapp">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <service
            android:name=".MyAccessibilityService"
            android:label="@string/accessibility_service_label"
            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/accessibility_service_config"/>
        </service>
    </application>

</manifest>

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="typeAllMask"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:accessibilityFlags="flagDefault"
    android:canRequestEnhancedWebAccessibility="true"
    android:canRetrieveWindowContent="true"
    android:description="@string/app_name"
    android:notificationTimeout="100"/>

MainActivity

public class MainActivity extends AppCompatActivity {

    public void onEnableAccClick(View view) {
        startActivityForResult(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 1);
    }

}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
Nikola Minoski
  • 416
  • 5
  • 7
  • Can you please demonstrate it in code? I don't see on the website you've provided an explanation of how to perform actions. It's just that there are almost nothing to try out there on the net... Maybe demonstrate on a known app, such as the Play Store? – android developer Feb 16 '17 at 09:36
  • I added the original answer, you can inspect view-ids with Android Device Monitor. – Nikola Minoski Feb 16 '17 at 13:40
  • Thank you, but I don't understand. How do you trigger this code from outside of the service? What would you need to put in other files? Is there any other code to write on this service class? Shouldn't I check the views only if the current activity is the one of the Play Store? – android developer Feb 16 '17 at 21:08
  • Everything else is just standard setup (create acc. service, declare it in manifest and provide xml config) follow this instruction https://developer.android.com/guide/topics/ui/accessibility/services.html After you install the app go to Settings -> Accessibility, your service should be listed there, so enable it and your service will start and receive events. – Nikola Minoski Feb 16 '17 at 22:15
  • Suppose I want to query the current views right now (locations, sizes, text, images, etc...), or perform a click somewhere right now, what would trigger this service? – android developer Feb 16 '17 at 22:21
  • 1
    Your service is started once you enable it from settings, and when you open and interact the app specified in the config (by default all apps) your onAccessibilityEvent is invoked. If you want to trigger something from other activity/service, user intent to call your Acc. Service and perform actions on `onStartCommand`. Use logs to inspect what and where is called while you interact with some apps (PlayStore for an ex.). Here is the example proj. [download android project link](http://www.filedropper.com/findfacebookapp) – Nikola Minoski Feb 16 '17 at 22:41
  • What does the "canRequestEnhancedWebAccessibility" used for?I found it here: https://developer.android.com/reference/android/accessibilityservice/AccessibilityServiceInfo.html#attr_android:canRequestEnhancedWebAccessibility . Is it useful for one of the things I mentioned I'd like to know (query views, perform clicks/typing, etc... ) ? Also, does this service always run? Is it possible to temporarily disable it? Can it get the bitmap of an ImageView, or a screenshot of it? – android developer Feb 17 '17 at 15:24
  • I'm accepting this answer and granting the bounty, but still. I'd like to know how much can be done with this service, and what is actually needed for each feature. – android developer Feb 17 '17 at 15:42
  • 2
    Good question `canRequestEnhancedWebAccessibility`. It is not well documented, so few months ago i needed to read code of [TalkBack](https://github.com/google/talkback) to understand what it is for. With this you can inspect html elements in webview (browsers) and perform actions (click, settext,..) on them. To inspect html elements you should use actions like ACTION_NEXT_HTML_ELEMENT and ACTION_PREVIOUS_HTML_ELEMENT, please do some search on this const. on TalkBack code, so you will see how its used. – Nikola Minoski Feb 17 '17 at 16:29
  • I see. Can you please answer the other questions? – android developer Feb 17 '17 at 20:50
  • 1
    Can the service also press on a specific coordinate? – android developer Feb 18 '17 at 18:38
  • Unfortunately, it can not perform any action on given coordinates only on the elements, but you can find out what's on your coordinates by recursively checking elements position on the screen with `getBoundsInScreen` – Nikola Minoski Feb 18 '17 at 19:29
  • I see, but this might be a problem, no? This would require me to go over the nodes, from root to leaf, to check on which node has the correct place, but even then, what if there are two on the same position (one on top of another) ? Doesn't it mean that the one behind might be pressed? Is there a way to know which of them is supposed to be pressed? – android developer Feb 18 '17 at 22:53
  • 2
    Yes, this solution is not very stable but it can work, and the case with two clickable els., one over another, it might be weird even when user need to click on it, however you can find what is clickable with `isClickable()`. Can we move on IM chat and give you help on specific use-case you want to implmenet? – Nikola Minoski Feb 19 '17 at 12:06
  • "isClickable" will work ? doesn't it just check the same attribute of whether it's possible to click the view or not? Can the service also capture the view image, or even get the bitmap of an ImageView ? Is disabling the service temporarily via code possible? – android developer Feb 19 '17 at 15:29
  • 1
    Since the download link doesn't work anymore, I've edited your answer to include it. – android developer Feb 26 '17 at 23:28
  • getting lot of ANR due to getRootInActiveWindow. mostly in Android 8.0 – Nikhil Oct 23 '17 at 12:44
  • @nikhil when/where exactly do you get the ANR? or maybe its better to open a new question. – Nikola Minoski Oct 24 '17 at 17:58
  • @NikolaMinoski please check https://stackoverflow.com/questions/46789325/getting-lots-of-anr-due-to-accessibilitynodeinfo-getchild – Nikhil Oct 25 '17 at 04:13
  • I was looking for this answer. Just now checked the accessibility service and the talkback app on selecting the text it was able to read it out. So thought of using accessibility service for app automation. your answer will be a great help for me to start. Thanks a lot @NikolaMinoski – Siva Apr 03 '18 at 00:15
  • @NikolaMinoski can you answer this question on accessibility https://stackoverflow.com/questions/49734263/click-on-the-notification-using-accessibility-service-programmatically – Siva Apr 13 '18 at 10:02
  • Can you also launch play store or any other app, from an accessibility service? – matanster Apr 02 '21 at 22:18