Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ buildWear:
./gradlew :wear:assemble $(params)

buildDesktop:
./gradlew :cmp-app:assemble $(params)
./gradlew :cmp-common:jvmJar $(params)

testCommon:
./gradlew :cmp-common:testDebugUnitTest $(params)
Expand Down
10 changes: 8 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ plugins {
id("com.android.application")
id("com.autonomousapps.dependency-analysis")
kotlin("android")
kotlin("plugin.serialization")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.protobuf")
id("com.github.triplet.play") version "3.12.2"
Expand Down Expand Up @@ -90,8 +91,9 @@ dependencies {
implementation(libs.core.coreKtx)
implementation(libs.material)

implementation(libs.compose.ui)
implementation(libs.compose.material3)
implementation(libs.cmp.ui)
implementation(libs.cmp.material3)
implementation(libs.cmp.components.resources)
implementation(libs.compose.icons.core)

debugImplementation(libs.compose.uiTooling)
Expand Down Expand Up @@ -123,6 +125,10 @@ dependencies {
implementation(libs.koin.android)
implementation(libs.koin.android.compose) // lifecycleScope

// Decompose for navigation
implementation(libs.decompose.core)
implementation(libs.decompose.compose)

implementation(libs.vico.multiplatform)
implementation(libs.vico.multiplatform.m3)

Expand Down
54 changes: 14 additions & 40 deletions app/src/main/java/ru/beryukhov/coffeegram/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,26 @@ import android.app.Application
import androidx.glance.appwidget.updateAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
import ru.beryukhov.coffeegram.model.DaysCoffeesStore
import ru.beryukhov.coffeegram.model.DaysCoffeesStoreImpl
import ru.beryukhov.coffeegram.model.NavigationStore
import ru.beryukhov.coffeegram.model.ThemeState
import ru.beryukhov.coffeegram.model.ThemeStore
import ru.beryukhov.coffeegram.pages.AppWidgetViewModelImpl
import ru.beryukhov.coffeegram.pages.CoffeeListViewModelImpl
import ru.beryukhov.coffeegram.pages.MapPageViewModelImpl
import ru.beryukhov.coffeegram.pages.StatsPageViewModelImpl
import ru.beryukhov.coffeegram.pages.TablePageViewModelImpl
import ru.beryukhov.coffeegram.repository.CoffeeStorage
import ru.beryukhov.coffeegram.repository.ThemeDataStoreProtoStorage
import ru.beryukhov.coffeegram.store_lib.Storage
import ru.beryukhov.coffeegram.widget.DefaultWidgetDataBridge
import ru.beryukhov.coffeegram.widget.FirstGlanceWidget
import ru.beryukhov.coffeegram.widget.WidgetDataBridge
import ru.beryukhov.coffeegram.widget.setWidgetPreview
import ru.beryukhov.repository.databaseModule

class Application : Application() {
open class Application : Application() {

override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@Application)
modules(
appModule,
androidAppModule,
databaseModule
)
}
Expand All @@ -46,31 +32,19 @@ class Application : Application() {
MainScope().launch {
withContext(Dispatchers.Default) {
FirstGlanceWidget().updateAll(this@Application)
setWidgetPreview(this@Application)
get<DaysCoffeesStore>().state.onEach {
FirstGlanceWidget().updateAll(this@Application)
}.launchIn(this)
// if (not robolectric test)
setWidgetPreview()
}
}
}
}

internal val appModule = module {
single<Storage<ThemeState>> {
// ThemeSharedPrefStorage(context = context)
// ThemeDataStorePrefStorage(context = context)
ThemeDataStoreProtoStorage(context = get())
}
single {
ThemeStore(get())
open suspend fun setWidgetPreview() {
setWidgetPreview(this@Application)
}
single<CoffeeStorage> { CoffeeStorage(get()) }
single<DaysCoffeesStore> { DaysCoffeesStoreImpl(get()) }
// single<DaysCoffeesStore> { LightDaysCoffeesStore() }
single { NavigationStore() }
viewModel { CoffeeListViewModelImpl(daysCoffeesStore = get(), navigationStore = get()) }
viewModel { TablePageViewModelImpl(daysCoffeesStore = get(), navigationStore = get()) }
viewModel { StatsPageViewModelImpl(daysCoffeesStore = get()) }
viewModel { MapPageViewModelImpl() }
viewModel { AppWidgetViewModelImpl(daysCoffeesStore = get()) }
}

internal val androidAppModule = module {
// Theme storage and store
// Widget data bridge
single<WidgetDataBridge> { DefaultWidgetDataBridge(daysCoffeesStore = get()) }
}
125 changes: 43 additions & 82 deletions app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,129 +3,90 @@ package ru.beryukhov.coffeegram
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.Wearable
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.koin.compose.koinInject
import com.arkivanov.decompose.defaultComponentContext
import org.koin.android.ext.android.get
import ru.beryukhov.coffeegram.animations.TransitionSlot
import ru.beryukhov.coffeegram.components.DefaultAndroidRootComponent
import ru.beryukhov.coffeegram.data.CoffeeTypes
import ru.beryukhov.coffeegram.data.DAY_COFFEE_PATH
import ru.beryukhov.coffeegram.data.DayCoffee
import ru.beryukhov.coffeegram.data.START_ACTIVITY_PATH
import ru.beryukhov.coffeegram.data.toDataMap
import ru.beryukhov.coffeegram.model.NavigationIntent
import ru.beryukhov.coffeegram.model.NavigationState.Companion.NAVIGATION_STATE_KEY
import ru.beryukhov.coffeegram.model.NavigationState.Companion.TODAYS_COFFEE_LIST
import ru.beryukhov.coffeegram.model.NavigationStore
import ru.beryukhov.coffeegram.model.DaysCoffeesStore
import ru.beryukhov.coffeegram.model.NavigationConstants.NAVIGATION_STATE_KEY
import ru.beryukhov.coffeegram.model.NavigationConstants.TODAYS_COFFEE_LIST
import ru.beryukhov.coffeegram.model.ThemeStore
import ru.beryukhov.coffeegram.pages.LandingPage
import kotlin.time.Clock
import ru.beryukhov.coffeegram.screens.AndroidRootScreen
import ru.beryukhov.coffeegram.wearable.WearableSyncService
import kotlin.time.ExperimentalTime

@OptIn(ExperimentalTime::class)
class MainActivity : ComponentActivity() {

internal val nodeClient by lazy { Wearable.getNodeClient(this) }
internal val messageClient by lazy { Wearable.getMessageClient(this) }
internal val dataClient by lazy { Wearable.getDataClient(this) }
private val wearableSyncService by lazy { WearableSyncService(this) }

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

val themeStore: ThemeStore = get()
val daysCoffeesStore: DaysCoffeesStore = get()
val showMap = checkCoarseLocationPermission()

val rootComponent = DefaultAndroidRootComponent(
context = defaultComponentContext(),
themeStore = themeStore,
daysCoffeesStore = daysCoffeesStore,
showMap = showMap,
onStartWearableActivity = ::startWearableActivity,
onIconChange = { isSummer -> changeIcon(this, isSummer) },
)

setContent {
var doAnimationState by rememberSaveable {
mutableStateOf(true)
}
val navigationStore: NavigationStore = koinInject()
TransitionSlot(
doAnimation = doAnimationState,
StartPage = { modifier -> LandingPage(modifier = modifier) },
EndPage = { modifier, topPadding ->
PagesContent(
EndPage = { modifier, _ ->
AndroidRootScreen(
rootComponent = rootComponent,
modifier = modifier,
topPadding = topPadding,
startWearableActivity = ::startWearableActivity,
showMap = checkCoarseLocationPermission(),
navigationStore = navigationStore
)
},
) {
doAnimationState = false
if (intent.getStringExtra(NAVIGATION_STATE_KEY) == TODAYS_COFFEE_LIST) {
navigationStore.newIntent(
NavigationIntent.OpenCoffeeListPage(
dayOfMonth = Clock.System.now()
.toLocalDateTime(TimeZone.currentSystemDefault()).date.day
)
)
}
handleDeepLink(rootComponent)
}
}
}
}

private fun MainActivity.startWearableActivity() {
lifecycleScope.launch {
try {
val nodes = nodeClient.connectedNodes.await() // todo depending on nodes count show or hide button

// Send a message to all nodes in parallel
nodes.map { node ->
async {
messageClient.sendMessage(node.id, START_ACTIVITY_PATH, byteArrayOf()).await()
}
}.awaitAll()

Log.d(TAG, "Starting activity requests sent successfully")
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (exception: Exception) {
Log.d(TAG, "Starting activity failed: $exception")
private fun handleDeepLink(rootComponent: DefaultAndroidRootComponent) {
if (intent.getStringExtra(NAVIGATION_STATE_KEY) == TODAYS_COFFEE_LIST) {
// Navigate to coffee list for today
// The CoffeeEditComponent handles the navigation to DayList internally
// We just need to ensure we're on the CoffeeEdit tab (index 0)
rootComponent.selectPage(0)
}
}
sendDayCoffee(
// todo replace mock
DayCoffee(

private fun startWearableActivity() {
// Send mock data for now - in production this would use real data from store
val mockDayCoffee = DayCoffee(
mapOf(
CoffeeTypes.Cappuccino to 1,
CoffeeTypes.Americano to 2
)
)
)
}

private fun MainActivity.sendDayCoffee(dayCoffee: DayCoffee) {
lifecycleScope.launch {
try {
val request = PutDataMapRequest.create(DAY_COFFEE_PATH).apply {
dayCoffee.toDataMap(dataMap)
}.asPutDataRequest().setUrgent()

val result = dataClient.putDataItem(request).await()

Log.d(TAG, "DataItem saved: $result")
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (exception: Exception) {
Log.d(TAG, "Saving DataItem failed: $exception")
}
wearableSyncService.startWearableActivity(lifecycleScope, mockDayCoffee)
}
}

private fun MainActivity.checkCoarseLocationPermission(): Boolean = checkSelfPermission(
ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED

private const val TAG = "TestWatch_"
private fun checkCoarseLocationPermission(): Boolean = checkSelfPermission(
ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
Loading