Содержание статьи
В одной из своих предыдущих статей я уже писал о механизме под названием «уровень доступа» (protection level), который определяет, может ли твой код обращаться к тем или иным функциям ОС. Высокий уровень доступа получает только системный софт, поэтому для простых смертных он закрыт. Однако есть в Android и еще одна интересность, имя которой — скрытый API. И чтобы получить к нему доступ, не нужен root, не надо подписывать приложение ключом прошивки, достаточно лишь немного пораскинуть мозгами.
Intro
Написать эту статью подвигла одна история. Началась она с попытки создать простенькое приложение, которое позволяло бы разворачивать строку состояния свайпом с какой-то из сторон экрана. Современные смартфоны слишком велики, чтобы дотянуться до статусбара одной рукой, а свайп позволил бы решить эту проблему быстро и легко.
Подобная функция есть во многих лаунчерах, поэтому задача казалась простой и совершенно очевидной: завариваем кофе, открываем доки Android, находим нужную функцию и пишем простой сервис, который отслеживал бы касание края экрана и разворачивал статусбар.
Но жестокая реальность поубавила оптимизма: как следовало из документации Android, API не предоставлял такую функциональность! А значит, софт, умеющий разворачивать строку состояния, использовал хаки, а что еще более интересно — хаки, работающие без root, прав администратора и вообще каких бы то ни было разрешений.
Начинаем разбираться
Проще всего выяснить, как это вообще возможно, — посмотреть чужой код. Такого с ходу не нашлось, зато обнаружилась очень простая софтина Drop Down Status Bar. Она состояла из иконки, при нажатии которой разворачивался статусбар, а сам код приложения умещался в файле размером 1252 байт — идеальный кандидат для декомпиляции.
Оставалось только скачать APK и натравить на него jadx:
Очень простой код, который создает объект класса StatusBarManager и вызывает его метод expandNotificationPanel()
, если приложение работает в среде Android 4.2, или метод expand()
, если это Android предыдущих версий. Все очень просто, и код можно было банально скопировать в свое приложение:
Но не тут-то было. Оказалось, что класс StatusBarManager не просто не был описан в документации, — его вообще не существовало в SDK. Как же работал Drop Down Status Bar?
На самом деле все элементарно. Фреймворк, содержащий все классы пакета android (включая требуемый android.app.StatusBarManager), не один и тот же на реальном устройстве и в SDK. Версия фреймворка в SDK, во-первых, довольно сильно урезана в плане доступных классов, а во-вторых, не включает в себя самого кода реализации классов (вместо методов и конструкторов — заглушки).
Это теория, а практика в том, что выдернутый с устройства фреймворк по логике можно было бы использовать не только чтобы сравнить с тем, что поставляется в SDK, но и чтобы подменить его! Сделать это оказалось несложно.
Кручу, верчу, запутать хочу
Фреймворк был выдернут с устройства (что такое adb shell):
$ adb shell
> su
> cp /system/framework/framework.jar /sdcard/
> exit
> exit
$ adb pull /sdcard/framework.jar
С помощью dex2jar байт-код Dalvik был транслирован обратно в байт-код Java:
$ unzip framework.jar
$ dex2jar-2.0/d2j-dex2jar.sh classes.dex
И затем размещен в проекте как обычная библиотека:
$ cp classes-dex2jar.jar ~/AndroidstudioProjects/ИМЯ_ПРИЛОЖЕНИЯ/app/libs/
Оставалось только запустить Android Studio, выбрать библиотеку и присоединить ее к проекту с помощью меню «Add as library».
INFO
Получившийся
classes-dex2jar.jar
можно было бы переименовать в android.jar
и положить его в SDK/platforms/android-23, заменив оригинал. Но это не самая лучшая идея, так как изменение отразилось бы на всех остальных проектах.Но Android Studio продолжал упорствовать. Теперь ему не нравилось слово statusbar:
StatusBarManager statusBarManager = (StatusBarManager) context.getSystemService("statusbar");
Оказалось, однако, что неправ в этой ситуации как раз Android Studio и это не что иное, как баг, обойти который можно с помощью комментария-директивы noinspection:
//noinspection ResourceType
Вот и все... нет, стоп, это я выдаю желаемое за действительное. На самом деле это еще далеко не все. Из-за огромного веса фреймворка Android Studio задыхался во время компиляции и постоянно прерывал этот процесс с самыми разными ошибками. И ошибки эти были вовсе не в коде, а в самих инструментах сборки. И даже не ошибки, а расход всей оперативной памяти, из-за которого инструменты сборки просто падали, как, например, утилита dx, перегоняющая байт-код Java в байт-код Dalvik:
Error:Execution failed for task ':app:transformClassesWithDexForDebug'.
Решение этому нашлось не сразу, и поначалу казалось, что нечего даже пытаться собрать код на ноуте с четырьмя гигами памяти. Однако и это было возможно, но только если указать Android Studio альтернативный каталог для хранения временных файлов (по умолчанию в Linux он использует каталог /tmp
, который зачастую сам находится в оперативке), подключить swap и провести небольшой тюнинг системы сборки.
Первые две задачи решились просто:
$ export _JAVA_OPTIONS="-Djava.io.tmpdir=$HOME/tmp"
$ dd if=/dev/zero of=swap.img bs=1m count=4096
$ mkswap swap.img
$ sudo swapon swap.img
Вторая чуть сложнее. Пришлось слегка подредактировать build.gradle
проекта, чтобы выделить побольше памяти виртуальной машине Java, отключить ProGuard и снять ограничение на 65 тысяч методов (multiDex):
android {
...
defaultConfig {
multiDexEnabled true
}
dexOptions {
javaMaxHeapSize "4g"
}
...
buildTypes {
debug {
minifyEnabled false
}
release {
minifyEnabled false
}
}
}
Оставалось только дождаться окончания сборки.
И тут я подумал о рефлексии...
На самом деле все сказанное выше — пустая болтовня. Не потому, что этот метод не работает, — он замечательно работает, и ты сам можешь в этом убедиться. Настоящая причина в том, что он невероятно избыточен, ведь есть более адекватный альтернативный путь. Итак, внимание, код для вытягивания шторки без замен фреймворков и возни с настройками Java и Gradle:
try {
// noinspection ResourceType
Object service = context.getSystemService("statusbar");
Class<?> statusbarManager = Class.forName("android.app.StatusBarManager");
Method expand = statusbarManager.getMethod("expandNotificationsPanel");
expand.invoke(service);
} catch (Exception e) {
Log.e("StatusBar", e.toString());
}
Все просто. Достаточно было использовать рефлексию, чтобы прямо во время исполнения найти класс StatusBarManager, найти его метод expandNotificationsPanel()
и вызвать. И все это без лишних телодвижений (кроме того, что после каждого редактирования код приходится запускать для проверки).
Какие еще скрытые API существуют?
На самом деле их не так уж много. В основном Android использует скрытые API для взаимодействия между системными классами, поэтому обычно это различные константы и подсобные функции, малоинтересные обычным программистам. Но есть и несколько полезных API, которые позволяют:
- монтировать, размонтировать и форматировать файловые системы (StorageManager);
- получить расширенную информацию о Wi-Fi (WifiManager);
- узнать UID и прочую информацию о текущем процессе (Process);
- получить расширенную информацию о базовой станции (CellInfoLte);
- узнать тип сети (ConnectivityManager);
- получить список установленных пакетов, принадлежащих указанному юзеру (PackageManager);
- узнать реальный размер экрана с учетом наэкранных кнопок навигации (Display).
Я не проверял все эти API, поэтому не буду давать гарантий, что они корректно работают и не требуют каких-то привилегий в системе. Ты можешь проверить сам — просто найди API в исходном коде, введя в поиске директиву @hide. Удобнее всего сделать это с помощью веб-сервиса AndroidXRef: просто укажи @hide в поле Full search, а в In project(s) выбери frameworks.
Мораль
Скрытые API и рефлексия позволили мне реализовать задуманное (если тебе интересно, это чудо есть в маркете). Однако это всего лишь маленькая софтинка, написанная для себя, и я настоятельно не рекомендую использовать скрытые API в больших проектах, особенно если ты собираешься их монетизировать.
В отличие от API с высоким уровнем доступа, наличие или неизменность скрытых API не гарантирована. В следующей версии Android они могут исчезнуть или измениться, они могут существовать в прошивках одних аппаратов и отсутствовать в других. Их использование — это всегда лотерея.