20 февраля в 10:13

Модификация стоковых прошивок для Android. Часть 5. Революция c Xposed Framework

В 2012 году пользователь с ником rovo89 на комьюнити XDA опубликовал исходные коды и готовый к использованию фреймворк, упрощающий кастомизацию прошивок, с подробной инструкцией и примерами, предложив альтернативу традицоинному, на тот момент, способу (деодексирование→ дизассемблирование → декомпиляция → рекомпиляция → тестирование → загрузка патчей в телефон): Часть 1, Часть 2, Часть 3, Часть 4

Он предложил использовать отдельные модули, которые можно изменять фактически налету, не вмешиваясь в исходный код прошивки или отдельных ее компонентов. Но авторитетные разработчики хором ответили: "Неа… никому это не нужно"

Rovo не забросил свое детище, а продолжил развитие. Когда в 2013-ом году вышла версия KitKat, все то же сообщество именитых девелоперов ответило: "Неа, это слишком опасно… хотя..."

Тем временем Google принял решение выпускать новую версию операционной системы ежегодно. Разумеется, стало накладно кастомизировать прошивки: не успеваешь сделать одну, как появляется новая версия ОС. Так в 2014-ом выходит версия Lollipop и разработчики наконец обращают внимание на фреймфорк с мыслями: "Может все же стоит посмотреть что это такое? Выглядит многообещающим".

В 2015-ом году вышла очередная версия (Marshmallow) и большая часть сообщества, в итоге, признала разработку весьма стоящей, на порядок сокращающей время разработки кастомизаций стоковых прошивок в виде установки отдельных модулей, расширяющих функционал. "Ничего себе, это же совсем безопасно и есть библиотека готовых полезных модулей!" — воскликнули они.

Сейчас конец февраля 2017-го. Xposed под Nougat все еще не доступен, а толпы страждущих разработчиков и пользователей то и дело открывают темы под тип "Xposed не работает! Он мне нужен! Rovo, пожалуйста!"

Сегодня я расскажу об Xposed Framework.

Отказ от ответственности


Любые названия продуктов и торговые марки, упоминаемые в тексте, являются собственностью их зарегистрированных владельцев. Тексты статьи опубликован в образовательных целях. Авторы (@Falseclock и xronofag) не несут ответственность за возможные убытки, порчу оборудования, недополученную прибыль вследствие попыток воспроизведения или использования информации в данной статье. Продолжая читать, читатель тем самым подтверждает, что любое использование исходного кода может повлечь для него риски.

Отступление


Больше трех лет назад я начал писать цикл статей про модификацию прошивок для Android. В данный момент актуальность данных статей весьма сомнительна: процесс очень трудоемок, требует много времени и инструментов, а малейшая ошибка может обернуться значительными проблемами вплоть до полной перепрошивки телефона.

Я прождал около двух лет в надежде, что кто-то все же опишет на Хабре что такое Xposed Framework и как он может упросить процесс кастомизации приложений, но подозреваю, что мало кто знаком с этим чудным творением. Для своих нужд я написал несколько десятков модулей, некоторые даже публиковал в общем репозитарии, который на данный момент содержит более 1000 готовых к эксплуатации разработок. Каждый модуль — это может быть не одна единственная кастомизация, а целый набор функций с интерфейсом настроек или дополнительного функционала. Яркий пример тому GravityBox или Sense ToolBox.

Я не буду описывать процесс установки и получение root прав, а хочу описать что из себя представляет фреймфорк и как написать простейший модуль.

Установка


Если раньше для установки сторонней прошивки требовалось производить разблокировку загрузчика телефона, то для использования Xposed пользователю необходимо всего лишь иметь root права на телефоне. Даже сейчас получение этих прав на большинстве девайсов не представляет больших сложностей: вы скачиваете приложение, а оно само делает все нужные манипуляции и через несколько минут вы становитесь обладателем телефона с уровнем прав «БОГ».

Принцип работы «на пальцах»


В системе Android есть процесс под название «Zygote». Это и есть основная исполнительная система. Любой процесс запускается как его копия. Zygote запускается через /init.rc как только загружается основное ядро системы. Запуск приложений осуществляется через скрипт /system/bin/app_process, который подгружает необходимые классы и запускает инициализацию приложения через задекларированные методы.

Именно в этом месте на сцену выходит Xposed. При установке фреймворка модифицированный app_process копируется в /system/bin. Суть модификации в том, что в переменную среду добавляется дополнительная jar библиотека, которая может исполнять специальные методы при определенных условиях и случаях. Например, мы можем вмешаться сразу, как только создается виртуальная машина Dalvik или даже перед тем как будет вызван основной метод Zygote. Являясь частью процесса Zygote, мы можем вмешиваться в работу любых методов, даже синтетических и производить любые действия в их контексте.

Практическая ценность Xposed


Предположим, необходимо изменить какой-то метод, и, например, вместо булева значения TRUE вернуть FALSE. Вместо трудоемкого и затратного традиционного метода (разбираем-собираем-тестируем приложение) вы можете «перехватить» этот метод класса и «вживить» свой Java код, который произведет необходимую операцию (изменит, в данном примере, значение) и вернет нужный нам результат. При этом, с Xposed можно: модифицировать или просто посмотреть какие данные передаются в метод, либо после исполнения метода узнать результат обработки данных и в зависимости от требований изменить их или воспользоваться ими.

Бывают случаи, когда вы не хотите, чтобы тот или иной метод вообще срабатывал, либо хотите полностью поменять логику и алгоритм. Xposed позволяет полностью заменить метод, либо не дать ему вообще сработать.

В случае с ресурсами стало вообще все просто. Вы загружаете в свой модуль свои ресурсы, хоть графические элементы, хоть цельные xml файлы и при загрузке модуля в память можете заменять их при инициализации.

А теперь главное — зачастую не приходится даже изменять работу модуля (или производить минимальные доработки) с выходом новой версии прошивки/приложения! Это логично, ведь названия методов, классов и переменных, как правило, остаются прежними.

Не правда ли, удобно?

Создание модуля


Сам модуль — это обычный apk файл, созданный в среде разработки. В нем не обязательно должно присутствовать какое-то Activity либо графические ресурсы. По сути там может быть только один файл с инструкцией, разумеется помимо обязательных файлов, и модуль может работать. Для настройки нам надо сделать три вещи:

Manifest.xml


При установке любого приложения Xposed проверяет наличие определенных заголовков в Manifest.xml. Если есть три нужные строки, то фреймворк сохраняет информацию о приложении в своих настройках и далее вы можете активировать установленный модуль (здесь и далее будут использоваться примеры кода разных модулей, в том числе и для из известных приложений).

<meta-data android:name="xposedmodule" android:value="true" />
<meta-data android:name="xposedminversion" android:value="2.6*" />
<meta-data android:name="xposeddescription" android:value="Uber-Driver application patch tool" /

С первой строкой все понятно.

Во второй строке указываем минимальную версию фреймворка для работы с модулем. Каждая версия заточена под определенный релиз Android и чтобы модуль случайно не был запущен на платформе, для которой он не предназначен, указывается версия.

Третья строка определяет как будет идентифицироваться ваше приложение в списке доступных или установленных модулей на вашем телефоне.

/assets/xposed_init


В корне вашего приложения необходимо создать папку assets и положить в него файл с названием xposed_init. Внутри этого файла вы просто пишите в каком Java классе описана работа вашего модуля. Я привык называть класс как XMain. В моем случае в файле присутствует строка

uber.hack.XMain

Java class


Сам класс должен содержать один из трех методов для работы с фреймворком. Не обязательно указывать все, можно лишь указать те, с которыми вы планируете делать модификацию приложения или приложений. Я по привычке указываю все как шаблон.
public class XMain implements IXposedHookInitPackageResources, IXposedHookLoadPackage, IXposedHookZygoteInit
{
    public void initZygote(StartupParam startupParam) throws Throwable { }

    public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable { }

    public void handleLoadPackage( LoadPackageParam paramLoadPackageParam) throws Throwable  { }
}

Метод initZygote исполняется тогда, когда ваш модуль загружается в память.
Весьма удобен, если в вашем приложении имеется Activity с настройками вашего модуля. Как правило, данные вашего приложения сохраняются в файл
/data/data/YOUR_PACKAGE_NAME/shared_prefs/YOUR_PACKAGE_NAME_preferences.xml. Во время загрузки вы можете единожды получить дескриптор и во время работы просто считывать с него ваши настройки. Также можно предопределить любые переменные, сделать проверки. По сути это аналог метода-конструктора.

Метод handleLoadPackage исполняется в тот момент, когда Dalvik загружает исходный код любого! приложения при запуске. Это очень важный момент. Если у вас в телефоне 10 различных модулей, то через этот метод исходники запускаемого приложения «прогонятся» во всех 10-ти случаях. Для фильтрации используется обычная проверка по названию пакета. Разумеется, если вы хотите изменить работу нескольких приложений, то ставите столько проверок сколько вам нужно.

public void handleLoadPackage( LoadPackageParam paramLoadPackageParam) throws Throwable {
    final LoadPackageParam llpm = paramLoadPackageParam;
    String packageName = llpm.packageName;

    if (packageName.contains("ubercab.driver"))
    {

    }
}

Метод handleInitPackageResources нужен вам тогда, когда вы хотите подменить ресурсы приложения во время их загрузки в память. Здесь тоже самое как и с handleLoadPackage — вы фильтруете по названию приложения.

public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable {
    String pkg = resparam.packageName;
    if (pkg.equals("com.ubercab.driver")) {
    }
}

Важно указать наименование приложения так, как оно указано в манифесте. На этом настройка модуля в среде разработки закончена и можем приступить непосредственно к программированию.

Примеры кода


Основной код, если вы конечно не решили всего лишь изменить цвета или шрифты какого-то приложения, происходят в handleLoadPackage. Для этого мы предварительно изучаем исходный код интересующего приложения, придумываем как модифицировать код и описываем нашу логику.
Основная логика работает через findAndHookMethod метод, реже через findAndHookConstructor и findClass. Все основные методы можно подглядеть в классе XposedHelpers.

Выглядит это примерно так:

if (packageName.contains("ubercab.driver"))
{
    try {
        XposedHelpers.findAndHookMethod(
            "com.ubercab.driver.feature.online.DispatchedFragment",
            llpm.classLoader,
            "onCreateView",
            "android.view.LayoutInflater", "android.view.ViewGroup", "android.os.Bundle",
            new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                
                }
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                
                }
            }
        );
    } catch (Throwable t) {
        XposedBridge.log(t);
    }
}

В первую очередь нам надо обернуть наши «хуки» в try/catch, так как если в вашем коде будет ошибка, без обертки основное приложение может завершить работу c ошибкой, а полный трейс может попасть разработчику, который станет в курсе, что на его приложение наложили модуль. При обертке весь трейс можно вывести в лог Xposed и понять где у нас произошла ошибка и почему.

В findAndHookMethod первый параметр — это название класса, вторая — ссылка на загрузчик класса, третья — название нужного нам метода, далее идет перечисление в виде строк какие переменные передаются в метод и в конце наш callback метод.

Вот пример как можно заполучить контекст класса и воспользоваться им

Ловля контекста
try{
    XposedHelpers.findAndHookMethod("com.ubercab.driver.core.app.DriverApplication", llpm.classLoader, "init", new XC_MethodHook()
    {
        @Override
        protected void afterHookedMethod(MethodHookParam param) throws Throwable
        {
            Application application = (Application) param.thisObject;
            Context context = application.getApplicationContext();
            Intent intent = new Intent("uber.hack.ACTION_BACKGROUND");
            context.sendBroadcast(intent);
        }
    });
} catch (Throwable t) {
    XposedBridge.log(t);
}

Мы отлавливаем загрузку метода init в классе DriverApplication и через param.thisObject используем контекст для отправки широковещательного сообщения нашему ресиверу или сервису.

или если необходимо изменить передаваемые переменные в метод, то сделать это можно через
beforeHookedMethod
try{
    XposedHelpers.findAndHookMethod("com.htc.htcdialer.widget.DividerDrawable", paramLoadPackageParam.classLoader, "setDividerColor", "int", "int", new XC_MethodHook()
        {
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable
            {
                int paramInt1 = (Integer) param.args[0];
                int paramInt2 = (Integer) param.args[1];

                if (paramInt1 == 4) {
                    if (paramInt2 == -13388315)
                        param.args[1] = Color.RED;

                    if (paramInt2 == -13128336)
                        param.args[1] = Color.BLUE;
                }
            }
        });
} catch (Throwable t) {
    XposedBridge.log(t);
}


Помимо XC_MethodHook можно использовать XC_MethodReplacement. Название говорит само за себя. Мы полностью заменяем какой-то метод нашим собственным. Вот типичный пример когда мне захотелось избавиться от всплывающего уведомления, когда я подключаю телефон по USB к ноутбуку
SetUSBNotification
findAndHookMethod("com.android.settings.PSService", paramLoadPackageParam.classLoader, "SetUSBNotification", "android.content.Context", boolean.class, new XC_MethodReplacement()
{
    protected Object replaceHookedMethod(MethodHookParam param) throws Throwable
    {
        return null;
    }
});


В данном void методе вызывалось уведомление. Я его просто заменил и забыл что мне когда-то это мешало.

Были случаи и посложней. Я своими модулями исправлял ошибки в системных приложениях. Ждать, когда производитель исправит баг в новом ОТА приложении меня не особо прельщало, поэтому я исправлял сам.

Исправленные баги
public static void tweak_fix98918()
{
    // --------------------------------------------------
    // https://android-review.googlesource.com/#/c/98918/
    // --------------------------------------------------
    try {
        final Class<?> TaskRecord = XposedHelpers.findClass("com.android.server.am.TaskRecord", null);

        XposedHelpers.findAndHookMethod(TaskRecord, "setFrontOfTask", new XC_MethodReplacement()
        {
            @Override
            protected Object replaceHookedMethod(MethodHookParam param) throws Throwable
            {
                Object mActivities = XposedHelpers.getObjectField(param.thisObject, "mActivities");

                boolean foundFront = false;
                final int numActivities = (Integer) XposedHelpers.callMethod(mActivities, "size");
                for (int activityNdx = 0; activityNdx < numActivities; activityNdx++) {
                    final Object r = XposedHelpers.callMethod(mActivities, "get", activityNdx);
                    if (foundFront || XposedHelpers.getBooleanField(r, "finishing")) {
                        XposedHelpers.setBooleanField(r, "frontOfTask", false);
                    } else {
                        XposedHelpers.setBooleanField(r, "frontOfTask", true);
                        foundFront = true;
                    }
                }
                if (!foundFront && numActivities > 0) {
                    Object get = XposedHelpers.callMethod(mActivities, "get", 0);
                    XposedHelpers.setBooleanField(get, "frontOfTask", true);
                }

                return null;
            }

        });
    } catch (Throwable t) {
        XposedBridge.log(t);
    }
}
// --------------------------------------------------
// https://android-review.googlesource.com/#/c/81970/
// --------------------------------------------------
public static void tweak_fix81970()
{
    try {
        final Class<?> ActiveServices = XposedHelpers.findClass("com.android.server.am.ActiveServices", null);

        XposedHelpers.findAndHookMethod(ActiveServices, "killServicesLocked", "com.android.server.am.ProcessRecord", "boolean", new XC_MethodReplacement()
        {
            @Override
            protected Object replaceHookedMethod(MethodHookParam param) throws Throwable
            {
                // XposedBridge.log("killServicesLocked happen");

                Boolean DEBUG_SERVICE = (Boolean) XposedHelpers.getStaticBooleanField(ActiveServices, "DEBUG_SERVICE");
                String TAG = (String) XposedHelpers.getStaticObjectField(ActiveServices, "TAG");

                Object app = param.args[0];
                boolean allowRestart = (Boolean) param.args[1];
                Object services = XposedHelpers.getObjectField(app, "services");
                int size = (Integer) XposedHelpers.callMethod(services, "size");

                // First clear app state from services.
                for (int i = size - 1; i >= 0; i--) {
                    Object sr = XposedHelpers.callMethod(services, "valueAt", i);
                    Object stats = XposedHelpers.getObjectField(sr, "stats");
                    synchronized (XposedHelpers.callMethod(stats, "getBatteryStats")) {
                        XposedHelpers.callMethod(stats, "stopLaunchedLocked");
                    }
                    Object sr_app = XposedHelpers.getObjectField(sr, "app");
                    Boolean persistent = XposedHelpers.getBooleanField(sr_app, "persistent");
                    Boolean stopIfKilled = XposedHelpers.getBooleanField(sr, "stopIfKilled");

                    if (sr_app != null && !persistent && stopIfKilled) {
                        Object sr_app_services = XposedHelpers.getObjectField(sr_app, "services");
                        XposedHelpers.callMethod(sr_app_services, "remove", sr);
                    }
                    XposedHelpers.setObjectField(sr, "app", null);
                    XposedHelpers.setObjectField(sr, "isolatedProc", null);
                    XposedHelpers.setObjectField(sr, "executeNesting", 0);
                    XposedHelpers.callMethod(sr, "forceClearTracker");

                    Object mDestroyingServices = XposedHelpers.getObjectField(param.thisObject, "mDestroyingServices");
                    Boolean check = (Boolean) XposedHelpers.callMethod(mDestroyingServices, "remove", sr);
                    if (check) {
                        if (DEBUG_SERVICE)
                            Slog.v(TAG, "killServices remove destroying " + sr);
                    }

                    Object bindings = XposedHelpers.getObjectField(sr, "bindings");
                    final int numClients = (Integer) XposedHelpers.callMethod(bindings, "size");
                    for (int bindingi = numClients - 1; bindingi >= 0; bindingi--) {
                        Object IntentBindRecord = XposedHelpers.callMethod(bindings, "valueAt", bindingi);
                        if (DEBUG_SERVICE)
                            Slog.v(TAG, "Killing binding " + IntentBindRecord + ": shouldUnbind=" + XposedHelpers.getObjectField(IntentBindRecord, "hasBound"));

                        XposedHelpers.setObjectField(IntentBindRecord, "binder", null);
                        XposedHelpers.setObjectField(IntentBindRecord, "requested", false);
                        XposedHelpers.setObjectField(IntentBindRecord, "received", false);
                        XposedHelpers.setObjectField(IntentBindRecord, "hasBound", false);
                    }
                }

                // Clean up any connections this application has to
                // other
                // services.
                Object connections = XposedHelpers.getObjectField(app, "connections");
                size = (Integer) XposedHelpers.callMethod(connections, "size");
                for (int i = size - 1; i >= 0; i--) {
                    Object ConnectionRecord = XposedHelpers.callMethod(connections, "valueAt", i);

                    XposedHelpers.callMethod(param.thisObject, "removeConnectionLocked", ConnectionRecord, app, null);
                }
                XposedHelpers.callMethod(connections, "clear");

                Object smap = XposedHelpers.callMethod(param.thisObject, "getServiceMap", XposedHelpers.getObjectField(app, "userId"));

                // Now do remaining service cleanup.
                services = XposedHelpers.getObjectField(app, "services");
                size = (Integer) XposedHelpers.callMethod(services, "size");
                for (int i = size - 1; i >= 0; i--) {
                    Object sr = XposedHelpers.callMethod(services, "valueAt", i);
                    Object mServicesByName = XposedHelpers.getObjectField(smap, "mServicesByName");
                    if (XposedHelpers.callMethod(mServicesByName, "get", XposedHelpers.getObjectField(sr, "name")) != sr) {
                        Object cur = XposedHelpers.callMethod(mServicesByName, "get", XposedHelpers.getObjectField(sr, "name"));
                        Slog.wtf(TAG, "Service " + sr + " in process " + app + " not same as in map: " + cur);
                        Object app_services = XposedHelpers.getObjectField(app, "services");
                        XposedHelpers.callMethod(app_services, "removeAt", i);
                        continue;
                    }
                    // Any services running in the application may
                    // need to be
                    // placed back in the pending list.
                    Object serviceInfo = XposedHelpers.getObjectField(sr, "serviceInfo");
                    Object applicationInfo = XposedHelpers.getObjectField(serviceInfo, "applicationInfo");
                    if (allowRestart && XposedHelpers.getIntField(sr, "crashCount") >= 2 && (XposedHelpers.getIntField(applicationInfo, "flags") & ApplicationInfo.FLAG_PERSISTENT) == 0) {
                        Slog.w(TAG, "Service crashed " + XposedHelpers.getIntField(sr, "crashCount") + " times, stopping: " + sr);
                        EventLog.writeEvent(EventLogTags.AM_SERVICE_CRASHED_TOO_MUCH, XposedHelpers.getObjectField(sr, "userId"), XposedHelpers.getObjectField(sr, "crashCount"), XposedHelpers.getObjectField(sr, "shortName"), XposedHelpers.getObjectField(app, "pid"));
                        XposedHelpers.callMethod(param.thisObject, "bringDownServiceLocked", sr);
                    } else if (!allowRestart) {
                        XposedHelpers.callMethod(param.thisObject, "bringDownServiceLocked", sr);
                    } else {
                        boolean canceled = (Boolean) XposedHelpers.callMethod(param.thisObject, "scheduleServiceRestartLocked", sr, true);
                        // Should the service remain running? Note
                        // that in the
                        // extreme case of so many attempts to
                        // deliver a command
                        // that it failed we also will stop it here.
                        if (XposedHelpers.getBooleanField(sr, "startRequested") && (XposedHelpers.getBooleanField(sr, "stopIfKilled") || canceled)) {
                            Object pendingStarts = XposedHelpers.getObjectField(sr, "pendingStarts");
                            if ((Integer) XposedHelpers.callMethod(pendingStarts, "size") == 0) {
                                XposedHelpers.setBooleanField(sr, "startRequested", false);
                                if (XposedHelpers.getObjectField(sr, "tracker") != null) {
                                    Object tracker = XposedHelpers.getObjectField(sr, "tracker");
                                    Object mAm = XposedHelpers.getObjectField(param.thisObject, "mAm");
                                    Object mProcessStats = XposedHelpers.getObjectField(mAm, "mProcessStats");
                                    XposedHelpers.callMethod(tracker, "setStarted", false, XposedHelpers.callMethod(mProcessStats, "getMemFactorLocked"), SystemClock.uptimeMillis());
                                }
                                if (!XposedHelpers.getBooleanField(sr, "hasAutoCreateConnections")) {
                                    // Whoops, no reason to restart!
                                    XposedHelpers.callMethod(param.thisObject, "bringDownServiceLocked", sr);
                                }
                            }
                        }
                    }
                }

                if (!allowRestart) {
                    Object app_services = XposedHelpers.getObjectField(app, "services");
                    XposedHelpers.callMethod(app_services, "clear");

                    // Make sure there are no more restarting
                    // services for this
                    // process.
                    Object mRestartingServices = XposedHelpers.getObjectField(param.thisObject, "mRestartingServices");

                    for (int i = (Integer) XposedHelpers.callMethod(mRestartingServices, "size") - 1; i >= 0; i--) {
                        Object r = XposedHelpers.callMethod(mRestartingServices, "get", i);
                        String processName = (String) XposedHelpers.getObjectField(r, "processName");
                        Object serviceInfo = XposedHelpers.getObjectField(r, "serviceInfo");
                        Object applicationInfo = XposedHelpers.getObjectField(serviceInfo, "applicationInfo");
                        Object info = XposedHelpers.getObjectField(app, "info");
                        if (processName.equals((String) XposedHelpers.getObjectField(app, "processName")) && XposedHelpers.getIntField(applicationInfo, "uid") == XposedHelpers.getIntField(info, "uid")) {
                            XposedHelpers.callMethod(mRestartingServices, "remove", i);
                            XposedHelpers.callMethod(param.thisObject, "clearRestartingIfNeededLocked", r);
                        }
                    }
                    Object mPendingServices = XposedHelpers.getObjectField(param.thisObject, "mPendingServices");
                    for (int i = (Integer) XposedHelpers.callMethod(mPendingServices, "size") - 1; i >= 0; i--) {
                        Object r = XposedHelpers.callMethod(mPendingServices, "get", i);

                        String processName = (String) XposedHelpers.getObjectField(r, "processName");
                        Object serviceInfo = XposedHelpers.getObjectField(r, "serviceInfo");
                        Object applicationInfo = XposedHelpers.getObjectField(serviceInfo, "applicationInfo");
                        Object info = XposedHelpers.getObjectField(app, "info");
                        if (processName.equals((String) XposedHelpers.getObjectField(app, "processName")) && XposedHelpers.getIntField(applicationInfo, "uid") == XposedHelpers.getIntField(info, "uid")) {
                            XposedHelpers.callMethod(mPendingServices, "remove", i);
                        }
                    }
                }
                // Make sure we have no more records on the stopping
                // list.
                Object mDestroyingServices = XposedHelpers.getObjectField(param.thisObject, "mDestroyingServices");
                int i = (Integer) XposedHelpers.callMethod(mDestroyingServices, "size");
                while (i > 0) {
                    i--;
                    Object sr = XposedHelpers.callMethod(mDestroyingServices, "get", i);
                    if (XposedHelpers.getObjectField(sr, "app") == app) {
                        XposedHelpers.callMethod(sr, "forceClearTracker");
                        XposedHelpers.callMethod(mDestroyingServices, "remove", i);
                        if (DEBUG_SERVICE)
                            Slog.v(TAG, "killServices remove destroying " + sr);
                    }
                }
                Object executingServices = XposedHelpers.getObjectField(app, "executingServices");
                XposedHelpers.callMethod(executingServices, "clear");

                return null;
            }
        });

    } catch (Throwable t) {
        XposedBridge.log(t);
    }
}


Вот так вот я разрешал всем приложениям писать информацию на SD карточку, когда этот функционал урезали в целях безопасности на KitKat.

hookSDcardPermission
try {
    XposedHelpers.findAndHookMethod("com.android.server.SystemConfig", paramLoadPackageParam.classLoader, "readPermission", "org.xmlpull.v1.XmlPullParser", "java.lang.String", new XC_MethodHook()
    {
        protected void afterHookedMethod(MethodHookParam param) throws Throwable
        {
            String permission = (String) param.args[1];

            if (permission.equals("android.permission.WRITE_EXTERNAL_STORAGE")) {
                Class<?> process = XposedHelpers.findClass("android.os.Process", null);
                int gid = (Integer) XposedHelpers.callStaticMethod(process, "getGidForName", "media_rw");

                Object mPermissions = XposedHelpers.getObjectField(param.thisObject, "mPermissions");
                Object localPermissionEntry = XposedHelpers.callMethod(mPermissions, "get", permission.intern());
                int[] gids = (int[]) XposedHelpers.getObjectField(localPermissionEntry, "gids");
                XposedHelpers.setObjectField(localPermissionEntry, "gids", ArrayUtils.appendInt(gids, gid));

            }
        }
    });
} catch (Throwable t) {
    XposedBridge.log(t);
}


Доступ к переменным и методам класса осуществляется через класс XposedHelpers. Например:

Object mActivity = XposedHelpers.getObjectField(param.thisObject, "mActivity");

Если объект является доступным импортируемым классом, то можно полученный объект сразу привести к нужному типу

(Activity) mActivity = (Activity) XposedHelpers.getObjectField(param.thisObject, "mActivity");

и облегчить дальнейший код. Если же объект является каким-то типом внутреннего класса, который нам не доступен для импорта, в дальнейшем использование его методов и свойств также доступно через XposedHelpers.

Резюмируя вышесказанное: мы можем изменять статические и финальные переменные любого класса, в любое время и в любом месте. Проверять переменные передаваемые в методы, изменять их до выполнения, изменять результат отдельных методов, либо полностью замещать их своим кодом.

В большинстве случаев достаточно отловить нужный метод и модифицировать либо изменить его логику можно сразу, как только приложение запускается. Но существует известная проблема с лимитом количества методов в одном DEX файле, поэтому многие громоздкие приложения имеют по 3-5 дополнительных DEX файла. Здесь скрывается подводный камень. Обойти его довольно таки просто:

multi dex
public void handleLoadPackage( LoadPackageParam paramLoadPackageParam) throws Throwable {
    final LoadPackageParam llpm = paramLoadPackageParam;
    String packageName = paramLoadPackageParam.packageName;

    if (packageName.contains("ubercab.driver"))
    {
        // Методы в основном DEX файле
        try{
            XposedHelpers.findAndHookMethod("com.ubercab.driver.feature.main.MainActivity", llpm.classLoader, "onNewIntent", "android.content.Intent", new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable
                {
                    // Тут наш код
                }
            });
        } catch (Throwable t) {
            XposedBridge.log(t);
        }
    
        // Ловим подгрузку дополнительных DEX файлов.
        XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {

            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable
            {
                // Тут ищем классы и методы, которые расположены в дополнительных DEX файлах.
                try {
                    XposedHelpers.findAndHookMethod(
                            "com.ubercab.driver.feature.online.DispatchedFragment",
                            llpm.classLoader,
                            "onCreateView",
                            "android.view.LayoutInflater", "android.view.ViewGroup", "android.os.Bundle",
                            new XC_MethodHook() {
                                // здесь будет код
                            }
                    );

                } catch (Throwable t) {
                    XposedBridge.log(t);
                }
            }
        });
    }
}


Что касается обфусцированных приложений, на первый взгляд нужно переписывать модуль при каждом новом релизе приложения. Выход из этого положения всегда есть, но это отдельная тема. Основной принцип — это найти класс, который не обфусцируется и бэктрейсом определить имена классов и методов.

Заключение


Я привел основные трюки и решения задач программирования, с которыми столкнулся за все время разработки различных модулей. Не вижу смысла разбирать каждый случай в деталях, так как опытным программистам достаточно воспользоваться руководствами от автора здесь и здесь.

На этом я планирую закончить цикл статей про модификацию прошивок, но тема практического применения Xposed, надеюсь, не закрыта. Есть в планах большая статья о том, как я работал водителем Uber и разработал Xposed модуль, который давал мне расширенную информацию как о предстоящей поездке, так и во время выполнения заказа, что, к моему удивлению, не предусмотрено в стандартном приложении. Получил весьма и весьма интересный опыт и сделал выводы: как о качестве самой архитектуры Uber, так и о том, какая информацию передается через приложение о пользователях и как, возможно, компания планирует монетизировать свой сервис и полученные данные в будущем.
Нурлан Муханов @Falseclock
карма
61,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (8)

  • 0
    Статья очень интересна. Спасибо! А как использование Xposed Framework и модулей сказывается на производительности системы?
    • 0
      Как правило, заметно снижение скорости загрузки самого телефона до броадкаста BOOT_COMPLETE, если модулей много, но дальше работает также как и прежде.

      Разумеется, есть и корявые модули, которые отжирают память из-за криворукости разработчиков.
  • 0
    GravityBox, Greenify, Flat Style bar indicators, Miui tweaks, xstana, Lucky patcher, Uret patcher, microG(FakeGapps и иже с ним) — даже ради одного из этих модулей xposed стоит поставить.
    • 0
      львиная доля модулей не публикуется в общем репозитарии, а делается для своих нужд, потому что дают преимущество перед другими пользователями какого-либо приложения.
    • 0
      +MinMinGuard, +RootCloak, +Masrsmallow SD fix
    • 0

      Так же очень полезны App Settings, YouTube Adaway и Fingerlock.
      Без App Settings вообще уже непривычно. Основной язык системы английский, а звонилку обычно переключал через этот модуль на русский. Ну а пока ждём на нугу.

  • +1
    деоксидирование

    Процесс называется «деодексирование» (от название формата — odex).
    • 0
      спасибо. исправил.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.