Skip to content

Commit

Permalink
feat: wear messenger
Browse files Browse the repository at this point in the history
  • Loading branch information
makeevrserg committed Oct 11, 2023
1 parent 3f6e5ca commit dcc060d
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 61 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ modules/services/file_system/build
modules/services/xlsx/build
modules/services/xlsx/libs
modules/services/ads-yandex/build
modules/services/wear-messenger/build
# Features ----------------------------------
modules/features/dialog-confirm/build
modules/features/words-local/build
Expand Down
5 changes: 5 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ android {
}

dependencies {
// Kotlin
implementation(libs.kotlin.serialization.json)
// Coroutines
implementation(libs.kotlin.coroutines.core)
implementation(libs.kotlin.coroutines.android)
Expand All @@ -117,6 +119,8 @@ dependencies {
implementation(libs.klibs.mikro.platform)
implementation(libs.klibs.kstorage)
implementation(libs.klibs.kdi)
// moko
implementation(libs.moko.resources.core)
// Decompose
implementation(libs.decompose.core)
implementation(libs.decompose.compose.jetpack)
Expand All @@ -132,4 +136,5 @@ dependencies {
implementation(projects.modules.features.ui)
implementation(projects.modules.services.coreUi)
implementation(projects.modules.services.resources)
implementation(projects.modules.services.wearMessenger)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import com.google.firebase.ktx.initialize
import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.RootModuleImpl
import com.makeevrserg.empireprojekt.mobile.work.CheckStatusWork
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import ru.astrainteractive.klibs.kdi.getValue
import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -62,16 +59,6 @@ class App : Application() {
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
statusWork
)
rootModule.servicesModule.mainScope.value.launch {
while (isActive) {
delay(5000L)
CheckStatusWork.sendMessageOnWear(
wearDataLayerRegistry = wearDataLayerRegistry,
rootModule = rootModule,
messageClient = messageClient
)
}
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.google.android.gms.wearable.MessageClient
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.WearDataLayerRegistry
import com.makeevrserg.empireprojekt.mobile.application.App.Companion.asEmpireApp
import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule
import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.message.StatusModelMessage
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model.StatusModel
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer.WearMessageProducerImpl
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.tasks.await
import ru.astrainteractive.klibs.kdi.Provider
import ru.astrainteractive.klibs.kdi.getValue

Expand All @@ -22,6 +22,21 @@ class CheckStatusWork(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
private val wearDataLayerRegistry by lazy {
WearDataLayerRegistry.fromContext(
application = applicationContext,
coroutineScope = rootModule.servicesModule.mainScope.value
)
}
private val messageClient by lazy {
wearDataLayerRegistry.messageClient
}
private val wearMessageProducer by lazy {
WearMessageProducerImpl(
wearDataLayerRegistry = wearDataLayerRegistry,
messageClient = messageClient
)
}

private val rootModule by lazy {
applicationContext.asEmpireApp().rootModule
Expand All @@ -30,59 +45,35 @@ class CheckStatusWork(
rootModule.rootStatusComponent.value
}

override suspend fun doWork(): Result = coroutineScope {
override suspend fun doWork(): Result {
Log.d(TAG, "doWork: ")
rootStatusComponent.statusComponents.map {
sendStatus()
return Result.success()
}

private suspend fun sendStatus() = coroutineScope {
val messages = rootStatusComponent.statusComponents.map {
async {
it.checkStatus()
val model = it.model.value
StatusModel(
title = model.title.toString(applicationContext),
isLoading = model.isLoading,
status = when (model.status) {
StatusComponent.Model.LoadingStatus.LOADING -> StatusModel.LoadingStatus.LOADING
StatusComponent.Model.LoadingStatus.SUCCESS -> StatusModel.LoadingStatus.SUCCESS
StatusComponent.Model.LoadingStatus.ERROR -> StatusModel.LoadingStatus.ERROR
}
)
}
}.awaitAll()
Result.success()
val statusModelMessage = StatusModelMessage(
json = rootModule.servicesModule.jsonConfiguration.value
)
wearMessageProducer.produce(statusModelMessage, messages)
}

companion object {
private const val TAG = "CheckStatusWork"
suspend fun sendMessageOnWear(
wearDataLayerRegistry: WearDataLayerRegistry,
rootModule: RootModule,
messageClient: MessageClient
) = coroutineScope {
kotlin.runCatching {
val nodes = wearDataLayerRegistry.nodeClient.connectedNodes.await()
Log.d(TAG, "Contains ${nodes.size} nodes")
val mapped = rootModule.rootStatusComponent.value.statusComponents.map {
if (it.model.value.isLoading) {
StatusComponent.Model.LoadingStatus.LOADING
} else {
it.model.value.status
}
}
val statuses = buildList {
StatusComponent.Model.LoadingStatus.SUCCESS.let { status ->
status to mapped.count { it == status }
}.run(::add)
StatusComponent.Model.LoadingStatus.ERROR.let { status ->
status to mapped.count { it == status }
}.run(::add)
StatusComponent.Model.LoadingStatus.LOADING.let { status ->
status to mapped.count { it == status }
}.run(::add)
}
nodes.flatMap { node ->
statuses.map { entry ->
async {
messageClient.sendMessage(
node.id,
"/statuses" + entry.first.name,
byteArrayOf(entry.second.toByte())
)
}
}
}.awaitAll()
Log.d(TAG, "Sended ")
}.onFailure {
it.printStackTrace()
}
}
}
}
36 changes: 36 additions & 0 deletions modules/services/wear-messenger/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@file:Suppress("UnusedPrivateMember")

import ru.astrainteractive.gradleplugin.util.ProjectProperties.projectInfo

plugins {
id("com.android.library")
kotlin("multiplatform")
id("ru.astrainteractive.gradleplugin.java.core")
id("ru.astrainteractive.gradleplugin.android.core")
alias(libs.plugins.kotlin.serialization)
}

kotlin {
android()
sourceSets {
val commonMain by getting {
dependencies {
// Kotlin
implementation(libs.kotlin.serialization.json)
// klibs
implementation(libs.klibs.mikro.core)
implementation(libs.klibs.mikro.platform)
implementation(libs.klibs.kstorage)
implementation(libs.klibs.kdi)
// horologist
implementation("com.google.android.horologist:horologist-datalayer:0.5.3")
// Coroutines
implementation(libs.kotlin.coroutines.core)
implementation(libs.kotlin.coroutines.playServices)
}
}
}
}
android {
namespace = "${projectInfo.group}.wear.messenger"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.message

import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model.StatusModel
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.InlineWearMessage
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage
import kotlinx.serialization.json.Json

class StatusModelMessage(
private val json: Json
) : WearMessage<List<StatusModel>> by InlineWearMessage(
json = json,
path = "/status"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model

import kotlinx.serialization.Serializable

@Serializable
class StatusModel(
val title: String,
val isLoading: Boolean,
val status: LoadingStatus
) {
enum class LoadingStatus {
LOADING, SUCCESS, ERROR
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message

data class DecodedWearMessage<T>(
val path: String,
val value: T
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message

import android.util.Log
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class InlineWearMessage<T>(
override val path: String,
private val encode: (T) -> ByteArray,
private val decode: (ByteArray) -> T
) : WearMessage<T> {
override fun encode(value: T): ByteArray = this.encode.invoke(value)

override fun decode(byteArray: ByteArray): T = this.decode.invoke(byteArray)
}

@Suppress("FunctionNaming")
inline fun <reified T> InlineWearMessage(
json: Json,
path: String
): WearMessage<T> = InlineWearMessage(
path = path,
encode = { value ->
val string = json.encodeToString(value)
Log.d("InlineWearMessage", "InlineWearMessage->encode: $string")
string.toByteArray()
},
decode = { byteArray ->
val string = byteArray.decodeToString()
Log.d("InlineWearMessage", "InlineWearMessage->decode: $string")
json.decodeFromString(string)
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message

interface WearMessage<T> {
val path: String

fun encode(value: T): ByteArray
fun decode(byteArray: ByteArray): T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer

import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage

interface WearMessageProducer {
suspend fun <T> produce(message: WearMessage<T>, value: T)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer

import android.util.Log
import com.google.android.gms.wearable.MessageClient
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.WearDataLayerRegistry
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.tasks.await

@OptIn(ExperimentalHorologistApi::class)
class WearMessageProducerImpl(
private val wearDataLayerRegistry: WearDataLayerRegistry,
private val messageClient: MessageClient,
) : WearMessageProducer {
override suspend fun <T> produce(message: WearMessage<T>, value: T): Unit = coroutineScope {
val nodes = wearDataLayerRegistry.nodeClient.connectedNodes.await()
Log.d(TAG, "produce: found ${nodes.size} nodes")
kotlin.runCatching {
val byteArray = message.encode(value)
nodes.map {
async {
messageClient.sendMessage(
it.id,
message.path,
byteArray
)
}
}.awaitAll()
}.onFailure {
Log.e(TAG, "produce: failed to send message ${it.stackTraceToString()}")
}.onSuccess {
Log.d(TAG, "produce: message sent")
}
}

companion object {
private const val TAG = "WearMessageProducer"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.receiver

import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.DecodedWearMessage
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage
import kotlinx.coroutines.flow.Flow

interface WearMessageReceiver {
val messagesFlow: Flow<DecodedWearMessage<*>>
suspend fun <T> consume(message: WearMessage<T>, byteArray: ByteArray)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.receiver

import android.util.Log
import com.google.android.gms.wearable.MessageClient
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.WearDataLayerRegistry
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.DecodedWearMessage
import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow

@OptIn(ExperimentalHorologistApi::class)
@Suppress("UnusedPrivateMember")
class WearMessageReceiverImpl(
private val wearDataLayerRegistry: WearDataLayerRegistry,
private val messageClient: MessageClient,
) : WearMessageReceiver {
private val messageChannel = Channel<DecodedWearMessage<*>>()
override val messagesFlow: Flow<DecodedWearMessage<*>> = messageChannel.receiveAsFlow()

override suspend fun <T> consume(message: WearMessage<T>, byteArray: ByteArray) {
kotlin.runCatching {
val decodedWearMessage = DecodedWearMessage(
path = message.path,
value = message.decode(byteArray)
)
messageChannel.send(decodedWearMessage)
}.onFailure {
Log.d(TAG, "consume: could not publish message: ${it.stackTraceToString()}")
}.onSuccess {
Log.d(TAG, "consume: published message")
}
}

companion object {
private const val TAG = "WearMessageReceiver"
}
}
Loading

0 comments on commit dcc060d

Please sign in to comment.