Préstamos de libros: Contactar a quien nos debe un libro

Hasta el momento la aplicación nos muestra la lista de libros que hemos prestado. Vamos a incorporarle la posibilidad de contactar a la persona que debe ese libro de diferentes maneras.

Primer approach: llamar al contacto

Definimos el menú, que en principio tendrá una sola opción: Contactar, como un texto. Lo llamamos prestamo_menu.xml y lo ubicamos en res/menu:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/action_call_contact"
        android:icon="@drawable/ic_action_call"
        android:orderInCategory="100"
        android:showAsAction="always"
        android:title="@string/action_call_contact"/>
    
    <item
        android:id="@+id/action_email_contact"
        android:icon="@drawable/ic_action_email"
        android:orderInCategory="200"
        android:showAsAction="always"
        android:title="@string/action_email_contact"/>

    <item
        android:id="@+id/action_return"
        android:icon="@drawable/ic_undo"
        android:orderInCategory="200"
        android:showAsAction="always"
        android:title="@string/action_return"/>

</menu>

El title hay que definirlo en el xml strings (res/values):

<string name="action_call_contact">Contactar</string>

En el MainActivity vamos a definir un listener que escuche el evento de un click largo sobre un elemento de la lista:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestContactPermission();
        super.onCreate(savedInstanceState);
        PrestamosAppBootstrap.initialize(this);
        setContentView(R.layout.activity_main);
        ListView lvPrestamos = (ListView) findViewById(R.id.lvPrestamos);

        lvPrestamos.setLongClickable(true);
        lvPrestamos.setOnItemLongClickListener((AdapterView<?> parent, View view, int position, long id) -> {
            if (mActionMode != null) {
                return false;
            }
            mActionMode = this.startActionMode(this);
            mActionMode.setTag(position);
            view.setSelected(true);
            return true;
        });
        registerForContextMenu(lvPrestamos);
    }


Cosas para comentar
  • para poder acceder a la lista de contactos, llamarlos y enviarles un mail necesitamos programáticamente pedirle permisos al usuario
  • configuramos la lista para que sus elementos se puedan seleccionar mediante el gesture long click
  • y para poder almacenar la información del elemento seleccionado, utilizamos la variable de instancia mActionMode
En el Listener recibimos tanto un int position como un identificador (id) que es long. Pero son la misma cosa, porque PrestamoAdapter tiene definido que el id de cada elemento es su posición:

    @Override
    public long getItemId(int position) {
        return position;
    }


Si queremos devolver el id del préstamo en lugar de la posición ordinal dentro de la lista tendríamos que modificar este método. Vamos a dejarlo tal como está.

Interfaz ActionMode.Callback

Y además el MainActivity va a implementar ActionMode.Callback, definiendo algunos métodos adicionales que la interfaz requiere. Por ejemplo, el menú adecuado para el long click:

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        mode.getMenuInflater().inflate(R.menu.prestamo_menu, menu);
        return true;
    }

Y también lo que va a ocurrir cuando el usuario presione click sobre el menú.

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        int posicion = Integer.parseInt(mActionMode.getTag().toString());
        List<Prestamo> prestamosPendientes = repoPrestamos.getPrestamosPendientes();
        if (prestamosPendientes.isEmpty()) {
            Toast.makeText(this, "No hay préstamos para trabajar", Toast.LENGTH_SHORT).show();
            return false;
        }
        Prestamo prestamo = prestamosPendientes.get(posicion);
        switch (item.getItemId()) {
            case R.id.action_call_contact:
                llamar(prestamo.getTelefono());
                break;
            case R.id.action_email_contact:
                enviarMail(prestamo);
                break;
            case R.id.action_return:
                devolver(prestamo);
                break;
            default:
        }
        return false;
    }

Vemos cómo se construye el Intent para hacer el llamado a nuestro contacto:


    private boolean llamar(String telefono) {
        Intent callIntent = new Intent(Intent.ACTION_CALL);
        callIntent.setData(Uri.parse("tel:" + telefono));
        try {
            Log.w("Voy a llamar", "Intent iniciado");
            startActivity(callIntent);
        } catch (Exception e) {
            Log.e("ERROR al llamar ", e.getMessage());
            return false;
        }
        return true;
    }

Los otros comandos los pueden ver en el ejemplo subido.

Configuración de permisos de la app

Para poder gestionar los permisos, hasta la versión 23 se utilizaba la configuración del AndroidManifest.xml. A partir de esa versión, tenemos que hacerlo programáticamente. El método onCreate() llama a este otro método:

    private void requestContactPermission() {
        // Check the SDK version and whether the permission is already granted or not.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 
            && checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, PERMISSIONS_REQUEST_READ_CONTACTS);
            //After this point you wait for callback in onRequestPermissionsResult(int, String[], int[]) overriden method
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 
            && checkSelfPermission(Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, PERMISSIONS_REQUEST_WRITE_CONTACTS);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 
            && checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.CALL_PHONE}, PERMISSIONS_REQUEST_CALL_PHONE);
        }
    }

Necesitamos definir algunas constantes propias, y un método

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == PERMISSIONS_REQUEST_WRITE_CONTACTS) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                this.llenarPrestamosPendientes();
            }
        }
    }

Elegimos un contacto, presionamos un tiempo y nos aparece el menú de opciones:


Si elegimos el ícono de llamar se dispara el llamado a ese contacto:

Enviar un mail

Nuestro menú contextual técnicamente es un Contextual Action Bar, por si el lector quiere investigar, tiene íconos que se descargan de http://developer.android.com/design/downloads/index.html (“Download All”). Para subirlos al proyecto
  1. Buscamos en los íconos el que tiene que ver con llamadas y envío de mails, y que sea para action bars: Icons\Action Bar Icons\holo_dark (o holo_light, depende del fondo)\01_core_call
  2. Vemos que hay cuatro carpetas, cada una para los diferentes tamaños de dispositivos. Copiamos las cuatro carpetas al directorio res de nuestro proyecto.
  3. Lo mismo hacemos con el envío de mails, en nuestro caso hemos elegido Icons\Action Bar Icons\holo_dark\05_content_email.
Eso es lo que va como android:icon de cada ítem de menú.


Y en strings.xml hay que incorporar el string que le corresponde al title, por las dudas (res/values/strings.xml):


<string name="action_email_contact">Enviar mail</string>


Implementamos el método enviarMail:

 private boolean enviarMail(Prestamo prestamo) {
     String uriText = null;
     try {
         uriText = "mailto:" + prestamo.getContactoMail() + "?subject=" +
                 URLEncoder.encode("Libro " + prestamo.getLibro().getTitulo(), StandardCharsets.UTF_8.name()) + 
                 "&body=" +
                 URLEncoder.encode("Por favor te pido que me devuelvas el libro", StandardCharsets.UTF_8.name());
     } catch (UnsupportedEncodingException e) {
         Log.e("Librex", e.getMessage());
         throw new RuntimeException("Hubo un error al generar el mail", e);
     }
     Uri uri = Uri.parse(uriText);
     Intent sendIntent = new Intent(Intent.ACTION_SENDTO);
     sendIntent.setData(uri);
     // Necesitas configurar en el emulador el mail
     startActivity(Intent.createChooser(sendIntent, "Enviar mail"));
     return true;
 }

Hay que tener en cuenta que el envío de mails requiere haber configurado previamente una cuenta (en el emulador o bien en nuestro dispositivo). Vemos cómo queda la aplicación



Para más información

Diagrama general de la solución