メインコンテンツへスキップ

Wear OS アプリの作成

GitHub

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/contextDataItem最新のアプリケーションコンテキスト(payload string形式のJSON)
/capgo/userinfo/{uuid}DataItemキューに保存されたユーザー情報(payload string形式のJSON)

スマートフォンが、プラグイン内の capgo_watch capability(WATCH_APP_CAPABILITY )を持つウォッチアプリを検出します)。

このガイドを完了すると、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. 次を開きます: android Android Studio 内のフォルダ(リポジトリのルートではありません)。

  2. 選択: ファイル → 新規 → 新規モジュール…

  3. 選択します Wear OS → 空のWear OSアプリを作成 (または Wear OS アプリ (Compose を使用する場合も同様です)。

  4. モジュールの設定:

    • モジュール名: wear (任意の名前を使用可能)
    • パッケージ名: 可能な限りスマホアプリのネームスペースと一致させてください
    • 最小 SDK: プロジェクトの設定に合わせてください (plugin minSdk は 24 です)
  5. クリック 完了 Gradle syncを実行します。

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.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)
}
}
}
}
}

ブリッジを以下のように接続します: 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: ビルド、インストール、テスト」セクション
  1. 次の スマートフォン app 実行構成を実行し、実機のAndroidスマートフォンにインストールします。

  2. 次の wear 実行構成を実行し、ペアリングされたウォッチ(またはスマホエミュレータにリンクされたウォッチエミュレータ)にインストールします。

  3. 確認 getInfo() スマートフォン側:

    • isSupported: true Google Play 開発者サービスと Wearable API が利用可能な場合
    • isReachable: true 少なくとも1つのノードが接続されている場合
    • isWatchAppInstalled: true ウォッチがアドバタイズしているとき capgo_watch
  4. 送信 CapgoWatch.sendMessage スマホから送信し、検証します bridge.onMessage ウォッチ側で。

  5. ウォッチから以下を使用して送信し、 sendMessageWithReply 確認します messageReceivedWithReply と replyToMessage が以下に届いたことを /capgo/reply/{callbackId}.

  6. テスト 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
  • 同じものも requestId reply JSONに含めてください(ウォッチが並行リクエストを紐付ける場合)
  • 保留中のreplyは次で期限切れになります 5分 Androidでは(プラグイン定数 PENDING_REPLY_TTL_MS)

「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 プラグインの代替案におけるプロダクトワークフローについては。