diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index ac19bd0..57bd0b4 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -16,7 +16,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AlfredAI"
+ android:enableOnBackInvokedCallback="true"
>
+
+
\ No newline at end of file
diff --git a/mobile/src/main/java/com/swooby/alfredai/AlfredAiApp.kt b/mobile/src/main/java/com/swooby/alfredai/AlfredAiApp.kt
index 2427b19..aaa56ea 100644
--- a/mobile/src/main/java/com/swooby/alfredai/AlfredAiApp.kt
+++ b/mobile/src/main/java/com/swooby/alfredai/AlfredAiApp.kt
@@ -1,13 +1,42 @@
package com.swooby.alfredai
import android.app.Application
+import androidx.activity.ComponentActivity
+import androidx.annotation.MainThread
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelLazy
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStore
+import androidx.lifecycle.ViewModelStoreOwner
+
+@MainThread
+inline fun ComponentActivity.appViewModels(): Lazy {
+ return ViewModelLazy(
+ VM::class,
+ { (application as AlfredAiApp).viewModelStore },
+ { ViewModelProvider.AndroidViewModelFactory.getInstance(application) },
+ { defaultViewModelCreationExtras }
+ )
+}
+
+class AlfredAiApp : Application(), ViewModelStoreOwner
+{
+ override val viewModelStore = ViewModelStore()
+
+ val mobileViewModel by lazy {
+ ViewModelProvider(
+ this,
+ ViewModelProvider.AndroidViewModelFactory.getInstance(this)
+ )[MobileViewModel::class.java]
+ }
-class AlfredAiApp : Application() {
override fun onCreate() {
super.onCreate()
+ mobileViewModel.init()
}
override fun onTerminate() {
super.onTerminate()
+ mobileViewModel.close()
}
}
diff --git a/mobile/src/main/java/com/swooby/alfredai/MobileActivity.kt b/mobile/src/main/java/com/swooby/alfredai/MobileActivity.kt
index 05dae73..feb920c 100644
--- a/mobile/src/main/java/com/swooby/alfredai/MobileActivity.kt
+++ b/mobile/src/main/java/com/swooby/alfredai/MobileActivity.kt
@@ -67,6 +67,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -122,15 +123,14 @@ class MobileActivity : ComponentActivity() {
private const val TAG = "PushToTalkActivity"
}
- private val mobileViewModel: MobileViewModel by viewModels()
+ private val mobileViewModel: MobileViewModel by appViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate()")
super.onCreate(savedInstanceState)
-
enableEdgeToEdge()
setContent {
- PushToTalkScreen(mobileViewModel)
+ MobileApp(mobileViewModel)
}
}
@@ -150,9 +150,9 @@ enum class ConversationSpeaker {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
-fun PushToTalkScreen(mobileViewModel: MobileViewModel? = null) {
+fun MobileApp(mobileViewModel: MobileViewModel? = null) {
@Suppress("LocalVariableName")
- val TAG = "PushToTalkScreen"
+ val TAG = "MobileApp"
@Suppress(
"SimplifyBooleanWithConstants",
@@ -973,6 +973,9 @@ fun PushToTalkScreen(mobileViewModel: MobileViewModel? = null) {
,
contentAlignment = Alignment.Center
) {
+ if (isConnected) {
+ KeepScreenOnComposable()
+ }
Box {
when {
isConnected -> {
@@ -1086,13 +1089,24 @@ fun PushToTalkScreen(mobileViewModel: MobileViewModel? = null) {
//
}
+@Composable
+fun KeepScreenOnComposable() {
+ val view = LocalView.current
+ DisposableEffect(Unit) {
+ view.keepScreenOn = true
+ onDispose {
+ view.keepScreenOn = false
+ }
+ }
+}
+
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_NO,
showBackground = true
)
@Composable
fun PushToTalkButtonActivityPreviewLight() {
- PushToTalkScreen()
+ MobileApp()
}
@Preview(
@@ -1101,5 +1115,5 @@ fun PushToTalkButtonActivityPreviewLight() {
)
@Composable
fun PushToTalkButtonActivityPreviewDark() {
- PushToTalkScreen()
+ MobileApp()
}
diff --git a/mobile/src/main/java/com/swooby/alfredai/MobileViewModel.kt b/mobile/src/main/java/com/swooby/alfredai/MobileViewModel.kt
index 9b05db6..bfd2a6d 100644
--- a/mobile/src/main/java/com/swooby/alfredai/MobileViewModel.kt
+++ b/mobile/src/main/java/com/swooby/alfredai/MobileViewModel.kt
@@ -59,7 +59,7 @@ class MobileViewModel(application: Application) :
override val remoteTypeName: String
get() = "MOBILE"
override val remoteCapabilityName: String
- get() = "verify_remote_example_wear_app"
+ get() = "verify_remote_alfredai_wear_app"
private val prefs = PushToTalkPreferences(application)
@@ -231,7 +231,38 @@ class MobileViewModel(application: Application) :
}
override fun pushToTalk(on: Boolean, sourceNodeId: String?) {
- TODO("Not yet implemented")
+ Log.i(TAG, "pushToTalk(on=$on)")
+ if (on) {
+ if (pushToTalkState.value != PttState.Pressed) {
+ setPushToTalkState(PttState.Pressed)
+ playAudioResourceOnce(getApplication(), R.raw.quindar_nasa_apollo_intro)
+ //provideHapticFeedback(context)
+ }
+ } else {
+ if (pushToTalkState.value != PttState.Idle) {
+ setPushToTalkState(PttState.Idle)
+ playAudioResourceOnce(getApplication(), R.raw.quindar_nasa_apollo_outro)
+ //provideHapticFeedback(context)
+ }
+ }
+
+ if (sourceNodeId == null) {
+ // request from local/mobile
+ Log.d(TAG, "pushToTalk: PTT $on **from** local/mobile...")
+ val remoteAppNodeId = remoteAppNodeId.value
+ if (remoteAppNodeId != null) {
+ // tell remote/wear app that we are PTTing...
+ sendPushToTalkCommand(remoteAppNodeId, on)
+ }
+ //...
+ } else {
+ // request from remote/wear
+ //_remoteAppNodeId.value = sourceNodeId
+ Log.d(TAG, "pushToTalk: PTT $on **from** remote/wear...")
+ //...
+ }
+
+ pushToTalkLocal(on)
}
override fun pushToTalkLocal(on: Boolean) {
diff --git a/mobile/src/main/res/values/wear.xml b/mobile/src/main/res/values/wear.xml
index 38efa61..9ff8fc8 100644
--- a/mobile/src/main/res/values/wear.xml
+++ b/mobile/src/main/res/values/wear.xml
@@ -4,6 +4,6 @@
tools:keep="@array/android_wear_capabilities"
>
- - verify_remote_example_phone_app
+ - verify_remote_alfredai_mobile_app
\ No newline at end of file
diff --git a/shared/src/main/java/com/swooby/alfredai/SharedViewModel.kt b/shared/src/main/java/com/swooby/alfredai/SharedViewModel.kt
index 2bfc56c..e9069ef 100644
--- a/shared/src/main/java/com/swooby/alfredai/SharedViewModel.kt
+++ b/shared/src/main/java/com/swooby/alfredai/SharedViewModel.kt
@@ -25,10 +25,18 @@ abstract class SharedViewModel(application: Application) :
protected abstract val remoteTypeName: String
protected abstract val remoteCapabilityName: String
- protected var _remoteAppNodeId = MutableStateFlow(null) // private mutable
+ private fun setRemoteAppNodeId(nodeId: String?) {
+ Log.i(TAG, "setRemoteAppNodeId(nodeId=${quote(nodeId)})")
+ _remoteAppNodeId.value = nodeId
+ }
+ private var _remoteAppNodeId = MutableStateFlow(null)
val remoteAppNodeId = _remoteAppNodeId.asStateFlow() // public readonly
- protected var _pushToTalkState = MutableStateFlow(PttState.Idle) // private mutable
+ protected fun setPushToTalkState(state: PttState) {
+ Log.i(TAG, "setPushToTalkState(state=$state)")
+ _pushToTalkState.value = state
+ }
+ private var _pushToTalkState = MutableStateFlow(PttState.Idle)
val pushToTalkState = _pushToTalkState.asStateFlow() // public readonly
private val capabilityClient by lazy { Wearable.getCapabilityClient(application) }
@@ -79,8 +87,8 @@ abstract class SharedViewModel(application: Application) :
return messageClient
.sendMessage(nodeId, path, data)
.addOnFailureListener { e ->
- Log.e(TAG, "sendMessageToNode: Message failed.", e)
- _remoteAppNodeId.value = null
+ Log.e(TAG, "sendMessageToNode: Message failed", e)
+ setRemoteAppNodeId(null)
}
}
@@ -99,8 +107,8 @@ abstract class SharedViewModel(application: Application) :
private fun handlePingCommand(messageEvent: MessageEvent) {
val nodeId = messageEvent.sourceNodeId
- _remoteAppNodeId.value = nodeId
- Log.i(TAG, "handlePingCommand: Got ping request from $remoteTypeName app nodeId=${quote(nodeId)}; Responding pong...")
+ Log.i(TAG, "handlePingCommand: Ping request from $remoteTypeName app nodeId=${quote(nodeId)}; Responding pong...")
+ setRemoteAppNodeId(nodeId)
sendPongCommand(nodeId)
}
@@ -110,8 +118,8 @@ abstract class SharedViewModel(application: Application) :
private fun handlePongCommand(messageEvent: MessageEvent) {
val nodeId = messageEvent.sourceNodeId
- _remoteAppNodeId.value = nodeId
- Log.i(TAG, "handlePongCommand: Got pong response from MOBILE app nodeId=${quote(nodeId)}")
+ Log.i(TAG, "handlePongCommand: Pong response from $remoteTypeName app nodeId=${quote(nodeId)}")
+ setRemoteAppNodeId(nodeId)
}
protected fun sendPushToTalkCommand(nodeId: String, on: Boolean): Task {
@@ -121,12 +129,13 @@ abstract class SharedViewModel(application: Application) :
}
private fun handlePushToTalkCommand(messageEvent: MessageEvent) {
- val payload = messageEvent.data
- val payloadString = String(payload)
- Log.i(TAG, "handlePushToTalkCommand: PushToTalk command received! payloadString=${quote(payloadString)}")
+ val nodeId = messageEvent.sourceNodeId
+ val payloadString = String(messageEvent.data)
+ Log.i(TAG, "handlePushToTalkCommand: PushToTalk request from $remoteTypeName app nodeId=${quote(nodeId)}! payloadString=${quote(payloadString)}")
+ setRemoteAppNodeId(nodeId)
when (payloadString) {
- "on" -> pushToTalk(true, sourceNodeId = messageEvent.sourceNodeId)
- "off" -> pushToTalk(false, sourceNodeId = messageEvent.sourceNodeId)
+ "on" -> pushToTalk(true, sourceNodeId = nodeId)
+ "off" -> pushToTalk(false, sourceNodeId = nodeId)
}
}
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
index 891f8c9..7c76d3a 100644
--- a/wear/src/main/AndroidManifest.xml
+++ b/wear/src/main/AndroidManifest.xml
@@ -11,8 +11,11 @@
+ android:theme="@android:style/Theme.DeviceDefault"
+ >
+
-
@@ -37,7 +39,6 @@
-
@@ -58,12 +59,14 @@
android:name=".presentation.WearActivity"
android:exported="true"
android:taskAffinity=""
- android:theme="@style/MainActivityTheme.Starting">
+ android:theme="@style/MainActivityTheme.Starting"
+ >
+
\ No newline at end of file
diff --git a/wear/src/main/java/com/swooby/alfredai/AlfredAiApp.kt b/wear/src/main/java/com/swooby/alfredai/AlfredAiApp.kt
new file mode 100644
index 0000000..3319e9c
--- /dev/null
+++ b/wear/src/main/java/com/swooby/alfredai/AlfredAiApp.kt
@@ -0,0 +1,41 @@
+package com.swooby.alfredai
+
+import android.app.Application
+import androidx.activity.ComponentActivity
+import androidx.annotation.MainThread
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelLazy
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStore
+import androidx.lifecycle.ViewModelStoreOwner
+
+@MainThread
+inline fun ComponentActivity.appViewModels(): Lazy {
+ return ViewModelLazy(
+ VM::class,
+ { (application as AlfredAiApp).viewModelStore },
+ { ViewModelProvider.AndroidViewModelFactory.getInstance(application) },
+ { defaultViewModelCreationExtras }
+ )
+}
+
+class AlfredAiApp : Application(), ViewModelStoreOwner {
+ override val viewModelStore = ViewModelStore()
+
+ val wearViewModel by lazy {
+ ViewModelProvider(
+ this,
+ ViewModelProvider.AndroidViewModelFactory.getInstance(this)
+ )[WearViewModel::class.java]
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ wearViewModel.init()
+ }
+
+ override fun onTerminate() {
+ super.onTerminate()
+ wearViewModel.close()
+ }
+}
diff --git a/wear/src/main/java/com/swooby/alfredai/WearViewModel.kt b/wear/src/main/java/com/swooby/alfredai/WearViewModel.kt
index 17f50d1..285d902 100644
--- a/wear/src/main/java/com/swooby/alfredai/WearViewModel.kt
+++ b/wear/src/main/java/com/swooby/alfredai/WearViewModel.kt
@@ -9,6 +9,7 @@ import com.google.android.gms.wearable.DataItem
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.PutDataRequest
import com.google.android.gms.wearable.Wearable
+import com.swooby.alfredai.Utils.playAudioResourceOnce
class WearViewModel(application: Application) :
SharedViewModel(application)
@@ -18,7 +19,7 @@ class WearViewModel(application: Application) :
override val remoteTypeName: String
get() = "MOBILE"
override val remoteCapabilityName: String
- get() = "verify_remote_example_mobile_app"
+ get() = "verify_remote_alfredai_mobile_app"
private val dataClient = Wearable.getDataClient(application)
private val dataClientListener = DataClient.OnDataChangedListener {
@@ -54,11 +55,50 @@ class WearViewModel(application: Application) :
}
override fun pushToTalk(on: Boolean, sourceNodeId: String?) {
- //...
+ Log.i(TAG, "pushToTalk(on=$on)")
+ if (on) {
+ if (pushToTalkState.value != PttState.Pressed) {
+ setPushToTalkState(PttState.Pressed)
+ playAudioResourceOnce(getApplication(), R.raw.quindar_nasa_apollo_intro)
+ //provideHapticFeedback(context)
+ }
+ } else {
+ if (pushToTalkState.value != PttState.Idle) {
+ setPushToTalkState(PttState.Idle)
+ playAudioResourceOnce(getApplication(), R.raw.quindar_nasa_apollo_outro)
+ //provideHapticFeedback(context)
+ }
+ }
+
+ var doPushToTalkLocal = true
+
+ if (sourceNodeId == null) {
+ // request from local/wear
+ Log.d(TAG, "pushToTalk: PTT $on **from** local/wear...")
+ val remoteAppNodeId = remoteAppNodeId.value
+ if (remoteAppNodeId != null) {
+ doPushToTalkLocal = false
+ // tell remote/mobile app to do the PTTing...
+ sendPushToTalkCommand(remoteAppNodeId, on)
+ }
+ } else {
+ // request from remote/mobile
+ //_remoteAppNodeId.value = sourceNodeId
+ Log.d(TAG, "pushToTalk: PTT $on **from** remote/mobile...")
+ doPushToTalkLocal = false
+ }
+
+ if (doPushToTalkLocal) {
+ pushToTalkLocal(on)
+ }
}
override fun pushToTalkLocal(on: Boolean) {
super.pushToTalkLocal(on)
- //...
+ if (on) {
+ TODO("Not yet implemented")
+ } else {
+ TODO("Not yet implemented")
+ }
}
}
diff --git a/wear/src/main/java/com/swooby/alfredai/presentation/WearActivity.kt b/wear/src/main/java/com/swooby/alfredai/presentation/WearActivity.kt
index 9988816..a060b8b 100644
--- a/wear/src/main/java/com/swooby/alfredai/presentation/WearActivity.kt
+++ b/wear/src/main/java/com/swooby/alfredai/presentation/WearActivity.kt
@@ -4,6 +4,7 @@ import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.awaitEachGesture
@@ -13,6 +14,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -24,6 +27,7 @@ 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.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -35,9 +39,11 @@ import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.TimeText
import androidx.wear.tooling.preview.devices.WearDevices
+import com.swooby.alfredai.AlfredAiApp
import com.swooby.alfredai.R
import com.swooby.alfredai.SharedViewModel
import com.swooby.alfredai.WearViewModel
+import com.swooby.alfredai.appViewModels
import com.swooby.alfredai.presentation.theme.AlfredAITheme
// TODO: If phone app is not running:
@@ -49,17 +55,13 @@ class WearActivity : ComponentActivity() {
private const val TAG = "MainActivity"
}
- private lateinit var wearViewModel: WearViewModel
+ private val wearViewModel: WearViewModel by appViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate()")
installSplashScreen()
super.onCreate(savedInstanceState)
-
- wearViewModel = ViewModelProvider(this)[WearViewModel::class.java]
-
setTheme(android.R.style.Theme_DeviceDefault)
-
setContent {
WearApp("Wear", wearViewModel)
}
@@ -68,29 +70,34 @@ class WearActivity : ComponentActivity() {
override fun onDestroy() {
Log.d(TAG, "onDestroy()")
super.onDestroy()
- wearViewModel.close()
}
}
@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true)
@Composable
fun DefaultPreview() {
- WearApp("Preview Wear")
+ WearApp("Preview")
}
@Composable
fun WearApp(
- greetingName: String,
+ targetNameDefault: String,
wearViewModel: WearViewModel? = null,
) {
@Suppress("LocalVariableName")
val TAG = "WearApp"
- val phoneAppNodeId by wearViewModel?.remoteAppNodeId?.collectAsState() ?: remember { mutableStateOf(null) }
+ val phoneAppNodeId by wearViewModel
+ ?.remoteAppNodeId
+ ?.collectAsState()
+ ?: remember { mutableStateOf(null) }
+ Log.d(TAG, "phoneAppNodeId is: $phoneAppNodeId")
+ LaunchedEffect(phoneAppNodeId) {
+ Log.d(TAG, "phoneAppNodeId changed to: $phoneAppNodeId")
+ }
var isConnectingOrConnected by remember { mutableStateOf(false) }
- var isConnected = phoneAppNodeId != null
- // by remember { mutableStateOf(false || nodeList.isNotEmpty()) }
+ val isConnected = phoneAppNodeId != null
val disabledColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
@@ -102,6 +109,9 @@ fun WearApp(
contentAlignment = Alignment.Center
) {
TimeText()
+ if (isConnected) {
+ KeepScreenOnComposable()
+ }
Box {
when {
isConnected -> {
@@ -112,6 +122,7 @@ fun WearApp(
modifier = Modifier.size(150.dp)
)
}
+
isConnectingOrConnected -> {
CircularProgressIndicator(
indicatorColor = MaterialTheme.colors.primary,
@@ -119,6 +130,7 @@ fun WearApp(
modifier = Modifier.size(150.dp)
)
}
+
else -> {
CircularProgressIndicator(
progress = 0f,
@@ -204,9 +216,11 @@ fun PushToTalkButton(
}
.then(Modifier.background(Color.Transparent))
.let {
- it.background(Color.Transparent).graphicsLayer {
- this.alpha = boxAlpha
- }
+ it
+ .background(Color.Transparent)
+ .graphicsLayer {
+ this.alpha = boxAlpha
+ }
}
) {
val iconRes = if (enabled) {
@@ -226,3 +240,14 @@ fun PushToTalkButton(
)
}
}
+
+@Composable
+fun KeepScreenOnComposable() {
+ val view = LocalView.current
+ DisposableEffect(Unit) {
+ view.keepScreenOn = true
+ onDispose {
+ view.keepScreenOn = false
+ }
+ }
+}
diff --git a/wear/src/main/res/raw/quindar_nasa_apollo_intro.wav b/wear/src/main/res/raw/quindar_nasa_apollo_intro.wav
new file mode 100644
index 0000000..378e02c
Binary files /dev/null and b/wear/src/main/res/raw/quindar_nasa_apollo_intro.wav differ
diff --git a/wear/src/main/res/raw/quindar_nasa_apollo_outro.wav b/wear/src/main/res/raw/quindar_nasa_apollo_outro.wav
new file mode 100644
index 0000000..284b71f
Binary files /dev/null and b/wear/src/main/res/raw/quindar_nasa_apollo_outro.wav differ
diff --git a/wear/src/main/res/values/wear.xml b/wear/src/main/res/values/wear.xml
index 5565869..f767c1b 100644
--- a/wear/src/main/res/values/wear.xml
+++ b/wear/src/main/res/values/wear.xml
@@ -4,6 +4,6 @@
tools:keep="@array/android_wear_capabilities"
>
- - verify_remote_example_wear_app
+ - verify_remote_alfredai_wear_app
\ No newline at end of file