Aller au contenu

Créer une app Wear OS

GitHub

Ce guide vous accompagne dans la création d'une application compagnon Wear OS qui communique avec votre application Capacitor sur téléphone via les mêmes chemins Data Layer que CapgoWatchPlugin sur Android (play-services-wearable).

Avant de commencer, assurez-vous d'avoir :

  • Android Studio (dernière version stable)
  • Un projet Android Capacitor existant (npx cap add android si nécessaire)
  • Une montre Wear OS physique appairée au téléphone (recommandé pour les tests de bout en bout)
  • La même clé de signature pour l'application mobile et les builds de release du module Wear OS

Le plugin côté téléphone (CapgoWatchPlugin.java) utilise ces routes Data Layer :

RouteTypeObjectif
/capgo/messageMessageOne-way messages (phone to watch and watch to phone)
/capgo/message/withreplyMessageMessages de la montre vers le téléphone qui attendent une réponse JS via replyToMessage
/capgo/reply/{callbackId}MessageLe payload de réponse du téléphone vers la montre
/capgo/contextDataItemDernier contexte d'application (payload une chaîne JSON)
/capgo/userinfo/{uuid}DataItemInfos utilisateur en attente (payload une chaîne JSON)

Le téléphone détecte votre application montre avec la capgo_watch capacité (WATCH_APP_CAPABILITY dans le plugin).

Une fois ce guide terminé, votre arborescence Android ressemblera à ceci :

  • Répertoireandroid/
    • Répertoireapp/ (module téléphone, hôte Capacitor)
      • …
    • Répertoirewear/ (nouveau module Wear OS)
      • Répertoiresrc/main/
        • AndroidManifest.xml
        • java/…/WearMainActivity.kt
        • java/…/CapgoWearBridge.kt
        • res/values/wear.xml
    • settings.gradle
  1. Ouvrez le android dossier dans Android Studio (pas la racine du repo).

  2. Choisissez Fichier → Nouveau → Nouveau module…

  3. Sélectionnez Wear OS → Application Wear vide (ou Application Wear OS avec Compose si vous préférez).

  4. Configurez le module :

    • Nom du module: wear (ou le nom de votre choix)
    • Nom du package: corresponde au namespace de votre app mobile si possible
    • Minimum SDK: alignez-le sur votre projet (le plugin minSdk est 24)
  5. Cliquez Terminer et laissez la synchronisation Gradle s'effectuer.

Le plugin Capacitor dépend déjà de com.google.android.gms:play-services-wearable:18.2.0. Votre wear le module nécessite le même API.

Dans wear/build.gradle (ou wear/build.gradle.kts), ajoutez :

dependencies {
implementation "com.google.android.gms:play-services-wearable:18.2.0"
// Your UI dependencies (Compose, etc.)
}

Synchronisez Gradle après modification.

Ensure settings.gradle inclut le module wear :

include ':app'
include ':wear'

Exécutez npx cap sync android de votre projet Capacitor lorsque vous modifiez des modules natifs, puis relancez un build dans Android Studio.

Étape 3 : Déclarer la capgo_watch capability

Étape 3 : Déclarer la capability capgo_watch

Créer wear/src/main/res/values/wear.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="android_wear_capabilities" translatable="false" tools:ignore="UnusedResources">
<item>capgo_watch</item>
</string-array>
</resources>

Cela permet au plugin téléphone de définir isWatchAppInstalled lorsqu'il interroge la capability capgo_watch.

Étape 4 : Manifeste du module Wear et ID d'application

Section intitulée « Étape 4 : Manifeste du module Wear et ID d'application »

Utilisez le même applicationId sur l'application téléphone et le module Wear. Définissez-le dans wear/build.gradle (et assurez-vous qu'il correspond dans android/app/build.gradle):

android {
defaultConfig {
applicationId "com.example.myapp" // must match android/app
}
}

Exemple wear/src/main/AndroidManifest.xml skeleton :

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<activity
android:name=".WearMainActivity"
android:exported="true"
android:taskAffinity=""
android:theme="@android:style/Theme.DeviceDefault">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Créer CapgoWearBridge.kt dans votre package wear. Il reflète les chemins de CapgoWatchPlugin.java.

Le plugin téléphone assigne un callbackId pour chaque /capgo/message/withreply et envoie la réponse sur /capgo/reply/{callbackId}. La montre ne voit jamais cet ID à l'avance. Pour les requêtes concurrentes, incluez un requestId dans votre payload JSON, renvoyez-le depuis le téléphone dans replyToMessage, et indexez les handlers de la montre sur ce champ.

package com.example.myapp.wear
import android.content.Context
import android.net.Uri
import android.util.Log
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.DataClient
import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.Wearable
import org.json.JSONObject
import java.nio.charset.StandardCharsets
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class CapgoWearBridge(private val context: Context) :
MessageClient.OnMessageReceivedListener,
DataClient.OnDataChangedListener {
companion object {
private const val TAG = "CapgoWearBridge"
const val CAPABILITY = "capgo_watch"
const val PATH_MESSAGE = "/capgo/message"
const val PATH_MESSAGE_WITH_REPLY = "/capgo/message/withreply"
const val PATH_REPLY_PREFIX = "/capgo/reply/"
const val PATH_CONTEXT = "/capgo/context"
const val PATH_USER_INFO_PREFIX = "/capgo/userinfo/"
}
private val executor = Executors.newSingleThreadExecutor()
private val timeoutScheduler = Executors.newSingleThreadScheduledExecutor()
private val messageClient = Wearable.getMessageClient(context)
private val dataClient = Wearable.getDataClient(context)
private val nodeClient = Wearable.getNodeClient(context)
var onMessage: ((JSONObject) -> Unit)? = null
var onContext: ((JSONObject) -> Unit)? = null
var onUserInfo: ((JSONObject) -> Unit)? = null
fun start() {
messageClient.addListener(this)
dataClient.addListener(this)
Wearable.getCapabilityClient(context)
.addLocalCapability(CAPABILITY)
}
fun stop() {
messageClient.removeListener(this)
dataClient.removeListener(this)
pendingReplyTimeouts.values.forEach { it.cancel(false) }
pendingReplyTimeouts.clear()
pendingReplyHandlers.clear()
timeoutScheduler.shutdownNow()
}
fun sendMessage(data: JSONObject, onError: (String) -> Unit = {}) {
executor.execute {
try {
val nodes = com.google.android.gms.tasks.Tasks.await(nodeClient.connectedNodes)
val payload = data.toString().toByteArray(StandardCharsets.UTF_8)
for (node in nodes) {
messageClient.sendMessage(node.id, PATH_MESSAGE, payload).let {
com.google.android.gms.tasks.Tasks.await(it)
}
}
} catch (e: Exception) {
Log.e(TAG, "sendMessage failed", e)
onError(e.message ?: "sendMessage failed")
}
}
}
private val pendingReplyHandlers = ConcurrentHashMap<String, (JSONObject) -> Unit>()
private val pendingReplyTimeouts = ConcurrentHashMap<String, java.util.concurrent.ScheduledFuture<*>>()
private val outboundUserInfoPaths = ConcurrentHashMap.newKeySet<String>()
fun sendMessageWithReply(
data: JSONObject,
onReply: (JSONObject) -> Unit,
onError: (String) -> Unit = {},
) {
executor.execute {
val requestId = UUID.randomUUID().toString()
try {
val nodes = com.google.android.gms.tasks.Tasks.await(nodeClient.connectedNodes)
if (nodes.isEmpty()) {
onError("No connected phone node")
return@execute
}
val payloadJson = JSONObject(data.toString()).apply {
put("requestId", requestId)
}
pendingReplyHandlers[requestId] = onReply
val timeout = timeoutScheduler.schedule(
{
if (pendingReplyHandlers.remove(requestId) != null) {
pendingReplyTimeouts.remove(requestId)
onError("Reply timed out after 5 minutes")
}
},
5,
TimeUnit.MINUTES,
)
pendingReplyTimeouts[requestId] = timeout
val node = nodes.first()
com.google.android.gms.tasks.Tasks.await(
messageClient.sendMessage(
node.id,
PATH_MESSAGE_WITH_REPLY,
payloadJson.toString().toByteArray(StandardCharsets.UTF_8),
),
)
} catch (e: Exception) {
pendingReplyHandlers.remove(requestId)
pendingReplyTimeouts.remove(requestId)?.cancel(false)
Log.e(TAG, "sendMessageWithReply failed", e)
onError(e.message ?: "sendMessageWithReply failed")
}
}
}
fun updateApplicationContext(contextData: JSONObject, onError: (String) -> Unit = {}) {
executor.execute {
try {
val request = PutDataMapRequest.create(PATH_CONTEXT)
request.dataMap.putString("payload", contextData.toString())
request.setUrgent()
com.google.android.gms.tasks.Tasks.await(dataClient.putDataItem(request.asPutDataRequest()))
} catch (e: Exception) {
Log.e(TAG, "updateApplicationContext failed", e)
onError(e.message ?: "updateApplicationContext failed")
}
}
}
fun transferUserInfo(userInfo: JSONObject, onError: (String) -> Unit = {}) {
executor.execute {
val path = PATH_USER_INFO_PREFIX + UUID.randomUUID()
try {
outboundUserInfoPaths.add(path)
val request = PutDataMapRequest.create(path)
request.dataMap.putString("payload", userInfo.toString())
request.setUrgent()
com.google.android.gms.tasks.Tasks.await(dataClient.putDataItem(request.asPutDataRequest()))
} catch (e: Exception) {
outboundUserInfoPaths.remove(path)
Log.e(TAG, "transferUserInfo failed", e)
onError(e.message ?: "transferUserInfo failed")
}
}
}
override fun onMessageReceived(event: MessageEvent) {
val path = event.path
val json = try {
JSONObject(String(event.data, StandardCharsets.UTF_8))
} catch (e: Exception) {
Log.w(TAG, "Ignoring non-JSON message on $path", e)
return
}
when {
path == PATH_MESSAGE -> onMessage?.invoke(json)
path.startsWith(PATH_REPLY_PREFIX) -> {
val requestId = json.optString("requestId", "")
if (requestId.isNotEmpty()) {
pendingReplyTimeouts.remove(requestId)?.cancel(false)
pendingReplyHandlers.remove(requestId)?.invoke(json)
}
}
}
}
override fun onDataChanged(dataEvents: DataEventBuffer) {
for (event in dataEvents) {
if (event.type != DataEvent.TYPE_CHANGED) continue
val item = event.dataItem
val path = item.uri.path ?: continue
val map = DataMapItem.fromDataItem(item).dataMap
val payload = map.getString("payload", "{}")
val json = try {
JSONObject(payload)
} catch (e: Exception) {
Log.w(TAG, "Ignoring non-JSON DataItem on $path", e)
continue
}
when {
path == PATH_CONTEXT -> onContext?.invoke(json)
path.startsWith(PATH_USER_INFO_PREFIX) -> {
if (outboundUserInfoPaths.remove(path)) {
continue
}
onUserInfo?.invoke(json)
dataClient.deleteDataItems(item.uri)
}
}
}
}
}

Branchez le bridge dans WearMainActivity.kt:

class WearMainActivity : ComponentActivity() {
private lateinit var bridge: CapgoWearBridge
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bridge = CapgoWearBridge(applicationContext)
bridge.onMessage = { json -> Log.d("Wear", "phone message: $json") }
bridge.onContext = { json -> Log.d("Wear", "context: $json") }
bridge.onUserInfo = { json -> Log.d("Wear", "userInfo: $json") }
bridge.start()
setContent { /* your Compose UI */ }
}
override fun onDestroy() {
bridge.stop()
super.onDestroy()
}
}

Utiliser applicationContext lors de la construction des clients pour que les listeners survivent aux changements de configuration.

Étape 6 : Capacitor côté téléphone (TypeScript)

Section intitulée « Étape 6 : Capacitor côté téléphone (TypeScript) »

Installez et synchronisez le plugin sur l'application téléphone (identique à iOS) :

import { CapgoWatch } from '@capgo/capacitor-watch';
export async function setupWatchListeners() {
await CapgoWatch.addListener('messageReceived', (event) => {
console.log('Wear message:', event.message);
});
await CapgoWatch.addListener('messageReceivedWithReply', async (event) => {
const requestId = event.message.requestId;
await CapgoWatch.replyToMessage({
callbackId: event.callbackId,
data: { requestId, ok: true, echo: event.message },
});
});
await CapgoWatch.addListener('applicationContextReceived', (event) => {
console.log('Wear context:', event.context);
});
await CapgoWatch.addListener('userInfoReceived', (event) => {
console.log('Wear userInfo:', event.userInfo);
});
}
export async function pingWatch() {
const info = await CapgoWatch.getInfo();
if (!info.isSupported) {
console.warn('Wear OS APIs not available on this device');
return;
}
if (!info.isReachable) {
console.warn('No connected Wear OS nodes');
return;
}
await CapgoWatch.sendMessage({
data: { action: 'ping', at: Date.now() },
});
}

Appelez setupWatchListeners() au début du bootstrap de votre application (par exemple après platform.ready()).

  1. Sélectionnez la téléphone app configuration d'exécution, installez sur un téléphone Android physique.

  2. Sélectionnez la wear configuration d'exécution, installez sur la montre associée (ou un émulateur de montre lié à l'émulateur du téléphone).

  3. Confirmez getInfo() sur le téléphone :

    • isSupported: true lorsque les services Google Play et Wearable API sont disponibles
    • isReachable: true lorsqu'au moins un nœud est connecté
    • isWatchAppInstalled: true quand la montre diffuse capgo_watch
  4. Envoyer CapgoWatch.sendMessage depuis le téléphone et vérifier bridge.onMessage sur la montre.

  5. Envoyer depuis la montre avec sendMessageWithReply et confirmer messageReceivedWithReply plus replyToMessage est livré sur /capgo/reply/{callbackId}.

  6. Tester updateApplicationContext et transferUserInfo dans les deux sens en utilisant les chemins DataItem ci-dessus.

isSupported: false sur un téléphone réel

  • Services Google Play manquants ou obsolètes
  • API Wearable indisponible sur l'appareil

isReachable: false mais la montre est appairée

  • Bluetooth désactivé ou l'application montre n'a pas tourné récemment
  • Différent applicationId ou clés de signature différentes entre les modules
  • Aucun nœud Wear OS actuellement connecté aux services Google Play

isWatchAppInstalled: false

  • L'APK compagnon Wear n'est pas installé sur la montre
  • Manquant capgo_watch entrée dans wear.xml
  • Capability non synchronisée : réinstallez l'APK Wear après avoir modifié les capabilities

Les messages n'arrivent jamais

  • Le chemin doit correspondre exactement (/capgo/message, pas un préfixe personnalisé)
  • Le payload doit être du texte JSON en UTF-8 (le plugin utilise des JSONObject / JSObject strings)

La réponse ne revient jamais vers la montre

  • Le téléphone doit appeler replyToMessage avec le callbackId depuis messageReceivedWithReply
  • Incluez le même requestId dans le JSON de réponse si la montre corrèle les requêtes simultanées
  • Les réponses en attente expirent après 5 minutes sur Android (constante du plugin PENDING_REPLY_TTL_MS)

Continuer après « Créer une application Wear OS »

Section intitulée « Continuer après Créer une application Wear OS »

Si vous utilisez Créer une app Wear OS pour planifier le travail sur les plugins natifs, connectez-l’à Utilisation de @capgo/capacitor-watch pour la capacité native dans Utilisation de @capgo/capacitor-watch, Capgo Répertoire des plugins pour le workflow produit dans Capgo Plugin Directory, Plugins Capacitor par Capgo pour les détails d'implémentation dans Plugins Capacitor par Capgo, Ajouter ou mettre à jour des plugins pour les détails d'implémentation dans Ajouter ou mettre à jour des plugins, et Alternatives aux plugins Ionic Enterprise pour le workflow produit dans Alternatives aux plugins Ionic Enterprise.