Développement d'applications sur une montre
Une application Android Wear est architecturée comme une application Android standard. Lorsque deux applications (une pour le téléphone mobile, une pour la montre) sont créées dans un même projet Android Studio, on manipule alors deux arborescences dans un même projet Android Studio. Chaque application a son propre fichier de configuration gradle pour la compilation. Au déploiement, il faut choisir quelle application (wear ou mobile) on souhaite déployer sur la montre ou le téléphone.
Comme pour une application Android, une application Wear est constituée d’activités. Celles-ci sont des WearableActivity
qui héritent de Activity
. Elles se comportent donc de la même façon, notamment du point de vue de leur cycle de vie. Le code d’une telle activité ressemble donc à :
public class MainActivity extends WearableActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setAmbientEnabled();
}
}
Elles ont cependant un comportement supplémentaire : la gestion des modes ambient et interactif que l’on peut utiliser pour changer le visuel d'une application suivant que l’on interagisse, ou pas, avec elle. Dans l’exemple ci-dessous, issu de la documentation de Google (CC-BY), on voit deux applications dans les deux modes. Par exemple, l’application pour faire les courses est colorée en vert et on voit clairement les cases à cocher. En mode ambient, l’application est en noir et blanc et les cases à cocher sont suggérées.

Dans le mode interactif, tous les éléments graphiques peuvent être utilisés : boutons, images, et autres éléments interactifs. D’un point de vue des couleurs, tout est autorisé. Dans le mode ambient, l’application se doit de basculer en niveau de gris, et supprimer les éléments interactifs. En effet, dans ce mode, l’affichage reste longtemps allumé, à des fins de consultation par l’utilisateur, mais sans interaction. Ces règles sont précisées dans la documentation de Google.
Du point de vue du code, il faut surcharger les méthodes onEnterAmbient()
, onUpdateAmbient()
et onExitAmbient()
, comme par exemple :
@Override
public void onEnterAmbient(Bundle ambientDetails) {
super.onEnterAmbient(ambientDetails);
updateDisplay();
}
@Override
public void onUpdateAmbient() {
super.onUpdateAmbient();
updateDisplay();
}
@Override
public void onExitAmbient() {
updateDisplay();
super.onExitAmbient();
}
Ces méthodes appellent la méthode updateDisplay()
qui va mettre à jour l’interface graphique :
private void updateDisplay() {
if (isAmbient()) {
mContainerView.setBackgroundColor(getResources().getColor(android.R.color.holo_red_dark));
mTextView.setTextColor(getResources().getColor(android.R.color.white));
mClockView.setVisibility(View.VISIBLE);
mClockView.setText(AMBIENT_DATE_FORMAT.format(new Date()));
} else {
mContainerView.setBackground(null);
mTextView.setTextColor(getResources().getColor(android.R.color.black));
mClockView.setVisibility(View.GONE);
}
}
Dans cet exemple, on fait disparaître ou apparaître les éléments graphiques et on change leurs couleurs. On évitera autant que faire se peut de générer des nouveaux éléments graphiques, car cela consomme des ressources supplémentaires. On obtient alors le comportement suivant :

Particularités des applications Wear
Il y a bien sûr certaines particularités propres aux montres connectées, en plus du mode ambient et interactif. Nous allons en voir deux dans cette section : la gestion des écrans ronds et la gestion de la géolocalisation.
Les gabarits et les écrans ronds
Lorsque l’on utilise les gabarits classiques tels que les LinearLayout
, il peut arriver que certains éléments graphiques soient mal représentés à l’écran car celui-ci n’est pas d’une forme classique, comme par exemple :

On peut même imaginer des montres triangulaires, hexagonales, etc., où le problème serait sans doute pire. Il faut donc adapter le gabarit. Une solution est de détecter la forme de l’écran et de choisir un gabarit adapté. Une solution plus facile à mettre en oeuvre consiste à utiliser l’élément racine BoxInsetLayout
dans le gabarit, qui va contraindre les éléments graphiques pour les inscrire dans la forme de la montre.
Pour ce faire, il suffit de créer un gabarit comme celui-ci :
<?xml version="1.0" encoding="utf-8"?>
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
...
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
app:boxedEdges="all"
…
Attention à bien modifier l’attribut app:boxedEdges
à "all" de l’élément enfant du BoxInsetLayout
, afin que le BoxInsetLayout
calcule les placements à partir de cet élément. De plus, pour les BoxInsetLayout
de la nouvelle version de Wear (Android Wear 2), il faut donc utiliser le tag android.support.wear.widget.BoxInsetLayout
(et pas le tag android.support.wearable.view.BoxInsetLayout
). Il faut en plus ajouter la dépendance wear dans le fichier de build gradle :
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:wear:26.1.0'
}
On obtient alors :

La géolocalisation
La gestion de la géolocalisation est aussi un peu particulière. La première question à se poser est de savoir si votre montre possède ou non un capteur GPS. Peu de montres en possèdent : seules les montres sportives qui enregistrent le tracé d’un running en sont pourvues en général. Pour les autres montres, ce n’est sans doute pas vital car la montre peut s’appuyer sur le téléphone mobile pour connaître sa géolocalisation. Ainsi, le code pourrait être difficile à écrire car il faudrait prendre en compte les deux situations suivantes, à gauche une montre Asus Zenwatch 3, dépourvue d’un capteur GPS, au centre, une montre Motorola 360 sport qui elle en possède un:

En fait, Google a résolu ce problème en proposant d’interroger la géolocalisation au travers des Google Play services d’Android Wear. Ce service fera le choix du mode de lecture appropriée de la géolocalisation (directement au travers du capteur ou via le téléphone). On se retrouve donc avec un code unifié faisant appel à ces services :

D’un point de vu du code, il reste tout de même pas mal de travail à effectuer.
Il faut tout d’abord déclarer les permission nécessaires dans le Manifest :
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:name="android.permission.ACCESS_FINE_LOCATION"
On peut aussi préciser que notre application utilise le composant hardware GPS, mais qu’il n’est pas obligatoire (sinon notre application ne pourrait pas s’installer sur une montre dépourvue du capteur) :
android:name="android.hardware.location.gps" android:required="false"
On peut ensuite tester explicitement la présence de ce capteur :
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS))
Cependant, cela ne changera rien au code qui suit et qui utilise les Google services. Pour ce faire, il faut se connecter au service :
GoogleApiClient gapi = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
gapi.connect();
En précisant bien que l’on souhaite utiliser LocationServices.API
.
Comme la callback de connection est “this”, votre WearActivity
doit implémenter GoogleApiClient.ConnectionCallbacks
et doit, entre autre, avoir la méthode onConnected(Bundle bundle)
qui sera appelée lorsque votre application sera connectée au service. A partir de ce moment, on pourra interroger la géolocalisation.
La géolocalisation utilise l’objet FusedLocationProviderClient flpc
, que l’on récupère via un appel static :
// Get the location provider through the GoogleApi
FusedLocationProviderClient flpc = LocationServices.getFusedLocationProviderClient(this);
Ensuite, pour obtenir une géolocalisation, si elle n’a jamais encore été obtenue, on va demander à être notifié régulièrement grâce à un LocationRequest
. Celui-ci est paramétré pour être notifié toutes les secondes si la géolocalisation change. On peut ajouter une callback pour exécuter du code dans ce cas, mais ce n’est pas nécessaire dans notre cas.
// Ask for update of the location
LocationRequest locationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(1000).setFastestInterval(1000);
lc = new LocationCallback();
flpc.requestLocationUpdates(locationRequest, lc, null);
Enfin, il faut lire la géolocalisation (par exemple dans le code d’un bouton). Pour ce faire, toujours sur l’objet FusedLocationProviderClient flpc
, on appelle la méthode getLastLocation()
. Celle-ci s’exécute de manière asynchrone et renvoie donc un Task<Location>
.
Task<Location> loc = flpc.getLastLocation();
loc.addOnSuccessListener(this);
Pour exécuter du code en fin de tâche asynchrone, il faut ajouter un écouteur, à l’aide de la méthode addOnSuccessListener
. Dans ce code, on met this en paramètre, ce qui oblige la WearActivity
à implémenter l’interface OnSuccessListener<Location>
, c’est-à-dire la méthode onSuccess(Location loc)
qui va gérer le code qui s’exécute quand la tâche asynchrone termine avec succès. Le code en question va lire la géolocalisation et mettre à jour l’interface graphique :
// Callback for the location task
@Override
public void onSuccess(Location loc) {
Log.i("CIO", "Task completed.");
if (loc != null) {
DecimalFormat df = new DecimalFormat("#.##");
String lon = df.format(loc.getLongitude());
String lat = df.format(loc.getLatitude());
Log.i("CIO", "Location: " + lon + " , " + lat );
TextView latTV = (TextView)findViewById(R.id.lat);
latTV.setText("latitude: " + lon);
TextView lonTV = (TextView)findViewById(R.id.lon);
lonTV.setText("longitude: " + lat);
}
Au final, on obtient bien la lecture de la géolocalisation sur les deux montres, et que le capteur soit présent ou non :
