Development tutorial

rovo89 edited this page Apr 3, 2016 · 12 revisions

Alright.. you want to learn how you can create a new module for Xposed? Then read this tutorial (or let's rather call it "extensive essay") and learn how to approach this. This includes not only the technical "create this file and insert ...", but also the thinking behind it, which is the step where value is created and for which you really need to understand what you do and why. If you feel like "TL;DR", you can just look at the final source code and read the "Making the project an Xposed module" chapter. But you will get a better understanding if you read the whole tutorial. You will save the time spent for reading this later because you don't have to figure everything out yourself.

The modification subject

You will recreate the red clock example that can be found at Github as well. It includes changing the color of the status bar clock to red and adding a smiley. I'm choosing this example because it is a rather small, but easily visible change. Also, it uses some of the basic methods provided by the framework.

How Xposed works

Before beginning with your modification, you should get a rough idea how Xposed works (you might skip this section though if you feel too bored). Here is how:

There is a process that is called "Zygote". This is the heart of the Android runtime. Every application is started as a copy ("fork") of it. This process is started by an /init.rc script when the phone is booted. The process start is done with /system/bin/app_process, which loads the needed classes and invokes the initialization methods.

This is where Xposed comes into play. When you install the framework, an extended app_process executable is copied to /system/bin. This extended startup process adds an additional jar to the classpath and calls methods from there at certain places. For instance, just after the VM has been created, even before the main method of Zygote has been called. And inside that method, we are part of Zygote and can act in its context.

The jar is located at /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar and its source code can be found here. Looking at the class XposedBridge, you can see the main method. This is what I wrote about above, this gets called in the very beginning of the process. Some initializations are done there and also the modules are loaded (I will come back to module loading later).

Method hooking/replacing

What really creates the power of Xposed is the possibility to "hook" method calls. When you do a modification by decompiling an APK, you can insert/change commands directly wherever you want. However, you will need to recompile/sign the APK afterwards and you can only distribute the whole package. With the hooks you can place with Xposed, you can't modify the code inside methods (it would be impossible to define clearly what kind of changes you want to do in which place). Instead, you can inject your own code before and after methods, which are the smallest unit in Java that can be addressed clearly.

XposedBridge has a private, native method hookMethodNative. This method is implemented in the extended app_process as well. It will change the method type to "native" and link the method implementation to its own native, generic method. That means that every time the hooked method is called, the generic method will be called instead without the caller knowing about it. In this method, the method handleHookedMethod in XposedBridge is called, passing over the arguments to the method call, the this reference etc. And this method then takes care of invoking callbacks that have been registered for this method call. Those can change the arguments for the call, change instance/static variables, invoke other methods, do something with the result... or skip anything of that. It is very flexible.

Ok, enough theory. Let's create a module now!

Creating the project

A module is normal app, just with some special meta data and files. So begin with creating a new Android project. I assume you have already done this before. If not, the official documentation is quite detailed. When asked for the SDK, I chose 4.0.3 (API 15). I suggest you try this as well and do not start experiments yet. You don't need to create an activity because the modification does not have any user-interface. After answering that question, you should have a blank project.

Making the project an Xposed module

Now let's turn the project into something loaded by Xposed, a module. Several steps are required for this.

Adding the Xposed Framework API to your project

You can find the required steps in Using the Xposed Framework API. Make sure to remember the API version, you'll need it in the next step.

AndroidManifest.xml

The module list in the Xposed Installer looks for applications with a special meta data flag. You can create it by going to AndroidManifest.xml => Application => Application Nodes (at the bottom) => Add => Meta Data. The name should be xposedmodule and the value true. Leave the resource empty. Then repeat the same for xposedminversion (the API version from the previous step) and xposeddescription (a very short description of your module). The XML source will now look like this:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="de.robv.android.xposed.mods.tutorial"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="Easy example which makes the status bar clock red and adds a smiley" />
        <meta-data
            android:name="xposedminversion"
            android:value="53" />
    </application>
</manifest>

Module implementation

Now you can create a class for your module. Mine is named "Tutorial" and is in the package de.robv.android.xposed.mods.tutorial:

package de.robv.android.xposed.mods.tutorial;

public class Tutorial {

}

For the first step, we will just do some logging to show that the module was loaded. A module can have a few entry points. Which one(s) you choose depends on the what you want to modify. You can have Xposed call a function in your module when the Android system boots, when a new app is about to be loaded, when the resources for an app are initialised and so on.

A bit further down this tutorial, you will learn that the necessary changes need to be done in one specific app, so let's go with the "let me know when a new app is loaded" entry point. All entry points are marked with a sub-interface of IXposedMod. In this case, it's IXposedHookLoadPackage which you need to implement. It's actually just one method with one parameter that gives more information about the context to the implementing module. In our example, let's log the name of the loaded app:

package de.robv.android.xposed.mods.tutorial;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        XposedBridge.log("Loaded app: " + lpparam.packageName);
    }
}

This log method writes the message to the standard logcat (tag Xposed) and to /data/data/de.robv.android.xposed.installer/log/debug.log` (which is easily accessible via the Xposed Installer).

assets/xposed_init

The only thing that is still missing now is a hint for XposedBridge which classes contain such entry points. This is done via a file called xposed_init. Create a new text file with that name in the assets folder. In this file, each line contains one fully qualified class name. In this case, this is de.robv.android.xposed.mods.tutorial.Tutorial.

Trying it out

Save your files. Then run your project as Android application. As this is the first time you install it, you need to enable it before you can use it. Open the Xposed Installer app and make sure you have installed the framework. Then go to the "Modules" tab. You should find your app in there. Check the box to enable it. Then reboot. You will not see a difference of course, but if you check the log, you should see something like this:

Loading Xposed (for Zygote)...
Loading modules from /data/app/de.robv.android.xposed.mods.tutorial-1.apk
  Loading class de.robv.android.xposed.mods.tutorial.Tutorial
Loaded app: com.android.systemui
Loaded app: com.android.settings
... (many more apps follow)

Voilà! That worked. You now have an Xposed module. It could just be a bit more useful than writing logs...

Exploring your target and finding a way to modify it

Ok, so now begins the part that can be very different depending on what you want to do. If you have modded APKs before, you probably know how to think here. In general, you first need to get some details about the implementation of the target. In this tutorial, the target is the clock in the statusbar. It helps to know that the statusbar and lots of other things are part of the SystemUI. So let's begin our search there.

Possibility one: Decompile it. This will give you the exact implementation, but it is hard to read and understand because you get smali format. Possibility two: Get the AOSP sources (e.g. here or here and look there. This can be quite different from your ROM, but in this case it is a similar or even the same implementation. I would look at AOSP first and see if that is enough. If I need more details, look at the actual decompiled code.

You can look for classes with "clock" in their name or containing that string. Other things to look for are resources and layout used. If you downloaded the official AOSP code, you can start looking in frameworks/base/packages/SystemUI. You will find quite a few places where "clock" appears. This is normal and indeed there will be different ways to implement a modification. Keep in mind that you can "only" hook methods. So you have to find a place where you can insert some code to do the magic either before, after or replacing a method. You should hook methods that are as specific as possible, not ones that are called thousands of times to avoid performance issues and unintended side-effects.

In this case, you might find that the layout res/layout/status_bar.xml contains a reference to a custom view with the class com.android.systemui.statusbar.policy.Clock. Multiple ideas might come to your mind now. The text color is defined via a textAppearance attribute, so the cleanest way to change it would be to change the appearance definition. However, the this is not possible to change styles with the Xposed framework and probably won't be (it's too deep in native code). Replacing the layout for the statusbar would be possible, but an overkill for the small change you are trying to make. Instead, look at this class. There is a method called updateClock, which seems to be called every minute to update the time:

final void updateClock() {
    mCalendar.setTimeInMillis(System.currentTimeMillis());
    setText(getSmallTime());
}

That looks perfect for modifications because it is a very specific method which seems to be the only means of setting the text for the clock. If we add something after every call to this method that changes the color and the text of the clock, that should work. So let's do it.

For the text color alone, there is an even better way. See the example for "Modifying layouts" on "Replacing resources".

Using reflection to find and hook a method

What do we already know? We have a method updateClock in class com.android.systemui.statusbar.policy.Clock that we want to intercept. We found this class inside the SystemUI sources, so it will only be available in the process for the SystemUI. Some other classes belong to the framework and are available everywhere. If we tried to get any information and references to this class directly in the handleLoadPackage mehod, this would fail because it is the wrong process. So let's implement a condition to execute certain code only when a specified package is about to be loaded:

public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
    if (!lpparam.packageName.equals("com.android.systemui"))
        return;

    XposedBridge.log("we are in SystemUI!");
}

Using the parameter, we can easily check if we are in the correct package. Once we verified that, we get access to the classes in that packages with the ClassLoader which is also referenced from this variable. Now we can look for the com.android.systemui.statusbar.policy.Clock class and its updateClock method and tell XposedBridge to hook it:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called before the clock was updated by the original method
            }
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called after the clock was updated by the original method
            }
    });
    }
}

findAndHookMethod is a helper function. Note the static import, which is automatically added if you configure it as described in the linked page. This method looks up the Clock class using the ClassLoader for the SystemUI package. Then it looks for the updateClock method in it. If there were any parameters to this method, you would have to list the types (classes) of these parameters afterwards. There are different ways to do this, but as our method doesn't have any parameters, let's skip this for now. As the last argument, you need to provide an implementation of the XC_MethodHook class. For smaller modifications, you can use a anonymous class. If you have much code, it's better to create a normal class and only create the instance here. The helper will then do everything necessary to hook the method as described above.

There are two methods in XC_MethodHook that you can override. You can override both or even none, but the latter makes absolutely no sense. These methods are beforeHookedMethod and afterHookedMethod. It's not too hard to guess that the are executed before/after the original method. You can use the "before" method to evaluate/manipulate the parameters of the method call (via param.args) and even prevent the call to the original method (sending your own result). The "after" method can be used to do something based on the result of the original method. You can also manipulate the result at this point. And of course, you can add your own code which should be executed exactly before/after the method call.

If you want to replace a method completely, have a look at the subclass XC_MethodReplacement instead, where you just need to override replaceHookedMethod.

XposedBridge keeps a list of registered callbacks for each hooked method. Those with highest priority (as defined in hookMethod) are called first. The original method has always the lowest priority. So if you have hooked a method with callbacks A (prio high) and B (prio default), then whenever the hooked method is called, the control flow will be this: A.before -> B.before -> original method -> B.after -> A.after. So A could influence the arguments B gets to see, which could further change them before passing them on. The result of the original method can be processed by B first, but A has the final word what the original caller gets.

Final steps: Execute your own code before/after the method call

Alright, you have now a method that is called every time the updateClock method is called, with exactly that context (i.e. you're in the SystemUI process). Now let's modify something.

First thing to check: Do we have a reference to the concrete Clock object? Yes we have, it's in the param.thisObject parameter. So if the method was called with myClock.updateClock(), then param.thisObject would be myClock.

Next: What can we do with the clock? The Clock class is not available, you can't cast param.thisObject to class (don't even try to). However it inherits from TextView. So you can use methods like setText, getText and setTextColor once you have casted the Clock reference to TextView. The changes should be done after the original method has set the new time. As there is nothing to do before the method call, we can leave out the beforeHookedMethod. Calling the (empty) "super" method is not necessary.

So here is the complete source code:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import android.graphics.Color;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                TextView tv = (TextView) param.thisObject;
                String text = tv.getText().toString();
                tv.setText(text + " :)");
                tv.setTextColor(Color.RED);
            }
        });
    }
}

Be happy about the result

Now install/start your app again. As you have already enabled it in the Xposed Installer when you started it the first time, you do not need to do that again, rebooting is enough. However, you will want to disable the red clock example if you were using it. Both use the default priority for their updateClock handler, so you cannot know which one will win (it actually depends on the string representation of the handler method, but don't rely on that).

Conclusion

I know that this tutorial was very long. But I hope you can now not only implement a green clock, but also something completely different. Finding good methods to hook is a matter of experience, so start with something rather easy. Try using the log functions a lot in the beginning to make sure that everything is called when you expect it. And now: Have fun!