Android 9 Pie și Android 10 lansează avertismente sau blochează direct accesul la API-urile ascunse. Iată cum pot dezvolta dezvoltatorii să ocolească restricțiile ascunse ale API-ului.
Flashback de acum peste un an și suntem cu toții încântați să vedem ce urmează în versiunea beta Android P. Utilizatorii așteaptă cu nerăbdare noi funcții, iar dezvoltatorii așteaptă cu nerăbdare câteva instrumente noi pentru a-și îmbunătăți aplicațiile. Din păcate pentru unii dintre acești dezvoltatori, primul Android P beta a venit cu o surpriză urâtă: restricții API ascunse. Înainte de a mă scufunda în ceea ce înseamnă exact asta, permiteți-mi să explic puțin despre contextul său.
Despre ce este vorba?
Dezvoltatorii de aplicații Android nu trebuie să înceapă de la zero atunci când creează o aplicație. Google oferă instrumente pentru a face dezvoltarea aplicațiilor mai ușoară și mai puțin repetitivă. Unul dintre aceste instrumente este Android SDK. SDK-ul este în esență o referință la toate funcțiile pe care dezvoltatorii le pot folosi în siguranță în aplicațiile lor. Aceste funcții vin standard pe toate variantele de Android pe care Google le aprobă. SDK-ul nu este, totuși, exhaustiv. Există destul de multe părți „ascunse” ale cadrului Android care nu fac parte din SDK.
Aceste părți „ascunse” pot fi incredibil de utile pentru lucruri mai hacker sau de nivel scăzut. De exemplu, al meu Aplicația Widget Drawer folosește o funcție non-SDK pentru a detecta când un utilizator lansează o aplicație dintr-un widget, astfel încât sertarul să se poată închide automat. S-ar putea să vă gândiți: „De ce nu faceți pur și simplu aceste funcții non-SDK parte din SDK?” Ei bine, problema este că funcționarea lor nu este complet previzibilă. Google nu se poate asigura că fiecare parte a cadrului funcționează pe fiecare dispozitiv pe care îl aprobă, așa că sunt selectate metode mai importante pentru a fi verificate. Google nu garantează că restul cadrului va rămâne consistent. Producătorii pot modifica sau elimina complet aceste funcții ascunse. Chiar și în diferite versiuni de AOSP, nu știi niciodată dacă o funcție ascunsă va mai exista sau va funcționa așa cum era înainte.
Dacă vă întrebați de ce am folosit cuvântul „ascuns”, este pentru că aceste funcții nici măcar nu fac parte din SDK. În mod normal, dacă încercați să utilizați o metodă sau o clasă ascunsă într-o aplicație, aceasta nu va reuși să compilați. Folosirea lor necesită reflecţie sau un SDK modificat.
Cu Android P, Google a decis că doar a le ascunde nu este suficient. Când a fost lansat prima versiune beta, s-a anunţat că majoritatea (nu toate) funcțiile ascunse nu mai erau disponibile pentru utilizarea dezvoltatorilor de aplicații. Prima versiune beta te-ar avertiza când aplicația ta a folosit o funcție pe lista neagră. Următoarele versiuni beta pur și simplu v-au blocat aplicația. Cel puțin pentru mine, lista neagră a fost destul de enervantă. Nu numai că s-a rupt destul de Gesturi de navigare, dar din moment ce funcțiile ascunse sunt incluse în mod implicit pe lista neagră (Google trebuie să treacă manual pe lista albă pentru fiecare lansare), am avut multe probleme cu Widget Drawer să funcționeze.
Acum, existau câteva moduri de a rezolva lista neagră. Prima a fost pur și simplu să păstrați aplicația dvs. care vizează API 27 (Android 8.1), deoarece lista neagră se aplica numai aplicațiilor care vizează cel mai recent API. Din păcate, cu Google cerințe minime API pentru publicare în Magazinul Play, ar fi posibil să vizați API-ul 27 doar atât de mult timp. Începând cu 1 noiembrie 2019, toate actualizările aplicației din Magazinul Play trebuie să vizeze API 28 sau o versiune ulterioară.
A doua soluție este de fapt ceva integrat de Google în Android. Este posibil să rulați o comandă ADB pentru a dezactiva complet lista neagră. Este grozav pentru uz personal și testare, dar vă pot spune direct că încercarea de a determina utilizatorii finali să configureze și să ruleze ADB este un coșmar.
Deci unde ne lasă asta? Nu mai putem viza API 27 dacă vrem să încărcăm în Play Store, iar metoda ADB pur și simplu nu este viabilă. Asta nu înseamnă însă că nu avem opțiuni.
Lista neagră API ascunsă se aplică numai aplicațiilor utilizatorilor care nu sunt incluse în lista albă. Aplicațiile de sistem, aplicațiile semnate cu semnătura platformei și aplicațiile specificate într-un fișier de configurare sunt toate exceptate de pe lista neagră. Destul de amuzant, suita de servicii Google Play sunt toate specificate în acel fișier de configurare. Cred că Google este mai bun decât noi ceilalți.
Oricum, hai să vorbim în continuare despre lista neagră. Partea care ne interesează astăzi este că aplicațiile de sistem sunt scutite. Aceasta înseamnă, de exemplu, că aplicația System UI poate folosi toate funcțiile ascunse pe care le dorește, deoarece se află pe partiția de sistem. Evident, nu putem doar să împingem o aplicație în partiția de sistem. Asta necesită root, un bun manager de fișiere, cunoștințe despre permisiuni... Ar fi mai ușor de utilizat ADB. Nu este singurul mod în care putem fi o aplicație de sistem, cel puțin în ceea ce privește lista neagră API ascunsă.
Întoarce-ți mintea la șapte paragrafe în urmă, când am menționat reflecție. Dacă nu știți ce este reflectarea, vă recomand să citiți această pagină, dar iată un rezumat rapid. În Java, reflectarea este o modalitate de a accesa clase, metode și câmpuri inaccesibile în mod normal. Este un instrument incredibil de puternic. După cum am spus în acel paragraf, reflectarea era o modalitate de a accesa funcții non-SDK. Lista neagră API blochează utilizarea reflexiei, dar nu blochează utilizarea dubla-reflecţie.
Acum, iată unde devine puțin ciudat. În mod normal, dacă doriți să apelați o metodă ascunsă, ați face ceva de genul acesta (acesta este în Kotlin, dar Java este similar):
val someHiddenClass = Class.forName("android.some.hidden.Class")
val someHiddenMethod = someHiddenClass.getMethod("someHiddenMethod", String::class.java)
someHiddenMethod.invoke(null, "some important string")
Cu toate acestea, datorită listei negre API, veți primi o excepție ClassNotFoundException. Cu toate acestea, dacă reflectați de două ori, funcționează bine:
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")
Ciudat nu? Ei bine, da, dar și nu. Lista neagră API urmărește cine apelează o funcție. Dacă sursa nu este scutită, se blochează. În primul exemplu, sursa este aplicația. Cu toate acestea, în al doilea exemplu, sursa este sistemul însuși. În loc să folosim reflecția pentru a obține ceea ce ne dorim în mod direct, o folosim pentru a spune sistemului să obțină ceea ce ne dorim. Deoarece sursa apelului la funcția ascunsă este sistemul, lista neagră nu ne mai afectează.
Deci am terminat. Avem o modalitate de a ocoli lista neagră API acum. Este puțin neplăcut, dar am putea scrie o funcție de wrapper, astfel încât să nu trebuie să reflectăm de fiecare dată. Nu este grozav, dar este mai bine decât nimic. Dar, de fapt, nu am terminat. Există o modalitate mai bună de a face acest lucru, care ne va permite să folosim reflexia normală sau un SDK modificat, ca în vremurile bune.
Deoarece aplicarea listei negre este evaluată în funcție de proces (care este la fel ca pentru fiecare aplicație în majoritatea cazurilor), ar putea exista o modalitate prin care sistemul să înregistreze dacă aplicația în cauză este sau nu scutită. Din fericire, există și ne este accesibil. Folosind acel nou hack de reflexie dublă, avem un bloc de cod de unică folosință:
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)) asMethodval vmRuntime = getRuntime.invoke(null)
setHiddenApiExemptions.invoke(vmRuntime, arrayOf("L"))
Bine, deci din punct de vedere tehnic, acest lucru nu spune sistemului că aplicația noastră este scutită de lista neagră API. De fapt, există o altă comandă ADB pe care o puteți rula pentru a specifica funcții care nu ar trebui să fie incluse pe lista neagră. De asta profităm mai sus. Codul înlocuiește, practic, tot ceea ce sistemul consideră că este scutit pentru aplicația noastră. Trecerea „L” la sfârșit înseamnă că toate metodele sunt scutite. Dacă doriți să scutiți anumite metode, utilizați sintaxa Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;
.
Acum chiar am terminat. Faceți o clasă de aplicație personalizată, puneți acel cod în onCreate()
metoda, și bam, nu mai există restricții.
Mulțumim membrului XDA weishu, dezvoltatorul VirtualXposed și Taichi, pentru că a descoperit inițial această metodă. De asemenea, am dori să mulțumim dezvoltatorului recunoscut XDA topjohnwu pentru că mi-a indicat această soluție. Iată puțin mai multe despre cum funcționează, deși este în chineză. și eu a scris despre asta pe Stack Overflow, cu un exemplu și în JNI.