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_processexecutable 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 mainmethod.
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.
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 (see
below) and xposeddescription (a
very short description of your module). The XML source will now
look like this:
package="de.robv.android.xposed.mods.tutorial"
android:versionCode="1"
android:versionName="1.0" >
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
android:name="xposedmodule"
android:value="true" />
android:name="xposeddescription"
android:value="Easy example which makes the status bar clock red and adds a smiley" />
android:name="xposedminversion"
android:value="30" />
XposedBridgeApi.jar
Next, make the XposedBridge API known to the project. You can
download XposedBridgeApi-.jar from
the first post of this
XDA thread. Copy it into a subfolder
called lib.
Then right-click on it and select Build Path => Add to Build
Path. The from the file name is
the one you insert as xposedminversion in
the manifest.
Make
sure that the API classes are not included (but only referenced) in
your compiled APK, otherwise you will get
an IllegalAccessError.
Files in the libs (with
"s") folder are automatically included by Eclipse, so don't put the
API file there.
Module implementation
Now you can create a class for your module. Mine is named
"Tutorial" and is in the packagede.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 theassets folder.
In this file, each line contains one fully qualified class name. In
this case, this isde.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 orhere 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 inframeworks/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
classcom.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 itsupdateClock 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 andafterHookedMethod.
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
subclassXC_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 theparam.thisObject parameter.
So if the method was called with myClock.updateClock(),
thenparam.thisObject would
be myClock.
Next: What can we do with the clock?
The Clock class
is not available, you can't castparam.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
toTextView.
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 theirupdateClock 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!