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)
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">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:
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);
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); } ...................................... }
<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">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>
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:
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:
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:
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(); }
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, en las versiones más recientes, el emulador ofrece una interfaz gráfica para llevarlo a cabo.
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:
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, el emulador ofrece una interfaz gráfica para llevar a cabo esta operación.