【Android Wear】WatchFaceの構成アクティビティの作成

# はじめに
WatchFaceの構成アクティビティの作成方法について記述。
下画像の鉛筆ボタンタップ時に起動する画面。
![ConfigActivity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694762/f15a6204-980b-f3ab-02c9-7c215adf0d2f.png)

WatchFaceの作成手順については、下記を参照。

https://qiita.com/nakashimaakio/items/639f161058107eaeec74

これを実装してから構成アクティビティを作成することをおすすめする。

# 実装手順

## 下準備

### ライブラリの導入

構成アクティビティに必要なライブラリの導入
```build.gradle
dependencies {
	implementation 'androidx.wear.watchface:watchface:1.1.1'
	implementation 'androidx.wear.watchface:watchface-editor:1.1.1'
	implementation 'androidx.activity:activity-ktx:1.6.0'
}
```

## 構成アクティビティ側

### ステートホルダーの作成

設定反映を行うためのステートホルダーの作成

``` WatchFaceConfigStateHolder.kt
class WatchFaceConfigStateHolder(scope: CoroutineScope, private val activity: ComponentActivity) {
	companion object {
		const val CUSTOM_VALUE = "CustomValue"
	}

	private lateinit var editorSession: EditorSession
	private lateinit var configDataKey: UserStyleSetting.CustomValueUserStyleSetting

	sealed class EditWatchFaceUiState {
		data class Success(val configData: String) : EditWatchFaceUiState()
		data class Loading(val message: String) : EditWatchFaceUiState()
	}

	val uiState = flow<EditWatchFaceUiState> {
		editorSession = EditorSession.createOnWatchEditorSession(activity)
		extractsUserStyles(editorSession.userStyleSchema)
		emitAll(
			combine(editorSession.userStyle, editorSession.complicationsPreviewData) { userStyle, _ ->
				yield()
				EditWatchFaceUiState.Success(getConfigData(userStyle))
			}
		)
	}.stateIn(scope + Dispatchers.Main.immediate, SharingStarted.Eagerly, EditWatchFaceUiState.Loading("Initializing"))

	private fun extractsUserStyles(userStyleSchema: UserStyleSchema) {
		for (setting in userStyleSchema.userStyleSettings) {
			if (setting.id.toString() == CUSTOM_VALUE) {
				configDataKey = setting as UserStyleSetting.CustomValueUserStyleSetting
			}
		}
	}

	private fun getConfigData(userStyle: UserStyle): String {
		val option = userStyle[configDataKey] as UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption
		return option.customValue.toString(Charsets.UTF_8)
	}

	fun setConfigData(configData: String) {
		val byteArray = configData.toByteArray(Charsets.UTF_8)
		setUserStyleOption(configDataKey, UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(byteArray))
	}

	private fun setUserStyleOption(userStyleSetting: UserStyleSetting, userStyleOption: UserStyleSetting.Option) {
		val mutableUserStyle = editorSession.userStyle.value.toMutableUserStyle()
		mutableUserStyle[userStyleSetting] = userStyleOption
		editorSession.userStyle.value = mutableUserStyle.toUserStyle()
	}
}
```

### 構成アクティビティのレイアウト作成
設定変更用のボタン作成

```activity_watch_face_config.xml
<?xml version="1.0" encoding="utf-8"?>
<RadioGroup
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/radio_group"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:paddingHorizontal="36dp">

	<RadioButton
		android:id="@+id/button_a"
		android:layout_width="match_parent"
		android:layout_height="72dp"
		android:text="A"/>

	<RadioButton
		android:id="@+id/button_b"
		android:layout_width="match_parent"
		android:layout_height="72dp"
		android:text="B"/>

	<RadioButton
		android:id="@+id/button_c"
		android:layout_width="match_parent"
		android:layout_height="72dp"
		android:text="C"/>
</RadioGroup>
```

### 構成アクティビティのクラス作成
構成アクティビティでボタン押下時の処理部分の作成

```WatchFaceConfigActivity.kt
class WatchFaceConfigActivity : ComponentActivity() {

	private val binding by lazy { ActivityWatchFaceConfigBinding.inflate(layoutInflater) }
	private val stateHolder by lazy { WatchFaceConfigStateHolder(lifecycleScope, this@WatchFaceConfigActivity) }

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(binding.root)

		lifecycleScope.launch(Dispatchers.Main.immediate) {
			stateHolder.uiState.collect { uiState: WatchFaceConfigStateHolder.EditWatchFaceUiState ->
				when (uiState) {
					is WatchFaceConfigStateHolder.EditWatchFaceUiState.Loading -> {
					}
					is WatchFaceConfigStateHolder.EditWatchFaceUiState.Success -> {
						val id = when (uiState.configData) {
							"A" -> R.id.button_a
							"B" -> R.id.button_b
							"C" -> R.id.button_c
							else -> null
						}
						if (id != null) binding.radioGroup.check(id)

						binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
							when (checkedId) {
								R.id.button_a -> stateHolder.setConfigData("A")
								R.id.button_b -> stateHolder.setConfigData("B")
								R.id.button_c -> stateHolder.setConfigData("C")
							}
							finish()
						}
					}
				}
			}
		}
	}
}
```

## ウォッチフェイス側

### WatchFace用のRenderer作成

設定値を取り出して表示
```CustomWatchFaceRenderer.kt
class CustomWatchFaceRenderer(
	context: Context,
	surfaceHolder: SurfaceHolder,
	currentUserStyleRepository: CurrentUserStyleRepository,
	watchState: WatchState
) : Renderer.CanvasRenderer2<Renderer.SharedAssets>(
	surfaceHolder,
	currentUserStyleRepository,
	watchState,
	CanvasType.HARDWARE,
	16L,
	false
) {
	private val binding = CustomWatchFaceBinding.inflate(LayoutInflater.from(context))

	override suspend fun init() {
		super.init()
		CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).launch {
			currentUserStyleRepository.userStyle.collect {
				binding.text.text = getConfigData(it)
			}
		}
	}

	override suspend fun createSharedAssets() = object : SharedAssets {
		override fun onDestroy() {}
	}

	override fun renderHighlightLayer(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime, sharedAssets: SharedAssets) {}

	override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime, sharedAssets: SharedAssets) {
		if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)) {
			binding.root.measure(
				View.MeasureSpec.makeMeasureSpec(bounds.width(), View.MeasureSpec.EXACTLY),
				View.MeasureSpec.makeMeasureSpec(bounds.height(), View.MeasureSpec.EXACTLY)
			)
			binding.root.layout(0, 0, bounds.width(), bounds.height())
			binding.time.text = String.format("%02d:%02d", zonedDateTime.hour, zonedDateTime.minute)
			binding.root.draw(canvas)
		}
	}

	private fun getConfigData(userStyle: UserStyle): String {
		userStyle.forEach {
			if (it.key.id.toString() == CUSTOM_VALUE) {
				val customValue = it.value as UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption
				return customValue.customValue.toString(Charsets.UTF_8)
			}
		}
		return ""
	}
}
```

### WatchFaceServiceの作成

`createUserStyleSchema`メソッドをoverrideして、設定値の初期化を行う。

```CustomWatchFaceService.kt
class CustomWatchFaceService : WatchFaceService() {
	override suspend fun createWatchFace(surfaceHolder: SurfaceHolder, watchState: WatchState, complicationSlotsManager: ComplicationSlotsManager, currentUserStyleRepository: CurrentUserStyleRepository) =
		WatchFace(WatchFaceType.DIGITAL, CustomWatchFaceRenderer(applicationContext, surfaceHolder, currentUserStyleRepository, watchState))

	override fun createUserStyleSchema(): UserStyleSchema {
		val setting = UserStyleSetting.CustomValueUserStyleSetting(
			listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY),
			"Default".toByteArray(Charsets.UTF_8)
		)
		return UserStyleSchema(listOf(setting))
	}
}
```

## マニフェスト部分

### マニフェスト定義
・service部分にメタデータ定義
・activity部分にWatchFaceConfigActivity定義

```AndroidManifest.xml
<manifest ...>
	<application ...>
		<service ...>
			...
			<meta-data
				android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
				android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
			<meta-data
				android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
				android:value="true" />
			...
		</service>

		<activity
			android:name=".WatchFaceConfigActivity"
			android:exported="true"
			android:label="Configuration">

			<intent-filter>
				<action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>

				<category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION"/>
				<category android:name="android.intent.category.DEFAULT"/>
			</intent-filter>
		</activity>
	</application>
</manifest>
```
# 参考サイト
https://developer.android.com/training/wearables/watch-faces/configuration?hl=ja