Documentación del código
El proyecto se desarrolló bajo el IDE de desarrollo Android Studio 3.4.
En este IDE se creó el proyecto BTSec, Android nos permite estructurarlo a través de módulos. En Android existe un módulo principal que es donde se genera el desarrollo del programa y es en el módulo app; allí se encuentran los principales directorios el manifests y java.
En el directorio de manifests se encuentra el archivo AndroidManifest.xml el cual es un archivo xml donde se editan las configuraciones y permisos básicos de nuestra app.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="bdprototypebt.darkbalrock.com.bdprototypebt">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <application
android:allowBackup="true"
~ 108 ~
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity android:name=".MainActivity"> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter>
<intent-filter>
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" /> <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" /> <action android:name="android.bluetooth.device.action" />
</intent-filter> </activity>
<activity
android:name=".LogsActivity"
android:label="@string/app_name" />
<activity
android:name=".devices.dispositivos"
android:label="@string/title_activity_dispositivos"
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".GattServer.BTGattServer" android:label="GattServer" />
</application> </manifest>
Dentro del directorio java tenemos las clases y los eventos principales que componen el proyecto el cual están en la siguiente ruta de proyecto
java/bdprotoypebt/darkbalrock/com/bdprototypebt. Tenemos el componente
devices el cual contiene todos los artefactos y componentes para consultar y mostrar los dispositivos emparejados con el bluetooth del dispositivo. El componente events el cual presta las interfaces y servicios para almacenar y guardar los eventos que detecte la aplicación. El componente GattServer el cual genera la actividad de servidor de comandos gatt transmitidos vía bluetooth; la clase de Logs para registro en los logs de aplicación y la clase MainActivity la cual provee la interfaz principal de la aplicación y el acceso a las características de bluetooth.
~ 109 ~
Desarrollo de la interfaz gráfica
Android permite definir la estructura visual para la interfaz de usuario por medio de elementos en archivos xml. A continuación, se observa los cinco layouts creados para el funcionamiento de esta aplicación.
~ 110 ~
Consulta_Logs:
~ 111 ~
fragment_dispositivos:
~ 112 ~
activity_dispositivos:
Explicación de los componentes de aplicación
Clase device: En esta clase solamente se definen los atributos que compone el dispositivo bluetooth como es el nombre, la dirección, el identificador único, la descripción de los servicios, el momento y estado de la conexión y otros necesarios para la programación.
~ 113 ~
/*
* Clase device
* Define los atributos del artefacto dispositivo (device) * */
public class device {
private int id; //Consecutivo de la BD
private String name; //Nombre del dispositivo
private String address; //Dirección MAC del dispositivo
private String UUIDs; //Utilizado como un campo adicional en los intentos de ACTION_UUID,
contiene los ParcelUuids del dispositivo remoto, que es una versión parcelable de UUID.
private String contentDesc; //Descripción de los servicios
private String time; //Momento de la conexion
private String bonded; //Estado de la conexión
private String hashCode; //Hash generado para el dispositivo
private String bloqueado; //Bandera para bloquear las comunicaciones
/*Constructor*/ public device() { }
Clase devicesContract: La clase devicesContract define un objeto BaseColumns que representa las columnas de la tabla de dispositivos. La interfaz BaseColumns genera nombres para las columnas _ID y _Count que son comunes, por lo que el uso de estos nombres comunes en Android permite abordar de forma unificada cualquier elemento de datos, independientemente de su estructura general.
/*
* Clase devicesContract
* Definir un tipo de Objecto BaseColumns para las columnas de la tabla dispositivos * */
import android.provider.BaseColumns; public class devicesContract {
public static abstract class deviceEntry implements BaseColumns{ public static final String tableName = "devices3";
public static final String ID = "id"; public static final String name = "name"; public static final String address = "address"; public static final String UUIDs = "UUIDs";
public static final String contentDesc = "contentDesc"; public static final String time = "time";
public static final String bonded = "bonded"; public static final String hashCode = "hashCode"; public static final String bloqueado = "bloqueado"; }
}
Clase devicesDBHelper: En esta clase se crea un artefacto de acceso y de manipulación de la base de datos para la aplicación. El motor de base de datos que se usa es SQLiteDataBase. Para esto comenzamos instanciando la clase SQLiteOpenHelper:
/*
* Clase devicesDBHelper
* Artefacto de acceso y manipulacion de la BD dispositivos * */
import android.content.ContentValues; import android.content.Context; import android.database.Cursor;
~ 114 ~
import android.database.sqlite.SQLiteOpenHelper; import android.util.Log;
public class devicesDBHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 2;
public static final String DATABASE_NAME = "devices3.db";
//Instanciamos la clase SQLiteOpenHelper
public devicesDBHelper (Context context){
super(context, DATABASE_NAME, null, DATABASE_VERSION); }
A través del método onCreate se hace uso del query que nos permite crear la tabla de los dispositivos en la base de datos.
/*
* Creamos la tabla de dispositivos * */
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase){
//Create Table
sqLiteDatabase.execSQL("CREATE TABLE "+ devicesContract.deviceEntry.tableName+ "("
+ devicesContract.deviceEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + devicesContract.deviceEntry.ID + " INTEGER, "
+ devicesContract.deviceEntry.name + " TEXT, " + devicesContract.deviceEntry.address + " TEXT, " + devicesContract.deviceEntry.UUIDs + " TEXT, " + devicesContract.deviceEntry.contentDesc + " TEXT, " + devicesContract.deviceEntry.time + " TEXT, " + devicesContract.deviceEntry.bonded + " TEXT, " + devicesContract.deviceEntry.hashCode + " TEXT, " + devicesContract.deviceEntry.bloqueado + " TEXT, " + " UNIQUE ("+devicesContract.deviceEntry.address+")" + ")");
}
ContentValues nos permite colocar información dentro de un objeto en forma de pares y se utiliza para insertar o actualizar valores en las tablas de la base de datos. En el método ContentValues lo generamos para manipular la base de datos.
/*
* Genera un artefacto ContentValues para manipular la BD * */
public ContentValues toContentValues(device device){ ContentValues values = new ContentValues();
values.put(devicesContract.deviceEntry.ID, device.getId()); values.put(devicesContract.deviceEntry.name, device.getName()); values.put(devicesContract.deviceEntry.address, device.getAddress()); values.put(devicesContract.deviceEntry.UUIDs, device.getUUIDs());
values.put(devicesContract.deviceEntry.contentDesc, device.getContentDesc()); values.put(devicesContract.deviceEntry.time, device.getTime());
values.put(devicesContract.deviceEntry.bonded, device.getBonded()); values.put(devicesContract.deviceEntry.hashCode, device.getHashCode()); values.put(devicesContract.deviceEntry.bloqueado, device.getBloqueado()); return values;
}
Con el método saveDevice guardamos la información del dispositivo en la base de datos:
/*
* Guarda un dispositivo en la BD * */
~ 115 ~
public long saveDevice(device device){
SQLiteDatabase sqLiteDatabase = getWritableDatabase();
return sqLiteDatabase.insert(devicesContract.deviceEntry.tableName, null,
toContentValues(device));
}
El siguiente método se denomina getDevice y hace uso del query que permite consultar la información de un dispositivo y retorna un Cursor. El cursor en Android y SQLiteDatabase se encarga de generar un conjunto de resultados cuando se llama por un método query en una instancia de la base de datos. Además, está clase Cursor tiene una API que permite que una aplicación lea las columnas que se devuelven de la consulta con las filas del conjunto de resultados.
/*
* Consulta un dispositivo y retorna un Cursor * */
public Cursor getDevice(String tabla, String[] columnas, String selection, String[] selectionArgs, String
groupBy, String having, String orderBy){
SQLiteDatabase sqLiteDatabase = getReadableDatabase(); Cursor c = sqLiteDatabase.query(
tabla, columnas, selection, selectionArgs, groupBy, having, orderBy ); return c; }
El método permite consultar todos los dispositivos de la base de datos, generando una consulta del tipo select * from al enviar los parámetros de consulta en null. Esta implementación la realiza el método getDevices.
/*
* Consulta todos los dispositivos y retorna un Cursor * */
public Cursor getDevices(){
String tabla = devicesContract.deviceEntry.tableName, selection = null,
groupBy = null, having = null, orderBy = null;
String[] columnas = null, selectionArgs = null;
Cursor d = getDevice(tabla,columnas,selection,selectionArgs,groupBy,having,orderBy); return d;
}
En la base de datos es necesario en algunos casos limpiar los registros almacenados en la base de datos para dar lugar a los nuevos registros que se generarán con otra consulta. Es por esto que se crea el método deleteDevices que se encarga de este proceso. La implementación usa el método execSQL, el cual permite ejecutar una sentencia sql directamente a la base de datos.
~ 116 ~
/*
* Elimina todos los dispositivos * */
public void deleteDevices(){
SQLiteDatabase db = getWritableDatabase();
db.execSQL("delete from "+devicesContract.deviceEntry.tableName); db.close();
}
La actualización de los datos se implementa por medio del siguiente método updateDevice, en donde se actualizará los datos del dispositivo y este nos retorna un booleano que indica el resultado de la operación.
/*
* Actualiza un dispositivo y retorna un boleano * */
public boolean updateDevice(device device){ boolean result = false;
try {
ContentValues cv = new ContentValues();
cv.put(devicesContract.deviceEntry.name, device.getName()); cv.put(devicesContract.deviceEntry.UUIDs, device.getUUIDs());
cv.put(devicesContract.deviceEntry.contentDesc, device.getContentDesc()); cv.put(devicesContract.deviceEntry.name, device.getName());
cv.put(devicesContract.deviceEntry.time, device.getTime()); cv.put(devicesContract.deviceEntry.hashCode, device.getHashCode()); SQLiteDatabase db = getWritableDatabase();
db.update(devicesContract.deviceEntry.tableName, cv, devicesContract.deviceEntry.address + "= '" + device.getAddress()+"'", null);
result = true; }catch (Exception e){
Log.e("deviceDBHelper", "Error actualizando Dispositivo: " + e.toString()); }
return result;
}
En la aplicación es muy importante el estado en que se encuentra el dispositivo, por lo tanto, si la aplicación encuentra un dispositivo y este está activo a realizar una conexión, se puede bloquear su estado para evitar que este realice acciones negativas a nuestro dispositivo. El siguiente método blockDevice valida el estado de este dispositivo y actualiza la columna de bloqueado.
/*
* Valida el estado del dispositivo y actualiza la columna bloqueado * */
public boolean blockDevice(device dev){ boolean result = false;
String estado = "bloqueado"; try {
String args [] = new String[1]; args[0] = dev.getAddress(); Cursor cursor =
this.getDevice(devicesContract.deviceEntry.tableName,null,devicesContract.deviceEntry.address+"=?",args,n ull,null,null);
if (cursor.getCount() > 0){ cursor.moveToFirst(); String bloqueado =
cursor.getString(cursor.getColumnIndex(devicesContract.deviceEntry.bloqueado)); if(bloqueado == null){
estado = "bloqueado"; }
else if(bloqueado.equals("bloqueado")) { estado = "desbloqueado";
~ 117 ~ }else{
Log.e("deviceDBHelper", "Error bloqueando Dispositivo no emparejado:"+dev.getAddress() ); }
ContentValues cv = new ContentValues();
cv.put(devicesContract.deviceEntry.bloqueado, estado); SQLiteDatabase db = getWritableDatabase();
db.update(devicesContract.deviceEntry.tableName, cv, devicesContract.deviceEntry.address + "='" + dev.getAddress()+"'", null);
result = true; }catch (Exception e){
Log.e("deviceDBHelper", "Error bloqueando Dispositivo: " + e.toString()); }
return result;
}
El siguiente método genera por medio de un query una consulta directa a la base de datos. Es usado de forma utilitaria por la aplicación.
/*
* Generar una consulta rapida retornando un cursor * */
public Cursor raw(String query){
SQLiteDatabase db = this.getWritableDatabase(); Cursor c = db.rawQuery(query, null);
return c;
}
La clase dispositivos instancia los elementos del layout activity_dispositivos, y se encarga de consultar y mostrar los dispositivos bluetooth emparejados por el dispositivo.
public class dispositivos extends AppCompatActivity { @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dispositivos); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar);
dispositivosFragment fragment = (dispositivosFragment)
getSupportFragmentManager().findFragmentById(R.id.content_dispositivos); if (fragment == null){
fragment = dispositivosFragment.newInstance();
getSupportFragmentManager().beginTransaction().add(R.id.content_dispositivos,
fragment).commit(); }
Button volverBtn;
volverBtn = findViewById(R.id.volverBtn2); //volver
volverBtn.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
dispositivosFragment fragment = (dispositivosFragment)
getSupportFragmentManager().findFragmentById(R.id.content_dispositivos);
getSupportFragmentManager().beginTransaction().remove(fragment).commit(); Intent intentMain = new Intent(dispositivos.this, MainActivity.class); startActivity(intentMain);
setContentView(R.layout.activity_main); dispositivos.this.finish();
} });
~ 118 ~
Esta función se ejecuta en cada llamado para tener la tabla actualizada con los dispositivos emparejados.
public boolean limpiaDispositivos(){
//limpiamos la BD de dispositivos boolean result = false;
try {
devicesDBHelper dbHelper;
dbHelper = new devicesDBHelper(getApplicationContext()); dbHelper.deleteDevices();
result = true; }catch(Exception e){
Log.e("dispositivos activity", "Limpiando la BD de dispositivos: " + e.toString()); }
return result;
}
La clase dispositivosCursor se encarga de instanciar los elementos del layout list_item_dispositivos a través de la clase padre CursorAdapter, el cual contiene uno a uno los registros de los dispositivos de la base de datos y genera la UI para la visualización de la información del dispositivo, así como la interfaz para el bloqueo del mismo.
public class dispositivosCursor extends CursorAdapter { Context context;
public dispositivosCursor (Context context, Cursor cursor){ super(context, cursor, 0);
this.context = context; }
@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup){ LayoutInflater inflater = LayoutInflater.from(context);
return inflater.inflate(R.layout.list_item_dispositivo, viewGroup, false); }
@Override
public void bindView(View view, final Context context, Cursor cursor){ View v = view;
//Referencias de UI
TextView nameText = (TextView) view.findViewById(R.id.tv_deviceName);
TextView addressText = (TextView) view.findViewById(R.id.tv_deviceAddress); ImageView btnBlock = (ImageView) view.findViewById(R.id.iv_btnBlock); //Get valores
final String name = cursor.getString(cursor.getColumnIndex(devicesContract.deviceEntry._ID))+" :
"+cursor.getString(cursor.getColumnIndex(devicesContract.deviceEntry.name)); final String address =
cursor.getString(cursor.getColumnIndex(devicesContract.deviceEntry.address));
En el método bindView seteamos los valores para la UI que se encuentran en el cursor.
//Setup
nameText.setText(name);
~ 119 ~
//Validamos que el estado del dispositivo en la BD devicesDBHelper dbHelper = new devicesDBHelper(context);
//Cursor c = dbHelper.raw("select "+devicesContract.deviceEntry.bloqueado+" from "+devicesContract.deviceEntry.tableName+" where
"+devicesContract.deviceEntry.address+"='"+devicesContract.deviceEntry.address+"'"); //Validamos que el dispositivo ya está en la BD
String args [] = new String[1]; String cols [] = new String[1];
args[0] = address;
cols[0] = devicesContract.deviceEntry.bloqueado;
Cursor c =
dbHelper.getDevice(devicesContract.deviceEntry.tableName,cols,devicesContract.deviceEntry.address+"=?",ar gs,null,null,null);
String lock = "empty";
if (c!= null && c.getCount() > 0){ c.moveToFirst();
// All the logic of retrieving data from cursor
lock = c.getString(c.getColumnIndex(devicesContract.deviceEntry.bloqueado));
}
Generamos los botones que disparan la acción de bloqueo de dispositivos, para cada registro, realizando la actualización en la base de datos del nuevo estado de conexión, bloqueado o desbloqueado, y es validado por el BroadCastReceiver que filtra los diferentes eventos bluetooth.
if(lock == null){
btnBlock.setImageResource(R.drawable.ic_unlock); }
else if(lock.equals("bloqueado")) {
btnBlock.setImageResource(R.drawable.ic_lock); }
else{
btnBlock.setImageResource(R.drawable.ic_unlock); }
//btn Bloqueo Dispositivo
btnBlock.setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View v) {
//Toast.makeText(context,address, Toast.LENGTH_SHORT).show();
device dev = new device();
dev.setAddress(address); dev.setName(name);
devicesDBHelper devHelper = new devicesDBHelper(context); boolean result = devHelper.blockDevice(dev);
ImageView btnBlock = (ImageView) v.findViewById(R.id.iv_btnBlock); if(btnBlock.getDrawable().getConstantState() ==
ContextCompat.getDrawable(context,R.drawable.ic_unlock).getConstantState()){ btnBlock.setImageResource(R.drawable.ic_lock);
Toast.makeText( context, "Dispositivo: "+dev.getName()+" Bloqueado.", Toast.LENGTH_SHORT).show();
}else{
btnBlock.setImageResource(R.drawable.ic_unlock);
Toast.makeText( context, "Dispositivo: "+dev.getName()+" Desbloqueado.", Toast.LENGTH_SHORT).show();
} } });
}
La clase dispositivosFragment que extiende de la clase Fragment se encarga de generar la vista, sirviéndose de la interfaz de base de datos devicesDBHelper. Instancia uno a uno cada dispositivo y lo representa en la UI.
~ 120 ~
public class dispositivosFragment extends Fragment { private devicesDBHelper mDevDbHelper;
private ListView mDevicesList;
private dispositivosCursor mDevicesAdapter; private FloatingActionButton mAddButton; public dispositivosFragment() {
// Required empty public constructor
}
/**
* Usamos este metodo factory para crear una nueva instancia de este fragmento. *
* @return una nueva instancia de dispositivosFragment. */
public static dispositivosFragment newInstance() { return new dispositivosFragment();
} @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
View root = inflater.inflate(R.layout.fragment_dispositivos, container, false);
//Referencias al UI
mDevicesList = (ListView) root.findViewById(R.id.dispositivos_list);
mDevicesAdapter = new dispositivosCursor(getActivity(), null); //Setup
mDevicesList.setAdapter(mDevicesAdapter);
//Instanciamos el helper
mDevDbHelper = new devicesDBHelper(getActivity());
//carga de dispositivos
loadDevices();
return root; }
private void loadDevices() {
new devicesLoadTask().execute(); }
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { }
public class devicesLoadTask extends AsyncTask<Void, Void, Cursor> { @Override
protected Cursor doInBackground(Void... voids) { return mDevDbHelper.getDevices();
} @Override
protected void onPostExecute(Cursor cursor) { if (cursor != null && cursor.getCount() > 0) { mDevicesAdapter.swapCursor(cursor); } else { cursor.close(); } } } } Paquete events
~ 121 ~
La clase event se encarga de los getters y los setters de las siguientes variables que podemos extraer de los eventos de conexión bluetooth, y se encarga de llevar el registro de los eventos generados en la persistencia de la base de datos.
public class event { private int id;
private String eventLog; private String Time; private String uriData; private String Host; private String Path; private String Query; private String Scheme; private String Port; private String userInfo; private String hashCode; public event() {
}
La clase eventsContract de igual forma que la clase deviceContract define un objeto BaseColumns, pero en este caso se usa para representar las columnas de la tabla de eventos que son las siguientes:
public class eventsContract {
public static abstract class eventEntry implements BaseColumns{ public static final String tableName = "events";
public static final String id = "id";
public static final String eventLog = "eventLog"; public static final String Time = "Time"; public static final String uriData = "uriData"; public static final String Host = "Host"; public static final String Path = "Path"; public static final String Query = "Query"; public static final String Scheme = "Scheme"; public static final String Port = "Port"; public static final String userInfo = "userInfo"; public static final String hashCode = "hashCode"; }
En la clase eventsDBHelper se genera los queries que son necesarios para la creación y manipulación de la base de datos de los eventos a través del motor de base de datos SQLiteDatabase. En esta clase se crea la base de datos events.db y generamos su versión. Luego se hace uso del método onCreate para crear la tabla de eventos:
public class eventsDBHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "events.db"; public eventsDBHelper (Context context){
super(context, DATABASE_NAME, null, DATABASE_VERSION); }
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL("CREATE TABLE " + eventsContract.eventEntry.tableName+ " (" + eventsContract.eventEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + eventsContract.eventEntry.id + " INTEGER ,"
+ eventsContract.eventEntry.eventLog + " TEXT ," + eventsContract.eventEntry.Time + " TEXT ,"
~ 122 ~
+ eventsContract.eventEntry.uriData + " TEXT ," + eventsContract.eventEntry.Host + " TEXT ," + eventsContract.eventEntry.Path + " TEXT ," + eventsContract.eventEntry.Query + " TEXT ," + eventsContract.eventEntry.Scheme + " TEXT ," + eventsContract.eventEntry.Port + " TEXT ," + eventsContract.eventEntry.userInfo + " TEXT ," + eventsContract.eventEntry.hashCode + " TEXT ," + "UNIQUE (" + eventsContract.eventEntry._ID + "))"); }
Ahora se hace uso del método ContetValues para manipular la base de datos. No olvidemos que ContentValues en Android permite colocar la información de un objeto en forma de pares para insertar o actualizar valores en las tablas.
public ContentValues toContentValues(event evt){ ContentValues values = new ContentValues();
values.put(eventsContract.eventEntry.id, evt.getId());
values.put(eventsContract.eventEntry.eventLog, evt.getEventLog()); values.put(eventsContract.eventEntry.Time, evt.getTime()); values.put(eventsContract.eventEntry.uriData, evt.getUriData()); values.put(eventsContract.eventEntry.Host, evt.getHost()); values.put(eventsContract.eventEntry.Path, evt.getPath()); values.put(eventsContract.eventEntry.Query, evt.getQuery()); values.put(eventsContract.eventEntry.Scheme, evt.getScheme()); values.put(eventsContract.eventEntry.Port, evt.getPort()); values.put(eventsContract.eventEntry.userInfo, evt.getUserInfo()); values.put(eventsContract.eventEntry.hashCode, evt.getHashCode()); return values;
}
Con el siguiente método saveEvent se logra guardar la información de un evento en la tabla de la base de datos event.
public long saveEvent(event evt){
SQLiteDatabase sqLiteDatabase = getWritableDatabase();
return sqLiteDatabase.insert(eventsContract.eventEntry.tableName, null,
toContentValues(evt));
}
El método getEvent nos permite consultar un evento en la base de datos por medio de la consulta tipo select *from.
public Cursor getEvent(String tabla, String[] columnas, String selection, String[] selectionArgs, String
groupBy, String having, String orderBy){
SQLiteDatabase sqLiteDatabase = getReadableDatabase(); Cursor c = sqLiteDatabase.query(
tabla, columnas, selection, selectionArgs, groupBy, having, orderBy ); return c; }
~ 123 ~
El log de los eventos realizados a través de bluetooth será mostrado al usuario final por medio de la aplicación. Es por esto que se crea la clase LogsActivity donde se crean las acciones con las que podrá interactuar con el usuario.
En el siguiente fragmento de código se puede observar la creación de la clase, los botones y el espacio del texto donde irá cada registro de evento en el log. La funcionalidad principal es poder visualizar un rango de tiempo en concreto para detectar todas las acciones realizadas por la interfaz bluetooth, una vez iniciada la aplicación. Se diseña de esta forma para que no sea intrusivo, creando, por ejemplo, un servicio que se ejecute aun cuando no está activa la aplicación.
public class LogsActivity extends AppCompatActivity { public static final Object[] DATA_LOCK = new Object[0]; TextView mLogBT;
Button VerLogsBtn, verLogBTABtn, volverBtn, borrarLogsBtn; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.consulta_logs); mLogBT = findViewById(R.id.logTv);
VerLogsBtn = findViewById(R.id.verLogsBtn); verLogBTABtn = findViewById(R.id.verLogBTABtn); volverBtn = findViewById(R.id.volverBtn); borrarLogsBtn = findViewById(R.id.borrarLogsBtn); //ver Log BLUETOOTH ADAPTER
verLogBTABtn.setOnClickListener(new View.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.N) @Override
public void onClick(View v) { mLogBT.setText("");
mLogBT.append("\n---Log Bluetooth Adapter---");
mLogBT.setText(Html.fromHtml(readLog("BluetoothAdapter.txt"), Html.FROM_HTML_MODE_LEGACY), TextView.BufferType.SPANNABLE);
mLogBT.setMovementMethod(new ScrollingMovementMethod()); }
});
Se tiene la opción de limpiar los logs de la aplicación para poder tener un marco limpio de referencia en cuanto a los eventos bluetooth.
//borrar Logs BLUETOOTH
borrarLogsBtn.setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View v) {
Context context = getBaseContext(); boolean result = false;
File path = context.getExternalFilesDir(null); File filedevices = new File(path, "devices.txt");
File fileBluetoothAdapter = new File(path, "BluetoothAdapter.txt"); Writer out1 = null, out2 = null;
try {
synchronized (DATA_LOCK) {
if (filedevices != null || fileBluetoothAdapter != null) { filedevices.createNewFile();
fileBluetoothAdapter.createNewFile();
out1 = new BufferedWriter(new FileWriter(filedevices, false), 1024);
out2 = new BufferedWriter(new FileWriter(fileBluetoothAdapter, false), 1024); out1.write("");
out1.close(); out2.write(""); out2.close(); result = true;
~ 124 ~
} }
} catch (FileNotFoundException e) {
Log.e("logs activity", "File not found: " + e.toString()); } catch (IOException e) {
Log.e("logs activity", "Can not read file: " + e.toString()); }
} });
La acción de volver para poder continuar interactuando con la aplicación en el menú principal.
//volver
volverBtn.setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View v) {
Intent intentMain = new Intent(LogsActivity.this, MainActivity.class); startActivity(intentMain);
setContentView(R.layout.activity_main); LogsActivity.this.finish();
} });
La función usada para leer los logs del sistema, busca en el directorio de la aplicación el archivo consultado y retorna su contenido. Existen dos archivos: el de eventos y el de dispositivos.
//lee el Log de app
public String readLog(String fileName){ Context context = getBaseContext();
File path = context.getExternalFilesDir(null); File file = new File(path, fileName);
StringBuilder textLog = new StringBuilder(); try {
BufferedReader br = new BufferedReader(new FileReader(file)); String line;
while((line = br.readLine()) != null){ textLog.append(line);
textLog.append('\n'); }
br.close();
if(textLog.toString() == ""){ textLog.append("Empty file"); }
}
catch (FileNotFoundException e) {
Log.e("read log activity", "File not found: " + e.toString()); } catch (IOException e) {
Log.e("read log activity", "Can not read file: " + e.toString()); }
return String.valueOf(textLog);
}
La clase MainActivity contiene la lógica principal que controla la aplicación, se