Bluetooth (II): Descubriendo dispositivos


Una vez que sabemos cómo funciona el proceso de activación y desactivación del Bluetooth, es hora de buscar dispositivos cercanos que nos permitan establecer una conexión. Para entender los ejemplos mostrados en este artículo se recomienda leer el artículo anterior, Bluetooth (I): Activando y desactivando el Bluetooth en Android.

Como vimos, para que se realice una conexión Bluetooth es necesaria la siguiente secuencia de pasos:

  1. El dispositivo B se configura como servidor, aceptando conexiones entrantes.
  2. El dispositivo A solicita la conexión al dispositivo B. Para ello, es necesario que A conozca la dirección física de B.
  3. En el caso de que A no conozca la dirección de B, la conexión no podrá llevarse a cabo.

Dado que las direcciones no se conocen a priori, es necesario un mecanismo que permita a un dispositivo Bluetooth “encontrar” a otro. Este proceso se denomina descubrimiento o discovery.

Para que el dispositivo A descubra al dispositivo B, es necesario que B, además de tener activado el Bluetooth, cambie su estado a “visible”. Esto lo hacíamos solicitándoselo de forma explícita al usuario, tal y como vimos en el artículo anterior.


	Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
	startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);

Partiendo de la base de que el dispositivo B se encuentra visible con el Bluetooth activo, ¿cómo hacemos que el dispositivo A le “descubra”?

Descubriendo dispositivos

Nuevamente estamos haciendo uso del Bluetooth del dispositivo y de su administración, por lo que debemos declarar los permisos pertinentes en el manifiesto:


    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Añadimos en el layout un Button que se encargue de activar el proceso de descubrimiento.


    <RelativeLayout
    	android:layout_width="match_parent"
	    android:layout_height="0dp"
	    android:layout_weight="2" >

        <Button
	        android:id="@+id/btnBuscarDispositivo"
    	    android:layout_width="wrap_content"
        	android:layout_height="wrap_content"
	        android:layout_centerHorizontal="true" />
    </RelativeLayout>

A continuación, declaramos como atributos en nuestro código una referencia al botón y otra al adaptador Bluetooth. También declararemos un ArrayList de dispositivos Bluetooth (BluetoothDevice) para almacenar aquellos dispositivos que vayamos descubriendo.


	private Button btnBuscarDispositivo = (Button)findViewById(R.id.btnBuscarDispositivo);
	private BluetoothAdapter bAdapter;
	private ArrayList<BluetoothDevice> arrayDevices;

Al igual que hicimos para detectar los cambios de estado, haremos uso de un BroadcastReceiver para detectar dos tipos de acciones:

  • BluetoothDevice.ACTION_FOUND: cada vez que se descubra un nuevo dispositivo. Nos servirá para añadirlo al array.
  • BluetoothAdapter.ACTION_DISCOVERY_FINISHED: lanzado cuando finalice el proceso de descubrimiento.

	// Instanciamos un BroadcastReceiver que se encargara de detectar cuando
	// un dispositivo es descubierto.
	private final BroadcastReceiver bReceiver = new BroadcastReceiver()
	{
		@Override
	    public void onReceive(Context context, Intent intent)
		{
			// Cada vez que se descubra un nuevo dispositivo por Bluetooth, se ejecutara
	        // este fragmento de codigo
	        if (BluetoothDevice.ACTION_FOUND.equals(action))
	        {
				// Acciones a realizar al descubrir un nuevo dispositivo
			}

			// Codigo que se ejecutara cuando el Bluetooth finalice la busqueda de dispositivos.
	        else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action))
	        {
				// Acciones a realizar al finalizar el proceso de descubrimiento
			}
	    }
	};

Cada vez que detectemos un nuevo dispositivo, lo añadiremos al ArrayList arrayDevices y mostraremos en el Toast que se ha descubierto un nuevo dispositivo. Este dispositivo estará disponible como objeto extra que implementa Parcelable, y se podrá acceder a él mediante la clave BluetoothDevice.EXTRA_DEVICE.


	// Si el array no ha sido aun inicializado, lo instanciamos
	if(arrayDevices == null)
		arrayDevices = new ArrayList<BluetoothDevice>();

	// Extraemos el dispositivo del intent mediante la clave BluetoothDevice.EXTRA_DEVICE
	BluetoothDevice dispositivo = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

	// Añadimos el dispositivo al array
	arrayDevices.add(dispositivo);

	// Le asignamos un nombre del estilo NombreDispositivo [00:11:22:33:44]
	String descripcionDispositivo = dispositivo.getName() + " [" + dispositivo.getAddress() + "]";

	// Mostramos que hemos encontrado el dispositivo por el Toast
	Toast.makeText(getBaseContext(), getString(R.string.DetectadoDispositivo) + ": " + descripcionDispositivo, Toast.LENGTH_SHORT).show();

A continuación codificaremos el comportamiento de la acción BluetoothAdapter.ACTION_DISCOVERY_FINISHED, capturada cuando finalice el proceso de descubrimiento. Para ello haremos uso del elemento de interfaz ListView. Sin embargo, será necesario crear un adaptador para que los dispositivos se muestren correctamente en este elemento.

Rellenando un ListView mediante una colección: ArrayAdapter y LayoutInflater

Comenzaremos por añadir un ListView a nuestro layout


    <ListView
        android:id="@+id/lvDispositivos"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="7">
    </ListView>

A continuación, referenciaremos el ListView desde nuestra actividad, tal y como hicimos con el botón encargado de iniciar el descubrimiento.


	private ListView lvDispositivos = (ListView)findViewById(R.id.lvDispositivos);

Es el momento de crear la clase encargada de adaptar los datos para que sean mostrados en nuestro ListView. Crearemos una clase a la que llamaremos BluetoothDeviceArrayAdapter y que heredará de android.widget.ArrayAdapter.

Contendrá dos atributos privados: una lista de dispositivos y el contexto. El constructor por defecto se encargará de asignar los objetos pasados como parámetros a los atributos locales.


	public class BluetoothDeviceArrayAdapter extends ArrayAdapter
	{

		private List<BluetoothDevice> deviceList;	// Contendra el listado de dispositivos
		private Context context;					// Contexto activo

		public BluetoothDeviceArrayAdapter(Context context, int textViewResourceId,
											List<BluetoothDevice> objects) {
			// Invocamos el constructor base
			super(context, textViewResourceId, objects);

			// Asignamos los parametros a los atributos
			this.deviceList = objects;
			this.context = context;
		}
	}

A continuación deberemos sobrecargar algunos métodos. Los más importantes son:

  • getCount(): modificamos su comportamiento para que devuelva el número de dispositivos contenidos en la lista.

	@Override
	public int getCount()
	{
		if(deviceList != null)
			return deviceList.size();
		else
			return 0;
	}

  • getItem(): modificamos su comportamiento para evitar el riesgo de excepciones en caso de que la lista sea null.

	@Override
	public Object getItem(int position)
	{
		return (deviceList == null ? null : deviceList.get(position));
	}

  • getView(): Este método es el núcleo del adaptador. Se encarga, literalmente, de generar dinámicamente un objeto de interfaz (View) personalizado a partir de cualquier otro tipo de dato para poder insertarlo en el ListView. En el caso que nos ocupa, crearemos una vista con dos TextView superpuestos. El superior, de mayor tamaño, mostrará el nombre del dispositivo. El inferior, de menor tamaño, mostrará la dirección del dispositivo. Comenzaremos con el cuerpo del método:

	@Override
	public View getView(int position, View convertView, ViewGroup parent)
	{
		return null;
	}

En primer lugar comprobamos que la lista de dispositivos y el contexto están correctamente inicializados.


	if((deviceList == null) || (context == null))
		return null;

A continuación usamos un LayoutInflater para generar los elementos de interfaz de forma dinámica. Simplificando, la clase LayoutInflater tiene como principal función realizar la traducción entre un layout y el objeto correspondiente, es decir, toma como entrada un fichero xml y proporciona como salida un objeto View con todos sus elementos instanciados.


	// Usamos un LayoutInflater para crear las vistas
	LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

Android proporciona un conjunto de layouts predefinidos, que pueden consultarse en el repositorio git de Android. Nosotros haremos uso de simple_list_item_2, accesible usando el identificador android.R.layout.simple_list_item_2.

Podemos echar un vistazo al código xml original de este fichero aquí.

Como podemos apreciar, el layout consta de dos elementos de tipo TextView, cuyos identificadores son text1 y text2, respectivamente. Si hacemos uso del LayoutInflater tomando este elemento como entrada, obtendremos un objeto de la clase View que estará compuesto por estos mismos elementos.


	// Creamos una vista a partir de simple_list_item_2, que contiene dos TextView.
	// El primero (text1) lo usaremos para el nombre, mientras que el segundo (text2)
	// lo utilizaremos para la direccion del dispositivo.
	View elemento = inflater.inflate(android.R.layout.simple_list_item_2, parent, false);

	// Referenciamos los TextView
	TextView tvNombre = (TextView)elemento.findViewById(android.R.id.text1);
	TextView tvDireccion = (TextView)elemento.findViewById(android.R.id.text2);

Hecho esto, ya sólo nos resta asignarle valor a ambos campos, que se corresponderán con el nombre y la dirección del dispositivo, respectivamente. Por tanto, invocamos el método getItem(int position) que sobrecargamos previamente pasándole el parámetro position para obtener el dispositivo correspondiente a la posición en el array. Obtenida la referencia al dispositivo, asignamos el texto correspondiente a los TextView y devolvemos el resultado. Nuestro adaptador estaría completo.


	// Obtenemos el dispositivo del array y obtenemos su nombre y direccion, asociandosela
	// a los dos TextView del elemento
	BluetoothDevice dispositivo = (BluetoothDevice)getItem(position);
	if(dispositivo != null)
	{
		tvNombre.setText(dispositivo.getName());
		tvDireccion.setText(dispositivo.getAddress());
	}
	else
	{
		txtNombre.setText("ERROR");
	}

	// Devolvemos el elemento con los dos TextView cumplimentados
	return elemento;

Ahora que hemos creado un adaptador para el ListView, es hora de hacer uso de él.

Regresemos al código del método onReceive() del BroadcastReceiver y completemos la sección destinada a detectar cuándo finaliza el proceso de descubrimiento. Instanciaremos un objeto de la clase que acabamos de crear pasándole como parámetros:

  • Context context: contexto base, que puede obtenerse invocando al método getBaseContext().
  • int textViewResourceId: identificador del recurso a utilizar. Será el mismo que usamos en el interior del adaptador.
  • List objects: ArrayList de dispositivos, correspondientes al objeto arrayDevices

Una vez instanciado el adapter, se lo asociamos al ListView y mostramos por el Toast que el proceso ha finalizado.


	if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action))
	{
		// Instanciamos un nuevo adapter para el ListView mediante la clase que acabamos de crear
		ArrayAdapter arrayAdapter = new BluetoothDeviceArrayAdapter(getBaseContext(), android.R.layout.simple_list_item_2, arrayDevices);

		lvDispositivos.setAdapter(arrayAdapter);
		Toast.makeText(getBaseContext(), "Fin de la búsqueda", Toast.LENGTH_SHORT).show();
	}

Completado nuestro método onReceive(), será necesario que el BroadcastReceiver sea capaz de recibir los resultados de las acciones, para lo cual deberemos suscribirlo a las dos acciones que estamos filtrando. El siguiente método se encargará de ello:


    private void registrarEventosBluetooth()
    {
   		// Registramos el BroadcastReceiver que instanciamos previamente para
   		// detectar las distintos acciones que queremos recibir
   		IntentFilter filtro = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
   		filtro.addAction(BluetoothDevice.ACTION_FOUND);

   	    this.registerReceiver(bReceiver, filtro);
    }

Iniciando el proceso de descubrimiento

Sólo nos queda hacer que se lance el proceso de descubrimiento. Lo codificaremos en el método onClick() filtrando por el id del botón btnBuscarDispositivo, tal y como hicimos en el artículo previo. Comprobaremos si existe un proceso de descubrimiento en curso, en cuyo caso lo cancelaremos y lo volveremos a iniciar. Finalmente, mostramos en el Toast que se ha comenzado el proceso.


	case R.id.btnBuscarDispositivo:
	{
		if(arrayDevices != null)
			arrayDevices.clear();

		// Comprobamos si existe un descubrimiento en curso. En caso afirmativo, se cancela.
		if(bAdapter.isDiscovering())
			bAdapter.cancelDiscovery();

		// Iniciamos la busqueda de dispositivos y mostramos el mensaje de que el proceso ha comenzado
		if(bAdapter.startDiscovery())
			Toast.makeText(this, "Iniciando búsqueda de dispositivos bluetooth", Toast.LENGTH_SHORT).show();
		else
			Toast.makeText(this, "Error al iniciar búsqueda de dispositivos bluetooth", Toast.LENGTH_SHORT).show();
		break;
	}

El resumen del proceso podría resumirse en lo siguiente:

  1. Codificar el comportamiento esperado al descubrir un dispositivo y finalizar la búsqueda en el método onReceive del BroadcastReceiver. Este código podría consistir, por ejemplo, en añadir el dispositivo al array cada vez que se detecte un nuevo dispositivo y mostrar un listado con todos los dispositivos detectados una vez haya finalizado el proceso de descubrimiento.
  2. Suscribir el BroadcastReceiver a las acciones BluetoothDevice.ACTION_FOUND y BluetoothAdapter.ACTION_DISCOVERY_FINISHED.
  3. Comenzar el proceso de descubrimiento al pulsar un botón.

Bluetooth discovery

El siguiente paso, ahora que ya conocemos cómo obtener la dirección de un dispositivo, será la de establecer una conexión. Sin embargo, para ello harán falta unas pequeñas nociones de la arquitectura cliente-servidor. Eso lo veremos más adelante.

Artículos relacionados

Bluetooth (I): Activando y desactivando el Bluetooth en Android

Anuncios

15 comments

  1. Hola Daniel, muchas gracias por tu post, está muy bueno, y muy bien explicado; pero tengo una pregunta con respecto al código de la app que publicaste. Para programar el botón de salir, tú delcaraste al inicio del programa la sentencia: “private BluetoothService servicio”, pero cuando yo intento hacer lo mismo (estoy trabajando con androidStudio, la última versión), me dice que no puede reconocer el símbolo “BluetoothService”. En ese caso… ¿Existe algún equivalente con nuevas versiones para reemplazarlo? o.. ¿Qué me sugieres que haga?. De ante mano, muchas gracias por tu ayuda.

  2. Genial aporte Roberto! estoy usando tu ejemplo para el desarrollo de una aplicacion para comunicacion con arduino.. te mando un saludo desde Argentina!

  3. Hola Daniel! Lo primero agradecerte este tutorial porque está genial explicado y me está sirviendo de mucho. Tengo un problema y te escribo a ver si puedes conocer la solución a este. Resulta que el apartado (I) funciona perfectamente, pero ahora en el (II) tengo problemas al hacer el Toast. Poníamos un if para ACTION_FOUND, pero estoy viendo que no me entra ahí nunca. No he escrito más código, me he quedado en ese paso. Mi smartphone reconoce diferentes dispositivos bluetooth pero luego no me los muestra con el toast, repito, ni siquiera me entra en la condicion….. Espero haberme explicado bien y muchas gracias de antemano.

    Un saludo,
    Ángela!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s