• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 13/01/2020

Architecture de communication téléphone-montre

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Dans ce chapitre, on va s’intéresser à la connectivité de la montre et plus particulièrement à l’échange de données entre la montre et le téléphone mobile.

Connectivité de la montre

La montre communique avec internet au travers de la connexion bluetooth avec le téléphone. Ce dernier relaie donc les requêtes HTTP par exemple. En cas de perte de connexion, la montre bascule sur le wifi (si disponible) et communique en direct avec internet. Cela consomme plus d’énergie mais permet à la montre de maintenir des interactions avec le réseaux.

Accès internet pour une montre connectée
Accès internet pour une montre connectée

Pour ce qui concerne les interactions entre la téléphone et la montre, il existe plusieurs façons de les réaliser.

Tout d’abord, les notifications sont automatiquement envoyées à la montre, ce qui permet de ne pas avoir à programmer cet envoi dans l’application mobile et à développer une application correspondante pour la montre. Quand les données à envoyer sont plus complexes qu’une notification, et surtout lorsque l’on veut réaliser des traitements du côté de la montre et un affichage particulier, on doit utiliser des classes dédiées à la communication entre téléphone et montre.

Ces classes particulières permettent de répondre à trois types de besoins :

  • Envoi de message : Un message est construit par l’un des appareils et envoyé aux autres. Les messages utilisent la classe  MessageClient.

  • Synchronisation de données : un des deux appareils peut publier des données (par exemple une image) qui sont synchronisées sur tous les appareils. La synchronisation utilise les classes  DataItem  et  Asset.

  • Envoi de gros fichiers: des données souvent volumineuses et que l’on ne veut pas synchroniser de manière persistante sont à envoyées. On utilise pour cela la classe  ChannelClient.

L’envoi de données ou fichiers entre les appareils se fait au travers du canal de communication bluetooth, mais peut aussi se faire par les serveurs Google si la montre connectée se connecte au réseau au travers du wifi. On peut donc réaliser des communications entre plusieurs appareils distants au travers de ces APIs sans se soucier de la manière dont ils sont connectés entre eux, comme représenté sur le schéma ci-dessous et issu de la documentation fournie par Google.

Deux montres ayant deux moyens de communication différents avec le téléphone - Crédit: Google CC BY
Deux montres ayant deux moyens de communication différents avec le téléphone - Crédit: Google CC BY

Dans la suite, nous explorerons trois moyens de communication : les notifications, l'envoi de messages, et l'envoi de fichiers.

Notifications

Voyons maintenant le cas le plus simple qui permet de réaliser une interaction entre le téléphone et la montre : l’envoi de notifications.

Une notification, composée d’une icône, d’un titre et d’un texte, se crée classiquement de la manière suivante, du côté téléphone :

NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(this, "M_CH_ID")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("Hey !")
.setContentText("You should check this information...");

On ajoute ensuite un  Intent  qui est “en attente” (PendingIntent) puisqu’il s’agit d’un  Intent  qui sera envoyé à l’application du téléphone si une action “My action” est réalisée sur la montre connectée. Du côté de la montre tout est automatique : la notification qui s’affiche possède alors un bouton “My action” et un clic sur celui-ci déclenche l'envoi de l'Intent  côté téléphone.

// Create an intent for the reply action getBroadcast enables to have a broadcast
// message sent to the phone
Intent actionIntent = new Intent("mynotification.intent");
PendingIntent actionPendingIntent = PendingIntent.getBroadcast(this, 0, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Create the action
NotificationCompat.Action action = new NotificationCompat.Action.Builder(R.drawable.ic_launcher_background,
"My action", actionPendingIntent)
.build();
// Add action
mBuilder.extend(new NotificationCompat.WearableExtender().addAction(action));

Sur le téléphone, pour réaliser un traitement associé à cet  Intent , il faut le filtrer et réaliser une action, par exemple ici, cocher la case à cocher :

// Waiting for the PendingIntent from the wear app
br_ = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i("CIO", "PendingIntent received !");
CheckBox cb = findViewById(R.id.checkBox);
cb.setChecked(true); // Check the box !
}
};
IntentFilter filter = new IntentFilter("mynotification.intent");
this.registerReceiver(br_, filter);

À l’écran, on obtient l’image suivante, où la case à cocher est cochée à cause de l’appui sur “My action” :

Notification avec action sur la montre envoyée depuis le téléphone
Notification avec action sur la montre envoyée depuis le téléphone

On note qu’avec de tels mécanismes, on évite l’implémentation d’une application spécifique du côté de la montre tout en conservant la possibilité d’interagir avec l’utilisateur.

Messages

L’envoi de messages depuis la montre vers le téléphone (et inversement) va permettre de réaliser des traitements côté téléphone (et inversement). Depuis Android Wear 2, les objets à utiliser pour communiquer avec Wear ont changé et GoogleApiClient est déprécié . On peut se référer à la documentation pour voir quels sont les changements.

Dans la nouvelle API, un simple appel statique permet de récupérer un objet  CapabilityClient  qui va servir à la communication :

CapabilityClient capabilityClient = Wearable.getCapabilityClient(this);

Puis, on peut interroger les capacités des noeuds connectés. Ainsi, on pourra filtrer ceux qui disposent d’une capacité qui nous intéresse. Les capacités sont simplement des chaînes de caractères que l’on déclare du côté du noeuds distants dans le fichier  res/values/wear.xml  (ici, côté téléphone) :

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="android_wear_capabilities">
<item>my_capability</item>
</string-array>
</resources>

Dans l’exemple ci-dessus, le téléphone déclare une capacité “my_capability”. Du côté montre, on peut alors chercher cette capacité dans une tâche asynchrone et filtrer celle qui nous intéresse. Dans l’exemple ci-dessous, côté montre, on affiche juste les capacités trouvées, et on enregistre tous les noeuds trouvés dans l’objet nodes (HashSet ) :

Task<Map<String, CapabilityInfo>> capabilitiesTask =
capabilityClient.getAllCapabilities(CapabilityClient.FILTER_REACHABLE);
capabilitiesTask.addOnSuccessListener(
new OnSuccessListener<Map<String, CapabilityInfo>>() {
@Override
public void onSuccess(Map<String, CapabilityInfo> capabilityInfoMap) {
nodes.clear();
if (capabilityInfoMap.isEmpty()) {
Log.i("CIO", "No capability advertised :/"); return;
}
for (String capabilityName : capabilityInfoMap.keySet()) {
CapabilityInfo capabilityInfo = capabilityInfoMap.get(capabilityName);
Log.i("CIO", "Capability found: " + capabilityInfo.getName());
if (capabilityInfo != null) {
nodes.addAll(capabilityInfo.getNodes());
}
}
Log.i("CIO", "Nodes found: " + nodes.toString());
TextView found = (TextView)findViewById(R.id.foundNodes);
StringBuilder printNodes = new StringBuilder();
for (Node node : nodes)
printNodes.append(node.getDisplayName()).append(" ");
found.setText("Nodes: " + printNodes);
}
});

Une fois les noeuds trouvés (le téléphone), la montre va envoyer un message en utilisant un objet  MessageClient. Sur celui-ci, on appelle la méthode  sendMessage  avec l’id du noeud (téléphone) auquel on veut envoyer un message, et en second argument le message lui-même sous forme d’un tableau de byte.

public void clickSendMessage(View view) {
Log.i("CIO", "Sending a message...");
MessageClient clientMessage = Wearable.getMessageClient(this);
for (Node node : nodes) {
Log.i("CIO", " - to " + node.getId());
String message = "Hello " + counter;
Task<Integer> sendMessageTask =
clientMessage.sendMessage(node.getId(),"CIO", message.getBytes());
counter++;
}

Côté téléphone, le message est reçu si l'on place un écouteur dans l’activité :

Wearable.getMessageClient(this).addListener(this);

Ce qui oblige la classe de l’activité à implémenter l’interface  OnMessageReceivedListener  :

public class MainActivity extends AppCompatActivity implements MessageClient.OnMessageReceivedListener

On implémente alors la méthode  onMessageReceived  qui ici, récupère le message et l’ajoute à un  TextView  :

@Override
public void onMessageReceived(@NonNull MessageEvent messageEvent) {
Log.i("CIO", "Received message: ");
Log.i("CIO", " - Path: " + messageEvent.getPath());
String message = new String(messageEvent.getData());
Log.i("CIO", " - Content: " + message);
TextView tv = (TextView)findViewById(R.id.textView);
tv.setText(tv.getText() + "\n" + message);
}

Le résultat d’une telle application est montré ci-dessous : le noeud détecté depuis la montre est un Nexus 5X et trois messages sont envoyés et affichés côté téléphone.

Résultat lors de la transmission de messages de la montre vers le téléphone
Résultat lors de la transmission de messages de la montre vers le téléphone

Synchronisation d’images

Pour finir, voyons comment il est possible de synchroniser une image du téléphone vers la montre.

De manière similaire à la précédente, on demande un client de communication de type  DataClient :

DataClient dataClient = Wearable.getDataClient(this);

Pour synchroniser l’image bitmap, il faut créer un objet Asset, et une map de correspondance qui va associer des clefs et des valeurs :

  • À “my_image” on associe l’asset (l’image) ;

  • À “time” on associe la date courante.

L’utilisation de la date courante permet de forcer l’envoi de la requête, même si l’image n’a pas changé. Cette map  dataMap  est ensuite mise dans un objet  PutDataRequest  qui est ensuite envoyé via l’objet  dataClient  et la méthode  putDataItem  :

Asset asset = toAsset(bitmap);
PutDataMapRequest dataMap = PutDataMapRequest.create("/Image");
dataMap.getDataMap().putAsset("my_image", asset);
dataMap.getDataMap().putLong("time", new Date().getTime());
PutDataRequest request = dataMap.asPutDataRequest();
request.setUrgent();
Log.i("CIO", "Preparing task...");
Task<DataItem> dataItemTask = dataClient.putDataItem(request);
dataItemTask.addOnSuccessListener(new OnSuccessListener<DataItem>() {
@Override
public void onSuccess(DataItem dataItem) {
Log.i("CIO", "Sent image: " + dataItem);
}
});

Du côté de la montre, il faut réceptionner l’asset et mettre à jour l’image sur l’écran. Pour se faire, on enregistre un écouteur de type  DataClient  sur l’activité :

Wearable.getDataClient(this).addListener(this);

Cela oblige à implémenter l’interface  OnDataChangedListener  :

public class MainActivity extends WearableActivity implements DataClient.OnDataChangedListener

On implémente alors la méthode  onDataChanged  qui récupère l’asset.

public void onDataChanged(@NonNull DataEventBuffer dataEvents) {
Log.i("CIO", "Data changed !");
for (DataEvent event : dataEvents) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
String path = event.getDataItem().getUri().getPath();
Log.i("CIO", " - Path: " + path);
Map<String, DataItemAsset> assets = event.getDataItem().getAssets();
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
Asset asset = dataMapItem.getDataMap()
.getAsset("my_image");
new LoadBitmapAsyncTask().execute(asset);

Il faut encore implémenter une tâche asynchrone pour décoder le  BitMap  depuis l’asset et mettre à jour l’interface graphique. Cette tâche est effectuée dans la classe  LoadBitmapAsyncTask.

private class LoadBitmapAsyncTask extends AsyncTask<Asset, Void, Bitmap> {
protected Bitmap doInBackground(Asset... params) {
if (params.length > 0) {
Asset asset = params[0];
Task<DataClient.GetFdForAssetResponse> getFdForAssetResponseTask =
Wearable.getDataClient(getApplicationContext()).getFdForAsset(asset);
try {
DataClient.GetFdForAssetResponse getFdForAssetResponse =
Tasks.await(getFdForAssetResponseTask);
InputStream assetInputStream = getFdForAssetResponse.getInputStream();
if (assetInputStream != null) {
return BitmapFactory.decodeStream(assetInputStream);
} else {
Log.w("CIO", "Requested an unknown Asset.");
return null;
}
} catch ...
} else {
Log.e("CIO", "Asset must be non-null");
return null;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
Log.i("CIO", " - Setting background image on second page..");
ImageView img = (ImageView)findViewById(R.id.imageView);
img.setImageBitmap(bitmap);
}
}
}

On obtient alors la même image du côté de la montre et on peut l’afficher :

Synchronization d'une image entre téléphone et montre
Synchronisation d'une image entre téléphone et montre

Exemple de certificat de réussite
Exemple de certificat de réussite