Publié par

Il y a 5 ans -

Temps de lecture 17 minutes

Enrichissez vos applications existantes avec le SDK Android Wear

Fraîchement arrivées, les premières montres connectées sous Android Wear veulent enrichir l’expérience utilisateur apportée par les applications mobiles de la façon la moins intrusive possible.
Tout est encore à imaginer, à développer; et qui dit « développer » dit également « se familiariser avec un SDK ».

Dans cet article, nous découvrirons le SDK d’Android Wear en partant d’une application existante et en l’améliorant afin de rajouter de nouvelles fonctionnalités et interactions avec une smartwatch : notifications, wear-app, communications entre appareils, composants UI. Votre pré-requis pour suivre ce billet étant de connaître les bases du développement pour Android.

Notre sujet d’expérimentation

Parmi les applications déjà disponibles et compatibles avec Android Wear, « Allthecooks » est celle qui a retenu notre attention par son design élégant et son utilisation adéquate et variée des fonctionnalités d’Android Wear.
Cette application propose des recettes de cuisine et les affiche en temps réel sur la montre, afin que les utilisateurs puissent consulter pas à pas les détails d’une recette depuis leur poignet sans salir leur bien-aimé smartphone.

Nous commencerons les développements sur une application pour smartphones aux fonctionnalités simplifiées mais similaires s’intitulant XebiaRecipes et ne possédant, pour l’instant, aucune fonctionnalité Android Wear.

Tout d’abord, téléchargeons son code source :

git clone git@github.com:Nilhcem/xebia-wear-tutorial.git
cd xebia-wear-tutorial
git checkout step1

L’application est composée de deux activités :

  • HomeActivity (source) présente à l’utilisateur une liste de recettes de cuisine
  • RecipeActivity (source) affiche les détails d’une recette de cuisine. On accède à RecipeActivity depuis HomeActivity en cliquant sur une des photos proposées.

Voici à quoi ressemble ces deux activités sur un téléphone :

Ajout de notifications

À chaque fois qu’un utilisateur consulte une recette de cuisine, nous souhaitons lui proposer une notification sur sa montre.

Pour créer des notifications sur Android, il est coutume d’utiliser NotificationCompat.Builder de la librairie support-v4, afin d’avoir le meilleur support de notification possible pour un large panel d’appareils.
Bonne nouvelle : les notifications créés avec ce builder s’afficheront automatiquement sur Android Wear.

Ainsi, si l’on rajoute le code suivant à la fin du onCreate() de RecipeActivity :

protected void onCreate(Bundle savedInstanceState) {
    […]
    int notifId = 1;
    // Nous souhaitons pouvoir lancer depuis la notification l'écran de détails d'une recette
    Intent intent = new Intent(this, RecipeActivity.class);
    intent.putExtra(EXTRA_RECIPE_NAME, mRecipe.name());

    // Notre intent contient l'entière backstack (gestion du bouton back et du up button)
    TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
    stackBuilder.addParentStack(RecipeActivity.class);
    stackBuilder.addNextIntent(intent);
    PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

    // Création de la notification en envoyant une version réduite de l'image (280x280) de la recette pour accélérer le transfert de données via bluetooth
    NotificationCompat.Builder builder =
        new NotificationCompat.Builder(this)
            .setContentIntent(pendingIntent)
            .setSmallIcon(R.drawable.ic_launcher)
            .setLargeIcon(Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(), mRecipe.drawableRes), 280, 280, false))
            .setContentTitle(getTitle())
            .setContentText(getString(mRecipe.nameRes));

    // Affichage de la notification
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    notificationManager.cancelAll();
    notificationManager.notify(notifId, builder.build());
}

On recevra la même notification, à la fois sur son téléphone et sur sa montre.

Sur le téléphone :

Et sur la montre :

Ces notifications sont synchronisées : lorsque l’utilisateur efface la notification sur un appareil, elle est automatiquement effacée sur l’autre appareil.
Le bouton d’action « Ouvrir sur tél. » lance l’application sur le téléphone de l’utilisateur à l’écran de recette désiré.

À noter qu’il est possible de compléter ces notifications pour, par exemple, ajouter de nouvelles pages et boutons d’actions uniquement sur la montre et non sur le téléphone.

Par exemple, le code suivant étend le NotificationCompat.Builder par un WearableExtender contenant plusieurs pages dans le but d’afficher toutes les étapes de préparation d’une recette sur la montre :

La notification sur le téléphone reste inchangée :

Pour plus d’information sur les bridged notifications, vous pouvez consulter la documentation officielle.

Les notifications réalisées à l’aide du NotificationCompat.Builder permettent d’exploiter facilement et rapidement les appareils Android Wear.
Cependant, les fonctionnalités apportées sont limitées pour nos besoins : nous ne voulons pas de notification sur le téléphone mais uniquement sur la montre.
De plus, nous souhaitons pouvoir lancer les détails d’une recette sur la montre, et non sur le téléphone comme c’est le cas à présent avec le bouton « Ouvrir sur tél ».
Pour réaliser cela, nous allons devoir créer notre propre wear-app.

Une première Wear-App

Une wear-app est une application Android fonctionnant nativement sur un appareil Android Wear (comme une smartwatch). L’application possède ses propres vues spécifiques et peut communiquer avec un smartphone si nécessaire.

Pendant le développement, il est possible d’installer une application wear directement sur un appareil compatible Android Wear, cependant, cela est impossible pour un utilisateur final avec un build de release.
Les binaires d’application pour Android Wear (apk) sont embarqués dans des applications Android. Cela signifie que lorsqu’un utilisateur lambda souhaite télécharger une application sur sa montre, il doit en réalité télécharger une application (dite application compagnon) sur son téléphone. À l’installation de cette application sur le téléphone, le binaire Wear-App sera transféré et installé sur la montre automatiquement. Lorsque l’utilisateur supprimera l’application sur son téléphone, l’application sur la montre sera automatiquement désinstallée.

Nous allons maintenant créer un sous-module « wear » dans notre projet gradle à l’aide d’Android Studio (file > new module > android wear module).
Le package-name de l’application wear doit être le même que celui de l’application mobile.

Sur ce commit, vous pouvez voir les changements qui ont été réalisés pour créer un module wear dans un projet gradle.
Comme vous pouvez le constater, la structure d’un projet Android Wear est très semblable à celle d’un projet Android, les deux changements principaux sont les suivants :

1. Le fichier AndroidManifest.xml du projet Wear précise que l’application est dédiée aux montres :

<uses-feature android:name="android.hardware.type.watch" />

2. Le layout principal est en fait un WatchViewStub qui inflate soit un layout carré, soit un rond selon la forme de l’appareil utilisé :

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WatchViewStub
    android:id="@+id/watch_view_stub"
    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"
    app:rectLayout="@layout/rect_activity_main"
    app:roundLayout="@layout/round_activity_main"
    tools:context=".MainActivity"
    tools:deviceIds="wear">
</android.support.wearable.view.WatchViewStub>

Voici comment est utilisé ce nouveau composant :

public class MainActivity extends Activity {
    private TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
        stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
            @Override
            public void onLayoutInflated(WatchViewStub stub) {
                mTextView = (TextView) stub.findViewById(R.id.text);
            }
        });
    }
}

Le composant WatchViewStub fait parti des nouveaux composants UI fournis par la Wearable Support Library. Nous vous conseillons d’y jeter un coup d’œil pour vous familiariser avec ces nouveaux éléments UI très utiles sur wearable.

On peut maintenant déployer et lancer l’application sur sa montre :

Bien que sympathique, le composant WatchViewStub ne nous est pas utile pour les besoins de ce tutoriel, nous allons l’enlever du projet.

Communications via Wearable Data Layer API et transfert d’objets synchronisés entre téléphone et montre

Maintenant que nous avons deux applications : une côté téléphone et une autre côté montre, nous souhaitons à présent afficher une notification sur la montre (et uniquement sur la montre) lorsqu’un utilisateur consulte une recette de cuisine depuis son téléphone.
Globalement, le téléphone enverra des données à la montre, qui les réceptionnera et les interprétera.

Les communications entre téléphone (handheld) et montre (wearable) se font à l’aide des Google Play Services.
Il faut donc intégrer cette librairie sur les modules wear et mobile.

Si vous souhaitez reprendre le projet à cet endroit, vous pouvez le cloner de nouveau et aller sur la branche step2

git clone git@github.com:Nilhcem/xebia-wear-tutorial.git
cd xebia-wear-tutorial
git checkout step2

Connexion

Pour transférer des données d’un appareil à un autre, il est nécessaire de passer par l’API de couche de données des Play Services.
Nous allons donc devoir instancier un objet GoogleApiClient dans le onCreate de notre RecipeAcitvity sur le projet mobile, se connecter dans le onStart, et se déconnecter dans le onStop

private GoogleApiClient mGoogleApiClient;

@Override
protected void onCreate(Bundle savedInstanceState) {
[...]
mGoogleApiClient = new GoogleApiClient.Builder(this)
    .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
        @Override
        public void onConnected(Bundle connectionHint) {
        }
        @Override
        public void onConnectionSuspended(int cause) {
        }
    })
    .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
        @Override
        public void onConnectionFailed(ConnectionResult result) {
        }
    })
    .addApi(Wearable.API)
    .build();
}

@Override
protected void onStart() {
    super.onStart();
    mGoogleApiClient.connect();
}

@Override
protected void onStop() {
    mGoogleApiClient.disconnect();
    super.onStop();
}

Maintenant que nous sommes connectés, nous pouvons (et allons) envoyer des données au wearable.

Transfert

Il existe deux manières principales d’envoyer des données entre appareils :

  • Soit on envoi un DataItem : un objet de données synchronisé entre les 2 appareils (quand il est modifié sur un appareil, l’autre appareil est notifié automatiquement)
  • Soit on envoi un Message : une communication à voie unique avec des données non synchronisées, du type « fire-and-forget » : on envoie un signal et on ne se soucie pas du reste.

Nous allons dans cette partie envoyer un DataItem à l’aide de DataMap. Pour simplifier, il faut voir un DataMap comme un Bundle : un conteneur de différents objets sérialisables, mais qui sera cette fois envoyé via bluetooth.
Cette mécanique de DataMap n’est uniquement possible que via DataItem.
Lorsqu’on communique par Message, le payload (les données que l’on compte envoyer) est obligatoirement sous la forme d’un tableau de byte : beaucoup moins agréable à utiliser qu’une map !
Pour plus d’informations sur la communication par Message, référez-vous à cette documentation.

Il nous faut donc rajouter le code suivant dans la méthode onConnected du GoogleApiClient.ConnectionCallbacks afin d’envoyer des données au wearable :

public void onConnected(Bundle connectionHint) {
    PutDataMapRequest dataMapRequest = PutDataMapRequest.create("/start/recipe");
    DataMap dataMap = dataMapRequest.getDataMap();
    dataMap.putString("TITLE", getString(mRecipe.nameRes));
    dataMap.putString("INGREDIENTS", getString(mRecipe.ingredientsRes));
    dataMap.putStringArrayList("STEPS",
        new ArrayList<String>(Arrays.asList(getResources().getStringArray(R.array.recipe_steps))));
    Wearable.DataApi.putDataItem(mGoogleApiClient, dataMapRequest.asPutDataRequest());
}

La méthode PutDataMapRequest.create a besoin d’un chemin (path) permettant d’identifier la requête. Nous lui avons donné le nom "/start/recipe".
Pour le reste, c’est une map : on y met une clé et une valeur.
Évidemment, à la fin, on envoie le tout à la DataApi.

Ici, nous avons envoyé le titre de la recette, les ingrédients nécessaires, et les différentes étapes de réalisation de la recette.
Nous ne détaillerons pas dans cet article comment transférer des assets, mais vous pouvez consulter l’excellente documentation officielle sur ce lien ou voir directement notre commit ici.

Pour plus d’informations sur les DataItem, vous pouvez consulter cette documentation.

Réception

Maintenant que notre application mobile est capable d’envoyer des données, la prochaine étape est de pouvoir les recevoir sur notre application wear.

Il existe deux façons principales de recevoir des données :

  • Soit depuis un service héritant de WearableListenerService
  • Soit depuis une activité implémentant DataApi.DataListener (si l’on a opté pour du DataItem) ou MessageApi.MessageListener (si l’on a opté pour du Message)

Il est intéressant d’opter pour un service si l’on souhaite recevoir ces données à n’importe quel moment.
On opte généralement pour une activité lorsqu’on souhaite recevoir des données uniquement quand l’activité implémentant le listener est active (visible).

Dans notre cas où nous souhaitons recevoir les notifications à n’importe quel moment, l’utilisation d’un WearableListenerService est plus adaptée.
Pour plus d’informations sur les ListenerActivity, vous pouvez consulter cette documentation.

Commençons par enregistrer le service que nous allons créer dans le AndroidManifest.xml du projet wear

<service
    android:name=".wear.NotificationUpdateService"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.google.android.gms.wearable.BIND_LISTENER"/>
    </intent-filter>
</service>

Maintenant, il faut écrire le service :

public class NotificationUpdateService extends WearableListenerService {
    private GoogleApiClient mGoogleApiClient;

    @Override
    public void onCreate() {
        super.onCreate();
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .build();
    }

    @Override
    public void onDataChanged(DataEventBuffer dataEvents) {
        for (DataEvent dataEvent : dataEvents) {
            if (dataEvent.getType() == DataEvent.TYPE_CHANGED) {
                if ("/start/recipe".equals(dataEvent.getDataItem().getUri().getPath())) {
                    DataMapItem dataMapItem = DataMapItem.fromDataItem(dataEvent.getDataItem());
                    DataMap dataMap = dataMapItem.getDataMap();
                    String title = dataMap.getString("TITLE");
                    String ingredients = dataMap.getString("INGREDIENTS");
                    ArrayList<String> steps = dataMap.getStringArrayList("STEPS");
                    sendNotification(title, ingredients, steps, null);
                }
            }
        }
    }

    private void sendNotification(String title, String ingredients, ArrayList<String> steps, Bitmap photo) {
    }
}

Le code est assez direct : notre service se connecte à la WearableApi dans le onCreate.
Lorsqu’un objet partagé (DataItem) a été modifié, la méthode onDataChanged est appelée.
La première étape est d’identifier le DataItem à l’aide de son chemin, qui doit être similaire à celui spécifié précédemment lorsqu’on avait envoyé les données (on avait choisi arbitrairement comme chemin : « /start/recipe »).
Ensuite, on récupère chaque item de la DataMap, et, encore une fois, nous ne détaillons pas le code pour récupérer une photo ici, mais vous pouvez en savoir plus en consultant notre commit ou la documentation officielle.

Pour plus d’informations sur le transfert de données synchronisées, vous pouvez consulter la documentation à cet endroit.

Ajout d’une notification depuis le wearable

Vous vous souvenez de la première partie du tutoriel où nous avons créé une notification ?
Nous allons utiliser exactement la même méthode pour créer une notification depuis le service du wearable.
La seule différence étant que nous ne sommes pas obligé d’utiliser les librairies de compatibilité (et nous n’allons pas le faire).

private void sendNotification(String title, String ingredients, ArrayList<String> steps, Bitmap photo) {
    Intent viewIntent = new Intent(this, MainActivity.class);
    viewIntent.putExtra("TITLE", title);
    viewIntent.putExtra("INGREDIENTS" ingredients);
    viewIntent.putExtra("STEPS", steps);
    PendingIntent pendingViewIntent = PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_CANCEL_CURRENT);

    Notification notification = new Notification.Builder(this)
        .setSmallIcon(R.drawable.ic_launcher)
        .setLargeIcon(photo)
        .setContentText(title)
        .addAction(new Notification.Action(R.drawable.ic_launcher, "Commencer", pendingViewIntent))
        .build();

    NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    notificationManager.notify(1, notification);

Et voila ! Lorsqu’on lance une recette depuis le téléphone, des données sont bien envoyées à la montre, qui les interprète et en fait une notification. La notification est uniquement visible sur le wearable.
Quand on clique sur le bouton d’action « Commencer », une activité provenant de la montre est démarrée.

Le commit lié à cette partie, intégrant en plus la gestion des assets est disponible ici.

Un layout pour l’activité principale de notre montre

Le bouton « Commencer » démarre notre MainActivity qui est actuellement vide.

Si vous souhaitez reprendre le projet à cet endroit, vous pouvez aller sur la branche step3

git clone git@github.com:Nilhcem/xebia-wear-tutorial.git
cd xebia-wear-tutorial
git checkout step3

Nous allons maintenant créer un layout pour notre MainActivity en utilisant certains composants de la Wearable UI Library.
Vous n’êtes pas obligé d’utiliser ces composants (bien que cela soit recommandé) : les Activity sur Wear et les Activity sur Android sont similaires, vous savez donc déjà comment faire des layouts pour Wear.

Modifions le fichier activity_main.xml :

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.GridViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true" />

Ici, nous avons utilisé un GridViewPager (ou 2D picker). C’est en fait un ViewPager sur 2 dimensions (lignes et colonnes).
Nous ne souhaitons qu’une dimension (une seule ligne) et pourrions très bien utiliser un simple ViewPager, mais puisque nous souhaitons vous faire découvrir de nouveaux composants, c’est le bon moment d’en utiliser quelques-uns.

Ce GridViewPager a besoin d’un Adapter spécifique (gérant les lignes et les colonnes), très similaire à un FragmentPagerAdapter :

public class MainAdapter extends FragmentGridPagerAdapter {

    private String mTitle;
    private String mIngredients;
    private List<String> mSteps = Collections.emptyList();
    private Context mContext;

    public MainAdapter(FragmentManager fm, Context context, String title, String ingredients, List<String> steps) {
        super(fm);
        mContext = context;
        mTitle = title;
        mIngredients = ingredients;
        mSteps = steps;
    }

    @Override
    public Fragment getFragment(int row, int col) {
        if (col == 0) {
            return CardFragment.create(mTitle, mIngredients, R.drawable.ic_launcher);
        } else {
            return CardFragment.create(mContext.getString(R.string.step_title, col), mSteps.get(col - 1), 0);
        }
    }

    @Override
    public int getRowCount() {
        return 1;
    }

    @Override
    public int getColumnCount(int row) {
        return mSteps.size() + 1;
    }
}

Le premier écran fourni par cet Adapter présentera la liste des ingrédients de la recette. Les autres écrans fourniront les étapes de réalisation de la recette.

Nous avons utilisé ici un CardFragment qui est un autre composant de la Wearable UI Library permettant d’avoir du contenu sous forme de carte (exemple de carte).

Si nous souhaitons afficher une image de fond derrière ces cartes, il est nécessaire d’implémenter la méthode getBackground(int row, int column) de ce même Adapter, comme nous l’avons fait dans notre commit.

Il ne nous reste plus qu’à instancier et lier ce MainAdapter à notre GridViewPager dans notre Activity :

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // TODO: Récupérer les title, ingredients, steps depuis les intent extras
    setContentView(R.layout.activity_main);
    mPager = (GridViewPager) findViewById(R.id.fragment_container);
    mAdapter = new MainAdapter(getFragmentManager(), this, title, ingredients, steps);
    mPager.setAdapter(mAdapter);
}

Maintenant, lorsqu’on démarre une recette depuis le téléphone, une notification s’affiche sur la montre.
Cette notification propose à l’utilisateur de lancer une activité spéciale directement sur la montre, sans passer par le téléphone.
Une fois l’activité lancée, l’utilisateur peut consulter chaque étape de la recette sur sa montre, pas à pas.

Conclusion

Votre Wear App est maintenant terminée. Il ne vous reste plus qu’à inclure le projet wear dans les dépendences du build.gradle de l’application mobile en rajoutant la ligne suivante :

wearApp project(':wear')

Cela est obligatoire et permettra de packager l’application Wear dans le build signé de l’application Mobile, comme indiqué dans cette documentation.

Nous n’avons pas pu aborder toutes les nouveautés du SDK Android Wear.
Si vous souhaitez en savoir plus sur l’envoi de messages et données non-synchronisées, vous pouvez consulter la documentation ou regarder notre commit (le but étant de faire en sorte que le téléphone scroll automatiquement vers la prochaine étape de la recette quand l’utilisateur change d’étape depuis sa montre).
Vous pouvez aussi vous pencher sur les fonctionnalités vocales (très intéressantes sur wearables), sur les nouveaux composants UI disponibles, et sur les cartes avec custom layout.

Vous pouvez voir ici une vidéo du projet final.
Un apk final du projet est disponible ici, et le code source est sur github.
Nous espérons que cet article vous a donné l’envie d’améliorer vos applications existantes, et d’en créer de nouvelles. Les appareils sous Android Wear viennent tout juste de sortir et il y a tant à faire !

Publié par

Commentaire

Laisser un commentaire

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

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.