Wear OS アプリの作成
インストール手順と、このプラグインの完全なMarkdownガイドを含むセットアッププロンプトをコピーします。
Capacitor phone app と、Android と同様の Data Layer パスを介して通信する Wear OS コンパニオンアプリの設定方法を解説します。 CapgoWatchPlugin Android (play-services-wearable).
前提条件
「前提条件」セクション開始する前に、以下を確認してください:
- デバッグ (最新の安定版)
- 既存の Capacitor Android プロジェクト (
npx cap add android(必要な場合) - スマートフォンとペアリング済みの実機の Wear OS ウォッチ (E2E テストを推奨)
- 同一の署名キー スマホアプリと Wear OS モジュールのリリースビルド用
プラグインの Wear OS へのマッピング
「プラグインの Wear OS へのマッピング」セクションスマートフォン側のプラグイン (CapgoWatchPlugin.java) は以下の Data Layer ルートを使用します:
| ルート | 種類 | 目的 |
|---|---|---|
/capgo/message | メッセージ | 一方向メッセージ (スマートフォンから Wear OS、および Wear OS からスマートフォン) |
/capgo/message/withreply | メッセージ | Wear OS からスマートフォンへのメッセージ。JS による返信を想定しており、 replyToMessage |
/capgo/reply/{callbackId} | メッセージ | スマートフォンからの返信ペイロードが Wear OS に送られます |
/capgo/context | DataItem | 最新のアプリケーションコンテキスト(payload string形式のJSON) |
/capgo/userinfo/{uuid} | DataItem | キューに保存されたユーザー情報(payload string形式のJSON) |
スマートフォンが、プラグイン内の capgo_watch capability(WATCH_APP_CAPABILITY )を持つウォッチアプリを検出します)。
プロジェクト構成
「Project structure」セクションこのガイドを完了すると、Androidのディレクトリ構造は以下のようになります:
ディレクトリandroid/
ディレクトリapp/ (phoneモジュール、Capacitor ホスト)
- …
ディレクトリwear/ (新規 Wear OS モジュール)
ディレクトリsrc/main/
- AndroidManifest.xml
- java/…/WearMainActivity.kt
- java/…/CapgoWearBridge.kt
- res/values/wear.xml
- settings.gradle
ステップ 1: Wear OS モジュールの追加
「ステップ 1: Wear OS モジュールを追加する」セクション-
次を開きます:
androidAndroid Studio 内のフォルダ(リポジトリのルートではありません)。 -
選択: ファイル → 新規 → 新規モジュール…
-
選択します Wear OS → 空のWear OSアプリを作成 (または Wear OS アプリ (Compose を使用する場合も同様です)。
-
モジュールの設定:
- モジュール名:
wear(任意の名前を使用可能) - パッケージ名: 可能な限りスマホアプリのネームスペースと一致させてください
- 最小 SDK: プロジェクトの設定に合わせてください (plugin
minSdkは 24 です)
- モジュール名:
-
クリック 完了 Gradle syncを実行します。
ステップ 2: Gradle の依存関係
「ステップ2: Gradleの依存関係」というセクションCapacitorプラグインはすでに以下に依存しています: com.google.android.gms:play-services-wearable:18.2.0。あなたの wear モジュールには同じ API が必要です。
では wear/build.gradle (または wear/build.gradle.kts), add:
dependencies { implementation "com.google.android.gms:play-services-wearable:18.2.0" // Your UI dependencies (Compose, etc.)}編集後は Gradle を Sync してください。
必ず settings.gradle wear モジュールを含みます:
include ':app'include ':wear'実行 npx cap sync android ネイティブモジュールを変更した際は、Capacitor プロジェクトから同期し、その後 Android Studio で再ビルドしてください。
ステップ 3: 次の capgo_watch capability を宣言する
「ステップ 3: capgo_watch capability を宣言する」セクション作成します 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>これにより、phone プラグインが設定できるようになります isWatchAppInstalled capability をクエリする際 capgo_watch.
ステップ 4: Wear モジュールの Manifest と Application ID
「ステップ 4: Wear モジュールの manifest と application ID」セクション同じものを使用してください applicationId phone アプリと wear モジュールで。これを ___ に設定し、 wear/build.gradle (および ___ 内で一致させてください) android/app/build.gradle):
android { defaultConfig { applicationId "com.example.myapp" // must match android/app }}例 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>ステップ 5: Wear 側のブリッジ (Kotlin)
「ステップ 5: Wear 側のブリッジ (Kotlin)」セクション作成します CapgoWearBridge.kt wear パッケージ内にあります。これは、〜内のパスをミラーリングします。 CapgoWatchPlugin.java.
Phone プラグインは、不透明な(opaque) callbackId ごとに /capgo/message/withreply 、その返信を 〜 で送信します。 /capgo/reply/{callbackId}。ウォッチ側は、事前にその ID を知ることはありません。並行リクエストを行う場合は、 requestId JSONペイロード内で、電話側から以下のようにechoします: replyToMessage、そしてそのフィールドを使用して、ウォッチ側のハンドラーを呼び出します。
package com.example.myapp.wear
import android.content.Contextimport android.net.Uriimport android.util.Logimport com.google.android.gms.wearable.CapabilityClientimport com.google.android.gms.wearable.DataClientimport com.google.android.gms.wearable.DataEventimport com.google.android.gms.wearable.DataEventBufferimport com.google.android.gms.wearable.DataMapItemimport com.google.android.gms.wearable.MessageClientimport com.google.android.gms.wearable.MessageEventimport com.google.android.gms.wearable.PutDataMapRequestimport com.google.android.gms.wearable.Wearableimport org.json.JSONObjectimport java.nio.charset.StandardCharsetsimport java.util.UUIDimport java.util.concurrent.ConcurrentHashMapimport java.util.concurrent.Executorsimport 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) } } } }
}ブリッジを以下のように接続します: 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() }}使用する applicationContext クライアント構築時にこれを行うことで、構成変更後もリスナーを維持できます。
ステップ 6: スマホ側の Capacitor (TypeScript)
「ステップ 6: 電話側の Capacitor (TypeScript)」セクション電話アプリにプラグインをインストールして同期します(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() }, });}呼び出す setupWatchListeners() アプリのブートストラップの早い段階(例: platform.ready()).
ステップ 7: ビルド、インストール、テスト
「ステップ 7: ビルド、インストール、テスト」セクション-
次の スマートフォン
app実行構成を実行し、実機のAndroidスマートフォンにインストールします。 -
次の
wear実行構成を実行し、ペアリングされたウォッチ(またはスマホエミュレータにリンクされたウォッチエミュレータ)にインストールします。 -
確認
getInfo()スマートフォン側:isSupported: trueGoogle Play 開発者サービスと Wearable API が利用可能な場合isReachable: true少なくとも1つのノードが接続されている場合isWatchAppInstalled: trueウォッチがアドバタイズしているときcapgo_watch
-
送信
CapgoWatch.sendMessageスマホから送信し、検証しますbridge.onMessageウォッチ側で。 -
ウォッチから以下を使用して送信し、
sendMessageWithReply確認しますmessageReceivedWithReplyとreplyToMessageが以下に届いたことを/capgo/reply/{callbackId}. -
テスト
updateApplicationContextとtransferUserInfo上記の DataItem パスを使用して双方向で。
トラブルシューティング
「トラブルシューティング」セクションisSupported: false 実機のスマートフォンで
- Google Play services が不足しているか、バージョンが古いです
- デバイスで Wearable API が利用できません
isReachable: false ウォッチがペアリングされている場合
- Bluetoothが無効、またはウォッチアプリが最近実行されていない
- 不一致
applicationIdまたはモジュール間で署名キーが異なる - Google Play servicesに接続されているWear OSノードがありません
isWatchAppInstalled: false
- ウォッチにWear companion APKがインストールされていません
- 不足
capgo_watchの項目がありませんwear.xml - Capabilityが同期されていません:capability変更後はwear APKを再インストールしてください
メッセージが届きません
- パスを完全に一致させる必要があります(
/capgo/messageカスタムプレフィックスは不可) - ペイロードはUTF-8形式のJSONテキストである必要があります(プラグインは
JSONObject/JSObject文字列を使用します)
Replyはウォッチには返されません
- Phone側から呼び出す必要があります
replyToMessageを使用して、callbackIdとmessageReceivedWithReply - 同じものも
requestIdreply JSONに含めてください(ウォッチが並行リクエストを紐付ける場合) - 保留中のreplyは次で期限切れになります 5分 Androidでは(プラグイン定数
PENDING_REPLY_TTL_MS)
次のステップ
「次のステップ」セクション- watchOSアプリの作成 Apple WatchおよびCapgoWatchSDK向け
- はじめに APIの共有とプラットフォームに関する注意点
- 例 アプリの完全なパターンについて
- プラグインのソース スマートフォン側のパス定数について
「Creating a Wear OS app」から続ける
「Wear OSアプリの作成」からの続き使用している場合 Wear OS アプリの作成 ネイティブプラグインの作業を計画するには、次と連携してください: @capgo/capacitor-watch の使用方法 Using @capgo/capacitor-watch でネイティブ機能を利用する場合 Capgo プラグインディレクトリ Capgo プラグインディレクトリのプロダクトワークフローについては、 Capgo による Capacitor プラグイン Capgo による Capacitor プラグインの実装詳細については、 プラグインの追加または更新 プラグインの追加または更新における実装詳細については、および Ionic Enterprise プラグインの代替案 Ionic Enterprise プラグインの代替案におけるプロダクトワークフローについては。