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