Sistema de Permissões em Tempo de Execução, Android M

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Sistema de Permissões em Tempo de Execução, Android M

Sistema de Permissões em Tempo de Execução, Android M

Vinícius Thiengo
(10220) (19) (1) (2)
Go-ahead
"Não podemos estar em modo de sobrevivência. Temos de estar no modo de crescimento."
Jeff Bezos
Kotlin Android
Capa do livro Desenvolvedor Kotlin Android - Bibliotecas para o dia a dia
TítuloDesenvolvedor Kotlin Android - Bibliotecas para o dia a dia
CategoriasAndroid, Kotlin
AutorVinícius Thiengo
Edição
Ano2018
Capítulos19
Páginas1035
Acessar Livro
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas186
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas936
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas599
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, blz?

O sistema de permissões do Android mantém todo o sistema / framework consistente fazendo com que APPs que necessitem acesso a dados não produzidos por elas (dentro do sandbox dedicado a elas) ou a funcionalidades não disponíveis nelas definam permissões para que esse consumo seja possível. Dentre os grupos de permissões, dois grupos são mais comuns: Grupo de Permissões Normais e Grupo de Permissões perigosas.

Com o release do Android Marshmallow o sistema de permissões no Android que tinha o formato de apresentar todos os grupos de permissões necessários logo no momento de instalação da APP (direto na PlayStore): 

Agora foi substituído pela requisição de permissões do grupo Dangerous Permissions (Permissões Perigosas) em tempo de execução quando o user navega pelas funcionalidades da APP, enquanto as permissões normais não precisam mais ser de conhecimento do user, não há mais a tela de permissões sendo apresentada para esse tipo de permissions e a APP continua com a utilização delas.

Além de acelerar o processo de instalação da APP por parte do user no device dele, esse novo modelo de permissões que funciona somente quando a versão do Android é igual ou superior a 6 (Marshmallow) e ao mesmo tempo o targetSdkVersion da APP é igual ou superior a API 23 traz também um esforço extra por parte do developer 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 developer está seguindo as "Permissions Best Practices" indicadas na documentação e não requisitando todas as permissões necessárias logo na inicialização da APP. 

Abaixo vamos seguir com a utilização do novo sistema de permissões em tempo de execução demonstrando parte do código necessário em um exemplo: PermissionTarget23 project.

Começando 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 é o build.gradle (APP level ou Module: app), note que o targetSdkVersion é que deve ser 23, ou inferior. No caso de ser inferior, somente se sua APP ainda não estiver com o novo padrão de solicitação de permissão já configurado nos scripts dela, algo comum se a APP não está com um código tão simples como no exemplo, segue build.gradle:

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 libraries padrões que já vêm com a criação de um novo projeto no AndroidStudio, 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 scripts de apresentação de dialog secundária e carregamento de imagem respectivamente, esse último para demonstrar que a permissão de Internet roda sem necessidade de requisição em tempo de execução por ela fazer parte do grupo de permissões normais, lista que segue abaixo:

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

 

O outro grupo de permissões, permissões perigosas, para esse novo modelo de requisição de permissões é importante que tenha em mente também os grupos 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 o mesmo privilégio (acesso permitido ou não), logo não será mais necessária a Dialog de permissão para utilizar WRITE_EXTERNAL_STORAGE caso READ_EXTERNAL_STORAGE já tenha sido concedido pelo user e vice-versa. Abaixo a lista de permissões perigosas e seus respectivos grupos:

PERMISSÕES PERIGOSAS
GRUPO DE PERMISSÕESPERMISSÕES
CALENDARREAD_CALENDAR
 WRITE_CALENDAR
  
CAMERACAMERA
  
CONTACTSREAD_CONTACTS
 WRITE_CONTACTS
 GET_ACCOUNTS
  
LOCATIONACCESS_FINE_LOCATION
 ACCESS_COARSE_LOCATION
  
MICROPHONERECORD_AUDIO
  
PHONEREAD_PHONE_STATE
 CALL_PHONE
 READ_CALL_LOG
 WRITE_CALL_LOG
 ADD_VOICEMAIL
 USE_SIP
 PROCESS_OUTGOING_CALLS
  
SENSORSBODY_SENSORS
  
SMSSEND_SMS
 RECEIVE_SMS
 READ_SMS
 RECEIVE_WAP_PUSH
 RECEIVE_MMS
  
STORAGEREAD_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 essas duas são as que importam. As permissões normais também tem cada uma seus grupos, mas os grupos são de importância mesmo quando se falando em permissões perigosas, pois como será apresentado no decorrer do post, assim que é solicitada em tempo de execução o acesso a permissões individuais, o Android System apresenta ao user da APP uma dialog com a chamada ao grupo da permissão solicitada, fazendo com que a ação do user (permitir ou negar) seja refletida em todas as outras permissões do mesmo grupo de permissões, evitando no caso a chamada ao Dialog Permission novamente caso uma outra permissão do mesmo grupo seja requisitada.

Abaixo segue o layout (activity_main.xml) que estamos utilizando na aplicação:

<?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 de nossa MainActivity.xml, porém sem a utilização do script de solicitação de permissão perigosa em tempo de execução, isso simulando em um device Android 6.0 Marshmallow com o targetSdkVersion da APP 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 será acionado pelo listener de click do button bt_location (apresentado no activity_main.xml anteriormente):

public void callAccessLocation(View view) {
Log.i(TAG, "callAccessLocation()");

readMyCurrentCoordinates();
}

O resultado quando tentamos acessar as últimas coordendas de nosso device guardadas no sistema é o seguinte: 

 

E em nosso System Log (LogCat) temos na pilha de Exception o seguinte:

 

Mesmo com a permissão definida no AndroidManifest.xml ainda não é possível acessá-la, então devemos solicitar a permissão ao user utilizando os métodos das classes públicas ContextCompat e ActivityCompat (com Fragments você consegue o mesmo efeito utilizando classe FragmentCompat). Vamos seguir com o código do mesmo trecho acima, porém com a solicitação permissão da maneira correta:

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() apresentado anteriormente continua da mesma maneira, aliás caso esteja com o AndroidStudio e o targetSdkVersion em 23 o AndroidStudio vai lhe apresentar as linhas de acesso aos providers NETWORK_PROVIDER e GPS_PROVIDER sublinhadas de vermelho, não se preocupe, na verdade a IDE está lhe informando que seu código precisa primeiro verificar se a permissão foi concedida, exatamente o que fazemos 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 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 dessa 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 concedida. Caso o valor retornado seja diferentes de PackageManager.PERMISSION_GRANTED devemos seguir na condicional. O método shouldShowRequestPermissionRationale() tem a mesma configuração de parâmetros do métodos anterior, porém esse é responsável por informar se a Dialog de permissão nativa do Android já foi ou não apresentada ao user, caso sim e o user tenha negado a permissão, esse método retorna sim. O método shouldShowRequestPermissionRationale() é utilizado para que nós developers possamos a apartir da segunda tentativa de acesso a funcionalidade que necessita do conjunto de permissões, apresentar ao user o porque das permissões solicitadas, ou seja, é a oportunidade que temos de antes de abrir a Dialog nativa de permissão do Android, abrirmos nossa Dialog personalizada informando esse porquê das permissões, dando a chance ao user de aceitar e seguir para acesso a funcionalidade. O método shouldShowRequestPermissionRationale() retorna false quando o método requestPermissions() ainda não foi chamado, ou quando o user marcou o Dialog nativo de permissão para nunca mais ser apresentado para a permissão solicitada (Never ask again box), ou quando o APP está requisitando permissões que fogem do escopo de autorização do user e das categorias Normais e Perigosas (essas têm um modelo próprio de solicitação ao sistema). O método requestPermissions() é o responsãl por chamar a Dialog nativa de permissão, apresentada logo na segunda imagem desse post. 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 String que contém todas as permissões necessárias para a execução da funcionalidade que o user solicitou na APP. O terceiro parâmetro é um inteiro 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 fucionalidade requisitada pleo user. Abaixo segue o script 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();
}

Essa é a library MaterialDialog sendo utilizada quando o user já teve a Dialog native permission sendo apresentada e então a permissão negada. A partir da segunda chamada a documentação solicita que nós developers apresentemos uma Dialog de explicação antes da chamada a Dialog nativa. Caso o user pressione Ok, então chamamos 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 elas no mesmo grupo de permissões. EM caso de Ok, o Dialog nativo com o checkbox "Never ask again" é apresentado:

 

Antes a Dialog do nosso MaterialDialog é apresentada ao user:

 

Assim, logo depois da Dialog nativa ter algum dos buttons clicado o método onRequestPermissionsResult() será acionado, segue código:

@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 verificamos no loop se a permission em permissions array 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 ara evitar que o user tenha de clicar novamente no button de acionamento do método para a execução do mesmo.

Para os testes desse exemplo, como não tenho em mãos o device real com o Android Marshmallow (6.0) instalado, criei 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 icon do AVD Manager:

 

2º clique em "Create Virtual Device..." como abaixo:

 

3º Escolha o device que quer montar e clique em "Next":

 

4º Escolha a versão / API do device, fique com a Versão 6.0 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 a targetSdkVersion da APP seja inferior a API 23 o sistema de requisição em tempo de execução será ignorado e as permissões concedidas, porém a partir da versão 6.0 do Android o user poderá mesmo assim revogar permissões da APP na área de permissões da APP, porém o Android informará ao user que o APP foi construído com um modelo anterior de permissions (em resumo é isso) e que isso pode ocasionar em Crash APP.

Na verdade a Exception não será a SecurityException pela falta de permissão e sim, provavelmente (caso sua APP não tenha um código protegido), pelo retorno não esperado dos métodos utilizados. Logo migrar sua APP 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 libraries que podem agilizar a codificação para ti com o uso de annotations e cia, veja as duas libraries abaixo:

GitHub WPAndroidPermissions

GitHub PermissionsDispatcher

O código completo do projeto acima pode ser encontrado no GitHub do Blog: https://github.com/viniciusthiengo/PermissionTarget23

Abaixo segue os links referência para a construção desse post:

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"

Everything every Android Developer must know about new Android's Runtime Permission - The Cheese Factory Blog post

Sistema de permissões do Android 6.0 - Ricardo Lecheta Blog post

Vlw. Segue vídeo de implementação:

 

 

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

GCM Downstream Messages. Push Message Android - Parte 1GCM Downstream Messages. Push Message Android - Parte 1Android
Injeção de Dependência Com a lib Dagger 2 no AndroidInjeção de Dependência Com a lib Dagger 2 no AndroidAndroid
Library Retrofit 2 no AndroidLibrary Retrofit 2 no AndroidAndroid
Sites, Canais e Blogs Gringos Para Estudar Desenvolvimento AndroidSites, Canais e Blogs Gringos Para Estudar Desenvolvimento AndroidAndroid

Compartilhar

Comentários Facebook (5)

Comentários Blog (14)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Valdir (1) (0)
24/06/2018
Olá, Thiengo!
Mais uma vez parabéns pelos videos!

Estou com um problema em um app que estou criando para abrir conteúdo pdf em um app externo (adobe). Ocorre que já fiz as implementações de permissões, mas ele só funciona com o targetSdkVersion 22, já a partir da targetSdkVersion 23 não funciona apesar de ter feito a implementação das permissões como segue abaixo:

NA MAINACTIVITY:

package com.itextpdf.br.pdfteste;

import android.Manifest;
import android.app.Dialog;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.Manifest;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;

import com.squareup.picasso.Picasso;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;

import me.drakeet.materialdialog.MaterialDialog;

import java.util.ArrayList;

import static android.location.LocationManager.NETWORK_PROVIDER;

public class MainActivity extends AppCompatActivity {


    public static final String TAG = "LOG";
    public static final int REQUEST_PERMISSIONS_CODE = 128;

    private MaterialDialog mMaterialDialog;


    private String[] header = {"Id", "Nome", "Apelido"};
    private String shortText = "Olá olá olá";
    private String longText = "Especificação da Infração: Violar, adulterar ou declarar dados incorretos ou falsos nos sistemas de informações da Semad ou de suas entidades vinculadas e/ou conveniadas para validar informações ou para emissão de documentos ambientais obrigatórios ou para obter proveito para si ou para outrem.\n" +
            "Classificação: Gravíssima.\n" +
            "Incidência da Pena: Por ato.\n";
    private TemplatePDF templatePDF;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        templatePDF = new TemplatePDF(getApplicationContext());
        templatePDF.openDocument();
        templatePDF.addMetaData("Clientes", "Ventas", "Marines");
        templatePDF.addTitles("Tienda CodigoFacilito", "Clientes", "06/12/2017");
        templatePDF.addParagraph(shortText);
        templatePDF.addParagraph(longText);
        templatePDF.createTable(header, getClients());
        templatePDF.closeDocument();
    }


    @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.WRITE_EXTERNAL_STORAGE)
                            && grantResults[i] == PackageManager.PERMISSION_GRANTED) {

                        templatePDF.appviewPDF(this);
                    } else if (permissions[i].equalsIgnoreCase(Manifest.permission.READ_EXTERNAL_STORAGE)
                            && grantResults[i] == PackageManager.PERMISSION_GRANTED) {

                        templatePDF.viewPDF();
                    }
                }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    // LISTENERS
    public void  pdfApp(View view) {
        Log.i(TAG, " pdfApp()");

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {

            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                callDialog("É preciso a permission WRITE_EXTERNAL_STORAGE para apresentação do contexto.", new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE});
            } else {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSIONS_CODE);
            }
        } else {
            templatePDF.appviewPDF(this);
        }

    }

    public void pdfView(View view) {
        Log.i(TAG, " pdfView()");

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {

            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
                callDialog("É preciso a permission READ_EXTERNAL_STORAGE para apresentação do conteúdo.", new String[]{Manifest.permission.READ_EXTERNAL_STORAGE});
            } else {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSIONS_CODE);
            }
        } else {
            templatePDF.viewPDF();
        }
    }


    // UTIL
    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();
    }


    private ArrayList<String[]>getClients(){
        ArrayList<String[]>rows=new ArrayList<>();

        rows.add(new String[]{"1","VALDIR","Teste"});
        rows.add(new String[]{"2","Sofia","Hernandez"});
        rows.add(new String[]{"3","Naomi","Alfaro"});
        rows.add(new String[]{"4","Lorena","Espejel"});
        return rows;

    }
}




NO TEMPLATEPDF.JAVA:

package com.itextpdf.br.pdfteste;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.util.Log;
import android.widget.Toast;

import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;

import org.w3c.dom.Document;

import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;

public class TemplatePDF {
    private Context context;
    private File pdfFile;
    private com.itextpdf.text.Document document;
    private PdfWriter pdfWriter;
    private Paragraph paragraph;
    private Font fTitle=new Font(Font.FontFamily.TIMES_ROMAN, 20,Font.BOLD);
    private Font fSubTitle=new Font(Font.FontFamily.TIMES_ROMAN, 18,Font.BOLD);
    private Font fText=new Font(Font.FontFamily.TIMES_ROMAN, 12,Font.BOLD);
    private Font fHighText=new Font(Font.FontFamily.TIMES_ROMAN, 15,Font.BOLD, BaseColor.RED);
    public TemplatePDF(Context context) {
        this.context=context;
    }
    public void openDocument(){
        createFile();
        try {
            document=new com.itextpdf.text.Document(PageSize.A4);
            pdfWriter=PdfWriter.getInstance(document,new FileOutputStream(pdfFile));
            document.open();

        }catch (Exception e){
            Log.e("openDocument",e.toString());
        }
    }

    private void createFile(){
        File folder=new File(Environment.getExternalStorageDirectory().toString(),"PDF");

        if (!folder.exists())
            folder.mkdirs();
        pdfFile=new File(folder, "Template.pdf");

    }

    public void closeDocument(){
        document.close();
    }
    public void addMetaData(String title, String subject, String author){
        document.addTitle(title);
        document.addSubject(subject);
        document.addAuthor(author);
    }
    public void addTitles(String title, String subTitle, String date){
        try {
        paragraph=new Paragraph();
        addChildP(new Paragraph(title, fTitle));
        addChildP(new Paragraph(subTitle, fSubTitle));
        addChildP(new Paragraph( "Gerado: "+date, fHighText));
        paragraph.setSpacingAfter(30);
        document.add(paragraph);
        }catch (Exception e){
            Log.e("addTitles",e.toString());
        }
    }
    private void addChildP(Paragraph childParagraph){
        childParagraph.setAlignment(Element.ALIGN_CENTER);
        paragraph.add(childParagraph);
    }
    public void addParagraph(String text){
        try {
        paragraph=new Paragraph(text,fText);
        paragraph.setSpacingAfter(5);
        paragraph.setSpacingBefore(5);
        document.add(paragraph);
        }catch (Exception e){
            Log.e("addParagraph",e.toString());
        }
    }
    public void createTable(String[]header, ArrayList<String[]>clients){
        try {
        paragraph=new Paragraph();
        paragraph.setFont(fText);
        PdfPTable pdfPTable=new PdfPTable(header.length);
        pdfPTable.setWidthPercentage(100);
        pdfPTable.setSpacingBefore(20);
        PdfPCell pdfPCell;
        int indexC=0;
        while (indexC<header.length){
            pdfPCell=new PdfPCell(new Phrase(header[indexC++],fSubTitle));
            pdfPCell.setHorizontalAlignment(Element.ALIGN_CENTER);
            pdfPCell.setBackgroundColor(BaseColor.GREEN);
            pdfPTable.addCell(pdfPCell);
        }
        for (int indexR=0;indexR<clients.size();indexR++){
            String[]row=clients.get(indexR);
            for ( indexC=0;indexC<header.length;indexC++){
                pdfPCell=new PdfPCell(new Phrase(row[indexC]));
                pdfPCell.setHorizontalAlignment(Element.ALIGN_CENTER);
                pdfPCell.setFixedHeight(40);
                pdfPTable.addCell(pdfPCell);
            }
        }

        paragraph.add(pdfPTable);
        document.add(paragraph);
        }catch (Exception e){
            Log.e("createTable",e.toString());
        }

    }

    public void viewPDF(){
        Intent intent=new Intent(context,ViewPDFActivity.class);
        intent.putExtra("path",pdfFile.getAbsolutePath());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

    public void appviewPDF(Activity activity){
       if (pdfFile.exists()){
           Uri uri=Uri.fromFile(pdfFile);
           Intent intent=new Intent(Intent.ACTION_VIEW);
           intent.setDataAndType(uri,"application/pdf");
           try {
               activity.startActivity(intent);
           }catch (ActivityNotFoundException e){
               activity.startActivity(new Intent(Intent.ACTION_VIEW,Uri.parse("https://play.google.com/store/apps/details?id=com.adobe.reader&hl=pt_BR ")));
               Toast.makeText(activity.getApplicationContext(),"Você não tem um aplicativo para visualizar PDF",Toast.LENGTH_LONG).show();
           }

       }else {
           Toast.makeText(activity.getApplicationContext(),"O arquivo não foi encontrado",Toast.LENGTH_LONG).show();
       }

    }
}





NO VIEWPDFACTIVITY.JAVA:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.github.barteksc.pdfviewer.PDFView;

import java.io.File;

public class ViewPDFActivity extends AppCompatActivity {

    private PDFView pdfView;
    private File file;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_pdf);
        pdfView=(PDFView)findViewById(R.id.pdfView);

        Bundle bundle=getIntent().getExtras();
        if (bundle!=null){
            file=new File(bundle.getString("path",""));
        }

        pdfView.fromFile(file)
                .enableSwipe(true)
                .swipeHorizontal(false)
                .enableDoubletap(true)
                .enableAntialiasing(true)
                .load();
    }
}





NA ACTIVITY_MAIN.XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android "
    xmlns:app="http://schemas.android.com/apk/res-auto "
    xmlns:tools="http://schemas.android.com/tools "
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <Button
        android:onClick="pdfView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="@string/local" />

    <Button
        android:onClick="pdfApp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Externo" />


</LinearLayout>



NA ACTIVITY_VIEW_PDF.XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android "
    xmlns:app="http://schemas.android.com/apk/res-auto "
    xmlns:tools="http://schemas.android.com/tools "
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ViewPDFActivity">

<com.github.barteksc.pdfviewer.PDFView
    android:id="@+id/pdfView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

</LinearLayout>


NO BUILD:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.itextpdf.br.pdfteste"
        minSdkVersion 16
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support:design:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.itextpdf:itextg:5.5.10'
    implementation 'com.github.barteksc:android-pdf-viewer:3.0.0-beta.5'
    implementation 'com.android.support:appcompat-v7:27.1.1'


    implementation 'me.drakeet.materialdialog:library:1.2.2'
    implementation 'com.squareup.picasso:picasso:2.5.2'
}



NO ANDROIDMANIFEST:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android "
    package="com.itextpdf.br.pdfteste">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    <uses-permission android:name="android.permission.INTERNET"/>

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        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>
        <activity android:name=".ViewPDFActivity" />
    </application>

</manifest>


Quando é clicado nos botões é solicitado as permissões. Após dado as permissões o botão Local gera o pdf e é exibido o documento, mas já o botão Externo, depois de dado as permissões, simplesmente da erro. Como disse antes, quando coloco o targetSdkVersion 22, ele funciona normal sem pedir permissões.

Peço desculpa por colocar tanto código, mas sou iniciante e não sei praticamente nada. Por favor me dê essa ajuda! Obrigado!
Responder
Vinícius Thiengo (0) (0)
31/07/2018
Valdir, tudo bem?

Se possível, informe o erro que está sendo gerado quando você tenta a execução com o targetSdkVersion 23+ definido.

O erro deve apontar o que está faltando.

Caso ainda não saiba obter erros pelos logs do Android Studio, não deixe de acessar o conteúdo do link a seguir: https://developer.android.com/studio/debug/am-logcat?hl=pt-br

Abraço.
Responder
Reinaldo (1) (0)
28/04/2018
Blz Thiengo?

Ótimo tutorial!

Estou criando um app soundboard e estou tendo problemas com permissões, para que o audio seja salvo como ringtone. O aplicativo em si funciona perfeitamente, mas ocorre problemas no momento de salvar o audio.

Estou usando essas 2 no manifest:  

  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <uses-permission android:name="android.permission.WRITE_SETTINGS" />

O problema ocorre com o "...permission..WRITE_SETTINGS", aparece a mensagem "permission is only granted to system apps".

Já fui em lint, desmarquei a caixa de seleção, escolhi uma gravidade menor que o erro, mas nada adiantou. Inclusive usei o comando tools:ignore="ProtectedPermissions".

Tem alguma sugestão de como fazer funcionar essa permissão no Manifest?

Utilizo Android Studio 3.1.2.

Grato
Responder
Vinícius Thiengo (0) (0)
28/04/2018
Reinaldo, tudo bem aqui.

Utilize o passo a passo do link a seguir para conseguir a permissão WRITE_SETTINGS:

https://stackoverflow.com/a/32083622/2578331

O usuário é que terá de dar a permissão em uma atividade fora de seu aplicativo, logo, é inteligente, antes de invocar essa atividade, apresentar um Dialog informando o porquê da necessidade de confirmar a permissão.

Abraço.
Responder
Erik Ramos (1) (0)
28/12/2017
Muito top seus tutoriais, sou iniciante em android e esta me ajudando muito, PARABÉNS !!!!!!!
Responder
24/04/2017
Bom thiengo. Tudo certo?

Restou uma duvida, na sua video aula nao vi a parte do "never ask again".

Na decisao é abordado o negar com aquela dialog que nos fizemos, da segunda negacao em diante.

Mas quando volta a dialog padrao do android vem com essa opcao, mas quando é flegado no " never ask again" e negado nao aparece mais opcoes.


Como ficaria pra abordar essa negacao?
Responder
Vinícius Thiengo (0) (0)
27/04/2017
Tiago, tudo bem?

Seu código somente deve utilizar o Dialog personalizado, o criado por ti, caso a permissão solicitada não tenha haver com o domínio do problema do aplicativo. Por exemplo:

Solicitar a permissão de acesso a câmera do device em um aplicativo de game é algo que deveria ter antes uma Dialog explicando o porquê dessa solicitação e ai sim, caso o usuário escolhe ?Ok?, abrir o Dialog nativo, o que realmente libera a permissão.

Mesmo assim o usuário ainda tem a oportunidade de negar colocando a opção "never ask again? no segundo Dialog.

Para respeitar a decisão dele, utilize a verificação shouldShowRequestPermissionRationale() antes do primeiro dialog.

Assim, se me lembro bem, caso o usuário tem negado a permissão e ainda marcado o check "never ask again?, você saberá que pode apresentar um outro Dialog (caso veja a necessidade) informando que o usuário deve ir nas configurações do aplicativo no app de configurações do device e assim liberar a permissão, caso contrário o aplicativo será utilizado com certas limitações.

Não sei se conseguir informar isso no texto e vídeo acima, mas seguindo as regras de negócio da documentação, é assim que devemos prosseguir com a solicitação de permissões em tempo de execução. Abraço.
Responder
Vinícius Thiengo (0) (0)
27/04/2017
O ?Ok? na verdade é: aspas duplas Ok fecha aspas duplas.
Responder
27/04/2017
Entendi, nao é preciso mostrar uma dialog de aviso no comeco, so depois que ele dar check no "never ask again".

Ou ate algo bacana q vi no album da SONY, ou no instagram, que é quando o user abre o app com tal permissao negada ja abre um aviso dizendo q permissao X deve ser ativada nas cfg do android.
Responder
Vinícius Thiengo (0) (0)
27/04/2017
Isso, seu segundo parágrafo é o que disse no comentário anterior.

O entendimento que descreveu no primeiro parágrafo está errado. Somente apresente o Dialog personalizado se a permissão não condiz com o domínio do problema do aplicativo, caso contrário invoque diretamente o Dialog de permissão nativo do Android.

Com o método shouldShowRequestPermissionRationale() você conseguirá saber se o usuário já negou tal permissão e então, posteriormente, apresentar um outro Dialog dizendo o que o App da Sony e Instagram fazem: que o user deve acessar as configurações do device e liberar as permissões. Abraço.
Responder
13/04/2016
Olá Thiengo

Estou estudando o novo sistema de permissões em tempo de execução e para isso criei um dispositivo virtual com o Genymotion (Google Nexus 7). Mas quando fui instalar o pacote gapps6.0 eu recebo a seguinte mensagem de erro: Failed to flash file open_gapps-arm-6.0-stock-20160413.zip.  Você saberia qual a causa do erro e como resolvê-lo?

Obrigada
Responder
Vinícius Thiengo (0) (0)
16/04/2016
Fala Patricia, blz?
Baixou o gapps na versão correta para sua instalação android? E outra, tente utilizando o AVD (emulador que já vem junto ao AndroidStudio), pois ele na nova versão do AS está bem mais eficiente. Abraço
Responder
30/10/2015
Fala Thiengo, tudo bem?
Cara, não sei se você pode me ajudar, estou tentando implementar em um APP aqui como exercício, uma activity que trás meus últimos posts no YouTube, facebook, Twitter e instagram, você tem alguma dica, por onde iniciar?
Curto muito seu trabalho, está mais do que parabéns!
Abraço
Responder
Vinícius Thiengo (0) (0)
31/10/2015
Fala Andre, blz?
Uma opção é utilizar libraries que lhe permite acesso a "n" social networks, como essa (https://github.com/gorbin/ASNE ). Mas dê uma pesquisada aqui tb (https://android-arsenal.com/search?q=social+network ). Outra opção é utilizar a library nativa de cada network, provavelmente com o YouTube vc terá de fazer isso (https://developers.google.com/youtube/android/player/ ). Abraço
Responder