Пожалуйста, опишите ошибку

Нашли баг? Помогите нам его исправить, заполнив эту форму

Новое в блоге: Анимация Skeleton

Xposed magic will be here

Сергей Опивалов
Android-разработчик

Всем привет. Начну с вопроса — как бы вы поступили (в контексте разработки под Android), если бы вам нужно было перехватить событие показа рекламы в ленте социальной сети? Или, например, событие изменения времени в системных часах? Или запрос системы об использовании фиктивных местоположений вашим устройством?

Я расскажу о Xposed Framework. Эта статья будет небольшим Xposed how to, будут примеры кода, и параллельно расскажу, как я использовал Xposed в своем первом проекте.

Статья рассчитана на более-менее опытного пользователя, знакомого с понятием root и процессом его получения. Итак, всех заинтересовавшихся прошу под кат.

How it works

Прежде чем писать код, предлагаю узнать, какие идеи лежат в основе Xposed, а также немного погрузиться в анатомию Android. Поехали!

Современный Android включает в себя десятки служб, но мы сейчас говорим о ключевой из них — Zygote. Это сердце Android Runtime.

Zygote запускается с помощью /system/bin/app_process. Задача app_process — запустить виртуальную машину Dalvik и вызвать main() метод у Zygote. Как только Zygote запущена, она начинает загрузку необходимых Java-классов и ресурсов Android. После запускается system_server, который содержит множество высокоуровневых системных сервисов: WindowManager, Status Bar, Package Manager, Activity Manager и т.д. В конце открывается сокет /dev/socket/zygote, и Zygote уходит в сон, слушая через сокет запросы на старт приложений.

Как только приходит запрос на старт нового приложения, срабатывает метод fork(). Zygote создает клон самой себя, запускает новую копию Dalvik VM и запускает на ней нужное приложение.

Очень важный момент — эта копия VM создается уже с подгруженными java-ресурсами. Этот факт делает запуск копии VM очень быстрым и эффективным.

Но почему это возможно и работает так? Все мы знаем, что Android работает на ядре Linux (кто не знал — surprise). Linux при форке процесса использует стратегию copy-of-write — для копии процесса не выделяется память, копия процесса просто ссылается на память оригинала. Это позволяет:

  • Свести расход памяти к минимуму
  • Существенно ускорить запуск приложения

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

Xposed включается в работу до старта Zygote. Когда мы устанавливаем Xposed, /system/bin/app_process подменяется на расширенный, который добавляет XposedBridge.jar в classpath. В XposeBridge есть метод hookMethodNative со следующей сигнатурой:

private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);

В этом методе перехватывается каждый вызов хука моего метода (переданного в качестве параметра) и вызывается handleHookedMethod(), который уже заботится о вызове коллбэков для него.

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

Итак, для чего нам хуки? Когда вы изменяете APK после декомпиляции, вы можете добавлять и изменять методы, где хотите и как хотите. Это плюс. Но после этого необходимо собирать и подписывать APK по-новой. Также, нельзя забывать возможность обфускации кода — не каждый APK может быть качественно декомпилирован. Это минус.

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

Окей, хватит теории, давайте создадим модуль для Xposed.

Preparations

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

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

Хорошо, root получили. Теперь устанавливаем Xposed Installer — все есть в гугле, скачиваем, устанавливаем.

Настроим проект. Добавляем зависимость в app/build.gradle:

repositories {
  jcenter();
}

dependencies {
  provided 'de.robv.android.xposed:api:82'
}

Обратите внимание — provided вместо compile. Compile — API классы добавятся в APK, что может привести к нежелательным эффектам, особенно на Android 4.х. Provided — APK будет ссылаться на API классы, их актуальная реализация будет предоставлена, когда Xposed Framework будет установлен. Вот и вся разница.

И еще одна рекомендация — отключите Instant Run (File -> Settings -> Build, Execution, Deployment -> Instant Run).

Example

Будем менять цвет у стандартных часов. Easy.

По сути, модуль для Xposed, это обычное приложение, отличия — мета-данные в манифесте, файл xposed_init, и наличие класса, реализующего один из Xposed интерфейсов (точка входа в модуль). Давайте по порядку.

AndroidManifest.xml

В блок Application мы добавим три блока с мета-данными. Выглядеть это будет следующим образом:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package=«com.smedialink.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 greeting" />
<meta-data
      android:name="xposedminversion"
      android:value="53" />
</application>
</manifest>

Entry Points

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

Все точки входа (по сути, интерфейсы) наследуются от IXposedMod. Реализуем интерфейс, метод которого вызывается при загрузке нового пакета. Такой есть — IXposedHookLoadPackage.

package com.smedialink.tutorial;

import com.smedialink.xposed.IXposedHookLoadPackage;
import com.smedialink.xposed.XposedBridge;
import com.smedialink.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

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

Один метод, принимающий один параметр lpparam, содержащий в себе информацию о загруженном пакете. Пока просто пишем в лог имя пакета.

assets/xposed_init

Папку assets необходимо создать, в ней простой текстовый файл (строго с этим названием). Тут мы будем описывать классы, содержащие entry points. Просто прямо вот так:

com.smedialink.tutorial.Tutorial.

Каждый класс на новой строке.

Let’s run it

Теперь сохраним проект и запустим его как обычное приложение. После первого запуска ничего не произойдет, так как мы должны включить наш модуль в XposedInstaller. Во вкладке Modules находим его, включаем, перезагружаем девайс (об этом — чуть позже). Заглянем в логи и увидим что то подобное:

Loading Xposed (for Zygote)...
Loading modules from /data/app/com.smedialink.tutorial.apk
Loading class com.smedialink.tutorial.Tutorial
Loaded app: com.android.systemui
Loaded app: com.android.settings
... (many more apps follow)

Вы наверное скажите: «Ну опять, показал элементарный пример, и рассказывает нам, какая технология топовая…» Пример и правда прост, но мы можем делать нечто более полезное, чем запись в лог.

Explore your target

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

Итак, если вы уже модифицировали какие либо APK ранее, то возможно некоторые моменты покажутся вам знакомыми. Очевидно, для начала неплохо было бы узнать, как реализовано то, что мы будем модифицировать.

В этом примере наша цель — это часы в статус баре. Мы знаем/догадываемся что статус бар это часть SystemUI. Сюда и будем копать.

Как мы можем действовать?

Первый способ: декомпиляция. Даст точную информацию о реализации, но трудно читать и разбираться (smali формат, возможная обфускация кода и т.д.) Плохая новость — в некоторых случаях это единственный способ. Хорошая новость — наша задача это не тот случай.

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

Поищем в frameworks/base/packages/SystemUI классы по слову «clock». Уверен, вы найдете несколько появлений. Нужно найти место, где мы будем делать нашу магию до, после или вместо какого либо метода. Очень желательно, чтобы этот метод не вызывался тысячи раз, потому что в таком случае возможны проблемы с производительностью и посторонние эффекты от хуков.

Хорошо, есть класс com.android.systemui.statusbar.policy.Clock, являющийся кастомной вьюшкой, ссылка на которую содержится в res/layout/status_bar.xml. У него видим метод:

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

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

package com.smedialink.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 {

      }
      @Override
      protected void afterHookedMethod(MethodHookParam param) throws Throwable {

      }
    });
  }
}

Все, в общем-то, понятно из кода. Проверяем, что находимся в нужном пакете, и вызываем findAndHookMethod. Он хочет на вход класс, в котором находится метод, класс Loader, имя метода и имплементацию хука последним аргументом. Помимо этого, если метод имеет входные параметры, мы должны перечислить их типы.

В beforeHookedMethod мы можем модифицировать входные параметры для метода (через param.args). В afterHookedMethod мы модифицируем возвращенный результат. Конечно, мы можем добавить любой код, который должен выполняться до/после метода.

Если необходимо полностью заменить метод, то в findAndHookMethod мы можем передать реализацию XC_MethodReplacement с реализованным replaceHookedMethod.

Final steps

Хорошо. Мы нашли метод, теперь можно его модифицировать. Что мы знаем? Что метод updateClock не статический (судя по сигнатуре), значит, он вызывается как-то так: myClock.updateClock(). Таким образом, myClock мы получим из params.thisObject. Но он вернет нам Object, хотелось бы что-то более конкретное.

Кастануть к Clock мы не можем, т.к. класс Clock нам не доступен, но мы можем видеть, что Clock наследуется от TextView. Отлично, после каста к TextView нам становятся доступны его методы, и мы можем делать с часами необходимые модификации.

Теперь можно написать реализацию afterHookMethod:

package com.smedialink.mods.tutorial;

import static com.smedialink.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 + "Hello SML");
        tv.setTextColor(Color.RED);
      }
    });
  }
}

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

Пару слов об этом. Как вы возможно уже поняли, каждое изменение в классах, реализующих интерфейсы Xposed, требует перезагрузки девайса. По этой же причине, дебаггер в этих классах будет недоступен. Michael Feathers (автор Working Effectively with Legacy Code, и по совместительству, библиотеки CppUnit) говорил: «Если вы хорошо отлаживаете программы, значит, вы провели много времени, делая это. Я не хочу уметь хорошо отлаживать программы». Святые угодники, я тоже не хочу, но хотя бы деббагер мне все-таки нужен.

Давайте посмотрим на результат:

screen

Outstanding.

Real project

Помните PokemonGo? Моим первым проектом было приложение, позволяющее играть в PokemonGo, не выходя из дома (кто незнаком с механикой игры — это Location Based App, передвижение персонажа обеспечивается перемещением устройства). То есть имеется некий стик, позволяющий управлять персонажем в игре.

Реализовано это было путем использования Mock Location Provider. «И для чего тут тебе Xposed, чувак?» спросите вы. Дело в том, что после очередного обновления пользователи, использующие модули типа моего, не могли войти в игру.

После изучения ситуации выяснилось — дело в ALLOW_MOCK_LOCATION пермишене.

Идея простая — находим метод, который запрашивает состояние пермишена ALLOW_MOCK_LOCATION, и просто не даем ему вызваться.

Давайте посмотрим, где вообще используется этот пермишен. Нас перекинет во вложенный класс Settings.Secure. Давайте исследуем его. Читаем описание класса — «…preferences thatthe user must explicitly modify through the system UI…» Ну точно ведь, через Developer Options.

Пробежимся по списку методов, почитаем описание, у нас ведь исследование =). Видим два метода: getInt(ContentResolver cr, String name, int def), который «Convenience function for retrieving a single secure settings values an integer», и в нем же вызывается getString(ContentResolver resolver, String name), который «Look up a name in database».

Говоря откровенно, прямо сейчас, я вижу, что можно было хукнуть только getString(), но в момент написания приложения эта мысль мне в голову не пришла.

Итак, теперь взглянем на хук:

XposedHelpers.findAndHookMethod(
    Settings.Secure.class,
    "getString",
    ContentResolver.class,
    String.class,
    new XC_MethodHook() {
      @Override
      protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        String name = ((String) param.args[1]);
        if (name.equals(Settings.Secure.ALLOW_MOCK_LOCATION)) {
          param.setResult("0");
        }
      }
  });

Передаем на вход класс Secure, говорим, что хотим хукать getString, передаем классы, которые требует getString() в качестве параметров, передаем анонимный класс хука, в котором возвращаем из метода «0». Таким образом, предотвращая вызов метода getString().

Хорошо, дальше копаясь в исходниках, я наткнулся на метод isFromMockProvider(). Такого случая я упустить не мог:

XposedHelpers.findAndHookMethod(
    Location.class,
"isFromMockProvider",
    new XC_MethodHook() {
      @Override
      protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        XposedHelpers.setBooleanField(param.thisObject, "mIsFromMockProvider", false);
        param.setResult(false);
      }
    });
  }

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

Completion

Как вы смогли увидеть, no rocket science here. В этой статье мы рассмотрели только основы и мой опыт, а ведь возможности фреймворка уникальны, только взгляните на доступные модули в репозитории. К сожалению, информации в сети по этой теме не слишком много, и практически вся что есть сосредоточена на xda-developers.com.

Друзья, на этом все. Надеюсь было полезно, интересно, не стандартно, всем добра… и помните, что в теории, теория и практика неразделимы. Но на практике это не так.

Читать и комментировать
Создаем расширение для Chrome

Александр Хисматулин

24 октября 2017

Создаем расширение для Chrome

Как использовать Redux в библиотеке для создания web-компонентов Polymer
Android Wear: основы разработки

Владислав Герасименко

15 ноября 2016

Android Wear: основы разработки

HERE maps для Android: обзор технологии

Владислав Герасименко

10 апреля 2017

HERE maps для Android: обзор технологии

Краснодар

Коммунаров, 268,
3 эт, офисы 705, 707

+7 (861) 200 27 34

Хьюстон

3523 Brinton trails Ln Katy

+1 833 933 0204

Москва

+7 (495) 145-01-05