exteraGram

Xposed Method Hooking

Xposed method hooking to intercept and modify app behavior in your plugins.

Introduction

Xposed method hooking allows your plugin to intercept calls to methods (or constructors) within the application, modify their parameters, change their behavior, or replace their implementation entirely. This is a powerful technique for altering app functionality at a low level.

Hooking Concepts

To hook a method, you need to provide a "hook handler" — a Python class that defines what code to run when the target method is called. The system supports three main ways to interact with a method call.

The Hook Handler Base Classes

For clarity and correctness, you should create your handler by inheriting from one of the abstract base classes provided in base_plugin.py:

  • MethodHook: Use this when you want to run code before and/or after the original method executes, but still allow the original method to run.
  • MethodReplacement: Use this when you want to completely replace the original method's logic with your own.

The param Object

All hook callback methods receive a param object (de.robv.android.xposed.XC_MethodHook.MethodHookParam) which is your key to interacting with the method call:

  • param.thisObject: The instance on which the method was called (None for static methods).
  • param.args: A list-like object of the arguments passed to the method. You can read and modify these. Changes made in before_hooked_method will affect the original call.
  • param.result: The value returned by the original method. Available in after_hooked_method. You can read and modify this.
  • param.method: A java.lang.reflect.Member object representing the hooked method or constructor.

A special and very useful feature is param.returnEarly = True. If you set this in before_hooked_method, the original method and any after_hooked_method logic will be skipped entirely. You must also set param.result to provide an immediate return value.

Reference: LSPosed XC_MethodHook.java

The Hooking Process (Step-by-Step)

1. Find the Target Method or Constructor

First, you need a reference to the java.lang.reflect.Method or java.lang.reflect.Constructor you want to hook. This is done using Java reflection.

from hook_utils import find_class
from java import jint, jboolean, jarray
from java.lang import String as JString
 
# Use find_class for safety. It returns None if the class is not found.
ActionBarClass = find_class("org.telegram.ui.ActionBar.ActionBar")
if not ActionBarClass:
    self.log("ActionBar class not found!")
    return
 
# --- Finding a Method ---
# Example: public void setTitle(CharSequence title)
try:
    # Get the class for the parameter type
    CharSequenceClass = find_class("java.lang.CharSequence")
    # Get the method
    method_to_hook = ActionBarClass.getDeclaredMethod("setTitle", CharSequenceClass)
    method_to_hook.setAccessible(True)  # Important for non-public methods
except Exception as e:
    self.log(f"Failed to find method 'setTitle': {e}")
 
# --- Finding a Constructor ---
# Example: public ActionBar(Context context)
try:
    ContextClass = find_class("android.content.Context")
    constructor_to_hook = ActionBarClass.getDeclaredConstructor(ContextClass)
    constructor_to_hook.setAccessible(True) # Important for non-public constructors
except Exception as e:
    self.log(f"Failed to find constructor: {e}")

2. Implement the Hook Handler

Create a Python class that inherits from MethodHook or MethodReplacement and implements the required callback(s).

from base_plugin import MethodHook, MethodReplacement
 
# For running code before/after the original method
class TitleLoggerHook(MethodHook):
    def __init__(self, plugin):
        self.plugin = plugin # Pass your plugin instance for logging, etc.
 
    def before_hooked_method(self, param):
        title = param.args[0]
        self.plugin.log(f"ActionBar title is being set to: {title}")
        # Let's add a prefix to every title
        param.args[0] = f"[Hooked] {title}"
 
    def after_hooked_method(self, param):
        self.plugin.log(f"ActionBar title has been set.")
 
 
# For completely replacing the original method
class TitleReplacer(MethodReplacement):
    def __init__(self, plugin):
        self.plugin = plugin
 
    def replace_hooked_method(self, param):
        self.plugin.log("ActionBar.setTitle() was called, but we are blocking it.")
        # The original method is NOT called.
        # Since the original method returns void, we don't need to return anything.
        return None

3. Apply the Hook

From your BasePlugin class, instantiate your handler and call self.hook_method().

# In your on_plugin_load method or another appropriate place:
 
# Get the method to hook (as shown in Step 1)
try:
    ActionBarClass = find_class("org.telegram.ui.ActionBar.ActionBar")
    CharSequenceClass = find_class("java.lang.CharSequence")
    set_title_method = ActionBarClass.getDeclaredMethod("setTitle", CharSequenceClass)
 
    # Instantiate your handler and apply the hook
    handler_instance = TitleLoggerHook(self)
    self.unhook_obj = self.hook_method(set_title_method, handler_instance, priority=10)
 
    if self.unhook_obj:
        self.log("Successfully hooked ActionBar.setTitle()")
    else:
        self.log("Failed to hook ActionBar.setTitle()")
 
except Exception as e:
    self.log(f"Error during hooking setup: {e}")
 
# Hooks are automatically removed when your plugin is unloaded.
# If you need to remove a hook manually, you can use the returned object:
# if self.unhook_obj:
#   self.unhook_method(self.unhook_obj)

Practical Examples

Example 1: Modifying Arguments (Before Hook)

Let's modify every "Toast" message to add a prefix.

from base_plugin import MethodHook
from hook_utils import find_class
from java import jint
 
class ToastHook(MethodHook):
    def before_hooked_method(self, param):
        # Method signature: makeText(Context context, CharSequence text, int duration)
        original_text = param.args[1]
        param.args[1] = f"(Plugin) {original_text}"
 
# In your plugin's on_plugin_load:
try:
    ToastClass = find_class("android.widget.Toast")
    ContextClass = find_class("android.content.Context")
    CharSequenceClass = find_class("java.lang.CharSequence")
 
    make_text_method = ToastClass.getDeclaredMethod(
        "makeText", ContextClass, CharSequenceClass, jint
    )
    self.hook_method(make_text_method, ToastHook())
    self.log("Hooked Toast.makeText() successfully.")
except Exception as e:
    self.log(f"Failed to hook Toast: {e}")

Example 2: Changing the Return Value (After Hook)

This example hooks BuildVars.isMainApp() and makes it always return False.

from base_plugin import MethodHook
from hook_utils import find_class
 
class BuildVarsHook(MethodHook):
    def after_hooked_method(self, param):
        # Original result is in param.result, let's change it
        param.result = False
 
# In your plugin's on_plugin_load:
try:
    BuildVarsClass = find_class("org.telegram.messenger.BuildVars")
    is_main_app_method = BuildVarsClass.getDeclaredMethod("isMainApp")
    self.hook_method(is_main_app_method, BuildVarsHook())
    self.log("Hooked BuildVars.isMainApp() to always return False.")
except Exception as e:
    self.log(f"Failed to hook BuildVars: {e}")

Example 3: Replacing a Method (MethodReplacement)

This example completely disables a specific internal logging method to reduce logcat spam.

from base_plugin import MethodReplacement
from hook_utils import find_class
from java.lang import String as JString
 
class NoOpLogger(MethodReplacement):
    def replace_hooked_method(self, param):
        # Do nothing. The original logging method is never called.
        # It's a void method, so we return None.
        return None
 
# In your plugin's on_plugin_load:
try:
    FileLogClass = find_class("org.telegram.messenger.FileLog")
    # Target method: public static void d(String message)
    log_method = FileLogClass.getDeclaredMethod("d", JString)
    self.hook_method(log_method, NoOpLogger())
    self.log("Disabled FileLog.d(String) method.")
except Exception as e:
    self.log(f"Failed to disable FileLog.d: {e}")

Return Values in MethodReplacement

When using MethodReplacement, your Python replace_hooked_method is the new implementation. You are responsible for returning a value of the correct type.

  • For void Java methods, return or return None.
  • For methods returning primitives (e.g., int, boolean), return a standard Python int or bool.
  • For methods returning objects (e.g., String), return a compatible Python object or None (which becomes null in Java).