Práctica de computación ubicua

Introducción y objetivos

El objetivo de este trabajo es afrontar 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, cinco 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 con el directorio Android de cada uno de los cinco proyectos.

El único trabajo previo que se le pide al lector es la instalación del entorno de desarrollo de Android (preferiblemente, Android Studio) y la creación y ejecución en un dispositivo virtual del proyecto que se crea automáticamente al usar uno de entornos (en el caso de Android Studio 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)

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. En las versiones más recientes del entorno de desarrollo, el emulador ofrece una interfaz gráfica que permite al usario introducir directamente valores para los distintos tipos de sensores, así como para manipular directamente el estado de la hipotética batería del dispositivo. En versiones más antiguas, se puede obtener esa misma funcionalidad ejecutando el mandato telnet localhost 5554 e ir escribiendo las órdenes correspondientes, tal como se explicará en cada sección que lo requiera. Nótese que la emulación a través de telnet sigue estando soportada en las versiones más recientes pero resulta mucho más cómodo trabajar a través de la interfaz gráfica.

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 definen las cadenas de caracteres que usará la aplicación (fichero res/values/strings.xml):
<resources>
<string name="app_name">SEU-App1</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>

<string name="action_settings">Settings</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 pero resaltando cuál es el código que realmente se ha añadido.
package com.example.fperez.seu_app1;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
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);
        Log.d("OPERACIÓN", "onCreate");
        setContentView(R.layout.activity_main);
        entrada = (EditText) findViewById(R.id.dato);
        salida = (TextView) findViewById(R.id.resultado);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
    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 (Control-F12) 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 onResume) 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 ea 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">SEU-App2</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>
    <string name="action_settings">Settings</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.seu_app3;

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. En versiones más antiguas (y taambién en las versiones actuales), para ello se puede ejecutar una sesión de telnet 5554 con la siguiente secuencia de mandatos para poner la batería en estado crítico:
power ac off
power capacity 5
y la siguiente para recuperarla:
power capacity 100
power ac on

Cuarta 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 donde se ha omitido aquella parte relacionada con la gestión de menús que incluye automáticamente el asistente (fichero MainActivity.java).

..............................................
public class MainActivity extends Activity 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();
                finish();
        }
    }

    .................................................................
    @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();
        sensorMgr.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
    }

    @Override
    protected void onPause() {
        super.onPause();
        sensorMgr.unregisterListener(this);
    }
}
A continuación, se resaltan algunos aspectos de ese código:

Para probar esta aplicación, es necesario usar los mandatos del emulador que gestionan sensores. En las versiones más recientes, el emulador ofrece una interfaz gráfica para llevarlo a cabo. En versiones más antiguas, debe ejecutar una sesión de telnet 5554 con el siguiente mandato para que la lectura del sensor devuelva una temperatura de 25 grados.

sensor set temperature 25

Quinta 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 donde se ha omitido aquella parte relacionada con la gestión de menús que incluye automáticamente el asistente (fichero MainActivity.java).

.......................................................
public class MainActivity extends Activity 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 ((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();
            finish();
        }
    }
    @Override
    protected void onPause() {
        super.onPause();
        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, es necesario usar los mandatos del emulador que gestionan información de posicionamiento. En las versiones más recientes, el emulador ofrece una interfaz gráfica para llevar a cabo esta operación. En versiones más antiguas, debe ejecutar una sesión de telnet 5554 con el siguiente mandato para que la lectura del GPS devuelva una posición con una logitod de 10 y una latitud de 20.
geo fix 10 20

Enunciado de la práctica propuesta

El proyecto plantea realizar una aplicación que muestre información de posicionamiento, basada en GPS, y de temperatura, tal que el comportamiento de la misma se adapte al contexto tanto en lo que se refiere a la orientación del dispositivo de presentación como al idioma seleccionado por el usuario y al nivel de batería disponible. La funcionalidad de dicha aplicación está ideada para que pueda desarrollarse tomando como base las cinco aplicaciones planteadas a lo largo de este documento.

La aplicación tendrá las siguientes características:

Entrega de la práctica

La entrega se podrá hacer hasta el 26 de enero de 2020, mediante correo electrónico al profesor encargado, incluyendo en el mismo una dirección desde donde descargar un fichero comprimido con el directorio completo del proyecto Android junto con un fichero de texto (leeme.txt) donde pueda incluir cualquier tipo de comentario o incidencia sobre el desarrollo de la práctica.