Desenvolvedores: é muito fácil contornar as restrições ocultas da API do Android

O Android 9 Pie e o Android 10 lançam avisos ou bloqueiam completamente o acesso a APIs ocultas. Veja como os desenvolvedores podem contornar as restrições ocultas da API.

Flashback de mais de um ano atrás, e estamos todos entusiasmados em ver o que está por vir nos betas do Android P. Os usuários estão ansiosos por novos recursos e os desenvolvedores estão ansiosos por algumas novas ferramentas para melhorar seus aplicativos. Infelizmente para alguns desses desenvolvedores, o primeiro Android P beta veio com uma surpresa desagradável: restrições ocultas de API. Antes de mergulhar no que exatamente isso significa, deixe-me explicar um pouco sobre seu contexto.

O que é isso?

Os desenvolvedores de aplicativos Android não precisam começar do zero ao criar um aplicativo. O Google fornece ferramentas para tornar o desenvolvimento de aplicativos mais fácil e menos repetitivo. Uma dessas ferramentas é o Android SDK. O SDK é essencialmente uma referência a todas as funções que os desenvolvedores podem usar com segurança em seus aplicativos. Essas funções são padrão em todas as variantes do Android aprovadas pelo Google. O SDK não é exaustivo. Existem algumas partes “ocultas” da estrutura do Android que não fazem parte do SDK.

Essas partes "ocultas" podem ser incrivelmente úteis para coisas mais hackeadas ou de baixo nível. Por exemplo, meu Aplicativo Widget Drawer faz uso de uma função não SDK para detectar quando um usuário inicia um aplicativo a partir de um widget para que a gaveta possa fechar automaticamente. Você pode estar pensando: "Por que não tornar essas funções não-SDK parte do SDK?" Bem, o problema é que o seu funcionamento não é totalmente previsível. O Google não pode garantir que todas as partes da estrutura funcionem em todos os dispositivos aprovados, portanto, métodos mais importantes são selecionados para verificação. O Google não garante que o restante da estrutura permanecerá consistente. Os fabricantes podem alterar ou remover completamente essas funções ocultas. Mesmo em diferentes versões do AOSP, você nunca sabe se uma função oculta ainda existirá ou funcionará como antes.

Se você está se perguntando por que estou usando a palavra “oculto”, é porque essas funções nem fazem parte do SDK. Normalmente, se você tentar usar um método ou classe oculto em um aplicativo, ele não será compilado. Usá-los requer reflexão ou um SDK modificado.

Com o Android P, o Google decidiu que apenas escondê-los não era suficiente. Quando o primeiro beta foi lançado, foi anunciado que a maioria (não todas) das funções ocultas não estava mais disponível para uso pelos desenvolvedores de aplicativos. A primeira versão beta avisaria quando seu aplicativo usasse uma função da lista negra. Os próximos betas simplesmente travaram seu aplicativo. Pelo menos para mim, essa lista negra era muito chata. Não só quebrou um pouco Gestos de navegação, mas como as funções ocultas estão na lista negra por padrão (o Google precisa colocar manualmente algumas na lista de permissões por versão), tive muitos problemas para fazer o Widget Drawer funcionar.

Agora, havia algumas maneiras de contornar a lista negra. A primeira era simplesmente manter seu aplicativo direcionado à API 27 (Android 8.1), já que a lista negra se aplicava apenas a aplicativos direcionados à API mais recente. Infelizmente, com o Google requisitos mínimos de API para publicação na Play Store, só seria possível direcionar a API 27 por um certo tempo. A partir de 1º de novembro de 2019, todas as atualizações de aplicativos na Play Store devem ser direcionadas à API 28 ou posterior.

A segunda solução alternativa é, na verdade, algo que o Google incorporou ao Android. É possível executar um comando ADB para desabilitar totalmente a lista negra. Isso é ótimo para uso e teste pessoal, mas posso dizer em primeira mão que tentar fazer com que os usuários finais configurem e executem o ADB é um pesadelo.

Então, onde isso nos deixa? Não podemos mais direcionar a API 27 se quisermos fazer upload para a Play Store, e o método ADB simplesmente não é viável. Isso não significa que estamos sem opções.

A lista negra de API oculta se aplica apenas a aplicativos de usuários não incluídos na lista de permissões. Aplicativos do sistema, aplicativos assinados com a assinatura da plataforma e aplicativos especificados em um arquivo de configuração estão todos isentos da lista negra. Curiosamente, o pacote Google Play Services está todo especificado nesse arquivo de configuração. Acho que o Google é melhor que o resto de nós.

De qualquer forma, vamos continuar falando sobre a lista negra. A parte que nos interessa hoje é que os aplicativos do sistema estão isentos. Isso significa, por exemplo, que o aplicativo System UI pode usar todas as funções ocultas que desejar, já que está na partição do sistema. Obviamente, não podemos simplesmente enviar um aplicativo para a partição do sistema. Isso precisa de root, um bom gerenciador de arquivos, conhecimento de permissões... Seria mais fácil usar ADB. Essa não é a única maneira de sermos um aplicativo de sistema, pelo menos no que diz respeito à lista negra de APIs ocultas.

Lembre-se de sete parágrafos atrás, quando mencionei a reflexão. Se você não sabe o que é reflexão, recomendo a leitura esta página, mas aqui está um rápido resumo. Em Java, reflexão é uma forma de acessar classes, métodos e campos normalmente inacessíveis. É uma ferramenta incrivelmente poderosa. Como eu disse naquele parágrafo, a reflexão costumava ser uma forma de acessar funções não SDK. A lista negra da API bloqueia o uso de reflexão, mas não bloqueia o uso de dobro-reflexão.

Agora, é aqui que fica um pouco estranho. Normalmente, se você quisesse chamar um método oculto, faria algo assim (isso é em Kotlin, mas Java é semelhante):

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

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

Graças à lista negra da API, você obteria apenas uma ClassNotFoundException. No entanto, se você refletir duas vezes, funciona bem:

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")

Estranho, certo? Bem, sim, mas também não. A lista negra da API rastreia quem está chamando uma função. Se a fonte não estiver isenta, ela falha. No primeiro exemplo, a fonte é o aplicativo. Porém, no segundo exemplo, a fonte é o próprio sistema. Em vez de usar a reflexão para conseguir o que queremos diretamente, estamos usando-a para dizer ao sistema para conseguir o que queremos. Como a origem da chamada para a função oculta é o sistema, a lista negra não nos afeta mais.

Então terminamos. Agora temos uma maneira de contornar a lista negra de APIs. É um pouco desajeitado, mas poderíamos escrever uma função wrapper para não precisarmos refletir duas vezes todas as vezes. Não é ótimo, mas é melhor que nada. Mas, na verdade, ainda não terminamos. Há uma maneira melhor de fazer isso que nos permitirá usar a reflexão normal ou um SDK modificado, como nos velhos tempos.

Como a aplicação da lista negra é avaliada por processo (que é o mesmo que por aplicativo na maioria dos casos), pode haver alguma maneira de o sistema registrar se o aplicativo em questão está isento ou não. Felizmente, existe e é acessível para nós. Usando esse novo hack de dupla reflexão, temos um bloco de código de uso único:

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"))

Ok, tecnicamente, isso não indica ao sistema que nosso aplicativo está isento da lista negra de API. Na verdade, existe outro comando ADB que você pode executar para especificar funções que não devem estar na lista negra. É disso que estamos aproveitando acima. O código basicamente substitui tudo o que o sistema considera isento para nosso aplicativo. Passar "L" no final significa que todos os métodos estão isentos. Se você quiser isentar métodos específicos, use a sintaxe Smali: Landroid/some/hidden/Class; Landroid/some/other/hidden/Class;.

Agora realmente terminamos. Faça uma classe Application personalizada, coloque esse código no onCreate() método, e bam, sem mais restrições.


Obrigado ao membro do XDA weishu, desenvolvedor do VirtualXposed e do Taichi, por descobrir este método originalmente. Também gostaríamos de agradecer ao desenvolvedor reconhecido do XDA, topjohnwu, por apontar esta solução alternativa para mim. Aqui está um pouco mais sobre como funciona, embora esteja em chinês. eu também escrevi sobre isso no Stack Overflow, com um exemplo em JNI também.