Sistema de Permissões em Tempo de Execução, Android M
(23587) (14)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vlad Khononov
EditoraAlta Books
Edição1ª
Ano2024
Páginas320
Tudo bem?
O sistema de permissões do Android mantém todo o sistema consistente fazendo com que aplicativos que necessitem de acesso a dados, dados não produzidos por eles, ou necessitem de acesso a funcionalidades não disponíveis neles, que esses aplicativos definam permissões para que o acesso, consumo, seja possível.
Dentre as categorias de permissões, duas categorias são mais comuns e merecem sua atenção:
- Categoria de permissões normais;
- e Categoria de permissões perigosas.
Com o release do Android Marshmallow, Android 6 (API 23), o sistema de permissões no Android, que tinha o formato de apresentar todos os grupos de permissões necessárias logo no momento de instalação do aplicativo - direto na Google Play Store:
Esse formato de solicitação de permissão foi agora substituído pelo modelo de requisição de permissão em tempo de execução, digo, requisição de permissões que estão dentro da categoria de permissões perigosas (dangerous permissions).
As permissões dentro da categoria de permissões normais não precisam mais ser de conhecimento do usuário, não há mais a tela de permissões sendo apresentada para este tipo de permissão e o aplicativo continua com o uso delas - mas note que a definição em AndroidManifest.xml ainda é necessária para permissões de qualquer categoria.
Além de acelerar o processo de instalação do aplicativo por parte do usuário, no aparelho dele, esse novo modelo de permissões, que funciona somente quando a versão do Android é igual ou superior à versão 6 (Marshmallow) e ao mesmo tempo o targetSdkVersion do aplicativo é igual ou superior a API 23.
Este modelo traz também a necessidade de um esforço extra por parte do desenvolvedor Android que terá de solicitar cada permissão necessária (podendo ser mais de uma em uma só solicitação) para a execução de funcionalidades que utilizam entidades que somente com algumas permissões perigosas liberadas podem ser acessadas.
Isso assumindo que o desenvolvedor Android está seguindo as "Permissions Best Practices" indicadas na documentação e assim não requisitando todas as permissões necessárias logo na inicialização do aplicativo (You do not do that, pls!).
Antes de prosseguir, não esqueça de se inscrever 📫na lista de e-mails do Blog para receber, em primeira mão, todas as novidades sobre o desenvolvimento de aplicativos Android.
A partir daqui vamos configurar um projeto Android de exemplo para conter algoritmos do novo sistema de solicitação de permissão em tempo de execução.
Você quer saber o nome do projeto? 🤔
Pode acreditar: PermissionTarget23 project 😂.
Vamos iniciar pelo AndroidManifest.xml. Temos as declarações de permissões como já utilizado no modelo antigo:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.permissiontarget23">
<!-- NORMALS PERMISSIONS -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- DANGEROUS PERMISSIONS -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
O próximo arquivo em configuração é o Gradle Nível de Aplicativo, ou build.gradle (Module: app), note que o targetSdkVersion é que passa por atualização, este deve ser 23, ou inferior.
Thiengo, "inferior"?
No caso de ser "inferior", somente se seu aplicativo ainda não estiver com o novo padrão de solicitação de permissão já configurado nos scripts dele, algo comum se o aplicativo não está com um código tão simples como no exemplo.
Segue a configuração do build.gradle (Module: app):
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "br.com.thiengo.permissiontarget23"
minSdkVersion 10
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.0'
compile 'me.drakeet.materialdialog:library:1.2.2'
compile 'com.squareup.picasso:picasso:2.5.2'
}
Note que utilizei, além das bibliotecas padrões que já vêm com a criação de um novo projeto no Android Studio, as libraries MaterialDialog (me.drakeet.materialdialog:library:1.2.2) e Picasso (com.squareup.picasso:picasso:2.5.2) para auxiliarem na execução de algoritmos de apresentação de dialog secundária e carregamento de imagem remota, respectivamente.
Este último, carregamento de imagem remota, para demonstrar que a permissão de Internet roda sem necessidade de requisição em tempo de execução, isso por ela fazer parte da categoria de permissões normais.
Abaixo a lista de permissões dentro do conjunto de Permissões Normais:
PERMISSÕES NORMAIS |
PERMISSÕES |
ACCESS_LOCATION_EXTRA_COMMANDS |
ACCESS_NETWORK_STATE |
ACCESS_NOTIFICATION_POLICY |
ACCESS_WIFI_STATE |
BLUETOOTH |
BLUETOOTH_ADMIN |
BROADCAST_STICKY |
CHANGE_NETWORK_STATE |
CHANGE_WIFI_MULTICAST_STATE |
CHANGE_WIFI_STATE |
DISABLE_KEYGUARD |
EXPAND_STATUS_BAR |
FLASHLIGHT |
GET_PACKAGE_SIZE |
INTERNET |
KILL_BACKGROUND_PROCESSES |
MODIFY_AUDIO_SETTINGS |
NFC |
READ_SYNC_SETTINGS |
READ_SYNC_STATS |
RECEIVE_BOOT_COMPLETED |
REORDER_TASKS |
REQUEST_INSTALL_PACKAGES |
SET_TIME_ZONE |
SET_WALLPAPER |
SET_WALLPAPER_HINTS |
TRANSMIT_IR |
USE_FINGERPRINT |
VIBRATE |
WAKE_LOCK |
WRITE_SYNC_SETTINGS |
SET_ALARM |
INSTALL_SHORTCUT |
UNINSTALL_SHORTCUT |
A outra categoria de permissões, permissões perigosas, necessita que você dê uma maior importância ao termo "grupo de permissões".
Pois assim que é permitido, ou negado, o acesso a determinada permissão (READ_EXTERNAL_STORAGE, por exemplo) todas as permissões do mesmo grupo de permissões (no caso do READ_EXTERNAL_STORAGE o grupo é o STORAGE) têm agora o mesmo resultado de acesso: permitido ou não.
Logo, não será mais necessária a caixa de diálogo de permissão para utilizar WRITE_EXTERNAL_STORAGE caso READ_EXTERNAL_STORAGE já tenha sido concedida pelo usuário, e vice-versa.
Abaixo a lista de Permissões Perigosas e seus respectivos grupos:
PERMISSÕES PERIGOSAS | |
GRUPO DE PERMISSÕES | PERMISSÕES |
CALENDAR | READ_CALENDAR |
WRITE_CALENDAR | |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS |
WRITE_CONTACTS | |
GET_ACCOUNTS | |
LOCATION | ACCESS_FINE_LOCATION |
ACCESS_COARSE_LOCATION | |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE |
CALL_PHONE | |
READ_CALL_LOG | |
WRITE_CALL_LOG | |
ADD_VOICEMAIL | |
USE_SIP | |
PROCESS_OUTGOING_CALLS | |
SENSORS | BODY_SENSORS |
SMS | SEND_SMS |
RECEIVE_SMS | |
READ_SMS | |
RECEIVE_WAP_PUSH | |
RECEIVE_MMS | |
STORAGE | READ_EXTERNAL_STORAGE |
WRITE_EXTERNAL_STORAGE |
Note que existem mais categorias de permissões além das Normais e Perigosas, porém para esse novo modelo, solicitação de permissão em tempo de execução, somente essas duas categorias é que importam.
As permissões normais também têm cada uma seus grupos, mas o entendimento de "grupos de permissões" ganha importância mesmo quando se trabalhando com permissões perigosas.
Pois como será apresentado no decorrer deste artigo, assim que é solicitada, em tempo de execução, o acesso a permissões individuais, o sistema Android apresenta ao usuário uma caixa de diálogo com a chamada ao grupo da permissão solicitada, fazendo com que a ação do usuário (permitir ou negar) seja refletida em todas as outras permissões do mesmo grupo de permissões.
Assim evitando, novamente, a chamada à caixa de diálogo de permissão caso uma outra permissão do mesmo grupo seja requisitada - ela então já estará disponível, ou não, dependendo da resposta do usuário à solicitação anterior.
A seguir o layout, activity_main.xml, que estaremos utilizando na aplicação de exemplo:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="br.com.thiengo.permissiontarget23.MainActivity">
<ImageView
android:id="@+id/iv_logo"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"/>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="Hello World!" />
<Button
android:id="@+id/bt_load_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_title"
android:layout_centerHorizontal="true"
android:onClick="callLoadImage"
android:text="Load Image" />
<Button
android:id="@+id/bt_write"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/bt_load_img"
android:layout_centerHorizontal="true"
android:onClick="callWriteOnSDCard"
android:text="Write on SDCard" />
<Button
android:id="@+id/bt_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/bt_write"
android:layout_centerHorizontal="true"
android:onClick="callReadFromSDCard"
android:text="Read from SDCard" />
<Button
android:id="@+id/bt_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/bt_read"
android:layout_centerHorizontal="true"
android:onClick="callAccessLocation"
android:text="Access Location" />
</RelativeLayout>
Abaixo segue um trecho de código da MainActivity do projeto de exemplo, porém sem a utilização dos algoritmos de solicitação de permissão em tempo de execução.
Isso já simulando em um aparelho Android 6.0, Marshmallow, com o targetSdkVersion em 23:
...
private void readMyCurrentCoordinates() {
LocationManager locationManager = (LocationManager) getSystemService( LOCATION_SERVICE );
boolean isGPSEnabled = locationManager.isProviderEnabled( LocationManager.GPS_PROVIDER );
boolean isNetworkEnabled = locationManager.isProviderEnabled( LocationManager.NETWORK_PROVIDER );
Location location = null;
double latitude = 0;
double longitude = 0;
if( !isGPSEnabled && !isNetworkEnabled ){
Log.i( TAG, "No geo resource able to be used." );
}
else{
if( isNetworkEnabled ){
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
2000,
0,
this
);
Log.d( TAG, "Network" );
location = locationManager.getLastKnownLocation( LocationManager.NETWORK_PROVIDER );
if( location != null ){
latitude = location.getLatitude();
longitude = location.getLongitude();
}
}
if( isGPSEnabled ){
if( location == null ){
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
2000,
0,
this
);
Log.d( TAG, "GPS Enabled" );
location = locationManager.getLastKnownLocation( LocationManager.GPS_PROVIDER );
if( location != null ){
latitude = location.getLatitude();
longitude = location.getLongitude();
}
}
}
}
Log.i( TAG, "Lat: " + latitude + " | Long: " + longitude );
}
...
No final do método temos nosso print das coordenadas via LogCat code.
Tenha em mente que esse método, readMyCurrentCoordinates(), será acionado pelo listener de clique do Button de ID bt_location (apresentado no layout activity_main.xml anteriormente):
...
public void callAccessLocation( View view ) {
Log.i( TAG, "callAccessLocation()" );
readMyCurrentCoordinates();
}
...
O resultado, quando tentamos acessar as últimas coordenadas, de nosso emulador de testes, guardadas no sistema, é o seguinte:
No LogCat temos na pilha de exceção o seguinte:
Mesmo com as permissões de LOCATION definidas no AndroidManifest.xml, ainda não é possível acessar as funcionalidades abaixo da liberação delas.
Então devemos solicitar a permissão ao usuário do aplicativo, utilizando os métodos das classes públicas ContextCompat e ActivityCompat (com fragmentos você consegue o mesmo efeito utilizando a classe FragmentCompat).
Vamos seguir com o código anterior, porém com a solicitação de permissão da maneira correta, segundo o novo modelo:
...
public void callAccessLocation( View view ) {
Log.i( TAG, "callAccessLocation()" );
if( ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED ){
if( ActivityCompat.shouldShowRequestPermissionRationale( this, Manifest.permission.ACCESS_FINE_LOCATION ) ){
callDialog(
"É preciso a permission ACCESS_FINE_LOCATION para apresentação dos eventos locais.",
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION
}
);
}
else{
ActivityCompat.requestPermissions(
this,
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION
},
REQUEST_PERMISSIONS_CODE
);
}
}
else{
readMyCurrentCoordinates();
}
}
...
O método readMyCurrentCoordinates() continua da mesma maneira, aliás, caso esteja com o Android Studio e o targetSdkVersion em 23, este IDE vai lhe apresentar as linhas de acesso aos providers NETWORK_PROVIDER e GPS_PROVIDER sublinhadas de vermelho.
Não se preocupe, na verdade o IDE está lhe informando que seu código precisa primeiro verificar se a permissão foi concedida, exatamente o que estamos fazendo no listener de clique do Button de ID bt_location, onde chamamos o método readMyCurrentCoordinates().
As classes ContextCompat e ActivityCompat nos permitem manter o código rodando, compatível, para versões anteriores ao Android 6.0, pois os métodos utilizados para verificação e solicitação de permissão foram adicionados somente a partir desta versão do Android.
O método checkSelfPermission() recebe como parâmetro o contexto atual (em nosso caso a Activity) e a permissão que devemos verificar para as funcionalidades seguintes serem utilizadas sem problemas caso a permissão tenha sido concedida.
Caso o valor retornado seja diferentes de PackageManager.PERMISSION_GRANTED, então devemos seguir para dentro do bloco condicional.
O método shouldShowRequestPermissionRationale() tem a mesma configuração de parâmetros do método anterior, porém esse é responsável por informar se a caixa de diálogo de permissão nativa do Android já foi ou não apresentada ao usuário, caso sim e o usuário tenha negado a permissão, esse método retorna true.
O método shouldShowRequestPermissionRationale() é utilizado para que nós desenvolvedores possamos, a partir da segunda tentativa de acesso a funcionalidade que necessita do conjunto de permissões, apresentar ao usuário o porquê das permissões solicitadas.
Ou seja, é a oportunidade que temos de antes de abrir o dialog nativo de permissão do Android, abrirmos nossa própria caixa de diálogo personalizada informando esse porquê das permissões, dando a chance ao usuário de aceitar e assim seguir para acesso à funcionalidade do app.
O método shouldShowRequestPermissionRationale() retorna false quando o método requestPermissions() ainda não foi chamado, ou quando o usuário marcou o dialog nativo de permissão para nunca mais ser apresentado para a permissão solicitada, "Never ask again box".
Ou quando o aplicativo está requisitando permissões que fogem do escopo de autorização do usuário e das categorias Normais e Perigosas - estas têm um modelo próprio de solicitação ao sistema.
O método requestPermissions() é o responsável por chamar a caixa de diálogo nativa de permissão:
Em casos onde o checkbox "Never ask again" tenha sido selecionado anteriormente ou a permissão já tenha sido concedida, a chamada ao dialog é ignorada e nada é apresentado.
Note que o segundo parâmetro é um array de Strings que contém todas as permissões necessárias para a execução da funcionalidade que o usuário solicitou no aplicativo.
O terceiro parâmetro é um int de 8 bits que será utilizado no método onRequestPermissionsResult() para verificar-mos se a permissão foi concedida ou não, caso sim, nesse método mesmo podemos chamar a funcionalidade requisitada pelo usuário.
Abaixo segue o algoritmo do método callDialog():
...
private void callDialog(
String message,
final String[] permissions ){
mMaterialDialog = new MaterialDialog( this )
.setTitle( "Permission" )
.setMessage( message )
.setPositiveButton( "Ok", new View.OnClickListener() {
@Override
public void onClick( View v ) {
ActivityCompat.requestPermissions(
MainActivity.this,
permissions,
REQUEST_PERMISSIONS_CODE
);
mMaterialDialog.dismiss();
}
})
.setNegativeButton( "Cancel", new View.OnClickListener() {
@Override
public void onClick( View v ) {
mMaterialDialog.dismiss();
}
});
mMaterialDialog.show();
}
...
No código acima a biblioteca MaterialDialog sendo utilizada quando o usuário já teve a caixa de diálogo nativa de permissão sendo apresentada e então ele negou a permissão.
A partir da segunda chamada a documentação oficial Android informa que nós desenvolvedores devemos apresentar uma caixa de diálogo de explicação, isso antes da chamada ao dialog nativo de permissão.
Caso o usuário escolha "Ok", então devemos invocar novamente o método requestPermissions().
Note que mesmo precisando das permissões ACCESS_FINE_LOCATION e ACCESS_COARSE_LOCATION, somente a utilização de uma das duas é o suficiente para a utilização do método readMyCurrentCoordinates(). Pois ambas vão acionar a mesma funcionalidade e estão no mesmo grupo de permissões.
💡 Informação importante: na verdade o Google Android recomenda que todas as permissões necessárias sejam configuradas mesmo que elas sejam de mesmo grupo, pois a regra de negócio "permissões de mesmo grupo sempre estarão liberadas se ao menos uma tiver sido concedida" pode mudar a qualquer momento.
Em caso de "Ok", a caixa de diálogo nativa com o checkbox "Never ask again" é apresentada:
Antes o dialog, do nosso MaterialDialog, é apresentada ao usuário:
Assim, logo depois da caixa de diálogo nativa ter algum dos Buttons clicado, o método onRequestPermissionsResult() é acionado.
Segue código dele:
...
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults ){
Log.i( TAG, "test" );
switch( requestCode ){
case REQUEST_PERMISSIONS_CODE:
for( int i = 0; i < permissions.length; i++ ){
if( permissions[i].equalsIgnoreCase( Manifest.permission.ACCESS_FINE_LOCATION )
&& grantResults[i] == PackageManager.PERMISSION_GRANTED ){
readMyCurrentCoordinates();
}
}
}
super.onRequestPermissionsResult(
requestCode,
permissions,
grantResults
);
}
...
Então é verificado no switch() se é o requestCode correto que definimos nas chamadas ao método requestPermissions().
Se sim, é o requestCode correto, verificamos no for() se a permissão no array permissions bate com alguma das permissões solicitadas no momento.
Caso o acesso tenha sido concedido, grantResults[i] == PackageManager.PERMISSION_GRANTED, devemos chamar o método correto para evitar que o usuário tenha de clicar novamente no botão de acionamento do método para a execução do mesmo, pois isso certamente, aos olhos do usuário, iria parecer um bug.
Para os testes do projeto de exemplo foi criado um emulador no AVD Manager com as configurações:
- Android 6.0;
- e Google APIs setadas no device.
Os passos são:
- 1º - Clique no ícone do AVD Manager:
- 2º - Clique em "Create Virtual Device..." como na imagem a seguir:
- 3º - Escolha o device que você quer montar e então clique em "Next":
- 4º - Escolha a versão / API do device. Fique com a Versão 6.0 (ou superior) e com Google APIs já instalada:
- 5º - Altere as configurações que achar necessário (eu mantive as padrões) e então clique em "Finish":
Depois é somente acionar o emulador pela mesma janela do AVD Manager e utiliza-lo.
Note que caso a sistema seja inferior ao Android 6.0 ou o targetSdkVersion do aplicativo seja inferior a API 23, então o sistema de requisição em tempo de execução será ignorado e as permissões serão concedidas.
Porém a partir da versão 6.0 do Android o usuário poderá mesmo assim revogar permissões liberadas ao aplicativo na área de permissões de apps do sistema Android.
Note também que o Android informará ao usuário que o aplicativo foi construído com um modelo configuração antigo de permissões e que isso pode ocasionar em crash.
Na verdade a Exception que ocorrerá não será a SecurityException pela falta de algoritmos de solicitação de permissão e sim, provavelmente (caso seu aplicativo não tenha um código protegido), alguma outra devido retorno não esperado dos métodos utilizados.
Métodos que somente poderiam ser utilizados depois de certas permissões terem sido concedidas em tempo de execução.
Logo, migrar seu aplicativo para o novo modelo de permissões é uma escolha inteligente.
Caso o app esteja com o código legado ou muito grande para uma alteração rápida, mantenha o targetSdkVersion abaixo da API 23 e então trate casos em que os valores null ou 0 são retornados dos métodos das entidades acessadas devido as permissões concedidas.
Há algumas bibliotecas que podem agilizar a codificação para você com o uso de annotations e outros, veja as três bibliotecas a seguir:
💎 Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, de se inscrever na 📩 lista de e-mails, respondo às suas dúvidas também por lá.
Abraço.
Vídeo
A seguir o vídeo de implementação do projeto de exemplo com os algoritmos de solicitação de permissão em tempo de execução:
O código completo do projeto de exemplo pode ser encontrado no GitHub dele em: https://github.com/viniciusthiengo/PermissionTarget23
Fontes
Trabalhando com o sistema de permissões do Android, tutorial na doc do Android
Grupo de permissões normais no Android
Grupo de permissões perigosas no Android, sesssão "Permission groups"
Sistema de permissões do Android 6.0 - Ricardo Lecheta Blog post
Comentários Facebook