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

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

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
(23196) (14)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição1ª
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

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:

Permissões sendo apresentadas 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.

Dialog de solicitação de permissão em tempo de execução

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Õ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, 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: 

Exception ao tentar acessar funcionalidades sem permissões liberadas

No LogCat temos na pilha de exceção o seguinte:

Pilha de erros da Exception gerada

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:

Solicitação de permissão para acesso as APIs de localização do Android

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:

Solicitação de permissão com a opção Never ask again

Antes o dialog, do nosso MaterialDialog, é apresentada ao usuário:

Caixa de diálogo personalizada explicando o porquê das permissões

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:

Ícone AVD Manager

  • 2º - Clique em "Create Virtual Device..." como na imagem a seguir:

Botão para criação de um novo emulador

  • 3º - Escolha o device que você quer montar e então clique em "Next":

Selecionando o modelo do novo emulador

  • 4º - Escolha a versão / API do device. Fique com a Versão 6.0 (ou superior) e com Google APIs já instalada:

Selecionando o sistema Android do novo emulador

  • 5º - Altere as configurações que achar necessário (eu mantive as padrões) e então clique em "Finish":

Definindo as configurações finais do novo emulador

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.

Informativo de que o app está utilizando um modelo antigo de permissões

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"

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

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
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

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