Materia de Aplicaciones Móviles y Servicios Telemáticos
Objetivo de Aprendizaje: Diseñar aplicaciones que utilicen los sensores embebidos en dispositivos móviles para la entrega de información a los usuarios en tiempo real.
Recursos: Android Studio. Google Cloud.
Duración: 6 horas
Introducción: Creación de una aplicación móvil capaz ejecutar hilos (runnables) para el seguimiento en soft-real time de los registros en una REST-API. Configuración de la aplicación móvil para recibir notificaciones mediante el servicio firebase cloud messaging.
Actividades
1) Crear un proyecto en blanco con actividad vacía.
El nombre del paquete será:
2) Creamos una actividad vacía con el nombre de Registros, donde presentaremos la información. En la carpeta app > java > com.amst.grupo# damos clic derecho: New > Activity > Empty Activity.
3) Creamos un menú sencillo para acceder a la actividad que acabamos de crear. Modificamos el archivo (res > layout > activity_main.xml) para agregar un título y un botón.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="104dp"
android:gravity="center"
android:text="Recarga en tiempo real"
android:textSize="30sp"/>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="abrirVistaRegistros"
android:text="Bar Chart" />
</LinearLayout>
4) Dentro de MainActivity.class creamos la función abrirVistaRegistros() para movernos entre vistas
public void abrirVistaRegistros(View view) { Intent intent = new Intent(this, Registros.class); startActivity(intent); }
5) Para la actividad registros, dentro de (res > layout > activity_registros.xml), agregamos un gráfico de barras y una lista para mostrar la información desde REST API. La declaración del gráfico de barras saldrá en error hasta que se agreguen las dependencias.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Registros">
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fillViewport="true"
android:scrollbars = "vertical"
android:scrollbarStyle="insideInset">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:padding="10dp"
android:id="@+id/contenedor">
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/barChart"
android:layout_width="match_parent"
android:layout_height="400dp">
</com.github.mikephil.charting.charts.BarChart>
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Registros de temperatura"
android:textSize="24sp"
android:textStyle="bold"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#635D5D"
android:text="Fecha obtenida"
android:textColor="#FFFFFF"
android:textStyle="bold"/>
<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#635D5D"
android:text="Valor"
android:textColor="#FFFFFF"
android:textStyle="bold"/>
</LinearLayout>
<LinearLayout
android:id="@+id/cont_temperaturas"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
6) Agregar las dependencias Volley (para realizar http Request) y MAndroid (para cuadros estadísticos). Las dependencias se encuentran en build.gradle (Module:app).
dependencies {
implementation 'com.android.volley:volley:1.2.1'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}
7) Agregar los repositorios de MPandroid en settings.gradle (Project Settings). Nota: Luego de modificar el grandle siempre es necesario dar clic en Sync Now en la esquina superior derecha de la pantalla.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
8) Permitirle acceso a internet, incluyendo en el archivo (app > Manifest > AndroidManifest.xml).
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCloudMessaging"
tools:targetApi="31">
9) Actualizar información en el archivo Registros.
Estructura del archivo (Nota: Deberá importar las librerías para widgets, estructuras y librerías. Si presenta algún problema puede utilizar el comando Ctrl + Alt). Recuerde generar un nuevo token.
public class Registros extends AppCompatActivity {
public BarChart graficoBarras;
private RequestQueue ListaRequest = null;
private LinearLayout contenedorTemperaturas;
private Map<String, TextView> temperaturasTVs;
private Map<String, TextView> fechasTVs;
private Registros contexto;
@Override
protected void onCreate(Bundle savedInstanceState) {
}
public void iniciarGrafico() {}
public void solicitarTemperaturas(){}
private void mostrarTemperaturas(JSONArray temperaturas){ }
private void actualizarGrafico(JSONArray temperaturas){ }
private void llenarGrafico(ArrayList<BarEntry> dato_temp){}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_registros);
setTitle("Grafico de barras");
temperaturasTVs = new HashMap<String,TextView>();
fechasTVs = new HashMap<String,TextView>();
ListaRequest = Volley.newRequestQueue(this);
contexto = this;
/* GRAFICO */
this.iniciarGrafico();
this.solicitarTemperaturas();
}
public void iniciarGrafico() {
graficoBarras = findViewById(R.id.barChart);
graficoBarras.getDescription().setEnabled(false);
graficoBarras.setMaxVisibleValueCount(60);
graficoBarras.setPinchZoom(false);
graficoBarras.setDrawBarShadow(false);
graficoBarras.setDrawGridBackground(false);
XAxis xAxis = graficoBarras.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setDrawGridLines(false);
graficoBarras.getAxisLeft().setDrawGridLines(false);
graficoBarras.animateY(1500);
graficoBarras.getLegend().setEnabled(false);
}
public void solicitarTemperaturas(){
String url_registros = " https://amst-lab-api.herokuapp.com/api/lecturas";
JsonArrayRequest requestRegistros = new JsonArrayRequest( Request.Method.GET, url_registros, null,
response -> {
mostrarTemperaturas(response);
actualizarGrafico(response);
}, error -> System.out.println(error)
);
ListaRequest.add(requestRegistros);
}
private void mostrarTemperaturas(JSONArray temperaturas){
String registroId;
JSONObject registroTemp;
LinearLayout nuevoRegistro;
TextView fechaRegistro;
TextView valorRegistro;
String fecharegistro_text;
String registrovalue_text;
contenedorTemperaturas = findViewById(R.id.cont_temperaturas);
LinearLayout.LayoutParams parametrosLayout = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT, 1);
try {
for (int i = 0; i < temperaturas.length(); i++) {
registroTemp =temperaturas.getJSONObject(i);
registroId = registroTemp.getString("id");
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
ZonedDateTime zonedDateTime = OffsetDateTime.parse(registroTemp.getString("date_created")).toZonedDateTime();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
fecharegistro_text = String.valueOf(zonedDateTime.format(formatter));
} else {
fecharegistro_text = registroTemp.getString("date_created");
}
registrovalue_text = registroTemp.getString("value");
if( temperaturasTVs.containsKey(registroId) && fechasTVs.containsKey(registroId) ){
fechaRegistro = fechasTVs.get(registroId);
valorRegistro = temperaturasTVs.get(registroId);
fechaRegistro.setText(fecharegistro_text);
valorRegistro.setText(registrovalue_text + " °C");
} else {
nuevoRegistro = new LinearLayout(this);
nuevoRegistro.setOrientation(LinearLayout.HORIZONTAL);
fechaRegistro = new TextView(this);
fechaRegistro.setLayoutParams(parametrosLayout);
fechaRegistro.setText(fecharegistro_text);
nuevoRegistro.addView(fechaRegistro);
valorRegistro = new TextView(this);
valorRegistro.setLayoutParams(parametrosLayout);
valorRegistro.setText(registrovalue_text + " °C");
nuevoRegistro.addView(valorRegistro);
contenedorTemperaturas.addView(nuevoRegistro);
fechasTVs.put(registroId, fechaRegistro);
temperaturasTVs.put(registroId, valorRegistro);
}
}
} catch (JSONException e) {
e.printStackTrace();
System.out.println("error");
}
}
private void actualizarGrafico(JSONArray temperaturas){
JSONObject registro_temp;
String temp;
String date;
int count = 1;
float temp_val;
ArrayList<BarEntry> dato_temp = new ArrayList<>();
try {
for (int i = 0; i < temperaturas.length(); i++) {
registro_temp =temperaturas.getJSONObject(i);
if( registro_temp.getString("key").equals("temperatura")){
temp = registro_temp.getString("value");
date = registro_temp.getString("date_created");
temp_val = Float.parseFloat(temp);
dato_temp.add(new BarEntry(count, temp_val));
count++;
}
}
} catch (JSONException e) {
e.printStackTrace();
System.out.println("error");
}
llenarGrafico(dato_temp);
}
private void llenarGrafico(ArrayList<BarEntry> dato_temp){
BarDataSet temperaturasDataSet;
if ( graficoBarras.getData() != null &&
graficoBarras.getData().getDataSetCount() > 0) {
temperaturasDataSet = (BarDataSet) graficoBarras.getData().getDataSetByIndex(0);
temperaturasDataSet.setValues(dato_temp);
graficoBarras.getData().notifyDataChanged();
graficoBarras.notifyDataSetChanged();
} else {
temperaturasDataSet = new BarDataSet(dato_temp, "Data Set");
temperaturasDataSet.setColors(ColorTemplate.VORDIPLOM_COLORS);
temperaturasDataSet.setDrawValues(true);
ArrayList<IBarDataSet> dataSets = new ArrayList<>();
dataSets.add(temperaturasDataSet);
BarData data = new BarData(dataSets);
graficoBarras.setData(data);
graficoBarras.setFitBars(true);
}
graficoBarras.invalidate();
new Handler(Looper.getMainLooper()).postDelayed(() -> solicitarTemperaturas(), 3000);
}
Firebase de Google brinda servicios gratuitos y de pago para aplicaciones moviles/web/iot. Durante este taller utilizaremos uno de sus servicios: Firebase cloud messaging, el cual nos permite realizar push notifications.
Push notifications: Igual que las notificaciones comunes de cualquier aplicación, estas muestran un evento que ocurre en la aplicación. Utilizadas por whatsapp para indicar cuando ha llegado un mensaje, o por Facebook para notificar el cumpleaños de un amigo. Push notifications se caracterizan por ser realizados del lado del servidor, el cliente / dueño del teléfono no ejecuta la acción sino es dueño del servidor o el cual requiere comunicar un evento a sus clientes.
Ejemplo: un dispositivo IOT detecta altos niveles de humedad en un cuarto, con fin de alertar al cliente, envía una notificación a su teléfono (El teléfono puede estar bloqueado y sin que la aplicación este abierta, llegara un mensaje en forma de notificación).
1) Creación de un proyecto de google
NOTA: Adicional, agregar el correo de la materia como visualizador. Correo: amst.investigacion@gmail.com
1) Dentro de nuestro proyecto en Android Studio, seleccionamos en la barra de menú > Tools > Firebase.
2) Del lado derecho, aparecerán todos los servicios que proporciona Firebase, por ahora seleccionaremos la opción Cloud Messaging > Set up Firebase Cloud Messaging.
Luego se abrirá un browser en el navegador de la consola de Google pidiendo que acepten los términos y condiciones, lanzando la conexión en el puerto 55253.
Seleccionamos el proyecto creado y damos clic en CONECTAR:
1) En la carpeta app > java > com.amst.grupo# deberá crear un package (click derecho > new > Package) el cual llamaremos Services.
2) Dentro del paquete, crearemos una clase Java. Dar clic derecho > new > Java Class, la cual llamaremos MyFirebaseInstanceService.
3) Registramos el servicio en el AndroidManifest.xml
</activity>
<service
android:name="Services.MyFirebaseInstanceService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
4) Modificamos MyFirebaseInstanceService.java para manejar el servicio.
public class MyFirebaseInstanceService extends FirebaseMessagingService {
//Ctrl + O
@Override
public void onNewToken(String s) {
super.onNewToken(s);
Log.d("TOKENFIREBASE", s);
}
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {}
private void showNotification(String title, String body){ }
}
public void onMessageReceived(RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
showNotification(remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getBody());
}
private void showNotification(String title, String body){
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
String NOTIFICATION_CHANNEL_ID = "com.amst.firebasenotify.test";
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
NotificationChannel notificationChannel =
new NotificationChannel(NOTIFICATION_CHANNEL_ID, "Notification",
NotificationManager.IMPORTANCE_DEFAULT);
notificationChannel.setDescription("EDMT Channel");
notificationChannel.enableLights(true); notificationChannel.setLightColor(Color.BLUE);
notificationChannel.setVibrationPattern(new long[]{0, 1000, 500, 1000});
notificationManager.createNotificationChannel(notificationChannel);
}
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
notificationBuilder.setAutoCancel(true).setDefaults(Notification.DEFAULT_ALL)
.setWhen(System.currentTimeMillis())
.setContentTitle(title)
.setContentText(body)
.setContentInfo("info");
notificationManager.notify(
new Random().nextInt(), notificationBuilder.build());
}
Nota: Realizar estos pasos si no encuentra el archivo google-services-json en el src de su proyecto. Para cada aplicacion, es requerido una librería única de Google con las claves generadas automáticamente. Debemos agregar este archivo a nuestra aplicacion para que nuestro servicio pueda funcionar.
1) Obtener el archivo Google-services.json. a. Dentro de https://console.firebase.google.com/ deben encontrase los proyectos en los cuales esta trabajando. Selecciona el proyecto compartido con usted.
b. En el dashboard inicial del proyecto, damos clic en las aplicaciones y buscamos nuestra aplicación.
c. Damos clic en el símbolo de Setting para ver las instrucciones del SDK.
d. Descargamos el archivo google-services.json. y lo agregamos en la vista de Proyecto. En la ruta de la carpeta app > src.
f. Podemos salir (dando clic en la X en el lado superior izquierdo). Los otros pasos ya los hemos realizado
Utilizamos la consola de firebase para enviar una notificación.
1) En el menú del lado izquierdo, seleccionamos el grupo Participación > Cloud messeging
2) Si es necesario, damos clic en Crear la primera campaña.
3) Seleccionamos tipos de mensaje y plataforma, y luego dar clic en Crear.
4) Aparecerá el formulario para ingresar una nueva notificación. Llenar título y cuerpo según convenga, también se puede enviar una imagen, y asignarle un nombre. Damos clic en Siguiente.
5) En orientación, seleccionamos nuestra aplicación y damos clic en siguiente.
Pantalla con la notificación en tiempo real:
Investigación
• ¿Cuál es la diferencia entre Soft Real time y Hard Real time?. • En el dashboard, actualizamos una lista y un grafico donde presentamos las temperaturas. ¿Qué otros componentes podemos actualizar (como tablas, edittexts, etc)? • Las apps de juegos utilizan hilos para el manejo de componentes (personaje, escenario, música), ¿Que otro tipo de aplicaciones (redes sociales, filtros de fotos, etc) utiliza hilos y con qué objetivo? • ¿Qué otros servicios de Firebase podrían ser útil para el desarrollo de dispositivos IOT y como los usaría? • Durante el taller creamos un package Servicios, donde alojamos el servicio de could messaging. ¿Que otro servicio (no de Firebase) podemos utilizar en las aplicaciones móviles? •
Desafío
Ninguna requiere autenticación. Ejemplo de estructura JSON para el POST: { “key”:”temperatura”, “value”: 60.10 }
La práctica de laboratorio será desarrollado en el siguiente formato:
Nombre del archivo: AMST_Práctica de Laboratorio A_Grupo B_Apellido1_Apellido2_Apellido3