Activité n°4 : Bluetooth

Objectif

L’objectif est d’écrire une application Android qui communique en Bluetooth pour commander des E/S à distance.

Pré-requis

En reprenant les applications réalisées dans les activités précédentes :

  • Copier et renommer le dossier de la première application en MyApplication2

  • Renommer l’attribut package dans $HOME/AndroidStudioProjects/MyApplication2/app/src/main/AndroidManifest.xml

  • Renommer la valeur d’applicationId dans /home/tv/AndroidStudioProjects/MyApplication2/app/build.gradle

  • Démarrer Android Studio et ouvrir le projet MyApplication2

  • Faire ‘Synchronize’

  • Refactoriser le nom du package puis renommer la valeur d’app_name dans strings.xml si nécessaire

Activité n°4

AndroidManifest.xml

L’application a besoin des droits d’accès (uses-permission) au Bluettoth.

Pour cela, il faut modifier le fichier AndroidManifest.xmlde l’application et reconstruire le projet :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.tv.myapplication4">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name=".MyActivity" android:theme="@style/AppTheme.NoTitleBar">
        </activity>
    </application>
</manifest>
  1. Éditer le fichier AndroidManifest.xml de l’application et reconstruire le projet.

Service Bluetooth

L’application nécessite la présence du Bluetooth pour fonctionner. On va tout d’abord vérifier son fonctionnement avant de valider les boutons qui en dépendent.

Pour cela, on ajoute le code suivant dans la méthode onCreate() de l’activité :

private final static int REQUEST_CODE_ENABLE_BLUETOOTH = 0;

...

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null)
{
   Toast.makeText(getApplicationContext(), "Bluetooth non activé !", Toast.LENGTH_SHORT).show();
}
else
{
    if (!bluetoothAdapter.isEnabled())
    {
        Toast.makeText(getApplicationContext(), "Bluetooth non activé !", Toast.LENGTH_SHORT).show();
        // Possibilité 1 :
        Intent activeBlueTooth = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(activeBlueTooth, REQUEST_CODE_ENABLE_BLUETOOTH);
        // ou Possibilité 2:
        //bluetoothAdapter.enable();
    }
    else
    {
        Toast.makeText(getApplicationContext(), "Bluetooth activé", Toast.LENGTH_SHORT).show();
    }
}

// Possibilité 1 :
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode != REQUEST_CODE_ENABLE_BLUETOOTH)
       return;
    if (resultCode == RESULT_OK)
    {
        Toast.makeText(getApplicationContext(), "Bluetooth activé", Toast.LENGTH_SHORT).show();
    }
    else
    {
        Toast.makeText(getApplicationContext(), "Bluetooth non activé !", Toast.LENGTH_SHORT).show();
    }
}
  1. Tester en activant puis en désactivant le Bluetooth du smartphone.

Obtenir la liste des périphériques

La méthode getBondedDevices() retourne la liste des périphériques connus :

private Set<BluetoothDevice> devices;

devices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice blueDevice : devices)
{
   Toast.makeText(getApplicationContext(), "Device = " + blueDevice.getName(), Toast.LENGTH_SHORT).show();
}
  1. Apairer un périphérique Bluetooth puis tester.

Recherche de nouveaux périphériques

Pour détecter de nouveaux périphériques, il va falloir être capable de recevoir des intents. Pour pouvoir recevoir des intents, Android fournit une classe BroadcastReceiver. Ces objets sont conçus pour recevoir des Intent et appliquer des comportements spécifiques en surchargeant la méthode onReceive().

Pour créer un nouveau BroadcastReceiver, on effectue un clic droit sur le nom du package puis New -> Other -> Broadcast Receiver et on donne un nom à la classe (par exemple ReceiverBluetooth). Le BroadcastReceiver est alors ajouté automatiquement au fichier AndroidManifest.xml sinon il faudra le faire manuellement.

public class ReceiverBluetooth extends BroadcastReceiver
{
    public ReceiverBluetooth()
    {
    }

    @Override
    public void onReceive(Context context, Intent intent)
    {
        String action = intent.getAction();
        if (BluetoothDevice.ACTION_FOUND.equals(action))
        {
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            Toast.makeText(context, "Nouveau périphérique : " + device.getName(), Toast.LENGTH_SHORT).show();
        }
    }
}

Android fournit également un système de filtres IntentFilter que l’on peut déclarer dans le fichier AndroidManifest.xml ou directement dans le code source. Il y a plusieurs niveaux de filtrage (action, catégorie ou données). On enregistre le filtre avec la méthode registerReceiver() et on le désenregistre avec unregisterReceiver().

receiverBluetooth = new ReceiverBluetooth();
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(bluetoothReceiver, filter);

On peut aussi créer un Broadcast Receiver directement dans le code :

private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver()
{
    public void onReceive(Context context, Intent intent)
    {
      String action = intent.getAction();
      if (BluetoothDevice.ACTION_FOUND.equals(action))
      {
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        Toast.makeText(getApplicationContext(), "Nouveau périphérique : " + device.getName(), Toast.LENGTH_SHORT).show();
      }
    }
};

On lance la recherche à partir de la méthode startDiscovery() de BluetoothAdapter et on l’arrête avec cancelDiscovery().

adaptateurBluetooth.startDiscovery();

@Override
protected void onDestroy()
{
    super.onDestroy();
    if (adaptateurBluetooth != null)
    {
        adaptateurBluetooth.cancelDiscovery();
        unregisterReceiver(receiverBluetooth);
        //unregisterReceiver(bluetoothReceiver);
    }
}

Rendre visible le périphérique

Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

Une classe Peripherique bluetooth

  1. Créer une nouvelle classe Peripherique :
public class Peripherique
{
    private String nom;
    private String adresse;
    private Handler handler = null;
    private BluetoothDevice device = null;

    public Peripherique(BluetoothDevice device, Handler handler)
    {
        if(device != null)
        {
            this.device = device;
            this.nom = device.getName();
            this.adresse = device.getAddress();
            this.handler = handler;
        }
        else
        {
            this.device = device;
            this.nom = "Aucun";
            this.adresse = "";
            this.handler = handler;
        }

        // TODO
    }

    public String getNom()
    {
        return nom;
    }

    public String getAdresse()
    {
        return adresse;
    }

    public boolean estConnecte()
    {
        // TODO

        return false;
    }

    public void setNom(String nom)
    {
        this.nom = nom;
    }

    public String toString()
    {
        return "\nNom : " + nom + "\nAdresse : " + adresse;
    }

    public void envoyer(String data)
    {
        // TODO
    }

    public void connecter()
    {
        // TODO
    }

    public boolean deconnecter()
    {
        // TODO
    }
}

On la complètera par la suite pour la partie communication.

L’IHM

  1. Créer le layout suivant :

L’activité

Dans la méthode onCreate() de l’activité, on va intégrer la prise en charge du service Bluetooth, la détection des périphériques connus et leur affichage dans un Spinner.

  1. Éditer la classe MyActivity :
@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);

    setContentView(R.layout.commande);

    ...

    adaptateurBluetooth = BluetoothAdapter.getDefaultAdapter();
    if (adaptateurBluetooth == null)
    {
        btnConnecter.setEnabled(false);
        btnDeconnecter.setEnabled(false);
        Toast.makeText(getApplicationContext(), "Bluetooth non activé !", Toast.LENGTH_SHORT).show();
    }
    else
    {
        if (!adaptateurBluetooth.isEnabled())
        {
            Toast.makeText(getApplicationContext(), "Bluetooth non activé !", Toast.LENGTH_SHORT).show();
            Intent activeBlueTooth = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(activeBlueTooth, REQUEST_CODE_ENABLE_BLUETOOTH);
            //bluetoothAdapter.enable();
        }
        else
        {
            Toast.makeText(getApplicationContext(), "Bluetooth activé", Toast.LENGTH_SHORT).show();

            // Recherche des périphériques connus
            peripheriques = new ArrayList<Peripherique>();
            noms = new ArrayList<String>();
            devices = adaptateurBluetooth.getBondedDevices();
            for (BluetoothDevice blueDevice : devices)
            {
                Toast.makeText(getApplicationContext(), "Périphérique = " + blueDevice.getName(), Toast.LENGTH_SHORT).show();
                peripheriques.add(new Peripherique(blueDevice, handler));
                noms.add(blueDevice.getName());
                btnConnecter.setEnabled(true);
                btnDeconnecter.setEnabled(false);
            }

            if(peripheriques.size() == 0)
                peripheriques.add(new Peripherique(null, handler));

            if(noms.size() == 0)
                noms.add("Aucun");

            ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, noms);
            adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
            spinnerListePeripheriques.setAdapter(adapter);
            adapter.setNotifyOnChange(true);

            spinnerListePeripheriques.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
            {
                @Override
                public void onItemSelected(AdapterView<?> arg0, View arg1, int position, long id)
                {
                    //Toast.makeText(getBaseContext(), noms.get(position), Toast.LENGTH_SHORT).show();
                    peripherique = peripheriques.get(position);
                }

                @Override
                public void onNothingSelected(AdapterView<?> arg0)
                {
                    // TODO Auto-generated method stub
                }
            });
        }
    }
}

La communication

Pour assurer une communication et échanger des données, il faut un côté serveur (le périphérique qui attend et accepte la connexion) et un côté client (le périphérique qui fait la demande de la connexion).

Le smartphone jouera le rôle du client :

  • créer une socket de type BluetoothSocket avec la méthode createRfcommSocketToServiceRecord()
  • faire une demande de connexion en appelant connect()

La communication se fera dans un thread et on aura les mêmes problèmes que pour l’activité n°3 : L’UI thread est responsable de l’affichage et des interactions avec l’utilisateur et surtout c’est le seul thread qui doit modifier l’affichage.

  1. Éditer les nouveaux attributs et le constructeur dans la classe Peripherique qui hérite maintenant de la classe Thread :
public class Peripherique extends Thread
{
    ...
    private BluetoothSocket socket = null;
    private InputStream receiveStream = null;
    private OutputStream sendStream = null;
    private TReception tReception;
    public final static int CODE_CONNEXION = 0;
    public final static int CODE_RECEPTION = 1;
    public final static int CODE_DECONNEXION = 2;

    public Peripherique(BluetoothDevice device, Handler handler)
    {
        if(device != null)
        {
            this.device = device;
            this.nom = device.getName();
            this.adresse = device.getAddress();
            this.handler = handler;
        }
        else
        {
            this.device = device;
            this.nom = "Aucun";
            this.adresse = "";
            this.handler = handler;
        }

        try
        {
            socket = device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
            receiveStream = socket.getInputStream();
            sendStream = socket.getOutputStream();
        }
        catch (IOException e)
        {
            e.printStackTrace();
            socket = null;
        }

        if(socket != null)
            tReception = new TReception(handler);
    }
  1. On va intégrer dans la classe un thread seulement pour la reception :
private class TReception extends Thread
{
    Handler handlerUI;
    private boolean fini;

    TReception(Handler h)
    {
        handlerUI = h;
        fini = false;
    }

    @Override public void run()
    {
        BufferedReader reception = new BufferedReader(new InputStreamReader(receiveStream));
        while(!fini)
        {
            try
            {
                String trame = "";
                if(reception.ready())
                {
                    trame = reception.readLine();
                }
                if(trame.length() > 0)
                {
                    Log.d(TAG, "run() trame : " + trame);
                    Message msg = Message.obtain();
                    msg.what = Peripherique.CODE_RECEPTION;
                    msg.obj = trame;
                    handlerUI.sendMessage(msg);
                }
            }
            catch (IOException e)
            {
                //System.out.println("<Socket> error read");
                e.printStackTrace();
            }
            try
            {
                Thread.sleep(250);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    public void arreter()
    {
        if(fini == false)
        {
            fini = true;
        }
        try
        {
            Thread.sleep(250);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

Remarque : on utilise la classe Handler pour assurer l’affichage dans l’IHM (cf. l’activité n°2).

  1. On ajoute maintenant les méthodes connecter(), deconnecter() et envoyer() :
public void connecter()
{
    new Thread()
    {
        @Override public void run()
        {
            try
            {
                socket.connect();

                Message msg = Message.obtain();
                msg.arg1 = CODE_CONNEXION;
                handler.sendMessage(msg);

                tReception.start();

            }
            catch (IOException e)
            {
                System.out.println("<Socket> error connect");
                e.printStackTrace();
            }
        }
    }.start();
}

public boolean deconnecter()
{
    try
    {
        tReception.arreter();

        socket.close();
        return true;
    }
    catch (IOException e)
    {
        System.out.println("<Socket> error close");
        e.printStackTrace();
        return false;
    }
}

public void envoyer(String data)
{
    if(socket == null)
        return;

    new Thread()
    {
        @Override public void run()
        {
            try
            {
                if(socket.isConnected())
                {
                    sendStream.write(data.getBytes());
                    sendStream.flush();
                }
            }
            catch (IOException e)
            {
                System.out.println("<Socket> error send");
                e.printStackTrace();
            }
        }
    }.start();
}
  1. Ajouter maintenant dans l’activité la prise en charge des boutons Connecter et Déconnecter :
public void onClick(View v)
{
    if(v.getId() == R.id.buttonConnecter)
    {
        peripherique.connecter();
    }

    if(v.getId() == R.id.buttonDeconnecter)
    {
        if(peripherique.deconnecter())
        {
            btnConnecter.setEnabled(true);
            btnDeconnecter.setEnabled(false);
        }
    }

    gererBoutons(v);
}
  1. Écrire la méthode gererBoutons() qui permet d’envoyer une chaîne de caractères au périphérique serveur en fonction du ToogleButton sélectionné.

  2. Tester avec la carte fournie :

Documentation

Retour