add basic updater implementation in gigawear

This commit is contained in:
2026-09-14 16:52:27 +03:00
parent 1c573c4366
commit e77046018d
23 changed files with 230 additions and 90 deletions
+2
View File
@@ -21,3 +21,5 @@ cache
kotlin-js-store
release.keystore
keystore.properties
tmp
run-gigawear-backend.sh
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":libs:base"))
implementation(project(":libs:http-client"))
implementation(project(":feature:updater:updater-common"))
implementation(project(":feature:updater:updater-backend"))
implementation(project(":feature:chat:chat-common"))
implementation(project(":feature:chat:chat-backend"))
@@ -10,8 +10,10 @@ import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import org.koin.core.annotation.KoinApplication
import org.koin.plugin.module.dsl.startKoin
import ru.shadowsparky.gigawear.backend.presentation.MOCK_USER_ID
import ru.shadowsparky.gigawear.backend.presentation.setupChat
import ru.shadowsparky.koin
import ru.shadowsparky.updater.backend.presentation.setupUpdates
@KoinApplication
object GigaWearApp
@@ -26,5 +28,6 @@ fun Application.module() {
routing {
get("health") { this.call.respond(HttpStatusCode.OK) }
setupChat(koin.get())
setupUpdates(koin.get()) { MOCK_USER_ID }
}
}
@@ -7,7 +7,7 @@ import io.ktor.server.routing.post
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
import ru.shadowsparky.chat.domain.Message
private const val MOCK_USER_ID = -1L
const val MOCK_USER_ID = -1L
fun Route.setupChat(useCase: ProcessUserMessageUseCase) {
post("chat") {
@@ -1,17 +0,0 @@
package ru.shadowsparky.gigawear.backend.presentation
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.Routing
import io.ktor.server.routing.post
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
import ru.shadowsparky.chat.domain.Message
fun Route.setupUpdate(useCase: ProcessUserMessageUseCase) {
post("chat") {
val message = call.receive<Message>().copy(timestamp = System.currentTimeMillis())
call.respond(useCase.execute(message, MOCK_USER_ID))
}
}
+3 -1
View File
@@ -30,7 +30,9 @@ dependencies {
implementation(project(":libs:http-client"))
implementation(project(":libs:base"))
implementation(project(":feature:chat:chat-common"))
implementation(project(":feature:updater:updater-common"))
implementation(project(":feature:updater:updater-client"))
implementation(libs.activity.compose)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material3)
+59
View File
@@ -0,0 +1,59 @@
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn java.lang.management.ManagementFactory
-dontwarn java.lang.management.RuntimeMXBean
-dontwarn kotlinx.atomicfu.**
-dontwarn io.netty.**
-dontwarn com.typesafe.**
-dontwarn org.slf4j.**
-dontwarn com.sun.nio.file.SensitivityWatchEventModifier
-dontwarn org.xmlpull.v1.**
-dontwarn javax.xml.namespace.**
-dontwarn androidx.test.platform.app.InstrumentationRegistry
-dontwarn androidx.window.extensions.WindowExtensions
-dontwarn androidx.window.extensions.WindowExtensionsProvider
-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation
-dontwarn androidx.window.extensions.core.util.function.Consumer
-dontwarn androidx.window.extensions.core.util.function.Function
-dontwarn androidx.window.extensions.core.util.function.Predicate
-dontwarn androidx.window.extensions.layout.DisplayFeature
-dontwarn androidx.window.extensions.layout.FoldingFeature
-dontwarn androidx.window.extensions.layout.WindowLayoutComponent
-dontwarn androidx.window.extensions.layout.WindowLayoutInfo
-dontwarn androidx.window.sidecar.SidecarDeviceState
-dontwarn androidx.window.sidecar.SidecarDisplayFeature
-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback
-dontwarn androidx.window.sidecar.SidecarInterface
-dontwarn androidx.window.sidecar.SidecarProvider
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent { *; }
-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent$OnSaved { *; }
-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent$OnRecent { *; }
-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent$OnSearch { *; }
-keep class ru.shadowsparky.videobox.multiplatform.**$Config { *; }
-keep class ru.shadowsparky.videobox.multiplatform.**$Configuration { *; }
-keepclassmembers class * implements android.os.Parcelable {
static ** CREATOR;
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-keep class com.arkivanov.decompose.** { *; }
-keep interface com.arkivanov.decompose.** { *; }
-dontwarn com.arkivanov.decompose.**
@@ -3,8 +3,21 @@ package ru.shadowsparky.gigawear
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module
import org.koin.core.annotation.Singleton
import ru.shadowsparky.domain.AppConfig
@Module
@ComponentScan
@Configuration
class GigaWearModule
class GigaWearModule {
@Singleton
fun provideAppConfig(): AppConfig {
return object : AppConfig {
override val debug = BuildConfig.DEBUG
override val appId = "GigaWearClient"
override val versionCode = BuildConfig.VERSION_CODE.toLong()
override val versionName = BuildConfig.VERSION_NAME
}
}
}
@@ -8,13 +8,15 @@ import org.koin.core.annotation.Factory
import ru.shadowsparky.chat.domain.ChatRepository
import ru.shadowsparky.chat.domain.Message
const val ENDPOINT = "http://192.168.0.76:8181"
@Factory
class GigaChatRepository(
private val httpClient: HttpClient
) : ChatRepository {
override suspend fun query(afterId: Long?): List<Message> = emptyList()
override suspend fun send(message: Message): Message {
return httpClient.post("http://192.168.0.76:8181/chat") {
return httpClient.post("$ENDPOINT/chat") {
setBody(message)
}.body<Message>()
}
@@ -0,0 +1,18 @@
package ru.shadowsparky.gigawear.data
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import org.koin.core.annotation.Factory
import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.updater.domain.UpdateInfo
@Factory
class UpdateFetcherImpl(private val httpClient: HttpClient) : UpdateFetcher {
override suspend fun fetch(versionCode: Long): UpdateInfo {
return httpClient.get("$ENDPOINT/checkUpdates") {
parameter(UpdateFetcher.ARG_VERSION_CODE, versionCode)
}.body()
}
}
@@ -1,6 +1,7 @@
package ru.shadowsparky.gigawear.presentation
import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.annotation.KoinApplication
import org.koin.plugin.module.dsl.startKoin
@@ -11,6 +12,8 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin<GigaWearApp>()
startKoin<GigaWearApp> {
androidContext(this@App)
}
}
}
@@ -1,6 +1,7 @@
package ru.shadowsparky.gigawear.presentation.chat
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.arkivanov.decompose.value.MutableValue
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.update
@@ -17,12 +18,15 @@ import ru.shadowsparky.gigawear.presentation.nav.Child
import ru.shadowsparky.gigawear.presentation.nav.Route
import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.updater.presentation.UpdateComponentFactory
class ChatComponent(
private val context: ComponentContext,
private val repository: ChatRepository
private val repository: ChatRepository,
private val updateComponentFactory: UpdateComponentFactory
) : ComponentContext by context {
private val scope = coroutineScope()
val updateComponent = updateComponentFactory.create(childContext("updater"))
private val _state = MutableValue(ChatUiState())
val state: Value<ChatUiState> = _state
@@ -30,6 +34,10 @@ class ChatComponent(
private val _events = Channel<ComponentEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
init {
updateComponent.checkUpdates(true)
}
fun onMicClicked() {
if (_state.value.isLoading) return
scope.launch {
@@ -75,14 +83,19 @@ class ChatComponent(
@Factory
@Named(Route.CHAT)
class ChatComponentFactory(
private val chatRepository: ChatRepository
private val chatRepository: ChatRepository,
private val updateComponentFactory: UpdateComponentFactory
) : ComponentFactory<Child, Route.ChatScreen, Route> {
override fun create(
route: Route.ChatScreen,
child: ComponentContext,
root: RouteNavigator<Route>
): Child {
val component = ChatComponent(child, chatRepository)
val component = ChatComponent(
child,
chatRepository,
updateComponentFactory
)
return Child.MainChild(component)
}
}
@@ -40,6 +40,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState
import kotlinx.coroutines.flow.collectLatest
import ru.shadowsparky.gigawear.R
import ru.shadowsparky.gigawear.presentation.WearErrorContent
import ru.shadowsparky.updater.presentation.UpdateDialog
import java.util.Locale
@Composable
@@ -171,6 +172,7 @@ fun ChatScreen(component: ChatComponent) {
}
}
) { contentPadding ->
UpdateDialog(component.updateComponent)
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":libs:base"))
implementation(project(":libs:http-client"))
implementation(project(":feature:updater:updater-common"))
implementation(project(":feature:updater:updater-backend"))
implementation(project(":feature:chat:chat-common"))
implementation(project(":feature:chat:chat-backend"))
@@ -1,23 +1,10 @@
package ru.shadowsparky.vbox.backend.presentation.routing
import io.ktor.server.response.respond
import io.ktor.server.response.respondFile
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.util.getOrFail
import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcherFactory
import ru.shadowsparky.vbox.backend.presentation.obtainUserId
import ru.shadowsparky.updater.backend.data.BackendUpdateFetcherFactory
import ru.shadowsparky.updater.backend.presentation.setupUpdates
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
fun Route.setupUpdates(updateFetcherFactory: BackendUpdateFetcherFactory) {
get("$DYNAMIC_PREFIX/checkUpdates") {
val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong()
val updateFetcher = updateFetcherFactory.create(call.obtainUserId())
call.respond(updateFetcher.fetch(versionCode))
}
get("$DYNAMIC_PREFIX/getApk") {
val updateFetcher = updateFetcherFactory.create(call.obtainUserId())
call.respondFile(updateFetcher.updateFile())
}
setupUpdates(updateFetcherFactory, "$DYNAMIC_PREFIX/")
}
@@ -0,0 +1,2 @@
/build
/bin
@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.convention.jvm)
alias(libs.plugins.convention.ktor.client)
alias(libs.plugins.convention.koin)
alias(libs.plugins.convention.serialization)
alias(libs.plugins.ktor)
}
dependencies {
implementation(libs.ktor.server.core.jvm)
implementation(project(":libs:backend-base"))
implementation(project(":libs:http-client"))
implementation(project(":feature:updater:updater-common"))
}
@@ -0,0 +1,10 @@
package ru.shadowsparky.updater.backend
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module
@Module
@ComponentScan
@Configuration
class BackendUpdaterModule
@@ -1,14 +1,14 @@
package ru.shadowsparky.vbox.backend.data
package ru.shadowsparky.updater.backend.data
import io.ktor.http.HttpStatusCode
import io.ktor.util.cio.readChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.koin.core.annotation.Factory
import ru.shadowsparky.backend.data.EnvFetcher
import ru.shadowsparky.domain.DispatcherProvider
import ru.shadowsparky.http.domain.HttpException
import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.updater.domain.UpdateInfo
@@ -17,18 +17,16 @@ import java.io.File
@Factory
class BackendUpdateFetcherFactory(
private val envFetcher: EnvFetcher,
private val json: Json,
private val dispatcherProvider: DispatcherProvider
private val json: Json
) {
fun create(userId: Long): BackendUpdateFetcher {
return BackendUpdateFetcher(envFetcher, json, dispatcherProvider, userId)
return BackendUpdateFetcher(envFetcher, json, userId)
}
}
class BackendUpdateFetcher(
private val envFetcher: EnvFetcher,
private val json: Json,
private val dispatcherProvider: DispatcherProvider,
private val userId: Long
) : UpdateFetcher {
private val fetchMutex = Mutex()
@@ -42,7 +40,7 @@ class BackendUpdateFetcher(
}
}
private suspend fun fetchInternal(): UpdateInfo = withContext(dispatcherProvider.io) {
private suspend fun fetchInternal(): UpdateInfo = withContext(Dispatchers.IO) {
val configPath = envFetcher.get("UPDATE_CONFIG")
if (configPath.isBlank()) {
UpdateInfo()
@@ -59,7 +57,7 @@ class BackendUpdateFetcher(
}
}
private suspend fun updateFileInternal(): File = withContext(dispatcherProvider.io) {
private suspend fun updateFileInternal(): File = withContext(Dispatchers.IO) {
val updateFilePath = envFetcher.get("UPDATE_FILE")
if (updateFilePath.isBlank()) throw HttpException(HttpStatusCode.NotFound.value, "Update not found")
val file = File(updateFilePath)
@@ -68,8 +66,8 @@ class BackendUpdateFetcher(
file
}
private companion object {
var updateInfo: UpdateInfo? = null
var updateFile: File? = null
}
private companion object {
var updateInfo: UpdateInfo? = null
var updateFile: File? = null
}
}
@@ -0,0 +1,27 @@
package ru.shadowsparky.updater.backend.presentation
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respond
import io.ktor.server.response.respondFile
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.util.getOrFail
import ru.shadowsparky.backend.presentation.obtainUserId
import ru.shadowsparky.updater.backend.data.BackendUpdateFetcherFactory
import ru.shadowsparky.updater.domain.UpdateFetcher
fun Route.setupUpdates(
updateFetcherFactory: BackendUpdateFetcherFactory,
prefix: String = "",
onUserId: suspend ApplicationCall.() -> Long = { obtainUserId() }
) {
get("${prefix}checkUpdates") {
val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong()
val updateFetcher = updateFetcherFactory.create(onUserId(call))
call.respond(updateFetcher.fetch(versionCode))
}
get("${prefix}getApk") {
val updateFetcher = updateFetcherFactory.create(onUserId(call))
call.respondFile(updateFetcher.updateFile())
}
}
@@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -42,7 +44,6 @@ import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.AlertContent
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.LazyColumn
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.updater.domain.UpdateState
@@ -128,8 +129,9 @@ private fun UpdateAvailableContent(
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
@@ -137,36 +139,26 @@ private fun UpdateAvailableContent(
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
LazyColumn(modifier = Modifier.fillMaxWidth()) {
item {
Text(
text = stringResource(Res.string.updater_changelog_title),
style = MaterialTheme.typography.titleMedium
)
}
item {
Spacer(modifier = Modifier.height(4.dp))
}
item {
Text(
text = changeLog ?: stringResource(Res.string.updater_changelog_empty),
style = MaterialTheme.typography.bodyMedium
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
Button(
onClick = onDownloadClick,
modifier = Modifier.focusRequester(focusRequester).fillMaxWidth()
) {
Text(
stringResource(Res.string.updater_btn_download),
style = MaterialTheme.typography.labelLarge
)
}
}
Text(
text = stringResource(Res.string.updater_changelog_title),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = changeLog ?: stringResource(Res.string.updater_changelog_empty),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onDownloadClick,
modifier = Modifier.focusRequester(focusRequester).fillMaxWidth()
) {
Text(
stringResource(Res.string.updater_btn_download),
style = MaterialTheme.typography.labelLarge
)
}
}
}
@@ -177,8 +169,9 @@ private fun UpdatePermissionContent(
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -210,8 +203,9 @@ private fun UpdateDownloadingContent(
totalBytes: Long,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -241,8 +235,9 @@ private fun UpdateReadyContent(
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -269,8 +264,9 @@ private fun UpdateFinishedContent(
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -32,12 +34,13 @@ fun ErrorContent(
retry: Boolean = true,
onClick: () -> Unit
) {
val scrollState = rememberScrollState()
val requester = remember { FocusRequester() }
val buttonTextRes = if (retry) Res.string.retry else Res.string.ok
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(4.dp)
modifier = modifier.verticalScroll(scrollState).padding(4.dp)
) {
Text(
text = stringResource(Res.string.error_title),
+1
View File
@@ -49,6 +49,7 @@ include(":libs:http-client")
include(":feature:video-player")
include(":feature:updater:updater-client")
include(":feature:updater:updater-common")
include(":feature:updater:updater-backend")
include(":feature:chat:chat-client")
include(":feature:chat:chat-common")
include(":feature:chat:chat-backend")