Développer une application pour les montres Android Wear

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 :

Capture d’écran 2015-03-18 à 13.53.51

Ceci fait, pensez aussi à installer Android Support Repository, Android Support Library, Google Play services et Google Repository :

Capture d’écran 2015-03-18 à 13.52.49

Vous pouvez maintenant créer votre émulateur :

Capture d’écran 2015-03-18 à 15.12.03 Capture d’écran 2015-03-18 à 15.12.13 Capture d’écran 2015-03-18 à 15.12.20

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

Capture d’écran 2015-03-18 à 15.21.54

Essayez vous un peu à l’ergonomie de la montre afin de passer le tutoriel, et arriver sur cet écran :

Capture d’écran 2015-03-18 à 15.22.09

Sur votre smartphone, lancez l’application Android Wear

Puis associez le à un émulateur :

device-2015-03-18-152852 2

 

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 🙂

device-2015-03-18-155116

Puis exécutez la commande suivante dans votre terminal

adb -d forward tcp:5601 tcp:5601

Et voilà !

device-2015-03-18-155438 - copie

Capture d’écran 2015-03-18 à 15.58.00

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.

device-2015-03-18-100802

Dans Options pour développeurs activez le Débogage ADB

device-2015-03-18-100826

device-2015-03-18-101041

 

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.

wear_association_smartphone_pc

 

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 :

Capture d’écran 2015-03-17 à 21.43.01

 

Une fois notre projet créé, nous pouvons remarquer que notre application contient 2 modules : mobile et wear

Capture d’écran 2015-03-17 à 22.43.12

 

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 :

Capture d’écran 2015-03-18 à 16.30.55

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

device-2015-03-18-163746

 

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

introduction-to-android-wear-tutorial-design-principles-19-638

 

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 scrollable05_uilib
  • 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’erreur08_uilib
  • DelayedConfirmationView – Une vue permettant d’afficher une action effectuée sous un temps donné09_uilib
  • DismissOverlayView  -Une vue permettant de quitter l’application via long-press-to-dismiss20140711174914
  • DotsPageIndicator – Un indicateur de page pour le GridViewPager qui indique la page affichée de la ligne courantes9S8B
  • 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 rectangles03_uilib
  • 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 scrollz6IG7

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 :

wear_element0

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 :

http://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 :

  1. La montre envoie path=”bonjour” et message=”smartphone”
  2. Si le smartphone reçois path=”bonjour” il envoie path=”bonjour” et comme message il envoie “affiche :” suivi d’un nombre aléatoire
  3. 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

device-2015-03-18-235905

 

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.

  1. Le smartphone télécharge les images
  2. Les bitmap sont ensuite sauvegardés dans les blocs mémoire de la DataAPI
  3. L’Android Wear est notifiée dès que la DataAPI reçoit de nouvelles données
  4. 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 :

  1. La montre envoie le message “bonjour” au smartphone
  2. A la récéption du message “bonjour”, le smartphone récupère les informations du webservice
  3. Le smartphone envoie les objets Element dans /element/list/
  4. 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)
  5. A la réception des données /element/list/ la montre va récupérer les objets Element partagés
  6. La smartwatch va charger les Bitmap envoyés via /image/ et les placer dans un cache (en RAM)
  7. 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 = "http://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

Un commentaire sur “Développer une application pour les montres Android Wear

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *