Développer une application pour les montres Android Wear
La grande nouveauté de 2014 et 2015 ? les montres connectées !!! Regardons ensemble comment interagir avec ce nouveau device.
Introduction
Premièrement, je tiens à signaler que les montres sont actuellement totalement dépendantes d’un smartphone, leur principale utilité étant d’afficher les notifications sur notre poignet. Vous me direz qu’il existe aussi des applications de running permettant de tracker son exploit sportif ainsi que son rythme cardiaque (oui vous avez vu cette publicité aussi), je vais vous dire oui, mais ! Mais ces applications ne sont que des extensions d’applications installées sur vos smartphones, les données sont pour la plupart stockées sur le smartphone, puis ensuite affichées sur la montre.
Première étape – Avoir une montre
Ce titre est un peu troll, en effet qui dit « développer pour Android Wear » dit posséder une montre, et je me doute bien que vous n’en voyez pas tous encore l’utilité. Pour les joueurs ne possédants pas de montre, il est possible de lancer un émulateur sur votre poste et de relier votre smartphone à ce dernier. Pour les autres, il est possible de développer directement sur leur montres, ce qui est quand même plus confortable que de devoir lancer un émulateur.
Emuler une Android wear
Premièrement, assurez vous d’avoir les images Android Wear et le SDK 4.4.2W (API20) d’installés sur votre machine :
Ceci fait, pensez aussi à installer Android Support Repository, Android Support Library, Google Play services et Google Repository :
Vous pouvez maintenant créer votre émulateur :
Dernière étape (la plus aléatoire), lançons maintenant notre émulateur et essayons de l’associer à notre smartphone.
Vous devrier arriver sur un écran similaire à celui-ci
Essayez vous un peu à l’ergonomie de la montre afin de passer le tutoriel, et arriver sur cet écran :
Sur votre smartphone, lancez l’application Android Wear
Puis associez le à un émulateur :
Si vous voyez Connexion en cours pendant plus de 2 minutes, déjà assurez vous que votre smartphone soit bien connecté en USB à votre PC 🙂
Puis exécutez la commande suivante dans votre terminal
adb -d forward tcp:5601 tcp:5601
Et voilà !
En cas d’erreur, relancez le serveur adb
adb kill-server adb start-server
Connecter sa montre
Aller dans les préférences du smartphone, clickez 7 fois sur Numéro de build pour activer le mode développeur.
Dans Options pour développeurs activez le Débogage ADB
Branchez la montre à votre PC, une fenêtre d’association va s’ouvrir sur votre smartphone, clickez sur Toujours autoriser sur cet ordinateur, puis OK.
Pour tester que votre montre est bien disponible en debug, connectez la en USB à votre poste, puis exécutez la commande
adb devices
En cas d’erreur, il est possible qu’adb ait mal fait son travail, relancez le simplement
adb kill-server adb start-server
Deuxième étape – On code !
Création du projet
Passons maintenant aux choses sérieuses : notre application wear !
Android Studio propose une option permettant de créer un projet compatible Wear, profitons de cette option pour débuter :
Une fois notre projet créé, nous pouvons remarquer que notre application contient 2 modules : mobile et wear
Le module mobile contiendra les sources de notre application smartphone/tablette, tandis que wear ne s’occupera que de l’application qui sera installée sur la montre. Il est possible de créer une application wear sans qu’elle possède une application smartphone, les fonctionnalités seront juste fortement réduites.
Jetons maintenant un oeil aux fichiers build.gradle de chaque module :
mobile/build.gradle
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) wearApp project(':wear') compile 'com.android.support:appcompat-v7:22.0.0' compile 'com.google.android.gms:play-services:6.5.87' }
wear/build.gradle
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.google.android.support:wearable:1.1.0' compile 'com.google.android.gms:play-services-wearable:6.5.87' }
Nous pouvons remarquer que les build.gradle contiennent tous deux une version des play services, celle-ci est indispensable pour le développement wear, en effet elle assure la communication entre smartphone et smartwatch.
Le build coté mobile contient la déclaration wearApp project(‘:wear’), qui sera utilisée lors du déploiement de notre application en APK afin d’embarquer la version Wear.
Regardons maintenant au fichiers AndroidManifest.xml du module wear :
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tutosandroidfrance.wearsample" > <uses-feature android:name="android.hardware.type.watch" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@android:style/Theme.DeviceDefault" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Il ressemble fortement à un manifest normal, à l’exception de la ligne
<uses-feature android:name="android.hardware.type.watch" />
Qui définit l’application compatible Android Wear (je parle bien ici du module wear et non de l’application smartphone).
Exécuter notre application sur Watch
Compilez et exécutez l’application wear :
Assurez vous que la montre soit bien connectée à adb, pour vous en assurer :
adb devices
L’application devrait s’ouvrir automatiquement sur la montre
L’application est pour l’instant très sommaire, voyons ensemble comment obtenir un résultat un peu plus pertinent.
Je vous propose de reproduire ensemble une application du stye 2D picker proposé par Google Wear
L’application va afficher en premier lieu une liste verticale d’éléments, contenant chacun au moins un texte et image, puis lorsque l’on se déplace vers la droite nous allons afficher les options liés à cet objet, par exemple Ouvrir sur le smartphone.
Les vues disponibles sur Watch
Le SDK watch embarque les vues natives à android, c’est à dire LinearLayout, FrameLayout, TextView, ImageView et j’en passe. Il apporte aussi de nouvelles vues, propres à ce nouveau support :
- BoxInsetLayout – Un Layout permettant de spécifier un padding différent pour les montres rondes ou rectangles
- CardFragment – Un fragment représenté sous forme de card (fond blanc), permettant d’afficher son contenu de façon minimaliste, et permettant lors du click d’afficher une version plus détaillée scrollable
- CircledImageView – Une ImageView en forme de cercle
- ConfirmationActivity – Une activité permettant d’afficher un message de confirmation à l’utilisateur. Elle peux aussi être utilisée afin de lui afficher un message d’erreur
- DelayedConfirmationView – Une vue permettant d’afficher une action effectuée sous un temps donné
- DismissOverlayView -Une vue permettant de quitter l’application via long-press-to-dismiss
- DotsPageIndicator – Un indicateur de page pour le GridViewPager qui indique la page affichée de la ligne courante
- GridViewPager – Un élément permettant d’afficher des vues sous forme de grille, gérant le swipe vertical et horizontal, en respectant le pattern 2D picker
- WatchViewStub – Objet permettant d’afficher une vue différente pour les montres rondes ou rectangles
- WearableListView – Version de ListView optimisée pour les écrans des Android Wears. Afficher une liste verticale d’éléments et stop directement sur l’élément le plus proche lors du scroll
Le package des vues est android.support.wearable.view, donc pour utiliser WatchViewStub dans votre layout xml il faudra écrire android.support.wearable.viewWatchViewStub comme dans l’exemple suivant :
<?xml version="1.0" encoding="utf-8"?> <android.support.wearable.view.WatchViewStub 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:id="@+id/watch_view_stub" android:layout_width="match_parent" android:layout_height="match_parent" app:rectLayout="@layout/rect_activity_main" app:roundLayout="@layout/round_activity_main" tools:context=".MainActivity" tools:deviceIds="wear"/>
Implémenter le pattern 2D Picker
Pour créer la grille respectant 2D Picker nous allons utiliser les objets GridViewPager et DotsPageIndicator, placez ces éléments dans votre layout activity_main.xml (nous n’utiliserons pas les ViewStubs dans ce tutoriel) :
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.wearable.view.GridViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true" /> <android.support.wearable.view.DotsPageIndicator android:id="@+id/page_indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal|bottom" android:layout_marginBottom="5dp"/> </FrameLayout>
Récupérons cet élément du coté Java :
public class MainActivity extends Activity { private GridViewPager pager; private DotsPageIndicator dotsPageIndicator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pager = (GridViewPager) findViewById(R.id.pager); dotsPageIndicator = (DotsPageIndicator) findViewById(R.id.page_indicator); dotsPageIndicator.setPager(pager); } }
Les écrans sous Android Wear sont aussi des Activity, donc vous ne devriez pas être trop perdu 🙂
Comme tout ViewPager ou AdapterView, notre GridViewPager se remplit via un Adapter, ici un FragmentGridPagerAdapter
Pour la suite de ce tutoriel, je manipulerai des objets models nommée éléments :
public class Element { private String titre; private String texte; private int color; public Element(String titre, String texte,, int color) { this.titre = titre; this.texte = texte; this.color = color; } ... }
Nous allons donc construire un ElementGridPagerAdapter permettant d’afficher une liste d’éléments
public class ElementGridPagerAdapter extends FragmentGridPagerAdapter { private List<Row> mRows = new ArrayList<Row>(); private List<Element> mElements; public ElementGridPagerAdapter(List<Element> elements, FragmentManager fm) { super(fm); this.mElements = elements; //Construit le tableau des éléménts à afficher for (Element element : elements) { mRows.add(new Row( //pour l'instant nous ne mettrons qu'un élément par ligne CardFragment.create(element.getTitre(), element.getTexte()) ) ); } } //Le fragment à afficher p @Override public Fragment getFragment(int row, int col) { Row adapterRow = mRows.get(row); return adapterRow.getColumn(col); } //le drawable affichée en background pour la ligne [row] @Override public Drawable getBackgroundForRow(final int row) { return new ColorDrawable(mElements.get(row).getColor()); } @Override public Drawable getBackgroundForPage(final int row, final int column) { //nous pouvons spécifier un drawable différent pour le swipe horizontal return getBackgroundForRow(row); } //Le nombre de lignes dans la grille @Override public int getRowCount() { return mRows.size(); } //Le nombre de colonnes par ligne @Override public int getColumnCount(int rowNum) { return mRows.get(rowNum).getColumnCount(); } /** * Représentation d'une ligne - Contient une liste de fragments */ private class Row { final List<Fragment> columns = new ArrayList<Fragment>(); public Row(Fragment... fragments) { for (Fragment f : fragments) { add(f); } } public void add(Fragment f) { columns.add(f); } Fragment getColumn(int i) { return columns.get(i); } public int getColumnCount() { return columns.size(); } } }
Utilisons maintenant cet Adapter avec notre ViewPager :
public class MainActivity extends Activity { private GridViewPager pager; private DotsPageIndicator dotsPageIndicator; //la liste des éléments à afficher private List<Element> elementList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pager = (GridViewPager) findViewById(R.id.pager); dotsPageIndicator = (DotsPageIndicator) findViewById(R.id.page_indicator); dotsPageIndicator.setPager(pager); elementList = creerListElements(); pager.setAdapter(new ElementGridPagerAdapter(elementList,getFragmentManager())); } /** * Créé une liste d'éléments pour l'affichage */ private List<Element> creerListElements() { List<Element> list = new ArrayList<>(); list.add(new Element("Element 1","Description 1", Color.parseColor("#F44336"))); list.add(new Element("Element 2","Description 2", Color.parseColor("#E91E63"))); list.add(new Element("Element 3","Description 3", Color.parseColor("#9C27B0"))); list.add(new Element("Element 4","Description 4", Color.parseColor("#673AB7"))); list.add(new Element("Element 5","Description 5", Color.parseColor("#3F51B5"))); list.add(new Element("Element 6","Description 6", Color.parseColor("#2196F3"))); return list; } }
Compilez et exécutez l’application, le résultat devrait être le suivant :
Troisième étape – Communiquer avec notre smartphone
Nous arrivons maintenant à créer une interface graphique plutôt dynamique sur la montre, mais les données que l’on affiche pour l’instant sont factices. Voyons ensemble comment utiliser de vrais données !
J’ai placé sur ce site un fichier JSON contenant l’historique des versions d’Android, avec leur image respective :
https://tutos-android-france.com/wp-content/uploads/2015/03/android_versions.json
Nous allons essayer d’afficher cette liste sur notre Android Wear !
Règle importante : Nous n’avons pas le droit d’effectuer des requêtes HTTP depuis notre montre ! inutile d’essayer d’importer picasso, retrofit ou que sais-je sur la montre, leur usage c’est interdit !
Coté Smartphone
Premièrement, il nous faut créer un Service coté Smartphone qui aura la capacité de communiquer avec la montre. Pour rappel, un Service est l’équivalent d’une activité, mais sans interface graphique. Ce dernier peux être actif à tout moment, et réveillé grâce à des événements nommés intent. Pour plus d’informations, rendez-vous sur la documentation officielle : Service.
Les services, au même titre que les Activités se déclarent dans le manifest :
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tutosandroidfrance.wearsample" > <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- Android Wear Service --> <service android:name=".WearService"> <intent-filter> <action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> </intent-filter> </service> </application> </manifest>
J’ai ajouté l’intent-filter com.google.android.gms.wearable.BIND_LISTENER afin qu’il soit réveillé lors d’un échange entre la montre et le smartphone.
Le service utilisé ici est un WearableListenerService, regardons ensemble comment l’implémenter :
public class WearService extends WearableListenerService { private final static String TAG = WearService.class.getCanonicalName(); protected GoogleApiClient mApiClient; @Override public void onCreate() { super.onCreate(); mApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .build(); mApiClient.connect(); } @Override public void onDestroy() { super.onDestroy(); mApiClient.disconnect(); } /** * Appellé à la réception d'un message envoyé depuis la montre * @param messageEvent message reçu */ @Override public void onMessageReceived(MessageEvent messageEvent) { super.onMessageReceived(messageEvent); //Ouvre une connexion vers la montre ConnectionResult connectionResult = mApiClient.blockingConnect(30, TimeUnit.SECONDS); if (!connectionResult.isSuccess()) { Log.e(TAG, "Failed to connect to GoogleApiClient."); return; } //traite le message reçu final String path = messageEvent.getPath(); } /** * Envoie un message à la montre * @param path identifiant du message * @param message message à transmettre */ protected void sendMessage(final String path, final String message) { //effectué dans un trhead afin de ne pas être bloquant new Thread(new Runnable() { @Override public void run() { //envoie le message à tous les noeuds/montres connectées final NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(mApiClient).await(); for (Node node : nodes.getNodes()) { Wearable.MessageApi.sendMessage(mApiClient, node.getId(), path, message.getBytes()).await(); } } }).start(); } }
Notre service utilise un GoogleApiClient relié à l’API Wearable. En effet c’est le seul élément ayant la capacité d’échanger des informations entre Smartphone et Smartwatch. Il faut penser à le connecter à l’ouverture du service, puis à le déconnecter à sa destruction.
J’ai ajouté 2 méthodes permettant de communiquer avec la montre : onMessageReceived et sendMessage. En effet une des façons d’échanger avec sa watch est d’envoyer des Messages. Ceux-ci sont constitués d’un Path (identifiant du message) et d’un Content (en byte).
Un appareil est ici identifié par un nodeId, c’est pourquoi lors de l’envoie de message je le diffuse à tous les appareils connectés (récupérés avec getConnectedNodes).
Coté SmartWatch
Les activités smartwatch peuvent aussi envoyer et recevoir des messages, regardons cela ensemble :
public class MainActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, MessageApi.MessageListener{ private GridViewPager pager; private DotsPageIndicator dotsPageIndicator; //la liste des éléments à afficher private List<Element> elementList; protected GoogleApiClient mApiClient; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pager = (GridViewPager) findViewById(R.id.pager); dotsPageIndicator = (DotsPageIndicator) findViewById(R.id.page_indicator); dotsPageIndicator.setPager(pager); elementList = creerListElements(); pager.setAdapter(new ElementGridPagerAdapter(elementList,getFragmentManager())); } /** * Créé une liste d'éléments pour l'affichage */ private List<Element> creerListElements() { List<Element> list = new ArrayList<>(); list.add(new Element("Element 1","Description 1", Color.parseColor("#F44336"))); list.add(new Element("Element 2","Description 2", Color.parseColor("#E91E63"))); list.add(new Element("Element 3","Description 3", Color.parseColor("#9C27B0"))); list.add(new Element("Element 4","Description 4", Color.parseColor("#673AB7"))); list.add(new Element("Element 5","Description 5", Color.parseColor("#3F51B5"))); list.add(new Element("Element 6","Description 6", Color.parseColor("#2196F3"))); return list; } /** * A l'ouverture, connecte la montre au Google API Client / donc au smartphone */ @Override protected void onStart() { super.onStart(); mApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); mApiClient.connect(); } /** * Si nous avons une connection aux Google API, donc au smartphone * Nous autorisons l'envoie de messages */ @Override public void onConnected(Bundle bundle) { Wearable.MessageApi.addListener(mApiClient, this); } /** * A la fermeture de l'application, desactive le GoogleApiClient * Et ferme l'envoie de message */ @Override protected void onStop() { if (null != mApiClient && mApiClient.isConnected()) { Wearable.MessageApi.removeListener(mApiClient, this); mApiClient.disconnect(); } super.onStop(); } @Override public void onConnectionSuspended(int i) { } @Override public void onConnectionFailed(ConnectionResult connectionResult) { } /** * Appellé à la réception d'un message envoyé depuis le smartphone * @param messageEvent message reçu */ @Override public void onMessageReceived(MessageEvent messageEvent) { //traite le message reçu final String path = messageEvent.getPath(); } /** * Envoie un message à au smartphone * @param path identifiant du message * @param message message à transmettre */ protected void sendMessage(final String path, final String message) { //effectué dans un trhead afin de ne pas être bloquant new Thread(new Runnable() { @Override public void run() { //envoie le message à tous les noeuds/montres connectées final NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(mApiClient).await(); for (Node node : nodes.getNodes()) { Wearable.MessageApi.sendMessage(mApiClient, node.getId(), path, message.getBytes()).await(); } } }).start(); } }
Le comportement est assez similaire à celui du smartphone. Essayons maintenant d’échanger quelques messages entre nos deux appareils :
- La montre envoie path= »bonjour » et message= »smartphone »
- Si le smartphone reçois path= »bonjour » il envoie path= »bonjour » et comme message il envoie « affiche : » suivi d’un nombre aléatoire
- La montre affiche le message reçu
Coté smartphone :
@Override public void onMessageReceived(MessageEvent messageEvent) { super.onMessageReceived(messageEvent); //Ouvre une connexion vers la montre ConnectionResult connectionResult = mApiClient.blockingConnect(30, TimeUnit.SECONDS); if (!connectionResult.isSuccess()) { Log.e(TAG, "Failed to connect to GoogleApiClient."); return; } //traite le message reçu final String path = messageEvent.getPath(); if(path.equals("bonjour")) { int random = (int)(Math.random() * 100); sendMessage("bonjour", "affiche :" + random); } }
Coté montre :
@Override public void onConnected(Bundle bundle) { Wearable.MessageApi.addListener(mApiClient, this); //envoie le premier message sendMessage("bonjour","smartphone"); } @Override public void onMessageReceived(MessageEvent messageEvent) { //traite le message reçu final String path = messageEvent.getPath(); if(path.equals("bonjour")){ //récupère le contenu du message final String message = new String(messageEvent.getData()); //penser à effectuer les actions graphiques dans le UIThread runOnUiThread(new Runnable() { @Override public void run() { //nous affichons ici dans notre viewpager elementList = new ArrayList<>(); elementList.add(new Element("Message reçu",message,Color.parseColor("#F44336"))); pager.setAdapter(new ElementGridPagerAdapter(elementList,getFragmentManager())); } }); } }
Plutot simple non ?
Penser à bien effectuer les modifications graphiques dans le UIThread, les messages sont par exemples reçus dans un Thread différent. Si vous ne savez pas encore ce qu’est un thread, je vous invite à lire la documentation suivante :
http://developer.android.com/guide/components/processes-and-threads.html
Partage de données
Je n’ai pas oublié notre objectif : afficher la liste des versions d’Android sur notre montre, pour cela il nous manque encore un elements : le partage d’images !
Partage d’images
Afin de partager des données entre smartphone et wear, Google a mit en place un service nommée DataApi, permettant de partager des zones mémoires entre différents terminaux. Dit comme cela la tâche semble compliquée, mais vous verrez que la mise en place est assez simple.
- Le smartphone télécharge les images
- Les bitmap sont ensuite sauvegardés dans les blocs mémoire de la DataAPI
- L’Android Wear est notifiée dès que la DataAPI reçoit de nouvelles données
- Nous pouvons récupérer ces images pour ensuite les afficher sur la montre
Les données envoyées contiennent un identifiant unique, nommé ici Path, qui représente un chemin vers cette donnée échangée via bluetooth. Ici, chaque image envoyée a comme path :
/image/[numéro de l’image]
exemple : /image/1
Je récupère les images avec une HttpURLConnection, les plus puristes préférerons utiliser Picasso, je n’en ai pas trouvé l’utilité ici 🙂
Les Bitmaps téléchargés sont compressées puis empaquetées en Asset pour ensuite notre objet ApiClient grâce à Wearable.DataApi.putDataItem.
Smartphone
/** * Permet d'envoyer une image à la montre */ protected void sendImage(String url, int position) { //télécharge l'image Bitmap bitmap = getBitmapFromURL(url); if (bitmap != null) { Asset asset = createAssetFromBitmap(bitmap); //créé un emplacement mémoire "image/[url_image]" final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create("/image/" + position); //ajoute la date de mise à jour, important pour que les données soient mises à jour putDataMapRequest.getDataMap().putString("timestamp", new Date().toString()); //ajoute l'image à la requête putDataMapRequest.getDataMap().putAsset("image", asset); //envoie la donnée à la montre if (mApiClient.isConnected()) Wearable.DataApi.putDataItem(mApiClient, putDataMapRequest.asPutDataRequest()); } } /** * Les bitmap transférés depuis les DataApi doivent être empaquetées en Asset */ public static Asset createAssetFromBitmap(Bitmap bitmap) { final ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream); return Asset.createFromBytes(byteStream.toByteArray()); } /** * Récupère une bitmap à partir d'une URL */ public static Bitmap getBitmapFromURL(String src) { try { HttpURLConnection connection = (HttpURLConnection) new URL(src).openConnection(); connection.setDoInput(true); connection.connect(); return BitmapFactory.decodeStream(connection.getInputStream()); } catch (Exception e) { // Log exception return null; } }
Un point important, dans chaque paquet je joins un objet timestamp contenant la date d’envoie. Cet élément a pour but d’invalider la précédente donnée envoyée avec le même path. Si elle n’est pas présente, la montre ne mettra pas à jour ses données et pourra par exemple afficher d’anciennes bitmap même si ces dernières ont été modifiées.
Voyons ensemble comment les récupérer depuis notre montre, premièrement il nous faut ajouter la DataApi :
Wear
public class MainActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, MessageApi.MessageListener, DataApi.DataListener { ... @Override public void onConnected(Bundle bundle) { Wearable.MessageApi.addListener(mApiClient, this); Wearable.DataApi.addListener(mApiClient, this); //envoie le premier message sendMessage("bonjour", "smartphone"); } @Override protected void onStop() { if (null != mApiClient && mApiClient.isConnected()) { Wearable.MessageApi.removeListener(mApiClient, this); Wearable.DataApi.removeListener(mApiClient, this); mApiClient.disconnect(); } super.onStop(); } ... }
Pour recevoir une notification lorsqu’une donnée a été mise à jours :
@Override public void onDataChanged(DataEventBuffer dataEvents) { for (DataEvent event : dataEvents) { //on attend ici des assets dont le path commence par /image/ if (event.getType() == DataEvent.TYPE_CHANGED && event.getDataItem().getUri().getPath().startsWith("/image/")) { DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem()); Asset profileAsset = dataMapItem.getDataMap().getAsset("image"); Bitmap bitmap = loadBitmapFromAsset(profileAsset); // On peux maintenant utiliser notre bitmap } } } /** * Récupère une bitmap depuis un asset de DataApi */ public Bitmap loadBitmapFromAsset(Asset asset) { final ConnectionResult result = mApiClient.blockingConnect(3000, TimeUnit.MILLISECONDS); if (!result.isSuccess()) { return null; } // convert asset into a file descriptor and block until it's ready final InputStream assetInputStream = Wearable.DataApi.getFdForAsset(mApiClient, asset).await().getInputStream(); if (assetInputStream == null) { return null; } // decode the stream into a bitmap return BitmapFactory.decodeStream(assetInputStream); }
Je vais maintenant vous montrer une autre façon de récupérer un élément depuis les assets, sans avoir à le faire à partir de la fonction onDataChanged :
/** * Récupère une bitmap partagée avec le smartphone depuis une position */ public Bitmap getBitmap(int position) { final Uri uri = getUriForDataItem("/image/" + position); if (uri != null) { final DataApi.DataItemResult result = Wearable.DataApi.getDataItem(mApiClient, uri).await(); if (result != null && result.getDataItem() != null) { final DataMapItem dataMapItem = DataMapItem.fromDataItem(result.getDataItem()); final Asset firstAsset = dataMapItem.getDataMap().getAsset("image"); if (firstAsset != null) { return loadBitmapFromAsset(firstAsset); } } } return null; } /** * Récupère une URI de donnée en fonction d'un path * via l'identifiant nodeId du smartphone */ protected Uri getUriForDataItem(String path) { try { //récupère le nodeId du smartphone final String nodeId = Wearable.NodeApi.getConnectedNodes(mApiClient).await().getNodes().get(0).getId(); //construit l'uri pointant vers notre path Uri uri = new Uri.Builder().scheme(PutDataRequest.WEAR_URI_SCHEME).authority(nodeId).path(path).build(); return uri; } catch (Exception e) { Log.e(TAG, e.getMessage(), e); return null; } }
Partage d’objets
La DataApi permet de partager des éléments primitifs, comme les Integers ou les Strings. Utilisons cette fonctionnalité afin de transférer notre objet Element (que j’ai modifié pour correspondre au webservice) :
public class Element { private String titre; private String description; private String url; ... }
smartphone
/** * Permet d'envoyer une liste d'elements */ protected void sendElements(final List<Element> elements) { //envoie chaque élémént 1 par 1 for (int position = 0; position < elements.size(); ++position) { Element element = elements.get(position); //créé un emplacement mémoire "element/[position]" final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create("/element/" + position); //ajoute la date de mi[jase à jour putDataMapRequest.getDataMap().putString("timestamp", new Date().toString()); //ajoute l'element champ par champ putDataMapRequest.getDataMap().putString("titre", element.getTitre()); putDataMapRequest.getDataMap().putString("description", element.getDescription()); putDataMapRequest.getDataMap().putString("url", element.getUrl()); //envoie la donnée à la montre if (mApiClient.isConnected()) Wearable.DataApi.putDataItem(mApiClient, putDataMapRequest.asPutDataRequest()); //puis envoie l'image associée sendImage(element.getUrl(), position); } }
Veuillez remarquer que j’envoie en même temps l’image associée à l’element
sendImage(element.getUrl(), position);
wear
/** * Récupère un element depuis sa position */ public Element getElement(int index) { final Uri uri = getUriForDataItem("/element/" + index); if (uri != null) { final DataApi.DataItemResult result = Wearable.DataApi.getDataItem(mApiClient, uri).await(); if (result != null && result.getDataItem() != null) { final DataMapItem dataMapItem = DataMapItem.fromDataItem(result.getDataItem()); return new Element( dataMapItem.getDataMap().getString("titre"), dataMapItem.getDataMap().getString("description"), dataMapItem.getDataMap().getString("url") ); } } return null; }
Nous pouvons maintenant finir notre application ! 🙂
Afficher les données de notre webservice
Je vous propose le protocole suivant :
- La montre envoie le message « bonjour » au smartphone
- A la récéption du message « bonjour », le smartphone récupère les informations du webservice
- Le smartphone envoie les objets Element dans /element/list/
- Le smartphone télécharge et envoie les images dans /image/[numero] (numéro correspond à la position de l’element dans la list envoyée)
- A la réception des données /element/list/ la montre va récupérer les objets Element partagés
- La smartwatch va charger les Bitmap envoyés via /image/ et les placer dans un cache (en RAM)
- La smartwatch peux enfin afficher le viewpager
Vu que je suis un grand fan de Jake Wharton, j’utiliserai ici la librairie Retrofit pour réaliser l’appel de webservice :
Smartphone
AndroidService.java
public interface AndroidService { public static final String ENDPOINT = "https://tutos-android-france.com/wp-content/uploads/2015/03"; @GET("/android_versions.json") public void getElements(Callback<List<Element>> callback); }
WearService.java
/** * Appellé à la réception d'un message envoyé depuis la montre * * @param messageEvent message reçu */ @Override public void onMessageReceived(MessageEvent messageEvent) { super.onMessageReceived(messageEvent); //Ouvre une connexion vers la montre ConnectionResult connectionResult = mApiClient.blockingConnect(30, TimeUnit.SECONDS); if (!connectionResult.isSuccess()) { Log.e(TAG, "Failed to connect to GoogleApiClient."); return; } //traite le message reçu final String path = messageEvent.getPath(); if (path.equals("bonjour")) { //Utilise Retrofit pour réaliser un appel REST AndroidService androidService = new RestAdapter.Builder() .setEndpoint(AndroidService.ENDPOINT) .build().create(AndroidService.class); //Récupère et deserialise le contenu de mon fichier JSON en objet Element androidService.getElements(new Callback<List<Element>>() { @Override public void success(List<Element> elements, Response response) { envoyerListElements(elements); } @Override public void failure(RetrofitError error) { } }); } } /** * Envoie la liste d'éléments à la montre * Envoie de même les images * @param elements */ private void envoyerListElements(final List<Element> elements) { new Thread(new Runnable() { @Override public void run() { //Envoie des elements et leurs images sendElements(elements); } }).start(); }
/** * Permet d'envoyer une liste d'elements */ protected void sendElements(final List<Element> elements) { final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create("/elements/"); ArrayList<DataMap> elementsDataMap = new ArrayList<>(); //envoie chaque élémént 1 par 1 for (int position = 0; position < elements.size(); ++position) { DataMap elementDataMap = new DataMap(); Element element = elements.get(position); //créé un emplacement mémoire "element/[position]" //ajoute la date de mi[jase à jour elementDataMap.putString("timestamp", new Date().toString()); //ajoute l'element champ par champ elementDataMap.putString("titre", element.getTitre()); elementDataMap.putString("description", element.getDescription()); elementDataMap.putString("url", element.getUrl()); //ajoute cette datamap à notre arrayList elementsDataMap.add(elementDataMap); } //place la liste dans la datamap envoyée à la wear putDataMapRequest.getDataMap().putDataMapArrayList("/list/",elementsDataMap); //envoie la liste à la montre if (mApiClient.isConnected()) Wearable.DataApi.putDataItem(mApiClient, putDataMapRequest.asPutDataRequest()); //puis envoie les images dans un second temps for(int position = 0; position < elements.size(); ++position){ //charge l'image associée pour l'envoyer en bluetooth sendImage(elements.get(position).getUrl(), position); } }
Watch
1. La montre envoie le message « bonjour » au smartphone
@Override public void onConnected(Bundle bundle) { Wearable.MessageApi.addListener(mApiClient, this); Wearable.DataApi.addListener(mApiClient, this); //envoie le premier message sendMessage("bonjour", "smartphone"); }
2. A la réception des données /element/list/ la montre va récupérer les objets Element partagés
@Override public void onDataChanged(DataEventBuffer dataEvents) { //appellé lorsqu'une donnée à été mise à jour, nous utiliserons une autre méthode for (DataEvent event : dataEvents) { //on attend les "elements" if (event.getType() == DataEvent.TYPE_CHANGED && event.getDataItem().getUri().getPath().startsWith("/elements/")) { DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem()); List<DataMap> elementsDataMap = dataMapItem.getDataMap().getDataMapArrayList("/list/"); if (elementList == null || elementList.isEmpty()) { elementList = new ArrayList<>(); for (DataMap dataMap : elementsDataMap) { elementList.add(getElement(dataMap)); } //charge les images puis affiche le main screen preloadImages(elementList.size()); } } } } public Element getElement(DataMap elementDataMap) { return new Element( elementDataMap.getString("titre"), elementDataMap.getString("description"), elementDataMap.getString("url")); }
3. La smartwatch va charger les Bitmap envoyés via /image/ et les placer dans un cache (en RAM)
Les images passés par la DataApi sont sauvegardés dans un LruCache afin d’être accessible facilement et rapidement dans notre application Wear. J’ai créé un objet Singletonné nommé DrawableCache pour cet usage :
/** * Singleton * Classe permettant de sauvegarder en mémoire des drawable * Possède deux fonctions principales : * - put(Integer key, Drawable drawable) * - get(Integer key) : Drawable */ public class DrawableCache extends LruCache<Integer, Drawable> { public static final String TAG = DrawableCache.class.getCanonicalName(); private static DrawableCache INSTANCE; public static DrawableCache init(int size) { if (INSTANCE == null) INSTANCE = new DrawableCache(size); return INSTANCE; } private DrawableCache(int size) { super(size); } public static DrawableCache getInstance() { return INSTANCE; } @Override protected Drawable create(final Integer entry) { return new ColorDrawable(Color.TRANSPARENT); } }
Nous allons l’utiliser dans la méthode preloadImages
/** * Précharge les images dans un cache Lru (en mémoire, pas sur le disque) * Afin d'être accessibles depuis l'adapter * Puis affiche le viewpager une fois terminé * * @param size nombre d'images à charger */ public void preloadImages(final int size) { //initialise le cache DrawableCache.init(size); //dans le UIThread pour avoir accès aux toasts runOnUiThread(new Runnable() { @Override public void run() { new AsyncTask<Void, Void, Void>() { @Override protected void onPreExecute() { super.onPreExecute(); Toast.makeText(MainActivity.this, "Chargement des images", Toast.LENGTH_LONG).show(); } @Override protected Void doInBackground(Void... params) { //charge les images 1 par 1 et les place dans un LruCache for (int i = 0; i < size; ++i) { Bitmap bitmap = getBitmap(i); Drawable drawable = null; if (bitmap != null) drawable = new BitmapDrawable(MainActivity.this.getResources(), bitmap); else drawable = new ColorDrawable(Color.BLUE); DrawableCache.getInstance().put(i, drawable); } return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); //affiche le viewpager startMainScreen(); } }.execute(); } }); }
Après le chargement des images (onPostExecute) j’appelle la fonction startMainScreen(); afin de demander l’affichage du ViewPager.
4. La smartwatch peux enfin afficher le viewpager
Modifier le ElementGridPagerAdapter afin qu’il utilise notre cache et puisse afficher des objets Element :
public class ElementGridPagerAdapter extends FragmentGridPagerAdapter { ... public ElementGridPagerAdapter(List<Element> elements, FragmentManager fm) { super(fm); this.mRows = new ArrayList<Row>(); //Construit le tableau des éléménts à afficher for (Element element : elements) { mRows.add(new Row( //pour l'instant nous ne mettrons qu'un élément par ligne CardFragment.create(element.getTitre(), element.getTexte()) ) ); } } //le drawable affichée en background pour la ligne [row] @Override public Drawable getBackgroundForRow(final int row) { return DrawableCache.getInstance().get(row); //les images ont déjà été chargées dans un LruCache } ... }
Compilez maintenant le module mobile et lancez le sur votre smartphone, puis exécutez le module wear sur votre montre, le résultat est plutôt waouh 🙂
Vous pouvez retrouver les sources de ce tutoriel à l’adresse suivante : https://github.com/florent37/TutosAndroidFrance/tree/master/WearSample
1 réponse
[…] vous avez lu mon tutoriel concernant le développement d’applications Android Wear https://tutos-android-france.com/developper-une-application-pour-les-montres-android-wear/ vous aurez pu remarquer que le développement d’un module Wear est assez fastidieux… […]