December 7, 2021

Programmatically talking to the Android system via ADB shell commands

A step by step instruction for building ADB shell tools that can be run from the commandline and call into the Android framework.

In my previous article (read it first. This one builds upon it). I left you off with how to run a Java programs on an Android device. However, since it is not possible to obtain a Context instance this way, such programs are not able to talk to (most parts of) the Android framework directly.

In this article, we will explore a method for asking a device for its screen size by calling into the framework using AIDL.

Do you speak Binder?

Without a Context , you can still talk to the Android framework, but you have to do everything the manual way.

Warning The Context class is the abstraction layer on which the Android API starts to become device independent. Android developers are not meant to directly communicate with system processes via the Binder interface, as device manufacturers may design AIDL interfaces differently.

Let’s ask the framework for the device’s screensize. For this, we need to manually make an IPC connection to the systemservice, responsible for managing screens and request a data transfer form it.

To setup the connection, we will cheat a bit and use a static method from the hidden system class ServiceManager . This is what actually happens inside of the Context.getSystemService() method as well and gets us a Binder reference that can be cast to a Binder proxy, able to handle all of the low level marshalling/unmarshalling operations for us. The code looks like this:

package com.example.shelltool;

import android.os.IBinder;
import android.os.ServiceManager;
import android.util.DisplayMetrics;
import android.hardware.display.IDisplayManager;

public class Main {

  public static void main(String[] args) {

    // The service name comes from the constants defined in android.content.Context
    IBinder displayBinder = ServiceManager.getService("display");
    IDisplayManager displayManager = IDisplayManager.Stub.asInterface(displayBinder);
    
    try {
      int ids[] = displayManager.getDisplayIds();
      for (int id : ids) {
        int h = displayManager.getDisplayInfo(id).appHeight;
        int w = displayManager.getDisplayInfo(id).appWidth;
        System.out.println("Screensize[" + id + "]: "+ h + " x "+w);
      }
    }
    catch(Throwable t) {
      t.printStackTrace();
    }
  }
}

The Main class, shown above, won’t compile straight away. Four additional things are required first:

  1. The 🗋 android.jar file from the Android SDK, containing stubs for classes, we are going to link against.
  2. Extra stubs that are not packaged in 🗋 android.jar because they are not meant to be accessed directly from apps (namely ServiceManager ).
  3. The 🗋 framework.aidl file from the Android SDK containing common AIDL definitions for marshalling / unmarshalling.
  4. The AIDL definitions specific to the displaymanager service.

If you haven’t done so already, please download the Android SDK now. The rest of this article will assume that your are using Linux and have it installed in your $HOME directory as 🖿 android-sdk-linux/ and have added the tools to your search path.

Preparing the workspace

Start by download and unzipping the project folder. I contains the following directory structure and source files:

.
├── bin
└── src
    ├── android
    │   ├── hardware
    │   │   └── display
    │   │       └── IDisplayManager.aidl
    │   ├── os
    │   │   └── ServiceManager.java
    │   └── view
    │       ├── CompatibilityInfo.java
    │       ├── DisplayAdjustments.java
    │       ├── DisplayInfo.aidl
    │       └── DisplayInfo.java
    └── com
        └── example
            └── shelltool
                └── Main.java

The ServiceManager class is simply a stub, a file that should be included in 🗋 android.jar , but isn’t because Apps are not suppose to link against it. Stubs are only needed to provide the symbols, needed to compile the Main class. Their code is never actually run or even included in the final executable.

Besides 🗋 Main.java , the most interesting file in the zip is 🗋 IDisplayManager.aidl . It is taken from AOSP and has been abbreviated to just the two methods, we are actually going to call, so it only depends on the three additional (stub) classes in the android.view package. Note that that package also contains an AIDL file which is needed to declare the DisplayInfo stub as parcelable.

Building the project

The first thing that needs to be done is to compile 🗋 IDisplayManager.aidl into java code. Use the Android SDK’s aidl tool for that. Run it in the folder containing the 🖿 src/ and 🖿 bin/ directories:

aidl -p$HOME/android-sdk-linux/platforms/android-21/framework.aidl -Isrc src/android/hardware/display/IDisplayManager.aidl

NOTE: you do not need to compile 🗋 DisplayInfo.aidl . It is a dependency. The -Isrc parameter takes care of it.

The next step is to actually compile the program. Change to the 🖿 src/ path and run the java compiler to generate regular class files:

javac -source 1.7  -target 1.7 -d ../bin -cp .:$HOME/android-sdk-linux/platforms/android-21/android.jar com/example/shelltool/Main.java

NOTE: make sure to compile to Java 1.7, the dx tool depends on it!

Time to dex the program. The stub files are no longer needed at this point. Remove the 🖿 bin/android/ directory and run the dx tool:

dx --output=shelltools.jar --dex bin

The newly generated shell tool is now ready to be pushed to the device and run there.

Running the program

Push the generated jar file to the device using ADB:

adb push shelltools.jar /data/local/tmp

Create the starter shellscript in 🖿 /data/local/tmp :

base=/data/local/tmp/
export CLASSPATH=$base/shelltools.jar
export ANDROID_DATA=$base
mkdir -p $base/dalvik-cache
exec app_process $base com.example.shelltool.Main "$@"

Make it executable and run it:

adb shell chmod 777 /data/local/tmp/shellscript.sh
adb shell /data/local/tmp/shellscript.sh

It should print the size of all connected screens to the shell. If it doesn’t, check logcat. The most common reason for a shelltool to crash with a segfault is a wrong class name in the shell script.