Reliable testing: test-butler and rooted devices

test-butler is a nice tool from LinkedIn to improve reliability of Android tests by disabling animations, getting rid of annoying dialogs like ANR/crash and granting runtime permissions amongst other useful features. Unfortunately due to the implementation of the app it can only be used on emulators. Well, at least it was like that up until now.

TL;DR
- Root and connect your device
- Make sure adb, wget, GNU sed and apktool are installed and in $PATH
- Run the script

In order to control the device test-butler needs access to several permissions. Let’s look at them:

Declaration of these permissions can be found in the source of android in the AndroidManifest for the core package:

The attribute protectionLevel is the important part for us to understand how to make it work on real device.
Gathering minimum protectionLevels from all of these permissions we need to provide to our app package we get:

  • signature
  • privileged
  • normal

Normal permissions are granted during installation of the app after user accepts to install the app. No trouble here.

Privileged permissions are granted to system apps. The easiest way to get this is to put the apk to privileged folder for system apps which is /system/priv-app/ starting from Android 18:

So in order to get privileged permissions we just need to place the test-butler app into /system/priv-app? Well, not so fast. test-butler declares that it wants to run as system uid in the manifest

Unfortunately this will trigger a check of signature of the apk. signature level permissions that test-butler declares will also trigger a check of the signature of the apk. In order to properly pass this validation we need to get the private key of the whole build of device’s OS. This key is public for emulators, hence it worked for test-butler before. But since we don’t have production keys for all the device manufacturers we’ll have to get around this validation.

PackageManagerService checks if the signature is valid by calling method grantSignaturePermission which returns (surprise) boolean:

So we just need to patch this to always return true. For that we’ll use the apktool and sed.

PackageManagerService resides in /system/framework/services.jar. So after pulling this jar and decompiling we need to replace the implementation of grantSignaturePermissions like this:

Next, we rebuild the jar back and replace the device’s version. We also need to push the test-butler apk into the /system/priv-app/ folder.

Restart all android services and voila!

⇒  adb shell dumpsys package com.linkedin.android.testbutler
Packages:
Package [com.linkedin.android.testbutler] (d646bb8):
userId=1000
sharedUser=SharedUserSetting{a2e7a0f android.uid.system/1000}
privateFlags=[ PRIVILEGED ]
requested permissions:
android.permission.SET_ACTIVITY_WATCHER
android.permission.GRANT_REVOKE_PERMISSIONS
android.permission.SET_ANIMATION_SCALE
android.permission.CHANGE_CONFIGURATION
android.permission.WRITE_SECURE_SETTINGS
android.permission.DISABLE_KEYGUARD
android.permission.WAKE_LOCK
android.permission.ACCESS_WIFI_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.WRITE_SETTINGS
install permissions:
android.permission.SET_ACTIVITY_WATCHER: granted=true
android.permission.SET_ANIMATION_SCALE: granted=true
android.permission.CHANGE_CONFIGURATION: granted=true
android.permission.WRITE_SECURE_SETTINGS: granted=true
android.permission.DISABLE_KEYGUARD: granted=true
android.permission.WAKE_LOCK: granted=true
android.permission.ACCESS_WIFI_STATE: granted=true
android.permission.CHANGE_WIFI_STATE: granted=true
android.permission.WRITE_SETTINGS: granted=true
...

All of this is packed into a nice script and available on GitHub. Keep in mind that you need to have wget, apktool, adb and a GNU version of sed installed as mentioned before.

Just have these tools in $PATH, connect your device and run the script. In case something goes wrong /system/framework/services.jar.backup will be waiting for you.

Happy testing!


Big thanks to Ilya Lim, without his in-depth knowledge of Android internals this wouldn’t be possible.