Guía básica sobre la programación en Android

El objetivo de este documento es proporcionar información básica sobre la programación en Android, de manera que permita afrontar el trabajo voluntario planteado en la asignatura con respecto a esta plataforma. Dicho trabajo recoge de manera aplicada algunos de los aspectos estudiados en el tema correspondiente de la asignatura tales como: Por su gran difusión y su carácter abierto, se ha optado por usar Android como plataforma de desarrollo. El trabajo propuesto está ideado para que pueda ser afrontando por una persona que previamente no haya tenido ningún contacto con esta plataforma en lo que se refiere a desarrollo de aplicaciones, pero también de manera que pueda aportar nuevos conocimientos a aquéllos que incluso hayan hecho desarrollos a nivel profesional.

Este documento no pretende ser una guía para la programación de aplicaciones en Android pero sí está concebido para que sea autocontenido en el sentido de que sea suficiente para abordar la funcionalidad pedida. El documento muestra, y explica, varias aplicaciones ya desarrolladas que contienen toda la funcionalidad requerida por el trabajo práctico propuesto cuyo enunciado aparece al final del documento. En este enlace se encuentra un paquete que contiene todos los proyectos presentados en esta guía. Al incluirse los proyectos Android completos, el fichero tiene un tamaño considerable. Alternativamente, en este otro enlace solo se incluyen los ficheros relevantes de cada ejemplo.

Algunas de las aplicaciones que se presentan a lo largo de este documento están pensadas para ejecutar en un sistema real, ya que usan sensores, GPS o suponen que el dispositivo usa batería. Sin embargo, podemos ejecutarlas en un emulador puesto que éste nos ofrece la posibilidad de, valga la redundancia, emularlas.

del entorno de desarrollo de Android (Android Studio) y la creación y ejecución en un dispositivo virtual del proyecto que se crea automáticamente al usar este entorno (se recomienda seguir los pasos explicados en estos enlaces: http://developer.android.com/intl/es/training/basics/firstapp/creating-project.html y http://developer.android.com/intl/es/training/basics/firstapp/running-app.html)

Primera aplicación: programa básico amnésico

La funcionalidad de esta aplicación no tiene ningún interés por sí misma: se trata de un programa que recibe como entrada valores numéricos y va mostrando como salida la media actual de los valores introducidos.

A continuación, se muestra la interfaz de usuario de esta aplicación:


Dado que se trata de la primera aplicación presentada en este documento y puesto que se pretende que este sea autocontenido, a continuación se explican algunos conceptos básicos de Android: Una vez introducidos esos conceptos básicos, vamos a mostrar y explicar los detalles de esta aplicación.

A continuación, se muestra el código XML correspondiente al diseño de interfaz gráfica mostrado en la primera figura (fichero layout/activity_main.xml):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    >
    <TextView android:text="@string/titulo"
        android:layout_marginBottom="100dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center"       >
        <EditText android:id="@+id/dato"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="numberDecimal"
            android:hint="@string/dato" />
        <Button
            android:layout_marginLeft="40dp"
            android:text="@string/media"
            android:onClick="media"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="20dp"
        android:gravity="center">
    <TextView android:text="@string/resultado"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView android:id="@+id/resultado"
        android:layout_marginLeft="40dp"
        android:freezesText="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
En dicho código destacaremos los siguientes aspectos: Acto seguido se muestra el contenido del fichero de recursos que define las cadenas de caracteres que usará la aplicación (fichero res/values/strings.xml):
<resources>
<string name="app_name">Ejemplo1</string>

<string name="titulo">Bienvenido a otra aplicación calculadora de medias</string>
<string name="dato">Introduzca número</string>
<string name="resultado">Resultado</string>
<string name="media">Media</string>

</resources>
Por último, se va a mostrar y analizar el código de la actividad (fichero MainActivity.java). Téngase en cuenta que gran parte del código mostrado a continuación ha sido generado automáticamente por el asistente. Dado que se trata del primer código que se presenta en esta guía se ha incluido completo.
package com.example.fernando.ejemplo1;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends Activity {
    private EditText entrada;
    private TextView salida;
    private float acumulado = 0;
    private int n = 0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        entrada = (EditText) findViewById(R.id.dato);
        salida = (TextView) findViewById(R.id.resultado);
    }

    public void media(View view) {
        String leido = entrada.getText().toString();
        if (!leido.equals("")) {
            acumulado += Float.parseFloat(leido);
            n++;
            entrada.setText("");
            salida.setText(Float.toString(acumulado/n));
        }
    }
}
A continuación, se resaltan algunos aspectos de ese código: ¿Por qué hemos calificado a esta aplicación básica como amnésica? Pruebe a cambiar la orientación de la pantalla o el locale asociado a la aplicación (por ejemplo, de español a inglés) y compruebe como, a pesar de qué lo valores presentados en pantalla son correctos, cuando se le solicita hacer la media, el resultado no lo es. En la siguiente sección abordamos el problema.

Segunda aplicación: adaptación de aplicaciones

En esta sección, además de responder, y resolver, la pregunta pendiente, vamos a afrontar dos de los aspectos que recoge este trabajo práctico: la adaptación de la aplicación a las preferencias del usuario y a las características del dispositivo.

Empecemos por afrontar la pregunta pendiente: ¿Qué ocurre cuando se produce una rotación o se cambia de idioma? Dado que ambos casos la interfaz puede cambiar, Android destruye la actividad (secuencia onPause, onStop y onDestroy) y la vuelve a crear (secuencia onCreate, onStart y onResume), de manera que la nueva instancia de la actividad pueda crear una nueva interfaz de usuario acorde con la nueva configuración.

En ese proceso de recreación, que puede ocurrir también en otras circunstancias (así, por ejemplo, cuando el sistema tiene poca memoria disponible, el sistema puede destruir actividades, principalmente, si éstan están pausadas o paradas), el sistema se encarga de salvar cierto estado (por ejemplo, el estado actual de los elementos de diálogo, lo que permite que el contenido de los mismos sea el mismo después de la recreación), pero, sin embargo, el estado propio de la actividad (en este caso, la suma acumulada de valores y el número de elementos leídos) se pierde.

Una manera de solucionar este problema es hacer que la actividad salve su estado, al igual que se hace automáticamente para los elementos de diálogo, siempre que pase a un segundo plano (el sistema invoca el método onSaveInstanceState antes de llamar a onStop) y que lo restaure al ser recreada.

En el siguiente fragmento del fichero de la actividad para esta nueva aplicación se puede observar qué modificaciones hay que llevar a cabo para realizar esta labor de salvado (dentro del método onSaveInstanceState) y restauración (al recrearse la actividad) de las variables cuyo estado se quiere mantener (fichero MainActivity.java).

.....................................................
public class MainActivity extends Activity {
    static final private String ACUMULADO="acumulado";
    static final private String NELEMS="nelems";
    private EditText entrada;
    private TextView salida;
    private float acumulado = 0;
    private int n = 0;
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState!=null) {
            acumulado = savedInstanceState.getFloat(ACUMULADO);
            n = savedInstanceState.getInt(NELEMS);
        }
        setContentView(R.layout.activity_main);
        entrada = (EditText) findViewById(R.id.dato);
        salida = (TextView) findViewById(R.id.resultado);
    }
    ...........................
    protected void onSaveInstanceState(Bundle estado){
        super.onSaveInstanceState(estado);
        estado.putFloat(ACUMULADO, acumulado);
        estado.putInt(NELEMS, n);
    }
    ......................................
}

Adaptación a las características del dispositivo

Arreglada esta cuestión, vamos a adaptar nuestra aplicación para que use una interfaz alternativa cuando la pantalla tiene una orientación de tipo horizontal (landscape). A continuación, se muestra la apariencia de esta nueva interfaz de usuario para esta aplicación:
El siguiente código XML correspondiente a ese nuevo diseño (fichero layout-land/activity_main.xml):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
   android:gravity="center"
   >
   <TextView android:text="@string/titulo"
      android:layout_marginBottom="100dp"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content" />
      <LinearLayout
	 android:layout_width="wrap_content"
	 android:layout_height="wrap_content"
	 android:orientation="horizontal"
	 >
	 <EditText android:id="@+id/dato"
	 android:layout_width="wrap_content"
	 android:layout_height="wrap_content"
	 android:inputType="numberDecimal"
	 android:hint="@string/dato" />
	 <Button
	 android:layout_marginLeft="10dp"
	 android:text="@string/media"
	 android:onClick="media"
	 android:layout_width="wrap_content"
	 android:layout_height="wrap_content" />
	 <TextView android:text="@string/resultado"
	 android:layout_marginLeft="40dp"
	 android:layout_width="wrap_content"
	 android:layout_height="wrap_content" />
	 <TextView  android:id="@+id/resultado"
	 android:layout_marginLeft="20dp"
	 android:freezesText="true"
	 android:layout_width="wrap_content"
	 android:layout_height="wrap_content" />
   </LinearLayout>
</LinearLayout>
 
Para conseguir que la aplicación use esa nueva interfaz, no hay que cambiar ninguna línea de código. Basta con crear un directorio denominado res/layout-land (obviamente, el sufijo land está predefinido y corresponde a la orientación landscape), si es que no existía previamente, e incluir en ese directorio el nuevo fichero activity_main.xml (téngase en cuenta que tiene que tener el mismo nombre). El asistente del entorno de desarrollo nos facilita esta labor de creación de este nuevo directorio. Nótese que estamos tomando por defecto la orientación portrait. Si en su lugar hubiésemos creado sólo un directorio res/layout-port, la orientación por defecto sería landscape.

Adaptación a las preferencias del usuario

En esta sección queremos cambiar el idioma de las cadenas de caracteres que usa la aplicación. Como ocurría con la orientación del dispositivo, no es necesario cambiar el código sino basta con definir esas nuevas cadenas de caracteres en un directorio que corresponda a esa nueva configuración. En este caso, el directorio se debe denominar res/values-en (sufijo predefinido correspondiente al idioma inglés) y define incluir las definiciones de las cadenas de caracteres en ese nuevo idioma (fichero res/values-en/strings.xml):
<resources>
    <string name="app_name">Example2</string>
    <string name="titulo">Welcome to another average calculator application</string>
    <string name="dato">Input number</string>
    <string name="resultado">Result</string>
    <string name="media">Average</string>
</resources>
Nótese que hemos tomado la versión española como el valor por defecto para la aplicación.

Rizando el rizo, nos planteamos a continuación que el texto que aparece como primer elemento en la pantalla, al que hemos denominado como título, sea más extenso cuando el dispositivo en modo landscape, tanto en su versión en español como en inglés. ¿Cómo lo haríamos?

Para la versión española, creamos un directorio res/values-land con el fichero res/values-land/strings.xml):

<resources>
    <string name="titulo">Bienvenido a otra aplicación calculadora de medias...............................................</string>

</resources>

En cuanto a la versión inglesa, creamos un directorio res/values-en-land (nótese que el orden de los prefijos está predefinido) con el fichero res/values-en-land/strings.xml):

<resources>
    <string name="titulo">Welcome to another average calculator application....................................................</string>
</resources>

Tercera aplicación: adaptación al nivel de batería

En este tercer ejemplo, se va a mostrar cómo una aplicación puede pedir al sistema ser notificada cuando el nivel de batería alcanza una situación crítica para, de esta forma, poder adaptarse a esa circunstancia. Realmente, en el ejemplo, no plantearemos ninguna adaptación: simplemente se mostrará un mensaje informando de esa situación. Sin embargo, la funcionalidad de esta aplicación servirá como base para resolver la práctica propuesta.

Esta aplicación usa la actividad que crea por defecto el asistente, tanto su código como sus recursos (layout y strings).

Para afrontar esta funcionalidad es necesario presentar un segundo componente, después de las actividades, de la arquitectura Android: broadcast receivers. Este componente permite registrarse para recibir eventos enviados como mensajes (en terminología Android, Intents) por otras aplicaciones o por el sistema, que es lo que nos interesa en esta circunstancia; concretamente, especificaremos que queremos ser notificados del momento que la batería tiene un nivel de carga crítico y cuando éste se recupera.

El asistente del entorno de desarrollo nos facilitará la labor de creación del broadcast receiver generando una plantilla con un código inicial que hay que completar e incorporando parte de la metainformación requerida por el mismo en el fichero AndroidManifest.xml. Hasta este momento no habíamos mostrado el contenido de este fichero ya que no había sido necesario modificarlo. En este punto sí que es preciso hacerlo y, por tanto, lo mostramos a continuación resaltando qué cambios hay que incorporar manualmente (fichero AndroidManifest.xml):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.fperez.seu_app3" >
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver
            android:name=".MyReceiver"
            android:enabled="true"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.BATTERY_LOW" />;
                <action android:name="android.intent.action.BATTERY_OKAY" />
            </intent-filter>
        </receiver>
    </application>

</manifest>
Nótese que las líneas que se han tenido que insertar manualmente corresponden a la especificación de en qué dos tipos de eventos se está interesado.

Además de este cambio, la única labor que hay que llevar a cabo es completar el método receive del broadcast receiver especificando qué hacer cada vez que se recibe una notificación de vinculada con el estado de la batería (fichero MyReceiver.java):

package com.example.fperez.ejemplo3;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class MyReceiver extends BroadcastReceiver {
    public MyReceiver() {
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String mensaje;
        if (intent.getAction().equals("android.intent.action.BATTERY_LOW"))
            mensaje=context.getResources().getString(R.string.NOK);
        else
            mensaje=context.getResources().getString(R.string.OK);

        Toast.makeText(context, mensaje, Toast.LENGTH_LONG).show();
    }
}
De ese código sería conveniente resaltar los siguientes aspectos: Para probar esta aplicación, es necesario que el emulador haga creer a la aplicación que está ejecutando en un dispositivo que usa batería. En las versiones más recientes del mismo, esto se puede hacer a través de la interfaz gráfica.

Cuarta aplicación: adaptación al nivel de batería; versión dinámica

En la aplicación presentada en la sección anterior, el componente receiver se declaraba en el fichero AndroidManifest.xml con lo que la aplicación quedaba estática y permanentemente vinculada con la recepción de notificaciones vinculadas con el nivel de batería. En muchas ocasiones, sin embargo, se requiere que esta vinculación sea dinámica pudiendo activarse y desactivarse según las necesidades de la aplicación, como, por ejemplo, que esté en primer plano o no. Este ejemplo modifica el previo en ese sentido de manera que el interés en recibir notificaciones del estado de la batería se restringe a cuando la aplicación está visible, al menos, parcialmente. Para ello, deben de realizarse las siguientes acciones: Nótese que el código de la clase receiver no requiere ningún cambio.

A continuación, se va a mostrar y analizar el código de la actividad resultante (fichero MainActivity.java).

package com.example.fernando.ejemplo4;
import android.content.IntentFilter;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    private MyReceiver receptor = new MyReceiver();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onStart() {
        super.onStart();
        IntentFilter filter = new IntentFilter("android.intent.action.BATTERY_LOW");
        filter.addAction("android.intent.action.BATTERY_OKAY");
        registerReceiver(receptor, filter);
    }
    @Override
    public void onStop() {
        super.onStop();
        unregisterReceiver(receptor);
    }
}
De ese código sería conveniente resaltar los siguientes aspectos:

Quinta aplicación: gestión de sensores

Esta aplicación realiza la lectura de un sensor de temperatura. Como en el caso previo, va a usarse directamente la interfaz de usuario creada automáticamente por el asistente incorporando únicamente en la actividad el código requerido para la gestión del sensor.

A continuación, se muestra el fichero que implementa la actividad (fichero MainActivity.java).

package com.example.fernando.ejemplo5;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements SensorEventListener {
    private SensorManager sensorMgr;
    private Sensor sensor;
    private float temp = 0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sensorMgr = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        if ((sensor = sensorMgr.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)) == null)
            Toast.makeText(this, getResources().getString(R.string.error), Toast.LENGTH_LONG).show();
    }
    @Override
    public final void onAccuracyChanged(Sensor sensor, int accuracy) {
    }
    @Override
    public final void onSensorChanged(SensorEvent event) {
        if (temp != event.values[0]) {
            Toast.makeText(this, getResources().getString(R.string.temp) + " / " + event.values[0], Toast.LENGTH_SHORT).show();
            temp = event.values[0];
        }
    }
    @Override
    protected void onResume() {
        super.onResume();
        if (sensor!=null)
            sensorMgr.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
    }
    @Override
    protected void onPause() {
        super.onPause();
        if (sensor!=null)
            sensorMgr.unregisterListener(this);
    }
}
A continuación, se resaltan algunos aspectos de ese código:

Para probar esta aplicación, en las versiones más recientes, el emulador ofrece una interfaz gráfica para llevarlo a cabo.

Sexta aplicación: aspectos de localización

Esta aplicación recoge lecturas de un dispositivo GPS para obtener información de localización. Nótese que en una aplicación de carácter profesional sería más adecuado usar el servicio de localización de más alto nivel que ofrece Google. Como en el caso previo, va a usarse directamente la interfaz de usuario creada automáticamente por el asistente incorporando únicamente en la actividad el código requerido para la gestión de las lecturas GPS.

A continuación, se muestra el fichero que implementa la actividad (fichero MainActivity.java).

package com.example.fernando.ejemplo6;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity implements LocationListener {
    private LocationManager locationManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
    }
    public void onLocationChanged(Location location) {
        String mensaje = getResources().getString(R.string.longi) + "= " + Double.toString(location.getLongitude()) +
                "; " + getResources().getString(R.string.lati) + "= " + Double.toString(location.getLatitude());
        Toast.makeText(this, mensaje, Toast.LENGTH_SHORT).show();
    }
    public void onStatusChanged(String provider, int status, Bundle extras) {    }
    public void onProviderEnabled(String provider) {    }
    public void onProviderDisabled(String provider) {    }
    @Override
    protected void onResume() {
        super.onResume();
        if (locationManager!=null)
            if ((ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) &&
                    (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED)) {
                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
            } else {
                Toast.makeText(this, getResources().getString(R.string.error), Toast.LENGTH_LONG).show();
            }
    }
    @Override
    protected void onPause() {
        super.onPause();
        if (locationManager!=null)
            if ((ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) &&
                    (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED))
                locationManager.removeUpdates(this);
    }
}
A continuación, se resaltan algunos aspectos de ese código: Dado que el obtener información de localización de un dispositivo puede comprometer aspectos de privacidad, en el manifiesto de la aplicación hay que informar de que la aplicación requiere esos permisos (ACCESS_COARSE_LOCATION y ACCESS_FINE_LOCATION) (fichero AndroidManifest.xml):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.fperez.seu_app5" >

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >

        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
Para probar esta aplicación, el emulador ofrece una interfaz gráfica para llevar a cabo esta operación.