diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml
new file mode 100644
index 0000000..c9d00de
--- /dev/null
+++ b/.github/workflows/android-build.yml
@@ -0,0 +1,81 @@
+name: Build Android App (apk)
+
+on:
+ push:
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code and submodule(s)
+ uses: actions/checkout@v4
+ with:
+ submodules: 'recursive'
+
+ # Set Current Date As Env Variable
+ - name: Set current date as env variable
+ run: echo "date_today=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
+
+ # Set Repository Name As Env Variable
+ - name: Set repository name as env variable
+ run: echo "repository_name=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV
+
+ - name: Set Up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu' # See 'Supported distributions' for available options
+ java-version: '21'
+ cache: 'gradle'
+
+ - name: Change wrapper permissions
+ run: chmod +x ./gradlew
+
+ # Run Build Project
+ - name: Build gradle project
+ run: ./gradlew build
+
+ # Run Tests Build
+ - name: Run gradle tests
+ run: ./gradlew test
+
+ # Create APK Debug
+ - name: Build apk debug project (APK)
+ run: ./gradlew assembleDebug
+
+ # Create APK Release
+ - name: Build apk release project (APK)
+ run: ./gradlew assembleRelease
+
+ # Upload Artifact Build
+ # Noted For Output mobile/build/outputs/apk/debug/
+ - name: Upload mobile APK Debug - ${{ env.repository_name }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.date_today }} - ${{ env.repository_name }} - mobile - APK(s) debug generated
+ path: mobile/build/outputs/apk/debug/
+
+ # Noted For Output mobile/build/outputs/apk/release/
+ - name: Upload mobile APK Release - ${{ env.repository_name }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.date_today }} - ${{ env.repository_name }} - mobile - APK(s) release generated
+ path: mobile/build/outputs/apk/release/
+
+ # Noted For Output wear/build/outputs/apk/debug/
+ - name: Upload wear APK Debug - ${{ env.repository_name }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.date_today }} - ${{ env.repository_name }} - wear - APK(s) debug generated
+ path: wear/build/outputs/apk/debug/
+
+ # Noted For Output wear/build/outputs/apk/release/
+ - name: Upload wear APK Release - ${{ env.repository_name }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.date_today }} - ${{ env.repository_name }} - wear - APK(s) release generated
+ path: wear/build/outputs/apk/release/
diff --git a/.gitignore b/.gitignore
index aa724b7..9c87163 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,6 @@
-*.iml
-.gradle
+/.gradle
+/.idea
+/.kotlin
+/build
/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
.DS_Store
-/build
-/captures
-.externalNativeBuild
-.cxx
-local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index b5a316e..0000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-Phone Wear Remote
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b86273d..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index edc4047..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
deleted file mode 100644
index 6d0ee1c..0000000
--- a/.idea/kotlinc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
deleted file mode 100644
index f8051a6..0000000
--- a/.idea/migrations.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 74dd639..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
deleted file mode 100644
index 16660f1..0000000
--- a/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 84b18a7..a6bcd2b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
agp = "8.8.0"
-kotlin = "2.0.0"
+kotlin = "2.0.21"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
@@ -23,6 +23,7 @@ tilesToolingPreview = "1.4.0"
horologistComposeTools = "0.6.17"
horologistTiles = "0.6.17"
watchfaceComplicationsDataSourceKtx = "1.2.1"
+wearPhoneInteractions = "1.0.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -53,6 +54,8 @@ androidx-tiles-tooling-preview = { group = "androidx.wear.tiles", name = "tiles-
horologist-compose-tools = { group = "com.google.android.horologist", name = "horologist-compose-tools", version.ref = "horologistComposeTools" }
horologist-tiles = { group = "com.google.android.horologist", name = "horologist-tiles", version.ref = "horologistTiles" }
androidx-watchface-complications-data-source-ktx = { group = "androidx.wear.watchface", name = "watchface-complications-data-source-ktx", version.ref = "watchfaceComplicationsDataSourceKtx" }
+androidx-wear-phone-interactions = { group = "androidx.wear", name = "wear-phone-interactions", version.ref = "wearPhoneInteractions" }
+kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1e448c1..2c52d33 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Thu Jan 23 19:43:43 PST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts
index 6c8fd63..cd93e58 100644
--- a/mobile/build.gradle.kts
+++ b/mobile/build.gradle.kts
@@ -8,9 +8,9 @@ android {
compileSdk = 35
defaultConfig {
- applicationId = "com.swooby.phonewearremote"
minSdk = 34
- targetSdk = 35
+
+ applicationId = "com.swooby.phonewearremote"
versionCode = 1
versionName = "1.0"
@@ -27,11 +27,11 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
- jvmTarget = "11"
+ jvmTarget = "21"
}
}
@@ -45,5 +45,4 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
- wearApp(project(":wear"))
-}
\ No newline at end of file
+}
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index 5afc1c0..a743f15 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools">
+ >
+ android:exported="true"
+ >
@@ -23,4 +23,4 @@
-
\ No newline at end of file
+
diff --git a/mobile/src/main/java/com/swooby/phonewearremote/MainActivity.kt b/mobile/src/main/java/com/swooby/phonewearremote/MainActivity.kt
index 4ae665a..cf06aa1 100644
--- a/mobile/src/main/java/com/swooby/phonewearremote/MainActivity.kt
+++ b/mobile/src/main/java/com/swooby/phonewearremote/MainActivity.kt
@@ -1,12 +1,23 @@
package com.swooby.phonewearremote
import android.os.Bundle
+import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
+import com.google.android.gms.wearable.MessageClient
+import com.google.android.gms.wearable.MessageEvent
+import com.google.android.gms.wearable.Wearable
+import com.swooby.phonewearremote.Utils.quote
+
+class MainActivity : AppCompatActivity(), MessageClient.OnMessageReceivedListener {
+ companion object {
+ private const val TAG = "MainActivity"
+ }
+
+ private val messageClient by lazy { Wearable.getMessageClient(this) }
-class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@@ -16,5 +27,30 @@ class MainActivity : AppCompatActivity() {
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
+
+ messageClient.addListener(this)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ messageClient.removeListener(this)
+ }
+
+ override fun onMessageReceived(messageEvent: MessageEvent) {
+ val path = messageEvent.path
+ when (path) {
+ "/pushToTalk" -> handlePushToTalkCommand(messageEvent)
+ else -> Log.d(TAG, "Unhandled messageEvent.path=${quote(path)}")
+ }
+ }
+
+ private fun handlePushToTalkCommand(messageEvent: MessageEvent) {
+ val payload = messageEvent.data
+ val payloadString = String(payload)
+ Log.i(TAG, "handlePushToTalkCommand: PushToTalk command received! payloadString=${quote(payloadString)}")
+
+ // TODO: Start your "PushToTalk" action:
+ // e.g., open a microphone, start voice recognition, etc.
+ // ...
}
}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/swooby/phonewearremote/Utils.kt b/mobile/src/main/java/com/swooby/phonewearremote/Utils.kt
new file mode 100644
index 0000000..78c4313
--- /dev/null
+++ b/mobile/src/main/java/com/swooby/phonewearremote/Utils.kt
@@ -0,0 +1,32 @@
+package com.swooby.phonewearremote
+
+import kotlin.reflect.KClass
+
+object Utils {
+ fun getShortClassName(value: Any?): String {
+ return when (value) {
+ is KClass<*> -> value.simpleName ?: "null"
+ else -> value?.javaClass?.simpleName ?: "null"
+ }
+ }
+
+ fun quote(value: Any?, typeOnly: Boolean = false): String {
+ if (value == null) {
+ return "null"
+ }
+
+ if (typeOnly) {
+ return getShortClassName(value)
+ }
+
+ if (value is String) {
+ return "\"$value\""
+ }
+
+ if (value is CharSequence) {
+ return "\"$value\""
+ }
+
+ return value.toString()
+ }
+}
\ No newline at end of file
diff --git a/mobile/src/main/res/values/mobile.xml b/mobile/src/main/res/values/mobile.xml
new file mode 100644
index 0000000..07963a0
--- /dev/null
+++ b/mobile/src/main/res/values/mobile.xml
@@ -0,0 +1,9 @@
+
+
+
+ - verify_remote_example_mobile_app
+
+
\ No newline at end of file
diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts
index 8ab3ca5..7a9164c 100644
--- a/wear/build.gradle.kts
+++ b/wear/build.gradle.kts
@@ -9,9 +9,9 @@ android {
compileSdk = 35
defaultConfig {
- applicationId = "com.swooby.phonewearremote"
minSdk = 34
- targetSdk = 35
+
+ applicationId = "com.swooby.phonewearremote"
versionCode = 1
versionName = "1.0"
@@ -27,11 +27,11 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
- jvmTarget = "11"
+ jvmTarget = "21"
}
buildFeatures {
compose = true
@@ -55,10 +55,12 @@ dependencies {
implementation(libs.horologist.compose.tools)
implementation(libs.horologist.tiles)
implementation(libs.androidx.watchface.complications.data.source.ktx)
+ implementation(libs.androidx.wear.phone.interactions)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
debugImplementation(libs.androidx.tiles.tooling)
- wearApp(project(":wear"))
-}
\ No newline at end of file
+
+ implementation(libs.kotlin.reflect)
+}
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
index 16b10e0..3c2aaa4 100644
--- a/wear/src/main/AndroidManifest.xml
+++ b/wear/src/main/AndroidManifest.xml
@@ -1,12 +1,14 @@
+
+
-
-
\ No newline at end of file
+
diff --git a/wear/src/main/java/com/swooby/phonewearremote/Utils.kt b/wear/src/main/java/com/swooby/phonewearremote/Utils.kt
new file mode 100644
index 0000000..2cdcfd7
--- /dev/null
+++ b/wear/src/main/java/com/swooby/phonewearremote/Utils.kt
@@ -0,0 +1,69 @@
+package com.swooby.phonewearremote
+
+import androidx.wear.phone.interactions.PhoneTypeHelper
+import kotlin.reflect.KClass
+import kotlin.reflect.full.companionObject
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.full.staticProperties
+
+object Utils {
+ /**
+ * Creates a map of a class' field values to names
+ */
+ fun getMapOfIntFieldsToNames(clazz: KClass<*>, startsWith: String?): Map {
+ val staticProperties = clazz.staticProperties
+ if (staticProperties.isEmpty()) {
+ val companion = clazz.companionObject
+ val companionInstance = companion?.objectInstance
+ if (companion != null && companionInstance != null) {
+ return companion.memberProperties.filter {
+ (it.returnType.classifier == Int::class) && // Check if the property type is Int
+ (startsWith.isNullOrBlank() || it.name.startsWith(startsWith))
+ }.associate {
+ it.getter.call(companionInstance) as Int to it.name
+ }
+ }
+ }
+ return staticProperties.filter {
+ (it.returnType.classifier == Integer::class) &&
+ (startsWith.isNullOrBlank() ||
+ it.name.startsWith(startsWith))
+ }.associate {
+ it.getter.call() as Int to it.name
+ }
+ }
+
+ fun phoneDeviceTypeToString(phoneType: Int): String {
+ val map = getMapOfIntFieldsToNames(
+ PhoneTypeHelper::class,
+ "DEVICE_TYPE_")
+ return (map[phoneType] ?: "INVALID") + "($phoneType)"
+ }
+
+ fun getShortClassName(value: Any?): String {
+ return when (value) {
+ is KClass<*> -> value.simpleName ?: "null"
+ else -> value?.javaClass?.simpleName ?: "null"
+ }
+ }
+
+ fun quote(value: Any?, typeOnly: Boolean = false): String {
+ if (value == null) {
+ return "null"
+ }
+
+ if (typeOnly) {
+ return getShortClassName(value)
+ }
+
+ if (value is String) {
+ return "\"$value\""
+ }
+
+ if (value is CharSequence) {
+ return "\"$value\""
+ }
+
+ return value.toString()
+ }
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/swooby/phonewearremote/WearViewModel.kt b/wear/src/main/java/com/swooby/phonewearremote/WearViewModel.kt
new file mode 100644
index 0000000..c6d9a55
--- /dev/null
+++ b/wear/src/main/java/com/swooby/phonewearremote/WearViewModel.kt
@@ -0,0 +1,91 @@
+package com.swooby.phonewearremote
+
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import com.google.android.gms.tasks.Task
+import com.google.android.gms.wearable.CapabilityClient
+import com.google.android.gms.wearable.MessageClient
+import com.google.android.gms.wearable.MessageEvent
+import com.google.android.gms.wearable.Node
+import com.google.android.gms.wearable.Wearable
+import com.swooby.phonewearremote.Utils.quote
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class WearViewModel(application: Application) :
+ AndroidViewModel(application) {
+ companion object {
+ private const val TAG = "WearViewModel"
+ }
+
+ // setter
+ private var _phoneAppNodeId = MutableStateFlow(null)
+ // getter
+ val phoneAppNodeId = _phoneAppNodeId.asStateFlow()
+
+ private val capabilityClient by lazy { Wearable.getCapabilityClient(application) }
+
+ private val messageClient by lazy { Wearable.getMessageClient(application) }
+ private val messageClientListener = MessageClient.OnMessageReceivedListener {
+ onMessageClientMessageReceived(it)
+ }
+
+ fun init() {
+ messageClient.addListener(messageClientListener)
+
+ val capabilityClientTask = capabilityClient.getCapability(
+ "verify_remote_example_mobile_app",
+ CapabilityClient.FILTER_REACHABLE
+ )
+ capabilityClientTask.addOnCompleteListener {
+ if (it.isSuccessful) {
+ Log.d(TAG, "Capability request succeeded");
+ _phoneAppNodeId.value = pickFirstNearbyNode(it.result.nodes)?.id
+ if (phoneAppNodeId.value == null) {
+ Log.d(TAG, "Detected no phone app node")
+ } else {
+ Log.i(TAG, "Detected phoneAppNodeId=${quote(phoneAppNodeId.value)}")
+ }
+ } else {
+ Log.e(TAG, "Capability request failed to return any results");
+ }
+ }
+ }
+
+ fun close() {
+ messageClient.removeListener(messageClientListener)
+ }
+
+ private fun onMessageClientMessageReceived(messageEvent: MessageEvent) {
+ val path = messageEvent.path
+ val payload = String(messageEvent.data, Charsets.UTF_8)
+ // Handle incoming message from watch/phone
+ Log.d(TAG, "onMessageReceived: $path, $payload")
+ }
+
+ private fun pickFirstNearbyNode(nodes: Set): Node? {
+ return nodes.firstOrNull { it.isNearby } ?: nodes.firstOrNull()
+ }
+
+ private fun sendMessageToNode(nodeId: String, path: String, data: String): Task {
+ return sendMessageToNode(nodeId, path, data.toByteArray())
+ }
+
+ private fun sendMessageToNode(nodeId: String, path: String, data: ByteArray): Task {
+ return messageClient
+ .sendMessage(nodeId, path, data)
+ .addOnSuccessListener {
+ Log.d(TAG, "sendMessageToNode: Message sent successfully to $nodeId")
+ }
+ .addOnFailureListener { e ->
+ Log.e(TAG, "sendMessageToNode: Message failed.", e)
+ }
+ }
+
+ fun sendPushToTalkCommand(nodeId: String, on: Boolean): Task {
+ val path = "/pushToTalk"
+ val payload = (if (on) "on" else "off")
+ return sendMessageToNode(nodeId, path, payload)
+ }
+}
\ No newline at end of file
diff --git a/wear/src/main/java/com/swooby/phonewearremote/presentation/MainActivity.kt b/wear/src/main/java/com/swooby/phonewearremote/presentation/MainActivity.kt
index 7274a7d..3d0d883 100644
--- a/wear/src/main/java/com/swooby/phonewearremote/presentation/MainActivity.kt
+++ b/wear/src/main/java/com/swooby/phonewearremote/presentation/MainActivity.kt
@@ -6,42 +6,82 @@
package com.swooby.phonewearremote.presentation
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.activity.viewModels
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText
import androidx.wear.tooling.preview.devices.WearDevices
-import com.swooby.phonewearremote.R
+import com.swooby.phonewearremote.WearViewModel
import com.swooby.phonewearremote.presentation.theme.PhoneWearRemoteTheme
class MainActivity : ComponentActivity() {
+
+ private val wearViewModel: WearViewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
-
super.onCreate(savedInstanceState)
-
setTheme(android.R.style.Theme_DeviceDefault)
-
setContent {
- WearApp("Android")
+ WearApp("Wear", wearViewModel)
}
}
+
+ override fun onResume() {
+ super.onResume()
+ wearViewModel.init()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ wearViewModel.close()
+ }
+}
+
+@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true)
+@Composable
+fun DefaultPreview() {
+ WearApp("Preview")
}
@Composable
-fun WearApp(greetingName: String) {
+fun WearApp(
+ targetNameDefault: String,
+ wearViewModel: WearViewModel? = null,
+) {
+ @Suppress("LocalVariableName")
+ val TAG = "WearApp"
+
+ val phoneAppNodeId by wearViewModel?.phoneAppNodeId?.collectAsState() ?: remember { mutableStateOf(null) }
+
PhoneWearRemoteTheme {
Box(
modifier = Modifier
@@ -50,23 +90,115 @@ fun WearApp(greetingName: String) {
contentAlignment = Alignment.Center
) {
TimeText()
- Greeting(greetingName = greetingName)
+ RemoteButton(
+ targetName = if (phoneAppNodeId != null) "Phone" else targetNameDefault,
+ onPushToTalkStart = {
+ @Suppress("NAME_SHADOWING")
+ val phoneAppNodeId = phoneAppNodeId
+ if (phoneAppNodeId != null) {
+ Log.d(TAG, "onClick: PTT on phone...")
+ wearViewModel?.sendPushToTalkCommand(phoneAppNodeId, true)
+ } else {
+ Log.d(TAG, "onClick: TODO: PTT on local ...")
+ // ...
+ }
+ true
+ },
+ onPushToTalkStop = {
+ @Suppress("NAME_SHADOWING")
+ val phoneAppNodeId = phoneAppNodeId
+ if (phoneAppNodeId != null) {
+ Log.d(TAG, "onClick: PTT off phone...")
+ wearViewModel?.sendPushToTalkCommand(phoneAppNodeId, false)
+ } else {
+ Log.d(TAG, "onClick: TODO: PTT off local ...")
+ // ...
+ }
+ true
+ },
+ )
}
}
}
-@Composable
-fun Greeting(greetingName: String) {
- Text(
- modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center,
- color = MaterialTheme.colors.primary,
- text = stringResource(R.string.hello_world, greetingName)
- )
+enum class PTTState {
+ Idle,
+ Pressed
}
-@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true)
@Composable
-fun DefaultPreview() {
- WearApp("Preview Android")
-}
\ No newline at end of file
+fun RemoteButton(
+ targetName: String,
+ enabled: Boolean = true,
+ onPushToTalkStart: (pttState: PTTState) -> Boolean = {
+ Log.d("PTT", "Push-to-Talk Start")
+ false
+ },
+ onPushToTalkStop: (pttState: PTTState) -> Boolean = {
+ Log.d("PTT", "Push-to-Talk Stop")
+ false
+ }
+) {
+ var pttState by remember { mutableStateOf(PTTState.Idle) }
+
+ val disabledColor = MaterialTheme.colors.onSurface.copy(alpha = 0.38f)
+ val boxAlpha = if (enabled) 1.0f else 0.38f
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(120.dp)
+ .border(4.dp, if (enabled) MaterialTheme.colors.primary else disabledColor, shape = CircleShape)
+ .background(
+ color = if (pttState == PTTState.Pressed) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
+ shape = CircleShape
+ )
+ .let { baseModifier ->
+ if (enabled) {
+ baseModifier.pointerInput(Unit) {
+ awaitEachGesture {
+ awaitFirstDown()
+ if (pttState == PTTState.Idle) {
+ pttState = PTTState.Pressed
+ if (!onPushToTalkStart(pttState)) {
+ //provideHapticFeedback(context)
+ //provideAudibleFeedback(context, pttState)
+ }
+ }
+ do {
+ val event = awaitPointerEvent()
+ } while (event.changes.any { !it.changedToUp() })
+ if (pttState == PTTState.Pressed) {
+ pttState = PTTState.Idle
+ if (!onPushToTalkStop(pttState)) {
+ //provideHapticFeedback(context)
+ //provideAudibleFeedback(context, pttState)
+ }
+ }
+ }
+ }
+ } else {
+ baseModifier.pointerInput(Unit) {
+ awaitEachGesture {
+ while (true) {
+ val event = awaitPointerEvent()
+ event.changes.forEach { it.consume() }
+ }
+ }
+ }
+ }
+ }
+ .then(Modifier.background(Color.Transparent))
+ .let {
+ it.background(Color.Transparent).graphicsLayer {
+ this.alpha = boxAlpha
+ }
+ },
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ text = targetName
+ )
+ }
+}
diff --git a/wear/src/main/res/values/wear.xml b/wear/src/main/res/values/wear.xml
new file mode 100644
index 0000000..5565869
--- /dev/null
+++ b/wear/src/main/res/values/wear.xml
@@ -0,0 +1,9 @@
+
+
+
+ - verify_remote_example_wear_app
+
+
\ No newline at end of file