ノッチ付ディスプレイや画面内指紋認証、多眼カメラなど多種多様なスマートフォンが登場している一方で、スマートウォッチ市場は(AppleWatchを除いて)いまいち盛り上がりませんね。
とは言えスマートウォッチはそれなりの性能で様々なセンサを積んでいるので趣味や研究用途でいじるのにはおもしろいと思います。
自分も最近スマートウォッチ(WearOS)のアプリを作っていたのですが、スマートフォン(Android)との通信を実装する際のAndroidデベロッパーガイドの説明が少しわかりづらかったので補足してまとめておきます。
概要
スマートフォン(Android) -> スマートウォッチ(WearOS)でメッセージ送受信を行う際の実装を想定します。(逆の場合も実装は同様のはず)
スマートフォンとスマートウォッチにはそれぞれ以下を使いました。
- Galaxy S9 (Android 9.0)
- TicWatch E (WearOS 2.9)
以下実装についてですが、すでにWearOSアプリのプロジェクトが作成されている状態を前提とします。
送信側(スマートフォン)
準備
プロジェクトにスマートフォンアプリのモジュールを追加します。
File -> New -> New Module... -> Phone&Tablet Module を選択
ここで注意点ですがパッケージ名が初期状態だと*com.hogehoge.[Module name]*のようになっているので、これをプロジェクト作成時に設定したものと同じにする必要があります。
理由としては、送信先ノード検出処理にてcontextを指定するため各ノードのアプリケーションIDが一致していなければならないからだと予想してます。
Androidデベロッパーガイドの手順には特に説明がないのですが、このあたりの作法は一般的なんですかね。
尚パッケージ名が違うと当然ですが検出されません。
(パッケージ名、アプリケーションID、Contextの関係はこの辺を参考に)
モジュール作成後build.gradle(Module)にWearOS APIの依存関係を追加 -> Sync
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.google.android.gms:play-services-wearable:17.0.0' // <- https://developers.google.com/android/guides/setup を参考に
}
実装
ノードの識別子 WEAR_CAPABIRITY_NAME
とメッセージの識別子 TO_WATCH_PATH
を定義
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
/* 省略 */
companion object {
private const val WEAR_CAPABIRITY_NAME = "wear_capabirity"
private const val TO_WATCH_PATH = "/hoge"
}
}
送信先ノード検出処理とメッセージ送信処理を追加。今回はとりあえずすべてonCreate内に書きました。
フローティングアクションボタンを押すとメッセージを送信する感じで。
送信するデータはバイナリにする必要があります。
class MainActivity : AppCompatActivity() {
var nodeSet: MutableSet<Node>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
// 送信先ノード検出
val capabilityInfoTask: Task<CapabilityInfo> =
Wearable.getCapabilityClient(applicationContext).getCapability(
WEAR_CAPABIRITY_NAME,
CapabilityClient.FILTER_REACHABLE
)
capabilityInfoTask.addOnCompleteListener(object : OnCompleteListener<CapabilityInfo> {
override fun onComplete(task: Task<CapabilityInfo>) {
if (task.isSuccessful) {
nodeSet = task.result?.nodes
} else {
Toast.makeText(applicationContext, "送信先が見つかりません", Toast.LENGTH_SHORT).show()
}
}
})
//ボタンを押して送信
fab.setOnClickListener { view ->
nodeSet?.let {
pickBestNodeId(it)?.let {
val dataText = "メッセージ"
val data = dataText.toByteArray(Charsets.UTF_8) //バイナリ変換
Wearable.getMessageClient(applicationContext)
.sendMessage(it, TO_WATCH_PATH, data)
.apply {
addOnSuccessListener {
Toast.makeText(
applicationContext,
dataText + " を送信",
Toast.LENGTH_SHORT
).show()
}
addOnFailureListener {
Toast.makeText(applicationContext, "送信失敗", Toast.LENGTH_SHORT)
.show()
}
}
}
}
}
}
//デベロッパーガイドに記載のものそのまま
private fun pickBestNodeId(nodes: Set<Node>): String? {
return nodes.firstOrNull { it.isNearby }?.id ?: nodes.firstOrNull()?.id
}
/* 省略 */
companion object {
private const val WEAR_CAPABIRITY_NAME = "wear_capabirity"
private const val TO_WATCH_PATH = "/hoge"
}
}
受信側(スマートウォッチ)
準備
WearOSアプリモジュールの res/values/
に wear.xml リソースを作成して以下のように記述。
item
要素はスマートフォン側実装で定義した WEAR_CAPABIRITY_NAME
に合わせます。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="android_wear_capabilities">
<item>wear_capabirity</item>
</string-array>
</resources>
実装
Activityに MessageClient.OnMessageReceivedListener
を実装して Wearable.getMessageClient
のリスナに指定します。
class MainActivity() : WearableActivity(), MessageClient.OnMessageReceivedListener {
override fun onMessageReceived(messageEvent: MessageEvent) {
when(messageEvent.path) {
FROM_PHONE_PATH -> {
val data = messageEvent.data.toString(Charsets.UTF_8) //文字列に変換
Log.d("debug", data) // -> D/debug: メッセージ
/* 受け取ったデータを処理 */
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Enables Always-on
setAmbientEnabled()
Wearable.getMessageClient(applicationContext).addListener(this) //リスナに自身を指定
}
companion object {
private const val FROM_PHONE_PATH = "/hoge"
}
}
onMessageReceived
ではスマートフォン側と同様に定義したメッセージの識別子(↑のFROM_PHONE_PATH
)に応じてメッセージを処理します。
今回はとりあえずLogcatに出力するだけ