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.
A continuación, se muestra la interfaz de usuario 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:
<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:
setContentView(R.layout.activity_main);que es la encargada de desplegar la interfaz de usuario definida en el fichero XML especificado.
entrada = (EditText) findViewById(R.id.dato); salida = (TextView) findViewById(R.id.resultado);Asimismo, para que sirva de ilustración, se ha incluido una llamada que imprime datos al log, lo que puede servir para depurar el código:
Log.d("OPERACIÓN", "onCreate");
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); } ...................................... }
<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.
<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>
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:
power ac off power capacity 5y la siguiente para recuperarla:
power capacity 100 power ac on
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:
sensorMgr = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
if ((sensor = sensorMgr.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)) == null){ Toast.makeText(this, getResources().getString(R.string.otroerror), Toast.LENGTH_LONG).show(); finish(); }
sensorMgr.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);y la desactivación en el método onPause:
sensorMgr.unregisterListener(this);
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
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:
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);y la desactivación en el método onPause:
locationManager.removeUpdates(this);
<?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
La aplicación tendrá las siguientes características: