7

I've found out that Android 9 now shows info if accessibility service stopped working.

That was always a pain for developers who try to leverage accessibility API.

  • Accessibility looks like enabled, but service is stopped. And to get it back to work it is required to turn accessibility off and back on.

  • I would be glad if Google fixes that completely, but now they just show a hint that it's good to disable-enable it manually.

Not the best stuff, but at least something.

  • So, I've tried to find out how the system gets to know if the service is crashed. There happened to be a class called AccessibilityUtil and it contains hasServiceCrashed method.

  • Unfortunately, it checks a hidden field crashed from AccessibilityNodeInfo, which is not available for third-party developers (because of reflection denial) as well as on previous android versions.

So I'm wondering if there is an alternative way to get the info from the system which clarifies that my accessibility service is crashed/stopped working and user's action is required. Starting from Lollipop. Hints appreciated.

Bringoff
  • 1,093
  • 1
  • 10
  • 24

3 Answers3

4

I came up with an idea to use a static boolean indicating the status of Accessibility Service and compare it with Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES. I've tested on multiple devices, haven't found any issue with this method.

1.Declare a static boolean in Accessibility Service.

private static boolean bServiceRunning = false;

2.In Accessibility Service, set the boolean value in onServiceConnected and onUnbind

@Override
protected void onServiceConnected() {

    super.onServiceConnected();
    bServiceRunning = true; //put this at the very beginning to reduce time gap
}

@Override
public boolean onUnbind(Intent intent) {
        
   bServiceRunning = false;
   return super.onUnbind(intent);
}

3.Create a static function in Accessibility Service

public static boolean bGetServiceStatus(){
    return bServiceRunning;
}

With the boolean flag, I can know if the accessibility service is running in the desired state. When the service is being forced to stop, onUnbind will be called so the boolean value turns into false.

4.We use Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES to get accessibility service switch status

public static boolean bIsAccessibilityServiceEnabled(Context context, Class<?> accessibilityService) {

        ComponentName expectedComponentName = new ComponentName(context, accessibilityService);
        String strServicesSettingResult = Settings.Secure.getString(context.getContentResolver(),  Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
        if (strServicesSettingResult == null){
            return false;
        }

        TextUtils.SimpleStringSplitter colonSplitter = new TextUtils.SimpleStringSplitter(':');
        colonSplitter.setString(strServicesSettingResult);

        while (colonSplitter.hasNext()) {
            String strComponentName = colonSplitter.next();
            ComponentName enabledService = ComponentName.unflattenFromString(strComponentName);

            if (enabledService != null && enabledService.equals(expectedComponentName))
                return true;
        }

        return false;
    }

5.And this is what we want, we check with the above two methods the determine the real state of accessibility service.

public static int intIsAccessibilityServiceEnabled_WithCrashCheck(Context context, Class<?> accessibilityService){
        //return 0 if Accessibility Service enabled and running
        //return -1 if Accessibility Service disabled and not running
        //return -2 if Accessibility Service enabled but stopped working or crashed

        //first check Accessibility Service boolean

        if(bGetServiceStatus()){
            //service is running
            return 0;
        }else{
            //service not running, now double check with Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
            boolean bResult = bIsAccessibilityServiceEnabled(context, accessibilityService);

            if(!bResult){
                //Accessibility Service is disabled
                return -1;
            }else{
                //Accessibility Service is enabled, but service is not actually running, Accessibility Service is crashed
                return -2;
            }
        }

    }

Using "AccessibilityManager" also works the same, but I prefer a more "lightweight" version with static boolean for better performance.


Note: Using Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES without another doublecheck will cause a bug. The value is not synced. The result doesn't always represent the real service status. The following steps can create such a case :

  1. Start an accessibility service then go to App settings force stop the App.
  2. Now you can see the accessibility service switch is turned off. (Seems enough currently, but the next step will create a problem)
  3. Start the accessibility service again,on Android 9.0+ devices you'll find out the switch is switched on, but the service is actually not running, and now it displays "Not working. Tap for info."

2022/11/29 Edit: This bug is fixed according to this issue tracker. I can no longer reproduce this bug on my new Android 12 & 13 devices. However, devices with old Android firmware still has this bug. (The patch is also applied to the newest AVD images. To test this bug with AVD, now you must download old revisions of the AVD images.)

Lynch Chen
  • 178
  • 2
  • 16
1

Android generally prevents apps from running if they crash repeatedly. This behavior for an accessibility service can obviously affect users who depend on the service, but since these services can effectively control the UI, having one that crashes repeatedly could also make the device unusable.

It hadn't occurred to me that anyone else would be interested in the crashed field in AccessibilityServiceInfo. I populated that field using data only available to the system unfortunately. I compare the list of services that are enabled with the list of those that are bound.

If you're interested if your service is prevented from running, you could probably do something similar by keeping track of when onBind and onUnbind is called and looking at the list of enabled services from AccessibilityManager.

Phil Weaver
  • 738
  • 3
  • 7
  • Thanks for answering and for idea with unbound event. Will try to implement. Every app crashes from time to time, and when accessibility service runs in the same process with the rest of the app, even some rare app UI bug may stop accessibility. And my app is dedicated to device control, which user should be unable to kill. On Samsung devices Knox helps a lot, but for other Androids I use a lot of tricks to prevent users from breaking the app. Accessibility service is like the weakest part as my app restarts after being killed, but not accessibility service. – Bringoff Mar 09 '19 at 10:40
  • Hey, @Bringoff, did you have any success with the Bind / Unbind solution? – Vasili Fedotov Aug 04 '20 at 13:39
  • @VasiliFedotov not really. We had improved our crash rate and user reports about lost accessibility reduced. Also, I've started to check accessibility status in background via ENABLED_ACCESSIBILITY_SERVICES setting. It's not immediate but was enough for our needs. – Bringoff Aug 05 '20 at 14:19
  • @VasiliFedotov I have posted a solution related to unbind. – Lynch Chen Feb 18 '21 at 06:49
0

I don't know if this is a solution. But I did find when it doesn't work: if I use "dumpsys accessibility", the services part is empty, looks like:

User state[attributes:{id=0, 
                       currentUser=true, 
                       touchExplorationEnabled=false,  
                       displayMagnificationEnabled=false,   
                       navBarMagnificationEnabled=false,  
                       autoclickEnabled=false}
           services:{}]

Maybe you can check if the services is empty.

סטנלי גרונן
  • 2,917
  • 23
  • 46
  • 68