Vamos a describir cómo configurar los ajustes antes de generar la versión del videojuego Unity que podrá ejecutarse…
Android 6.0: desarrollo App ejemplo con gestión de permisos
3.
Android 6.0: desarrollo App ejemplo con gestión de permisos
Vamos a desarrollar un proyecto Android para ver de forma práctica la asignación de permisos de una App en tiempo de ejecución, una de las novedades más interesantes de Android 6.0 Marshmallow.
En este proyecto, la App Android creada solicitará de manera explícita al usuario, la necesidad de asignar permisos de lectura de los contactos y del estado del teléfono.
Empezamos describiendo la estructura general del proyecto, para explicar a continuación, más en detalle, las clases, métodos y demás elementos que contendrá. Como siempre, podrás descargar el código completo al final de este tutorial.
Estructura y elementos del Proyecto Android
Mostramos en el siguiente diagrama las pantallas de la aplicación, con el flujo natural de uso de esta app (pincha para ampliar):
Comenzamos enumerando los elementos necesarios para el desarrollo del proyecto denominado «Android60″:
- Clase SplashScreen, que herede de la clase base Activity, encargada de lanzar una pantalla de presentación al iniciar la aplicación, proporcionando una mayor inmersión del usuario en la aplicación.
- Clase MainActivity, que herede de la clase base ListActivity, y que además implementa la interfaz ActivityCompat.OnRequestPermissionsResultCallback, cuya tarea será solicitar en tiempo de ejecución los permisos necesarios para la lectura de los contactos y el estado del teléfono.
- Clase AdaptadorContactos, que herede de la clase base BaseAdapter, encargado de construir la vista de cada uno de los contactos que se mostrará en el componente ListView.
- Clase Contacto, que definirá el constructor con los argumentos para la creación de un nuevo objeto Contacto, además de los métodos de acceso getter y setter para las propiedades privadas.
- Layout
activity_splash_screen.xml
, formado por una componente de tipo ImageView, que mostrará el logotipo de la aplicación. - Layout
activity_main.xml
, formado por dos controles de tipoButton
, encargados de solicitar los permisos de lectura de contactos y estado del teléfono, un control de tipo TextView, que mostrará el estado del teléfono, y un control de tipo selección ListView, que mostrará el listado de contactos. - Layout
contactos.xml
, que definirá la vista personalizada de cada ítem que construya el controlListView
.
Estructura del proyecto
Aquí podemos ver la estructura de esta Aplicación:
Documentación código fuente
A continuación revisaremos más en detalle las clases y ficheros de layout de la aplicación
Android_6_0\app\src\main\java\com\academiaandroid\android_6_0\MainActivity.java
Se define la clase MainActivity
, que hereda de la clase base ListActivity
, y que además implementa la interfaz ActivityCompat.OnRequestPermissionsResultCallback
, cuya tarea será solicitar en tiempo de ejecución los permisos necesarios para la lectura de los contactos y del número del dispositivo:
1 2 |
public class MainActivity extends ListActivity implements ActivityCompat.OnRequestPermissionsResultCallback{ |
Continuando con la clase comentada anteriormente, se declaran dos variables de tipo int
, que permitirán definir identificadores únicos asociados a cada uno de los permisos que se solicitarán en tiempo de ejecución:
1 2 3 |
private static final int PERMISO_READ_CONTACTS = 1; private static final int PERMISO_READ_PHONE_STATE = 0; |
Posteriormente, dentro del método onCreate()
, se asocian las variables de tipo TextView
, View
, Button
y ListView
declaradas, con sus controles a nivel de layout. Además se implementa la lógica de eventos onClick
para solicitar los permisos de lectura de contactos y de acceso al estado del teléfono:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); vista = findViewById(R.id.main_layout); tvNumero = (TextView)findViewById(R.id.tvNumero); lvContactos = (ListView)findViewById(android.R.id.list); lvContactos.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { tvNumero.setText(registroContactos().get(position).getTelefono().toString()); } }); btnContactos = (Button)findViewById(R.id.btnContactos); |
Al pulsar el botón, se comprobarán los permisos disponibles para la tarea a realizar, solicitándolos en caso de que no estén asignados:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
btnContactos.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int permisoLecturaContactos = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_CONTACTS); if (permisoLecturaContactos != PackageManager.PERMISSION_GRANTED) { peticionPermisos(Manifest.permission.READ_CONTACTS,new String[]{Manifest.permission.READ_CONTACTS},PERMISO_READ_CONTACTS, "a los contactos"); } else { lvContactos.setAdapter(new AdaptadorContactos(getApplicationContext(), registroContactos())); } } }); btnNumero = (Button)findViewById(R.id.btnNumero); btnNumero.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int permisoEstadoTelefono = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_PHONE_STATE); if (permisoEstadoTelefono != PackageManager.PERMISSION_GRANTED) { peticionPermisos(Manifest.permission.READ_PHONE_STATE,new String[]{Manifest.permission.READ_PHONE_STATE},PERMISO_READ_PHONE_STATE, "al número de teléfono"); } else { Snackbar.make(vista, "Ya dispone de los permisos de estado del teléfono.", Snackbar.LENGTH_LONG).show(); } } }); } |
Otro de los métodos a comentar dentro de esta clase, sería el método peticionPermisos
, que se encargará de solicitar los permisos necesarios al usuario mediante el sistema de notificaciones Snackbar
(Material Design):
1 2 3 4 5 |
private void peticionPermisos(String permiso, final String[] manifest, final int id, String tipo) { if (ActivityCompat.shouldShowRequestPermissionRationale(this,permiso)) { |
Como curiosidad, para la descripción de los permisos solicitados, se utiliza el tipo de notificación Snackbar
, y que invocando al método make
, permitirá mostrar un mensaje emergente desde la parte inferior de la pantalla, recibiendo como parámetros de entrada la View
donde se mostrará la notificación, el texto a mostrar, y la duración del mensaje (es posible utilizar la constante Snackbar.LENGTH_INDEFINITE
, que mantendrá el mensaje en pantalla hasta que el usuario pulse el botón definido). Para este último caso, será necesario invocar al método setAction
, donde se muestra el texto a pulsar por el usuario, y el evento onClick
que contiene la acción a realizar (en este caso la solicitud de permisos en tiempo de ejecución):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Snackbar.make(vista, "Es necesario el acceso " + tipo + " para su gestión desde la app.", Snackbar.LENGTH_INDEFINITE) .setAction("ACEPTAR", new View.OnClickListener() { @Override public void onClick(View view) { ActivityCompat.requestPermissions(MainActivity.this, manifest , id); } }).show(); } else { |
Al no aceptarse los permisos, se solicitarán de manera directa:
1 2 3 4 |
ActivityCompat.requestPermissions(MainActivity.this, manifest , id); } } |
Continuando con la clase MainActivity
, nos encontramos con el método onRequestPermissionsResult
, que será llamado cuando el usuario acepte el permiso requerido. Dicho método será implementado por la interfaz ActivityCompat.OnRequestPermissionsResultCallback
:
1 2 3 4 5 6 |
@Override public void onRequestPermissionsResult(int requestCode,String permissions[], int[] grantResults) { switch (requestCode) { |
Identificador para realizar la tarea de lectura de contactos:
1 2 3 4 5 |
case PERMISO_READ_CONTACTS: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
Permiso aceptado, se podría acceder a los contactos del dispositivo. En este caso, se listan utilizando el control de tipo selección ListView
:
1 2 3 4 5 |
lvContactos.setAdapter(new AdaptadorContactos(getApplicationContext(), registroContactos())); Snackbar.make(vista, "Permiso de lectura de contactos establecido", Snackbar.LENGTH_LONG).show(); } else { |
Permiso denegado. Desactivar la funcionalidad que dependía de dicho permiso:
1 2 3 4 |
Snackbar.make(vista, "Permiso de lectura de contactos denegado",Snackbar.LENGTH_LONG).show(); } return; |
Identificador para realizar la tarea de recibir el estado del teléfono:
1 2 3 4 5 |
case PERMISO_READ_PHONE_STATE: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
Permiso aceptado, se mostrará el número del teléfono:
1 2 3 4 5 |
numeroTelefono(); Snackbar.make(vista, "Permiso de estado de teléfono establecido", Snackbar.LENGTH_LONG).show(); } else { |
Permiso denegado. Desactivar la funcionalidad que dependía de dicho permiso:
1 2 3 4 5 6 |
Snackbar.make(vista, "Permiso de estado de teléfono denegado",Snackbar.LENGTH_LONG).show(); } return; } } |
Por último dentro de esta clase, se define el método encargado de devolver la lista de objetos Contacto
, almacenados en el dispositivo:
1 2 3 4 5 6 7 8 9 |
private List<Contacto> registroContactos() { List<Contacto> arrayContactos = new ArrayList<>(); try { Contacto contactos = null; Uri identificadorContactos = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; |
Se declara una instancia de ContentResolver
, a la que se le asigna el método getContentResolver()
, que proporciona el acceso a los datos del Content Provider
:
1 2 3 4 5 6 7 |
ContentResolver cr = getContentResolver(); cursor = cr.query(identificadorContactos, columnas, null, null, columnas[1] + " DESC" ); int columNombre, columNumero, columTipo = 0; if(cursor != null) { |
Se recorrerán los resultados almacenados de la consulta en el objeto Cursor
, comprobando en cada iteración que existe el registro siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if(cursor.moveToFirst()) { columTipo = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE); columNombre = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME); columNumero = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); int tipoContacto; String numeroContacto, nombreContacto, contactoTipo = null; do{ tipoContacto = cursor.getInt(columTipo); numeroContacto = cursor.getString(columNumero); nombreContacto = cursor.getString(columNombre); |
Se controla el tipo de contacto para asignarle los valores de ‘Teléfono Casa’ o ‘Teléfono móvil’:
1 2 3 4 5 6 7 8 9 |
if(tipoContacto == ContactsContract.CommonDataKinds.Phone.TYPE_HOME) { contactoTipo = "Teléfono Casa"; }else if(tipoContacto == ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE) { contactoTipo = "Teléfono móvil"; } |
Se crea un objeto de la clase Contacto
por cada iteración, con los valores de tipo, datos y número de contacto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
contactos = new Contacto(nombreContacto,numeroContacto, contactoTipo); arrayContactos.add(contactos); }while(cursor.moveToNext()); } } }catch(Exception ex) { ex.getMessage(); } finally { if(cursor != null) { cursor.close(); } } return arrayContactos; } |
Android_6_0\app\src\main\java\com\academiaandroid\android_6_0\AdaptadorContactos.java
Como es habitual cuando utilizamos un control ListView
personalizado, se define una nueva clase llamada AdaptadorContactos, que hereda de la clase base BaseAdapter, y que construirá la vista personalizada de cada uno de los contactos disponibles en el dispositivo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class AdaptadorContactos extends BaseAdapter { private Context context; private List listas; private LayoutInflater inflater; public AdaptadorContactos(Context context, List<Contacto> listas) { this.context = context; this.listas = listas; inflater = LayoutInflater.from(context); } [...] @Override public View getView(int position, View convertView, ViewGroup parent) { View filas = null; if(convertView == null) { filas = inflater.inflate(R.layout.contactos, parent, false); }else { filas = convertView; } |
Se asocian las variables de tipo TextView
con sus controles a nivel de layout:
1 2 3 4 |
ViewHolder.tvItemNombre = (TextView)filas.findViewById(R.id.tvItemNombre); ViewHolder.tvItemTelefono = (TextView)filas.findViewById(R.id.tvItemTelefono); ViewHolder.tvItemTipo = (TextView)filas.findViewById(R.id.tvItemTipo); |
Se asigna a cada control los datos de nombre, teléfono y tipo de contacto:
1 2 3 4 5 6 7 |
ViewHolder.tvItemNombre.setText("Nombre: " + listas.get(position).getNombre()); ViewHolder.tvItemTelefono.setText("Teléfono: " + listas.get(position).getTelefono()); ViewHolder.tvItemTipo.setText("Tipo: " + listas.get(position).getTipo()); return filas; } |
Clase estática donde se declaran las variables de tipo TextView
:
1 2 3 4 5 6 |
static class ViewHolder { public static TextView tvItemNombre, tvItemTelefono, tvItemTipo; } } |
AndroidStudioProjects\Android_6_0\app\src\main\res\layout\activity_main.xml
A nivel de layout, tendríamos activity_main.xml
, que define el layout de la Activity MainActivity.java
, y en el que se implementan dos controles de tipo Button
encargados de solicitar los permisos de estado y contactos del teléfono, un control de tipo selección ListView
, que mostrará el listado de contactos disponibles, y un control de tipo TextView
, que mostrará el número de teléfono del dispositivo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
<LinearLayout ... <TableLayout ... <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" android:text="@string/cabecera" android:id="@+id/tvCabecera" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <Button style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/listarcontactos" android:id="@+id/btnContactos" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <Button style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/numerotelefono" android:id="@+id/btnNumero" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:id="@+id/tvNumero" android:editable="false" android:hint="@string/numero" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@android:id/list" android:layout_marginLeft="28dp" android:layout_marginStart="28dp" /> </TableRow> </TableLayout> </LinearLayout> |
AndroidStudioProjects\Android_6_0\app\src\main\res\layout\contactos.xml
Layout que define la vista personalizada de cada uno de los contactos que se mostrarán en el control ListView
, y que estará formado por tres controles de tipo TextView
y un control ImageView
con el logo de la aplicación:<?xml version=»1.0″ encoding=»utf-8″?>
<TableLayout …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<TableRow ... <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/sin_datos" android:id="@+id/tvItemNombre" android:textColor="#0015ff" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imvlogoAcademia" android:src="@drawable/foto_user" android:contentDescription="@string/logo" /> </TableRow> <TableRow ... <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceSmall" android:text="@string/sin_datos" android:id="@+id/tvItemTelefono" android:textColor="#0015ff" /> </TableRow> <TableRow ... <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceSmall" android:text="@string/sin_datos" android:id="@+id/tvItemTipo" android:textColor="#0015ff" /> </TableRow> |
Permisos declarados en AndroidManifest.xml
Por último recordar que será necesario declarar dentro del AndroidManifest.xml
los diferentes permisos necesarios para la lectura de contactos y estado del teléfono. Como se puede apreciar, esta sería la sintaxis del nuevo modelo de permisos de Android 6 Marshmallow:
1 2 3 4 |
<!-- Los siguientes permisos serán requeridos si se trata de la versión Android Marshmallow o superior.--> <uses-permission-sdk-m android:name="android.permission.READ_CONTACTS" /> <uses-permission-sdk-m android:name="android.permission.READ_PHONE_STATE" /> |
Dependencias gradle implementadas
Será necesario añadir las dependencias para hacer uso de la notificación Snackbar que proporciona Material Design:
1 2 3 4 5 |
dependencies { compile 'com.android.support:design:22.2.1' ... } |
Descarga del proyecto
A continuación puedes descargar todo el código de la Aplicación:
DownloadPara finalizar esta serie, en la próxima publicación describiremos este proyecto en un video, viendo además el funcionamiento de la aplicación.