Разработчици: Изключително лесно е да заобиколите скритите ограничения на API на Android

Android 9 Pie и Android 10 изпращат предупреждения или направо блокират достъпа до скрити API. Ето как разработчиците могат да заобиколят скритите ограничения на API.

Ретроспекция преди повече от година и всички сме развълнувани да видим какво предстои в бета версиите на Android P. Потребителите очакват с нетърпение нови функции, а разработчиците с нетърпение очакват някои нови инструменти, за да направят своите приложения по-добри. За съжаление на някои от тези разработчици, първата бета версия на Android P дойде с малко неприятна изненада: скрити API ограничения. Преди да се потопя в какво точно означава това, нека обясня малко за неговия контекст.

Какво е всичко това?

Разработчиците на приложения за Android не трябва да започват от нулата, когато създават приложение. Google предоставя инструменти, за да направи разработката на приложения по-лесна и по-малко повтаряща се. Един от тези инструменти е Android SDK. SDK по същество е препратка към всички функции, които разработчиците могат безопасно да използват в своите приложения. Тези функции са стандартни за всички варианти на Android, които Google одобрява. SDK обаче не е изчерпателен. Има доста "скрити" части от рамката на Android, които не са част от SDK.

Тези „скрити“ части могат да бъдат невероятно полезни за по-хакерски неща или неща от ниско ниво. Например моята Приложение Widget Drawer използва функция, която не е SDK, за да открие кога потребителят стартира приложение от джаджа, така че чекмеджето да може автоматично да се затвори. Може би си мислите: "Защо просто не направите тези функции, които не са SDK, част от SDK?" Е, проблемът е, че тяхната работа не е напълно предвидима. Google не може да се увери, че всяка една част от рамката работи на всяко отделно устройство, което одобрява, така че по-важните методи са избрани за проверка. Google не гарантира, че останалата част от рамката ще остане последователна. Производителите могат да променят или напълно да премахнат тези скрити функции. Дори в различни версии на AOSP никога не знаете дали скрита функция все още ще съществува или ще работи както преди.

Ако се чудите защо използвам думата „скрити“, това е, защото тези функции дори не са част от SDK. Обикновено, ако се опитате да използвате скрит метод или клас в приложение, то няма да успее да се компилира. Използването им изисква отражение или модифициран SDK.

С Android P Google реши, че просто скриването им не е достатъчно. Когато беше пусната първата бета версия, беше обявено, че повечето (не всички) скрити функции вече не бяха достъпни за използване от разработчиците на приложения. Първата бета ще ви предупреди, когато приложението ви използва функция от черен списък. Следващите бета версии просто сринаха приложението ви. Поне за мен този черен списък беше доста досаден. Не само че се счупи доста Жестове за навигация, но тъй като скритите функции са в черния списък по подразбиране (Google трябва ръчно да включи в белия списък някои за всяко издание), имах много проблеми с това да накарам Widget Drawer да работи.

Сега имаше няколко начина за заобикаляне на черния списък. Първият беше просто да поддържате приложението си насочено към API 27 (Android 8.1), тъй като черният списък се прилага само за приложения, насочени към най-новия API. За съжаление, с тези на Google минимални изисквания за API за публикуване в Play Store би било възможно да се насочите само към API 27 толкова дълго. Към 01.11.2019г, всички актуализации на приложения в Play Store трябва да са насочени към API 28 или по-нова версия.

Второто решение всъщност е нещо, което Google е вградило в Android. Възможно е да изпълните ADB команда, за да деактивирате изцяло черния списък. Това е чудесно за лична употреба и тестване, но мога да ви кажа от първа ръка, че опитът да накарате крайните потребители да настроят и стартират ADB е кошмар.

И така, къде ни оставя това? Вече не можем да се насочваме към API 27, ако искаме да качваме в Play Store, а методът ADB просто не е жизнеспособен. Това обаче не означава, че сме без опции.

Скритият черен списък на API се прилага само за потребителски приложения, които не са в белия списък. Системните приложения, приложенията, подписани с подписа на платформата, и приложенията, посочени в конфигурационен файл, са изключени от черния списък. Колкото и да е странно, всички пакети от услуги на Google Play са посочени в този конфигурационен файл. Предполагам, че Google е по-добър от останалите от нас.

Както и да е, нека продължим да говорим за черния списък. Частта, която ни интересува днес, е, че системните приложения са освободени. Това означава, например, че приложението System UI може да използва всички скрити функции, които иска, тъй като е в системния дял. Очевидно не можем просто да избутаме приложение в системния дял. Това изисква root, добър файлов мениджър, познания за разрешенията... Би било по-лесно да използвате ADB. Това обаче не е единственият начин да бъдем системно приложение, поне що се отнася до скрития черен списък на API.

Върнете ума си назад до седем параграфа, когато споменах размисъл. Ако не знаете какво е отражение, препоръчвам да прочетете тази страница, но ето кратко резюме. В Java отражението е начин за достъп до обикновено недостъпни класове, методи и полета. Това е невероятно мощен инструмент. Както казах в този параграф, отражението беше начин за достъп до функции, които не са SDK. Черният списък на API блокира използването на отражение, но не блокира използването на двойно- отражение.

Ето къде става малко странно. Обикновено, ако искате да извикате скрит метод, бихте направили нещо подобно (това е в Kotlin, но Java е подобна):

val someHiddenClass = Class.forName("android.some.hidden.Class")
val someHiddenMethod = someHiddenClass.getMethod("someHiddenMethod", String::class.java)

someHiddenMethod.invoke(null, "some important string")

Благодарение на черния списък на API обаче, вие просто ще получите ClassNotFoundException. Въпреки това, ако отразявате два пъти, работи добре:

val forName = Class::class.java.getMethod("forName", String:: class.java)
val getMethod = Class::class.java.getMethod("getMethod", String:: class.java, arrayOf>()::class.java)
val someHiddenClass = forName.invoke(null, "android.some.hidden.Class") asClass
val someHiddenMethod = getMethod.invoke(someHiddenClass, "someHiddenMethod", String::class.java)

someHiddenMethod.invoke(null, "some important string")

Странно нали? Е, да, но и не. Черният списък на API проследява кой извиква функция. Ако източникът не е освободен, той се срива. В първия пример източникът е приложението. Във втория пример обаче източникът е самата система. Вместо да използваме отражение, за да получим директно това, което искаме, ние го използваме, за да кажем на системата да получи това, което искаме. Тъй като източникът на извикване на скритата функция е системата, черният списък вече не ни засяга.

Така че ние сме готови. Сега имаме начин да заобиколим черния списък на API. Малко е тромаво, но бихме могли да напишем функция за обвивка, така че да не се налага да отразяваме двойно всеки път. Не е страхотно, но е по-добре от нищо. Но всъщност не сме приключили. Има по-добър начин да направим това, който ще ни позволи да използваме нормално отражение или модифициран SDK, както в добрите стари времена.

Тъй като прилагането на черния списък се оценява за всеки процес (което е същото като за приложение в повечето случаи), може да има някакъв начин системата да запише дали въпросното приложение е освободено или не. За щастие има и е достъпно за нас. Използвайки този новооткрит хак с двойно отражение, имаме кодов блок за еднократна употреба:

val forName = Class::class.java.getDeclaredMethod("forName", String:: class.java)
val getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod", String:: class.java, arrayOf>()::class.java)

val vmRuntimeClass = forName.invoke(null, "dalvik.system.VMRuntime") asClass
val getRuntime = getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null) as Method
val setHiddenApiExemptions = getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", arrayOf(arrayOf<String>()::class.java)) asMethod

val vmRuntime = getRuntime.invoke(null)

setHiddenApiExemptions.invoke(vmRuntime, arrayOf("L"))

Добре, технически това не казва на системата, че приложението ни е изключено от черния списък на API. Всъщност има друга ADB команда, която можете да изпълните, за да посочите функции, които не трябва да бъдат в черния списък. Това е, от което се възползваме по-горе. Кодът основно отменя всичко, което системата смята за изключено за нашето приложение. Преминаването на "L" в края означава, че всички методи са освободени. Ако искате да изключите определени методи, използвайте синтаксиса на Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;.

Сега всъщност сме готови. Направете персонализиран клас Application, поставете този код в onCreate() метод и бам, няма повече ограничения.


Благодаря на члена на XDA weishu, разработчика на VirtualXposed и Taichi, за първоначалното откриване на този метод. Също така бихме искали да благодарим на XDA Recognized Developer topjohnwu, че ми посочи това заобиколно решение. Ето малко повече за това как работи, въпреки че е на китайски. Аз също писа за това в Stack Overflow, с пример и в JNI.