Page 128
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
El
objeto TelephonyManager
se
obtiene
utilizando
el
método
getSystemService() que, como ya se vio con el GPS o la Wi-Fi, provee de los
diferentes servicios integrados en el dispositivo móvil. Aquí se especifica la constante
Context.TELEPHONY_SERVICE de la clase Context, que representa el contexto de la
aplicación.
El método que proporciona el número de teléfono es getLine1Number(), pero la
clase dispone de otros útiles métodos como getCallState(), que informa del estado
de la llamada en curso, getCellLocation(), que informa de la celda GSM en la que
se encuentra el dispositivo móvil, o getNetworkOperatorName(), que devuelve el
nombre del operador de telefonía utilizado.
__rendered_path__11
Como cabía esperar, el acceso a este tipo de información implica la declaración del
siguiente permiso en el manifiesto de ContactMap:
__rendered_path__47
__rendered_path__47
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
__rendered_path__48
__rendered_path__47
__rendered_path__47
Código 39. Declaración en el manifiesto del permiso de acceso al dispositivo
__rendered_path__49
__rendered_path__49
4.2.12
Uso de la base de datos SQLite
__rendered_path__52
__rendered_path__52
La información de localización de los contactos se actualiza de forma periódica
__rendered_path__47__rendered_path__49
mediante conexiones con el servidor. Sin embargo, en caso de no tener una señal Wi-Fi
__rendered_path__47__rendered_path__49
disponible ContactMap utiliza la información almacenada en la base de datos SQLite de
__rendered_path__48
Android para mostrar al menos las últimas ubicaciones conocidas.
__rendered_path__47
__rendered_path__47
Toda la gestión relacionada con la base de datos se lleva a cabo en la clase
FriendDataBase. En ella están implementados dos métodos importantes:
getFriends() que devuelve un objeto Cursor con todos los contactos almacenados
en la base de datos, e insertFriend() que actualiza los datos de un contacto y en
caso de no existir lo inserta. El manejo de la base de datos SQLite es muy similar al que
se hace cuando se accede, por ejemplo, a la información de los contactos almacenados
en el propio dispositivo móvil, ya explicado en el apartado 4.2.10.
En la clase FriendDataBase se declaran dos variables globales de ámbito privado. La
primera, mDataBase, representa un objeto de la clase SQLiteDatabase. Esta clase
forma parte del paquete android.database.sqlite, que incluye otras muchas
clases e interfaces para el manejo de SQLite. La variable mDataBase representa un
controlador de la base de datos y proporciona métodos para la creación, consulta o
actualización de la misma. La otra variable global, llamada mTable, contiene la
sentencia SQL necesaria para crear la base de datos en caso de ser la primera ejecución
de ContactMap.
Jaime Aranaz Tudela
128
Ingeniería en Informática

Page 129
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
Crear la base de datos
Como ya se ha mencionado, la variable mTable contiene la instrucción en SQL
necesaria para crear la base de datos. Esta implica una única tabla donde se almacena
número de teléfono, latitud, longitud y fecha de actualización de cada contacto para el
que se ha obtenido alguna vez información desde el servidor. Se puede comprobar que
su estructura, mostrada a continuación, es igual a la base de datos en MySQL residente
en el servidor (ver apartado 4.1.4):
__rendered_path__30
__rendered_path__28__rendered_path__30
public class FriendDataBase {
__rendered_path__11__rendered_path__31
__rendered_path__30
// Sentencia de creación de la BD
__rendered_path__30
private String mTable =
__rendered_path__32
"CREATE TABLE IF NOT EXISTS friend " +
__rendered_path__32
"(number INTEGER PRIMARY KEY, " +
__rendered_path__33
"latitude REAL NOT NULL, " +
__rendered_path__38
"longitude REAL NOT NULL, " +
__rendered_path__38
"date TEXT NOT NULL)";
__rendered_path__33
}
__rendered_path__38
__rendered_path__38
__rendered_path__33
Código 40. Script SQL de la base de datos SQLite de Android
__rendered_path__38
__rendered_path__38
__rendered_path__42
Cuando se llama al constructor de la clase, lo primero que se hace es abrir la base de
__rendered_path__50
datos, de nombre “ContactMapBD”, utilizando para ello el contexto de la aplicación y
__rendered_path__50
el método openOrCreateDatabase(). Como es de suponer, en caso de no existir la
__rendered_path__33
base de datos con el nombre indicado, dicho método la crea.
__rendered_path__38
__rendered_path__38
A continuación, ya se dispone en la variable mDataBase de una controlador con el que
__rendered_path__33
manejar la base de datos. El siguiente paso es ejecutar el script SQL contenido en la
__rendered_path__38
variable mTable. Así, si la base de datos ha tenido que ser creada, se creará también la
__rendered_path__38
tabla necesaria. En caso de existir, la sentencia SQL no produce ningún efecto. Para
__rendered_path__42
lanzar la sentencia se utiliza el método execSQL() de la clase SQLiteDatabase.
__rendered_path__50
__rendered_path__50
En el Código 41. Creación de la base de datos SQLite.Código 41 se expone el
__rendered_path__33
constructor de FriendDataBase:
__rendered_path__38
__rendered_path__38
__rendered_path__33
__rendered_path__38
public class FriendDataBase {
__rendered_path__38
__rendered_path__42
public FriendDataBase(Context ctx){
__rendered_path__50
__rendered_path__50
//apertura o creación de la base de datos
__rendered_path__67
this.mDataBase =
__rendered_path__30__rendered_path__69
ctx.openOrCreateDatabase("ContactMapBD",0,null);
__rendered_path__30__rendered_path__69
__rendered_path__31
Jaime Aranaz Tudela
129
__rendered_path__30__rendered_path__30__rendered_path__108__rendered_path__30__rendered_path__30__rendered_path__31__rendered_path__30__rendered_path__30__rendered_path__110__rendered_path__110__rendered_path__33__rendered_path__38__rendered_path__38__rendered_path__33__rendered_path__38__rendered_path__38__rendered_path__42__rendered_path__50__rendered_path__50__rendered_path__33__rendered_path__38__rendered_path__38__rendered_path__33__rendered_path__38__rendered_path__38__rendered_path__42__rendered_path__50__rendered_path__50__rendered_path__127__rendered_path__134__rendered_path__134__rendered_path__135__rendered_path__30__rendered_path__30__rendered_path__31__rendered_path__30__rendered_path__30__rendered_path__137__rendered_path__137
Ingeniería en Informática

Page 130
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
__rendered_path__14
__rendered_path__12__rendered_path__14
//creación de la tabla
__rendered_path__15
this.mDataBase.execSQL(mTable);
__rendered_path__14
__rendered_path__14
}
__rendered_path__16
}
__rendered_path__16
__rendered_path__17
__rendered_path__20
Código 41. Creación de la base de datos SQLite.
__rendered_path__20
__rendered_path__21
Consulta, actualización e inserción de filas
__rendered_path__29
__rendered_path__11__rendered_path__29
Dentro de la clase FriendDataBase existen dos métodos que interactúan con la base
__rendered_path__21
de datos: getFriends() e insertFriend(). El primero devuelve todos los
__rendered_path__29
contactos y el segundo actualiza los datos de un contacto o, en caso de no existir, lo
__rendered_path__29
inserta. Seguidamente, se mostrará el código del segundo método al ser el más
__rendered_path__17
interesante de los dos:
__rendered_path__20
__rendered_path__20
__rendered_path__21
__rendered_path__29
public class FriendDataBase {
__rendered_path__29
__rendered_path__33
public void insertFriend(String number, String latitude, String
__rendered_path__35__rendered_path__37
longitude, String date){
__rendered_path__35__rendered_path__37
__rendered_path__36
// Consulta en la BD la existencia del número de teléfono
__rendered_path__35
Cursor c = mDataBase.query("friend", new String[] {
__rendered_path__35
"number"},
__rendered_path__58__rendered_path__14
"number="+number,
__rendered_path__14
null,
__rendered_path__15
null,
__rendered_path__14
null,
__rendered_path__14
null);
__rendered_path__60
__rendered_path__60
// Crea una tupla completa para el contacto
__rendered_path__21
ContentValues values = new ContentValues();
__rendered_path__29
values.put("latitude", latitude);
__rendered_path__29
values.put("longitude", longitude);
__rendered_path__65
values.put("date", date);
__rendered_path__67
__rendered_path__67
__rendered_path__68
// No existe, insertar en la tabla
__rendered_path__74
if (c.getCount() == 0){
__rendered_path__74
values.put("number", number);
__rendered_path__68
mDataBase.insert("friend", null, values);
__rendered_path__74
}else{ // Sí existe, actualizar
__rendered_path__74
mDataBase.update("friend", values, "number="+number, null);
__rendered_path__21
}
__rendered_path__29
}
__rendered_path__29
}
__rendered_path__68
__rendered_path__74
__rendered_path__74
Código 42. Consulta, actualización e inserción de filas en la base de datos SQLite
__rendered_path__68
Jaime Aranaz Tudela
130
__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__21__rendered_path__29__rendered_path__29__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__21__rendered_path__29__rendered_path__29__rendered_path__17__rendered_path__20__rendered_path__20__rendered_path__21__rendered_path__29__rendered_path__29__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__21__rendered_path__29__rendered_path__29__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__21__rendered_path__29__rendered_path__29__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__21__rendered_path__29__rendered_path__29__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__21__rendered_path__29__rendered_path__29__rendered_path__68__rendered_path__74__rendered_path__74__rendered_path__155__rendered_path__14__rendered_path__14__rendered_path__15__rendered_path__14__rendered_path__14__rendered_path__157__rendered_path__157
Ingeniería en Informática

Page 131
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
La primera acción es consultar la base de datos para conocer si existe el contacto al que
se le van a actualizar los datos de localización. El método query() de la clase
SQLiteDatabase permite lanzar una consulta, de forma que mediante sus parámetros
se construye una sentencia SELECT completa: tabla a consultar, columnas que se desea
devolver, posible cláusula WHERE y orden de las filas (ver el apartado 4.2.10). Esta
consulta arrojará su resultado sobre un objeto de la clase Cursor.
En caso de devolver alguna fila, según indique el método getCount() de la clase
Cursor, significa que el contacto ya existe y se puede actualizar con el método
update() de SQLiteDatabase; si no devuelve ninguna, el contacto no existe y debe
ser insertado con el método insert().
__rendered_path__11
En cualquier caso, es necesario agrupar de alguna forma los valores para la tupla, ya sea
para su inserción o su actualización. Dentro del paquete android.content (aquel
importante que contiene clases como Intent o BroadcastReceiver) existe una
clase llamada ContentValues que permite asociar pares del tipo campo-valor
perfectos para su utilización en bases de datos como SQLite. De esta forma, se compone
un objeto ContentValues con la latitud, longitud y fecha del contacto para su
utilización en insert() o update().
Cuándo guardar la información de los contactos en SQLite
Como se recordará, la información de localización de los contactos se obtiene a través
de conexiones periódicas con el servidor. Cada vez que se realiza una conexión exitosa,
la lista con los contactos mFriendList se actualiza. En caso de no ser posible la
conexión, se utiliza la información presente en la base de datos SQLite. Pero, ¿en qué
momento se guarda esta información en la base de datos para ser recuperada en caso de
necesidad?
La información de localización de los contactos se guardará desde la lista
mFriendList a la base de datos SQLite siempre que se cierre la aplicación
ContactMap, o cada vez que exista el riesgo de que la aplicación pueda ser eliminada
por falta de recursos. De esta forma, se pretende asegurar que siempre se tendrá acceso
a la última información de localización obtenida.
La clase principal ContactMap, como clase que deriva de Activity, tiene un ciclo de
vida bien definido y gestionado por el propio Android, tal y como se explicó en un
capítulo anterior. Cada transición entre estados de la actividad (en ejecución, en pausa,
etc.) implica la llamada del método correspondiente.
Por ejemplo, al crearse por primera vez la actividad se lanza siempre el método
onCreate(). Así mismo, cuando la actividad es eliminada de forma intencionada por
el usuario, se llama al método onDestroy(). Una de las situaciones bajo la cual la
actividad ContactMap puede ser eliminada por el sistema por falta de recursos es
Jaime Aranaz Tudela
131
Ingeniería en Informática

Page 132
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
cuando esta pasa a un segundo plano y, además, ya no es visible para el usuario. Tal
circunstancia va precedida por la llamada al método onStop().
Es en ambos estados del ciclo de vida, onStop() y onDestroy(), cuando se recorre
la lista mFriendList con los contactos y se almacena, uno por uno, en la base de datos
de SQLite para poder ser repuesta en caso de que no exista posibilidad de establecer
conexión con el servidor. Cuando tal circunstancia se detecta, se utiliza el método
updateFriendsFromDataBase() definido en ContactMap.
4.2.13
Construcción del menú principal
__rendered_path__11
Para poder acceder a algunas de las opciones ofrecidas por ContactMap, el usuario debe
utilizar el menú principal de la aplicación. Este menú aparece al pulsar la tecla
correspondiente en el dispositivo móvil.
La clase ContactMap deriva de la clase Activity y tiene por ello un ciclo de vida
bien definido. Además, cuenta con otra serie de métodos nativos que permiten tomar el
control de diversos eventos provocados por el usuario. Este es el caso de la utilización
del menú principal, cuyo comportamiento viene determinado por los métodos
onCreateOptionsMenu() y onMenuItemSelected().
Crear un menú
El método onCreateOptionsMenu() es invocado cuando se pulsa la tecla
correspondiente al menú, y permite configurar el tipo de menú que se quiere desplegar.
Este menú viene representado generalmente por una ventana que se desliza sobre la
interfaz mostrada en ese momento y que debe ofrecer las funcionalidades principales de
la aplicación en curso, que en el caso de ContactMap son las siguientes:
Listar los contactos
__rendered_path__75
Cambiar el tipo de mapa
__rendered_path__75
Realizar una llamada al contacto
__rendered_path__75
Mandar un SMS al contacto
__rendered_path__75
Mandar un correo electrónico al contacto
__rendered_path__75
En el siguiente fragmento de código se enseña al lector la forma en la que se compone
el menú principal de ContactMap.
Jaime Aranaz Tudela
132
Ingeniería en Informática

Page 133
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
__rendered_path__15
__rendered_path__13__rendered_path__15
public class ContactMap extends MapActivity {
__rendered_path__16
__rendered_path__15
@Override
__rendered_path__15
public boolean onCreateOptionsMenu(Menu menu) {
__rendered_path__17
__rendered_path__17
// Crear las siguientes opciones de menú
__rendered_path__18
__rendered_path__25
MenuItem item0 = menu.add(0, 0, 0, R.string.option_list);
__rendered_path__25
MenuItem item1 = menu.add(0, 1, 0, R.string.option_mapa);
__rendered_path__26
MenuItem item2 = menu.add(0, 2, 0, R.string.option_call);
__rendered_path__28
MenuItem item3 = menu.add(0, 3, 0, R.string.option_sms);
__rendered_path__28
MenuItem item4 = menu.add(0, 4, 0, R.string.option_email);
__rendered_path__18
__rendered_path__11__rendered_path__25
// Asocia una imagen a cada opción de menú
__rendered_path__25
item0.setIcon(R.drawable.agenda);
__rendered_path__31
item1.setIcon(R.drawable.mapmode);
__rendered_path__36
item2.setIcon(R.drawable.call);
__rendered_path__36
item3.setIcon(R.drawable.sms);
__rendered_path__31
item4.setIcon(R.drawable.email);
__rendered_path__36
__rendered_path__36
return super.onCreateOptionsMenu(menu);
__rendered_path__18
}
__rendered_path__25
}
__rendered_path__25
__rendered_path__31
__rendered_path__36
Código 43. Construcción del menú principal
__rendered_path__36
__rendered_path__31
__rendered_path__36
La clase Menu representa el menú principal de una aplicación y forma parte del paquete
__rendered_path__36
android.view, que ofrece un considerable número de clases para manejar los
__rendered_path__31
elementos de una interfaz de usuario y gestionar los eventos asociados. A través del
__rendered_path__36
objeto Menu se gestionan las opciones que componen el menú principal.
__rendered_path__36
__rendered_path__18
Un clase MenuItem (también del paquete android.view) representa una opción
__rendered_path__25
dentro del menú y permite tener acceso a sus propiedades. Cada uno de estos objetos se
__rendered_path__25
crea a partir del método add() de Menu, donde se especifica entre otras cosas el orden
__rendered_path__31
que debe ocupar la aplicación dentro del menú y el texto que la debe acompañar.
__rendered_path__36
__rendered_path__36
Las opciones declaradas para el menú principal cuentan con un texto asociado que las
__rendered_path__31
describe y una imagen o icono representativo, ambos visibles para el usuario cuando
__rendered_path__36
despliegue el menú. Para lo primero, el texto, se incluye en la propia llamada a
__rendered_path__36
Menu.add() y se utiliza el texto declarado en el fichero de recursos “strings.xml”,
__rendered_path__18
dentro de la carpeta “\res\values” de la aplicación.
__rendered_path__25
__rendered_path__25
Para las imágenes, se pueden asociar a cada opción a través del método setIcon() de
__rendered_path__26
la clase MenuItem. Al igual que con el texto, las imágenes se incluyen haciendo una
__rendered_path__28
referencia a la carpeta de recursos “\res\drawable”, donde deberán estar ubicadas. Como
__rendered_path__28
información de utilidad al lector, se dirá que el SDK de Android incluye por defecto una
__rendered_path__18
importante colección de iconos útiles para todo tipo de aplicaciones. Estos iconos se
__rendered_path__25
encuentran disponibles en la carpeta “\tools\lib\res\default\drawable”.
__rendered_path__25
Jaime Aranaz Tudela
133
__rendered_path__18__rendered_path__25__rendered_path__25__rendered_path__26__rendered_path__28__rendered_path__28__rendered_path__18__rendered_path__25__rendered_path__25__rendered_path__31__rendered_path__36__rendered_path__36__rendered_path__31__rendered_path__36__rendered_path__36__rendered_path__18__rendered_path__25__rendered_path__25__rendered_path__31__rendered_path__36__rendered_path__36__rendered_path__31__rendered_path__36__rendered_path__36__rendered_path__13__rendered_path__95__rendered_path__95__rendered_path__96__rendered_path__95__rendered_path__95__rendered_path__17__rendered_path__17
Ingeniería en Informática

Page 134
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
__rendered_path__11Image_766_0
Figura 27. Despliegue del menú principal
Controlar la opción seleccionada
En este punto, el usuario ya puede desplegar el menú y comprobar que opciones
principales le ofrece ContactMap.Ahora es necesario poder conocer qué opción pulsa
el usuario y actuar en consecuencia.
El otro método importante para controlar el menú y sus opciones es
onMenuItemSelected(). Cada vez que el usuario selecciona una opción del menú
principal, este método se llama recibiendo como parámetro un objeto MenuItem que va
a permitir conocer qué opción exactamente ha sido la pulsada.
Utilizando una sencilla sentencia de control switch y el método getItemId() de
MenuItem, se obtiene la opción pulsada y se ejecuta la acción correspondiente a la
misma, tal y como se hace en el Código 44.
Jaime Aranaz Tudela
134
Ingeniería en Informática

Page 135
__rendered_path__11
Proyecto Fin de Carrera
Desarrollo de aplicaciones para dispositivos móviles sobre la plataforma Android de Google
__rendered_path__14
__rendered_path__12__rendered_path__14
public class ContactMap extends MapActivity {
__rendered_path__15
__rendered_path__14
@Override
__rendered_path__14
public boolean onMenuItemSelected(int featureId, MenuItem item) {
__rendered_path__16
__rendered_path__16
switch (item.getItemId()) {
__rendered_path__17
__rendered_path__24
case 0:// Listar contactos
__rendered_path__24
// (...)
__rendered_path__17
return true;
__rendered_path__24
__rendered_path__24
case 0:// Cambiar mapa
__rendered_path__26
// (...)
__rendered_path__29
return true;
__rendered_path__11__rendered_path__29
__rendered_path__17
case 0:// Llamar
__rendered_path__24
// (...)
__rendered_path__24
return true;
__rendered_path__36
__rendered_path__38
case 0:// Enviar SMS
__rendered_path__38
// (...)
__rendered_path__36
return true;
__rendered_path__38
__rendered_path__38
case 0:// Enviar correo electrónico
__rendered_path__17
// (...)
__rendered_path__24
return true;
__rendered_path__24
}
__rendered_path__36
__rendered_path__38
return super.onMenuItemSelected(featureId, item);
__rendered_path__38
}
__rendered_path__36
__rendered_path__38
}
__rendered_path__38
__rendered_path__36
__rendered_path__38
Código 44. Control del menú principal
__rendered_path__38
4.2.14
Mostrar listado de contactos
__rendered_path__17
__rendered_path__24
Acciones como mandar un SMS, un correo electrónico o realizar una llamada se
__rendered_path__24
realizan sobre el contacto que en ese momento tenga el foco de la aplicación. El usuario
__rendered_path__36
puede cambiar el foco de un contacto a otro de diferentes maneras: pulsando sobre ellos,
__rendered_path__38
utilizando los botones de anterior y siguiente (se verán más adelante) o a través de un
__rendered_path__38
listado con todos los contactos ordenados alfabéticamente. Éste último caso es el que se
__rendered_path__36
explica en este apartado.
__rendered_path__38
__rendered_path__38
Cualquier aplicación en Android está formada por una serie de componentes básicos
__rendered_path__17
entre los que se cuenta Activity. Una clase Activity, como ya sabrá el lector,
__rendered_path__24
representa una funcionalidad importante, con entidad propia, y que está asociada
__rendered_path__24
generalmente a una interfaz con la que el usuario puede interactuar. La clase
__rendered_path__26
ContactMap, por ejemplo, es una Activity que muestra un mapa y permite al usuario
__rendered_path__29
realizar una serie de acciones. Una Activity también puede invocar a su vez a otros
__rendered_path__29
componentes Activity.
__rendered_path__17
Jaime Aranaz Tudela
135
__rendered_path__24__rendered_path__24__rendered_path__17__rendered_path__24__rendered_path__24__rendered_path__26__rendered_path__29__rendered_path__29__rendered_path__17__rendered_path__24__rendered_path__24__rendered_path__36__rendered_path__38__rendered_path__38__rendered_path__36__rendered_path__38__rendered_path__38__rendered_path__17__rendered_path__24__rendered_path__24__rendered_path__36__rendered_path__38__rendered_path__38__rendered_path__36__rendered_path__38__rendered_path__38__rendered_path__36__rendered_path__38__rendered_path__38__rendered_path__17__rendered_path__24__rendered_path__24__rendered_path__36__rendered_path__38__rendered_path__38__rendered_path__36__rendered_path__38__rendered_path__38__rendered_path__17__rendered_path__24__rendered_path__24__rendered_path__26__rendered_path__29__rendered_path__29__rendered_path__17__rendered_path__24__rendered_path__24__rendered_path__17__rendered_path__24__rendered_path__24__rendered_path__12__rendered_path__14__rendered_path__14__rendered_path__15__rendered_path__14__rendered_path__14__rendered_path__16__rendered_path__16
Ingeniería en Informática