Содержание статьи
В Android приложения с поддержкой root имеют особый статус. Во-первых, они будут работать только на рутованных смартфонах, то есть у довольно узкого круга пользователей. Во-вторых, возможности root-приложений безграничны. Они могут удалять или устанавливать системные приложения, изменять системные конфиги, прошивать в смартфон кастомные ядра или прошивки, да и вообще без проблем превратят смартфон в кирпич. Думаешь, реализовать все это сложно? Отнюдь!
Написание приложений с поддержкой прав root сильно отличается от традиционного программирования для Android. И не потому, что нам придется задействовать низкоуровневые системные API (хотя это тоже возможно), а потому, что, по сути, мы будем иметь дело с консолью и ее командами. То есть в буквальном смысле нажатия кнопочек на экране будут приводить к исполнению консольных команд. Поэтому первое, что мы должны сделать, — это научиться запускать команды без прав root.
Командуем
Запуск внешних команд в Android выполняется точно так же, как и в Java, а именно с помощью такой строки:
Runtime.getRuntime().exec("команда");
Правда, в коде придется обрамить ее try/catch:
try {
Runtime.getRuntime().exec("команда");
} catch (IOException e) {
throw new RuntimeException(e);
}
Метод exec() запускает шелл и команду в нем в отдельном потоке. После завершения команды поток дестроится. Все просто и понятно, но вот толку нам от этого мало — вывода команды мы все равно не видим. Чтобы решить эту проблему, мы должны прочитать стандартный выходной поток, для чего понадобится примерно такая функция:
public String runCommand(String cmd) {
try {
// Выполняем команду
Process process = Runtime.getRuntime().exec(cmd);
// Читаем вывод
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
int read;
char[] buffer = new char[4096];
StringBuffer output = new StringBuffer();
while ((read = reader.read(buffer)) > 0) {
output.append(buffer, 0, read);
}
reader.close();
// Дожидаемся завершения команды и возвращаем результат
process.waitFor();
return output.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Теперь у нас есть возможность запускать команды и видеть результат их работы, но что это нам дает? Ну, как вариант — мы можем прочитать инфу о процессоре. Создаем простую формочку с одной кнопкой сверху и TextView ниже нее. Далее в теле метода onCreate пишем такой код:
// Наш TextView
final TextView textView = (TextView) findViewById(R.id.textView);
// Кнопка
final Button button = (Button) findViewById(R.id.button);
// При нажатии кнопки
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Запускаем команду и размещаем вывод в TextView
String cpuinfo = runCommand("cat /proc/cpuinfo");
textView.setText(cpuinfo);
}
});
Все, результат, как говорится, налицо. Вместо команды cat /proc/cpuinfo
можно использовать и что-то поэкзотичнее, например uname -a
, которая выводит версию ядра Linux, или cat /system/build.prop
для просмотра файла системных настроек. Однако так мы далеко не уедем, система не даст нам сделать что-то серьезнее, чем чтение некоторых файлов или запуск простых команд. Хардкор начнется только при наличии прав суперпользователя.
И пришел root
Как я уже сказал, функциональность root-приложений построена на том, чтобы запускать команды от имени пользователя root, то есть суперпользователя. В UNIX-системах (а ею Android является на самом низком уровне) эта операция выполняется с помощью команды su. По умолчанию она просто открывает шелл с правами root, но с помощью флага -c правами root можно наделить любую команду. Например, чтобы выполнить команду id, с правами root достаточно такой строки:
runCommand("su -c id");
Команда id, кстати говоря, возвращает идентификатор текущего пользователя (0 = root), так что сразу можно проверить, как все работает. Однако при таком методе запуска возникают проблемы с парсингом. Если ты укажешь дополнительные аргументы (например, su -c uname -a
), команда просто не отработает. Обойти ограничение можно, передав ее как массив строк. Для удобства немного модифицируем код метода runCommand, заменив в нем первую строку:
public String runSuCommand(String cmd) {
try {
Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", cmd});
...
Все, теперь с его помощью можно запускать хоть несколько команд одновременно, все с правами root:
runSuCommand("id; uname -a; cat /proc/cpuinfo");
В сущности, это все, и можно переходить к примерам, но есть еще один нюанс: смартфон может быть не рутован. Этот момент необходимо обязательно учитывать и проверять наличие прав root. Наиболее простой и эффективный способ проверки — посмотреть, есть ли бинарник su в системе. Обычно он располагается в каталоге /system/bin/
или /system/xbin/
(в большинстве случаев), поэтому просто напишем такую функцию:
private boolean checkSu() {
String[] places = {"/system/bin/", "/system/xbin/"};
for (String where : places) {
if (new File(where + "su").exists()) {
return true;
}
}
return false;
}
Объяснять тут особо нечего, есть su — true, нет — false.
И еще один, последний нюанс. Чтобы получить доступ на редактирование (удаление/перемещение) файлов в системном разделе (/system
), необходимо перемонтировать его в режиме чтение/запись:
runSuCommand("mount -o remount,rw /system");
После этого можно спокойно работать с файлами системного раздела, если, конечно, на устройстве не используется блокировка доступа к /system
(S-ON). По-хорошему, после окончания работы с файлами рекомендуется снова смонтировать /system
с правами «только чтение»:
runSuCommand("mount -o remount,ro /system");
Однако это, скорее, правило хорошего тона, на работу системы смонтированный на запись раздел /system никак не влияет.
Несколько примеров
В маркете полно разнообразных root-приложений с, казалось бы, действительно крутой функциональностью. Это и софт для запуска ADB в сетевом режиме, и приложения для установки recovery и ядер, и софт для перезагрузки напрямую в recovery. Сейчас я покажу, насколько на самом деле сложны эти приложения. Итак, первый тип софта: bloatware cleaner, приложение для очистки Android от системных (неудаляемых) приложений. Реализуется с помощью трех (можно одной) строк:
String apk = "/system/app/Email.apk";
runSuCommand("mount -o remount,rw /system");
runSuCommand("rm " + apk);
Все! Мы удалили системное приложение Email. Добавь сюда интерфейс с возможностью выбора приложений (на основе списка, сформированного из содержимого /system/app/
и /system/priv-app/
), и у тебя есть полноценный bloatware cleaner. Можно прикручивать рекламу и выкладывать в маркет. Только имей в виду, что, начиная с Android 5.0, приложения в каталоге /system/app/
располагаются в собственных подкаталогах, так что придется смотреть, какая версия ОС стоит, и при необходимости использовать уже другой код:
String app = "/system/app/Email";
runSuCommand("mount -o remount,rw /system");
runSuCommand("rm -rf " + app);
ОK, а как насчет приложения для перезагрузки в recovery? В кастомах это штатная функция, доступная в Power Menu, но в стоковых прошивках для этого приходится использовать специальный софт. Все сложно? Отнюдь:
runSuCommand("reboot recovery");
Да, это всего одна команда. То есть полноценное приложение с кнопкой «Перезагрузиться в recovery» уместится в 15–20 строк. Неплохо, не так ли? Ладно-ладно, возьмем более сложный пример — прошивку кастомной консоли восстановления прямо из Android. Звучит круто? Конечно, но в коде это выглядит так:
String recoveryImg = "/sdcard/recovery.img";
String recoveryPtn = "/dev/block/platform/msm_sdcc.1/by-name/recovery";
runSuCommand("dd if=" + recoveryImg + " of=" + recoveryPtn);
Стоит отметить, что это пример для чипов Qualcomm, в устройствах на базе других SoC путь до раздела recovery будет другим. Ну и в целом пример не вполне корректный, по-хорошему надо писать интерфейс выбора образа recovery (здесь это /sdcard/recovery.img
), а дальше прописывать его в переменную recoveryImg. Но в любом случае исходник полноценного приложения вряд ли будет длиннее тридцати строк.
Кстати, проверить, какой чип используется в девайсе, можно с помощью все того же /proc/cpuinfo
:
String cpuinfo = runCommand("cat /proc/cpuinfo");
if (cpuinfo.contains("Qualcomm")) {
// Имеем дело с Qualcomm, отлично, продолжаем
} else {
// Другой SoC
}
Идем дальше, на очереди приложения в стиле WiFi ADB (это реальное название). В маркете таких полно, и все они выглядят одинаково: экран с одной кнопкой для включения/выключения режима отладки по сети (ADB over WiFi). Функция очень удобная, а потому приложения пользуются популярностью. Как реализовать то же самое? Как всегда, очень просто:
runSuCommand("setprop service.adb.tcp.port 5555; stop adbd; start adbd");
Все, теперь сервер ADB на смартфоне работает в сетевом режиме и к нему можно подключиться с помощью команды «adb connect IP-адрес». Для отключения сетевого режима используем такой код:
runSuCommand("setprop service.adb.tcp.port -1; stop adbd; start adbd");
Ну и в завершение поговорим о настройщиках ядра. Таких в маркете достаточно много, один из наиболее популярных — TricksterMod. Он позволяет изменять алгоритм энергосбережения ядра, включать/выключать ADB over WiFi, настраивать подсистему виртуальной памяти и многое другое. Почти все эти операции TricksterMod (и другой схожий софт) выполняет, записывая определенные значения в синтетические файлы в каталогах /proc
и /sys
.
Реализовать функции TricksterMod не составит труда. К примеру, нам надо написать код, изменяющий алгоритм энергосбережения процессора (governor). Для выполнения этой операции следует записать имя нужного алгоритма в файл /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
, однако для начала следует выяснить, какие алгоритмы поддерживает ядро. Они перечислены в другом файле, поэтому мы напишем простую функцию для его чтения:
private String[] getGovs() {
return runCommand("cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors").split(" ");
}
Имея список поддерживаемых алгоритмов, мы можем выбрать один из них, записав в файл scaling_governor
. Для удобства будем использовать такую функцию:
private boolean changeGov(String gov) {
runSuCommand("echo " + gov + " > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor");
String newgov = runCommand("cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor");
if (newgov == gov) {
return true;
}
return false;
}
Обрати внимание, что после записи значения мы вновь читаем файл, чтобы удостовериться, что ядро действительно переключилось на указанный алгоритм. Далее можно создать формочку с кнопочками и менюшками и повесить на них наши функции.
Вместо выводов
С помощью обычных консольных команд и прав root в Android можно сделать очень и очень многое. Это подтверждает огромное количество root-софта в маркете и приведенные мной примеры. Единственная проблема — это отсутствие документации, поэтому способы решения тех или иных проблем придется искать самостоятельно, читая исходники и терроризируя форумчан. Скромный список команд есть в моем wiki. Начать можно оттуда.
Сторонние библиотеки
Приведенный мной способ выполнения команд с правами root отлично работает, но имеет небольшой недостаток. Дело в том, что права суперпользователя будут запрашиваться во время каждого исполнения функции RunSuCommand(), а это значит, что, если юзер не поставит галочку «Больше не спрашивать» в окне запроса прав root, он быстро устанет от таких запросов.
Решить проблему можно один раз, открыв root-шелл, а затем выполняя все нужные команды уже в нем. В большинстве случаев такой метод запуска избыточен, так как обычно нам необходимо выполнить только одну-две команды, которые можно объединить в одну строку. Но он будет полезен при разработке сложных root-приложений вроде комплексных настройщиков ядра и просто комбайнов.
Реализация удобной в использовании обертки для запуска root-шелла доступна как минимум в двух проектах. Первый — это RootTools от Stricson, разработчика инсталлятора BusyBox для Android, второй — libsuperuser от легендарного Chainfire. Достоинство RootTools в том, что это комбайн, кроме шелла включающий в себя огромное количество функций, позволяющих копировать и перемещать файлы, менять права доступа, монтировать файловые системы и многое другое. Libsuperuser, с другой стороны, имеет шикарную документацию.