A fork of Simple Time Tracker with WearOS support. (Now integrated upstream)
0

Configure Feed

Select the types of activity you want to include in your feed.

switch wear architecture to viewModels

razeeman (Mar 2, 2024, 9:50 PM +0300) f8213ca1 ca5062da

+347 -332
+1
wear/build.gradle.kts
··· 70 70 implementation(Deps.Compose.wearFoundation) 71 71 implementation(Deps.Compose.wearToolingPreview) 72 72 implementation(Deps.Compose.horologist) 73 + implementation(Deps.Compose.hilt) 73 74 debugImplementation(Deps.Compose.uiTooling) 74 75 kapt(Deps.Kapt.dagger) 75 76
+1
wear/src/main/AndroidManifest.xml
··· 6 6 <uses-feature android:name="android.hardware.type.watch" /> 7 7 8 8 <application 9 + android:name=".presentation.WearApp" 9 10 android:allowBackup="true" 10 11 android:icon="@mipmap/ic_launcher" 11 12 android:label="@string/app_name"
+2
buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt
··· 83 83 "androidx.wear:wear-tooling-preview:${Versions.wear_compose_tooling_preview}" 84 84 const val horologist = 85 85 "com.google.android.horologist:horologist-compose-layout:${Versions.horologist}" 86 + const val hilt = 87 + "androidx.hilt:hilt-navigation-compose:${Versions.compose_hilt}" 86 88 const val uiTooling = 87 89 "androidx.compose.ui:ui-tooling:${Versions.compose_version}" 88 90 }
+1
buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt
··· 36 36 const val wear_compose_tooling_preview = "1.0.0" 37 37 const val horologist = "0.2.7" 38 38 const val compose_kotlin_compiler = "1.4.0-alpha02" 39 + const val compose_hilt = "1.0.0" 39 40 40 41 const val junit = "4.13" 41 42 const val junitUi = "1.1.4"
+5 -9
wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt
··· 8 8 import android.os.Bundle 9 9 import androidx.activity.ComponentActivity 10 10 import androidx.activity.compose.setContent 11 - import androidx.compose.runtime.Composable 12 11 import com.example.util.simpletimetracker.presentation.navigation.WearNavigator 13 12 import com.example.util.simpletimetracker.presentation.theme.WearTheme 13 + import dagger.hilt.android.AndroidEntryPoint 14 14 15 + @AndroidEntryPoint 15 16 class MainActivity : ComponentActivity() { 16 17 override fun onCreate(savedInstanceState: Bundle?) { 17 18 super.onCreate(savedInstanceState) 18 19 setContent { 19 - WearApp() 20 + WearTheme { 21 + WearNavigator() 22 + } 20 23 } 21 - } 22 - } 23 - 24 - @Composable 25 - fun WearApp() { 26 - WearTheme { 27 - WearNavigator() 28 24 } 29 25 }
+12
wear/src/main/java/com/example/util/simpletimetracker/presentation/WearApp.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.presentation 7 + 8 + import android.app.Application 9 + import dagger.hilt.android.HiltAndroidApp 10 + 11 + @HiltAndroidApp 12 + class WearApp : Application()
+20
features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearModule.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.feature_wear 7 + 8 + import com.example.util.simpletimetracker.wear_api.WearCommunicationAPI 9 + import dagger.Binds 10 + import dagger.Module 11 + import dagger.hilt.InstallIn 12 + import dagger.hilt.components.SingletonComponent 13 + 14 + @Module 15 + @InstallIn(SingletonComponent::class) 16 + interface WearModule { 17 + 18 + @Binds 19 + fun WearCommunicationInteractor.bindWearCommunicationInteractor(): WearCommunicationAPI 20 + }
-20
features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt
··· 1 - /* 2 - * This Source Code Form is subject to the terms of the Mozilla Public 3 - * License, v. 2.0. If a copy of the MPL was not distributed with this 4 - * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 - */ 6 - package com.example.util.simpletimetracker.feature_wear 7 - 8 - import com.example.util.simpletimetracker.wear_api.WearCommunicationAPI 9 - import dagger.Binds 10 - import dagger.Module 11 - import dagger.hilt.InstallIn 12 - import dagger.hilt.components.SingletonComponent 13 - 14 - @Module 15 - @InstallIn(SingletonComponent::class) 16 - interface WidgetModule { 17 - 18 - @Binds 19 - fun WearCommunicationInteractor.bindWearCommunicationInteractor(): WearCommunicationAPI 20 - }
-1
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt
··· 123 123 ) 124 124 } 125 125 126 - 127 126 @Preview(device = WearDevices.LARGE_ROUND) 128 127 @Composable 129 128 private fun MultiSelectMode() {
+2 -1
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt
··· 40 40 for (tag in tags) { 41 41 item { 42 42 TagChip( 43 - tag = tag, mode = mode, 43 + tag = tag, 44 + mode = mode, 44 45 onClick = { 45 46 // No duplicate tags 46 47 onSelectionComplete(selectedTags.minus(tag).plus(tag))
+9 -3
wear/src/main/java/com/example/util/simpletimetracker/presentation/data/Messenger.kt
··· 12 12 import com.google.android.gms.wearable.CapabilityInfo 13 13 import com.google.android.gms.wearable.Node 14 14 import com.google.android.gms.wearable.Wearable 15 + import dagger.hilt.android.qualifiers.ApplicationContext 15 16 import kotlinx.coroutines.CompletableDeferred 17 + import kotlinx.coroutines.Dispatchers 18 + import kotlinx.coroutines.withContext 16 19 import java.util.concurrent.CancellationException 17 20 import javax.inject.Inject 18 21 ··· 24 27 } 25 28 26 29 class ContextMessenger @Inject constructor( 27 - private val context: Context, 30 + @ApplicationContext private val context: Context, 28 31 ) : Messenger { 29 32 private val TAG: String = ContextMessenger::class.java.name 30 33 31 - override suspend fun send(capability: String, message: ByteArray): ByteArray? { 34 + override suspend fun send( 35 + capability: String, 36 + message: ByteArray, 37 + ): ByteArray? = withContext(Dispatchers.IO) { 32 38 val deferred = CompletableDeferred<ByteArray?>() 33 39 val bestNode = findNearestNode(capability) 34 40 ··· 57 63 deferred.cancel(CancellationException(logMessage)) 58 64 } 59 65 60 - return deferred.await() 66 + deferred.await() 61 67 } 62 68 63 69 private fun findNearestNode(capability: String): Node? {
+21
wear/src/main/java/com/example/util/simpletimetracker/presentation/di/WearModule.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.presentation.di 7 + 8 + import com.example.util.simpletimetracker.presentation.data.ContextMessenger 9 + import com.example.util.simpletimetracker.presentation.data.Messenger 10 + import dagger.Binds 11 + import dagger.Module 12 + import dagger.hilt.InstallIn 13 + import dagger.hilt.components.SingletonComponent 14 + 15 + @Module 16 + @InstallIn(SingletonComponent::class) 17 + interface WearModule { 18 + 19 + @Binds 20 + fun ContextMessenger.bindMessenger(): Messenger 21 + }
+12 -9
wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/CurrentActivitiesMediator.kt
··· 1 1 package com.example.util.simpletimetracker.presentation.mediators 2 2 3 + import com.example.util.simpletimetracker.presentation.data.WearRPCClient 3 4 import com.example.util.simpletimetracker.wear_api.WearCurrentActivity 4 5 import com.example.util.simpletimetracker.wear_api.WearSettings 5 6 import com.example.util.simpletimetracker.wear_api.WearTag 6 - import com.example.util.simpletimetracker.presentation.data.WearRPCClient 7 + import javax.inject.Inject 7 8 8 - class CurrentActivitiesMediator( 9 + class CurrentActivitiesMediator @Inject constructor( 9 10 private val rpc: WearRPCClient, 10 - private val currents: List<WearCurrentActivity>, 11 11 ) { 12 - suspend fun start(activityId: Long) { 13 - start(activityId, emptyList()) 14 - } 15 - suspend fun start(activityId: Long, tags: List<WearTag>) { 12 + 13 + suspend fun start( 14 + activityId: Long, 15 + tags: List<WearTag> = emptyList(), 16 + ) { 16 17 val newCurrent = WearCurrentActivity( 17 18 id = activityId, 18 19 startedAt = System.currentTimeMillis(), 19 - tags = tags 20 + tags = tags, 20 21 ) 21 22 if (settings().allowMultitasking) { 23 + val currents = rpc.queryCurrentActivities() 22 24 this.rpc.setCurrentActivities(currents.plus(newCurrent)) 23 25 } else { 24 26 this.rpc.setCurrentActivities(listOf(newCurrent)) ··· 26 28 } 27 29 28 30 suspend fun stop(currentId: Long) { 31 + val currents = rpc.queryCurrentActivities() 29 32 val remaining = currents.filter { it.id != currentId } 30 33 this.rpc.setCurrentActivities(remaining) 31 34 } 35 + 32 36 private suspend fun settings(): WearSettings { 33 37 return this.rpc.querySettings() 34 38 } 35 - 36 39 }
+22 -8
wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/StartActivityMediator.kt
··· 5 5 */ 6 6 package com.example.util.simpletimetracker.presentation.mediators 7 7 8 + import com.example.util.simpletimetracker.presentation.data.WearRPCClient 8 9 import com.example.util.simpletimetracker.wear_api.WearActivity 9 10 import com.example.util.simpletimetracker.wear_api.WearSettings 10 - import com.example.util.simpletimetracker.wear_api.WearCommunicationAPI 11 + import javax.inject.Inject 11 12 12 - class StartActivityMediator( 13 - private val api: WearCommunicationAPI, 14 - private val onRequestStartActivity: suspend (activity: WearActivity) -> Unit, 15 - private val onRequestTagSelection: suspend (activity: WearActivity) -> Unit, 13 + class StartActivityMediator @Inject constructor( 14 + private val api: WearRPCClient, 16 15 ) { 17 - suspend fun requestStart(activity: WearActivity) { 16 + 17 + suspend fun requestStart( 18 + activity: WearActivity, 19 + onRequestStartActivity: suspend (activity: WearActivity) -> Unit, 20 + onRequestTagSelection: suspend (activity: WearActivity) -> Unit, 21 + ) { 18 22 val settings = api.querySettings() 19 23 if (settings.showRecordTagSelection) { 20 - requestTagSelectionIfNeeded(activity, settings) 24 + requestTagSelectionIfNeeded( 25 + activity = activity, 26 + settings = settings, 27 + onRequestStartActivity = onRequestStartActivity, 28 + onRequestTagSelection = onRequestTagSelection, 29 + ) 21 30 } else { 22 31 onRequestStartActivity(activity) 23 32 } 24 33 } 25 34 26 - private suspend fun requestTagSelectionIfNeeded(activity: WearActivity, settings: WearSettings) { 35 + private suspend fun requestTagSelectionIfNeeded( 36 + activity: WearActivity, 37 + settings: WearSettings, 38 + onRequestStartActivity: suspend (activity: WearActivity) -> Unit, 39 + onRequestTagSelection: suspend (activity: WearActivity) -> Unit, 40 + ) { 27 41 val tags = api.queryTagsForActivity(activity.id) 28 42 val generalTags = tags.filter { it.isGeneral } 29 43 val nonGeneralTags = tags.filter { !it.isGeneral }
-46
wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/activities.kt
··· 1 - /* 2 - * This Source Code Form is subject to the terms of the Mozilla Public 3 - * License, v. 2.0. If a copy of the MPL was not distributed with this 4 - * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 - */ 6 - package com.example.util.simpletimetracker.presentation.remember 7 - 8 - import androidx.compose.runtime.Composable 9 - import androidx.compose.runtime.LaunchedEffect 10 - import androidx.compose.runtime.getValue 11 - import androidx.compose.runtime.mutableIntStateOf 12 - import androidx.compose.runtime.mutableStateOf 13 - import androidx.compose.runtime.remember 14 - import androidx.compose.runtime.setValue 15 - import com.example.util.simpletimetracker.wear_api.WearActivity 16 - import kotlinx.coroutines.Dispatchers 17 - import kotlinx.coroutines.async 18 - 19 - /** 20 - * Handles asynchronous retrieval of the available activities in Simple Time Tracker on the 21 - * connected phone. 22 - * 23 - * Usage: 24 - * ``` 25 - * val (activities, refresh) = rememberActivities() 26 - * ``` 27 - * 28 - * Initially, `activities` will be an empty array. Once the actual activities are received 29 - * from the phone, `activities` will *automatically* update to that array and the encapsulating 30 - * Composable will *automatically* re-render. 31 - * 32 - * `refresh` is a function you can call to forcibly re-request the array of available activities 33 - * from the phone. 34 - */ 35 - @Composable 36 - fun rememberActivities(): Pair<List<WearActivity>, () -> Unit> { 37 - val rpc = rememberRPCClient() 38 - var activities: List<WearActivity> by remember { mutableStateOf(emptyList()) } 39 - var activitiesQueryCount by remember { mutableIntStateOf(0) } 40 - LaunchedEffect(activitiesQueryCount) { 41 - async(Dispatchers.Default) { 42 - activities = rpc.queryActivities() 43 - } 44 - } 45 - return Pair(activities) { activitiesQueryCount++ } 46 - }
-49
wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/currentActivities.kt
··· 1 - /* 2 - * This Source Code Form is subject to the terms of the Mozilla Public 3 - * License, v. 2.0. If a copy of the MPL was not distributed with this 4 - * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 - */ 6 - package com.example.util.simpletimetracker.presentation.remember 7 - 8 - import androidx.compose.runtime.Composable 9 - import androidx.compose.runtime.LaunchedEffect 10 - import androidx.compose.runtime.getValue 11 - import androidx.compose.runtime.mutableIntStateOf 12 - import androidx.compose.runtime.mutableStateOf 13 - import androidx.compose.runtime.remember 14 - import androidx.compose.runtime.setValue 15 - import com.example.util.simpletimetracker.wear_api.WearCurrentActivity 16 - import kotlinx.coroutines.Dispatchers 17 - import kotlinx.coroutines.async 18 - 19 - /** 20 - * Handles asynchronous retrieval of the currently running activities in Simple Time Tracker on the 21 - * connected phone. 22 - * 23 - * Usage: 24 - * ``` 25 - * val (currents, setCurrents, refresh) = rememberCurrentActivities() 26 - * ``` 27 - * 28 - * Initially, `currents` will be an empty array. Once the actual current activities are received 29 - * from the phone, `currents` will *automatically* update to that array and the encapsulating 30 - * Composable will *automatically* re-render. 31 - * 32 - * `setCurrents` is a function you can call to set the array of current activities on the phone. 33 - * 34 - * `refresh` is a function you can call to forcibly re-request the array of current activities 35 - * from the phone. 36 - */ 37 - @Composable 38 - fun rememberCurrentActivities(): Pair<List<WearCurrentActivity>, () -> Unit> { 39 - val rpc = rememberRPCClient() 40 - var currentActivities: List<WearCurrentActivity> by remember { mutableStateOf(emptyList()) } 41 - var currentActivitiesQueryCount by remember { mutableIntStateOf(0) } 42 - val queryCurrentActivities = { currentActivitiesQueryCount++ } 43 - LaunchedEffect(currentActivitiesQueryCount) { 44 - async(Dispatchers.Default) { 45 - currentActivities = rpc.queryCurrentActivities() 46 - } 47 - } 48 - return currentActivities to { queryCurrentActivities() } 49 - }
-12
wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/rpcClient.kt
··· 1 - package com.example.util.simpletimetracker.presentation.remember 2 - 3 - import androidx.compose.runtime.Composable 4 - import androidx.compose.ui.platform.LocalContext 5 - import com.example.util.simpletimetracker.presentation.data.ContextMessenger 6 - import com.example.util.simpletimetracker.presentation.data.WearRPCClient 7 - 8 - @Composable 9 - fun rememberRPCClient(): WearRPCClient { 10 - val context = LocalContext.current 11 - return WearRPCClient(ContextMessenger(context)) 12 - }
-44
wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/settings.kt
··· 1 - /* 2 - * This Source Code Form is subject to the terms of the Mozilla Public 3 - * License, v. 2.0. If a copy of the MPL was not distributed with this 4 - * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 - */ 6 - package com.example.util.simpletimetracker.presentation.remember 7 - 8 - import androidx.compose.runtime.Composable 9 - import androidx.compose.runtime.LaunchedEffect 10 - import androidx.compose.runtime.getValue 11 - import androidx.compose.runtime.mutableIntStateOf 12 - import androidx.compose.runtime.mutableStateOf 13 - import androidx.compose.runtime.remember 14 - import androidx.compose.runtime.setValue 15 - import com.example.util.simpletimetracker.wear_api.WearSettings 16 - import kotlinx.coroutines.Dispatchers 17 - import kotlinx.coroutines.async 18 - 19 - /** 20 - * Handles asynchronous retrieval of the settings for Simple Time Tracker on the connected phone. 21 - * 22 - * Usage: 23 - * ``` 24 - * val (settings, refresh) = rememberSettings() 25 - * ``` 26 - * 27 - * Initially, `settings` will be `null`. Once the actual settings are received 28 - * from the phone, `settings` will *automatically* update to the object and the encapsulating 29 - * Composable will *automatically* re-render. 30 - * 31 - * `refresh` is a function you can call to forcibly re-request the settings from the phone. 32 - */ 33 - @Composable 34 - fun rememberSettings(): Pair<WearSettings?, () -> Unit> { 35 - var rpc = rememberRPCClient() 36 - var settings: WearSettings? by remember { mutableStateOf(null) } 37 - var settingsQueryCount by remember { mutableIntStateOf(0) } 38 - LaunchedEffect(settingsQueryCount) { 39 - async(Dispatchers.Default) { 40 - settings = rpc.querySettings() 41 - } 42 - } 43 - return Pair(settings) { settingsQueryCount++ } 44 - }
-47
wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/tags.kt
··· 1 - /* 2 - * This Source Code Form is subject to the terms of the Mozilla Public 3 - * License, v. 2.0. If a copy of the MPL was not distributed with this 4 - * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 - */ 6 - package com.example.util.simpletimetracker.presentation.remember 7 - 8 - import androidx.compose.runtime.Composable 9 - import androidx.compose.runtime.LaunchedEffect 10 - import androidx.compose.runtime.getValue 11 - import androidx.compose.runtime.mutableIntStateOf 12 - import androidx.compose.runtime.mutableStateOf 13 - import androidx.compose.runtime.remember 14 - import androidx.compose.runtime.setValue 15 - import com.example.util.simpletimetracker.wear_api.WearTag 16 - import kotlinx.coroutines.Dispatchers 17 - import kotlinx.coroutines.async 18 - 19 - /** 20 - * Handles asynchronous retrieval of the available tags for a specific activity in 21 - * Simple Time Tracker on the connected phone. 22 - * 23 - * Usage: 24 - * ``` 25 - * val activityId: Long = /* obtain the ID (e.g. as a parameter) */ 26 - * val (tags, refresh) = rememberTags(activityId) 27 - * ``` 28 - * 29 - * Initially, `tags` will be an empty array. Once the actual tags are received 30 - * from the phone, `tags` will *automatically* update to that array and the encapsulating 31 - * Composable will *automatically* re-render. 32 - * 33 - * `refresh` is a function you can call to forcibly re-request the array of available tags 34 - * from the phone. 35 - */ 36 - @Composable 37 - fun rememberTags(activityId: Long): Pair<List<WearTag>, () -> Unit> { 38 - val rpc = rememberRPCClient() 39 - var tags: List<WearTag> by remember { mutableStateOf(emptyList()) } 40 - var tagsQueryCount by remember { mutableIntStateOf(0) } 41 - LaunchedEffect(tagsQueryCount) { 42 - async(Dispatchers.Default) { 43 - tags = rpc.queryTagsForActivity(activityId) 44 - } 45 - } 46 - return Pair(tags) { tagsQueryCount++ } 47 - }
+21 -55
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt
··· 5 5 */ 6 6 package com.example.util.simpletimetracker.presentation.screens 7 7 8 - import android.util.Log 9 8 import androidx.compose.runtime.Composable 10 - import androidx.compose.runtime.rememberCoroutineScope 9 + import androidx.compose.runtime.LaunchedEffect 10 + import androidx.compose.runtime.collectAsState 11 + import androidx.hilt.navigation.compose.hiltViewModel 11 12 import com.example.util.simpletimetracker.presentation.components.ActivitiesList 12 13 import com.example.util.simpletimetracker.presentation.components.CreditsButton 13 - import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator 14 - import com.example.util.simpletimetracker.presentation.remember.rememberActivities 15 - import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities 16 - import com.example.util.simpletimetracker.presentation.remember.rememberRPCClient 17 - import com.example.util.simpletimetracker.wear_api.WearActivity 18 - import com.example.util.simpletimetracker.presentation.mediators.StartActivityMediator 19 - import kotlinx.coroutines.Dispatchers 20 - import kotlinx.coroutines.launch 14 + import com.example.util.simpletimetracker.presentation.screens.ActivitiesViewModel.Effect 15 + import com.example.util.simpletimetracker.presentation.utils.collectEffects 16 + import kotlinx.coroutines.flow.collect 17 + import kotlinx.coroutines.flow.onEach 21 18 22 19 @Composable 23 20 fun ActivitiesScreen( 24 21 onRequestTagSelection: (activityId: Long) -> Unit, 25 22 onRequestCredits: () -> Unit, 26 23 ) { 27 - val coroutineScope = rememberCoroutineScope() 28 - val rpc = rememberRPCClient() 29 - val (activities, refreshActivities) = rememberActivities() 30 - val (currentActivities, refreshCurrentActivities) = rememberCurrentActivities() 31 - val refresh = { 32 - refreshActivities() 33 - refreshCurrentActivities() 34 - } 35 - val currentActivitiesMediator = CurrentActivitiesMediator(rpc, currentActivities) 36 - val startActivityWithoutTags: (WearActivity) -> Unit = { 37 - Log.d("ActivitiesScreen", "Starting ${it.name} (#${it.id}) without tags") 38 - coroutineScope.launch(Dispatchers.Default) { 39 - currentActivitiesMediator.start(it.id) 40 - refresh() 41 - } 42 - } 43 - val startActivityWithTags: (WearActivity) -> Unit = { 44 - coroutineScope.launch(Dispatchers.Main) { 45 - Log.d("ActivitiesScreen", "Starting ${it.name} (#${it.id}) with tags") 46 - onRequestTagSelection(it.id) 47 - } 48 - } 49 - val stopActivity: (WearActivity) -> Unit = { 50 - Log.d("ActivitiesScreen", "Stopping ${it.name} (#${it.id})") 51 - coroutineScope.launch(Dispatchers.Default) { 52 - currentActivitiesMediator.stop(it.id) 53 - refresh() 54 - } 55 - } 24 + val viewModel = hiltViewModel<ActivitiesViewModel>() 25 + viewModel.init() 26 + val state = viewModel.state.collectAsState() 56 27 57 - val startActivitiesMediator = StartActivityMediator( 58 - rpc, 59 - onRequestStartActivity = startActivityWithoutTags, 60 - onRequestTagSelection = startActivityWithTags, 61 - ) 62 - 28 + viewModel.effects.collectEffects(key = viewModel) { 29 + when (it) { 30 + is Effect.OnRequestTagSelection -> onRequestTagSelection(it.activityId) 31 + } 32 + } 63 33 64 34 ActivitiesList( 65 - activities = activities, 66 - currentActivities = currentActivities, 67 - onSelectActivity = { 68 - coroutineScope.launch(Dispatchers.Default) { 69 - startActivitiesMediator.requestStart(it) 70 - } 71 - }, 72 - onEnableActivity = startActivityWithoutTags, 73 - onDisableActivity = stopActivity, 74 - onRefresh = refresh, 35 + activities = state.value.activities, 36 + currentActivities = state.value.currentActivities, 37 + onSelectActivity = viewModel::onSelectActivity, 38 + onEnableActivity = viewModel::startActivityWithoutTags, 39 + onDisableActivity = viewModel::stopActivity, 40 + onRefresh = viewModel::refresh, 75 41 footer = { CreditsButton(onClick = onRequestCredits) }, 76 42 ) 77 43 }
+91
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesViewModel.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.presentation.screens 7 + 8 + import android.util.Log 9 + import androidx.lifecycle.ViewModel 10 + import androidx.lifecycle.viewModelScope 11 + import com.example.util.simpletimetracker.presentation.data.WearRPCClient 12 + import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator 13 + import com.example.util.simpletimetracker.presentation.mediators.StartActivityMediator 14 + import com.example.util.simpletimetracker.wear_api.WearActivity 15 + import com.example.util.simpletimetracker.wear_api.WearCurrentActivity 16 + import dagger.hilt.android.lifecycle.HiltViewModel 17 + import kotlinx.coroutines.channels.BufferOverflow 18 + import kotlinx.coroutines.flow.MutableSharedFlow 19 + import kotlinx.coroutines.flow.MutableStateFlow 20 + import kotlinx.coroutines.launch 21 + import javax.inject.Inject 22 + 23 + @HiltViewModel 24 + class ActivitiesViewModel @Inject constructor( 25 + private val rpc: WearRPCClient, 26 + private val startActivitiesMediator: StartActivityMediator, 27 + private val currentActivitiesMediator: CurrentActivitiesMediator, 28 + ) : ViewModel() { 29 + 30 + val state = MutableStateFlow(State()) 31 + val effects = MutableSharedFlow<Effect>( 32 + extraBufferCapacity = 1, 33 + onBufferOverflow = BufferOverflow.DROP_OLDEST, 34 + ) 35 + 36 + fun init() { 37 + refresh() 38 + } 39 + 40 + fun startActivityWithoutTags(wearActivity: WearActivity) { 41 + Log.d("ActivitiesScreen", "Starting ${wearActivity.name} (#${wearActivity.id}) without tags") 42 + viewModelScope.launch { 43 + currentActivitiesMediator.start(wearActivity.id) 44 + refresh() 45 + } 46 + } 47 + 48 + fun stopActivity(wearActivity: WearActivity) { 49 + Log.d("ActivitiesScreen", "Stopping ${wearActivity.name} (#${wearActivity.id})") 50 + viewModelScope.launch { 51 + currentActivitiesMediator.stop(wearActivity.id) 52 + refresh() 53 + } 54 + } 55 + 56 + fun onSelectActivity(wearActivity: WearActivity) { 57 + viewModelScope.launch { 58 + startActivitiesMediator.requestStart( 59 + activity = wearActivity, 60 + onRequestStartActivity = ::startActivityWithoutTags, 61 + onRequestTagSelection = ::startActivityWithTags, 62 + ) 63 + } 64 + } 65 + 66 + fun refresh() { 67 + viewModelScope.launch { 68 + val newState = State( 69 + activities = rpc.queryActivities(), 70 + currentActivities = rpc.queryCurrentActivities(), 71 + ) 72 + state.emit(newState) 73 + } 74 + } 75 + 76 + private fun startActivityWithTags(wearActivity: WearActivity) { 77 + viewModelScope.launch { 78 + Log.d("ActivitiesScreen", "Starting ${wearActivity.name} (#${wearActivity.id}) with tags") 79 + effects.emit(Effect.OnRequestTagSelection(wearActivity.id)) 80 + } 81 + } 82 + 83 + data class State( 84 + val activities: List<WearActivity> = emptyList(), 85 + val currentActivities: List<WearCurrentActivity> = emptyList(), 86 + ) 87 + 88 + sealed interface Effect { 89 + data class OnRequestTagSelection(val activityId: Long) : Effect 90 + } 91 + }
+23 -28
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt
··· 6 6 package com.example.util.simpletimetracker.presentation.screens 7 7 8 8 import androidx.compose.runtime.Composable 9 - import androidx.compose.runtime.rememberCoroutineScope 9 + import androidx.compose.runtime.LaunchedEffect 10 + import androidx.compose.runtime.collectAsState 11 + import androidx.hilt.navigation.compose.hiltViewModel 10 12 import com.example.util.simpletimetracker.presentation.components.TagList 11 - import com.example.util.simpletimetracker.presentation.components.TagSelectionMode 12 - import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator 13 - import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities 14 - import com.example.util.simpletimetracker.presentation.remember.rememberRPCClient 15 - import com.example.util.simpletimetracker.presentation.remember.rememberSettings 16 - import com.example.util.simpletimetracker.presentation.remember.rememberTags 17 - import kotlinx.coroutines.Dispatchers 18 - import kotlinx.coroutines.launch 13 + import com.example.util.simpletimetracker.presentation.screens.TagsViewModel.Effect 14 + import com.example.util.simpletimetracker.presentation.utils.collectEffects 15 + import kotlinx.coroutines.flow.collect 16 + import kotlinx.coroutines.flow.onEach 19 17 20 18 @Composable 21 - fun TagsScreen(activityId: Long, onComplete: () -> Unit) { 22 - val coroutineScope = rememberCoroutineScope() 23 - val rpc = rememberRPCClient() 24 - val (settings) = rememberSettings() 25 - val (tags) = rememberTags(activityId) 26 - val (currentActivities) = rememberCurrentActivities() 27 - val currentActivitiesMediator = CurrentActivitiesMediator(rpc, currentActivities) 19 + fun TagsScreen( 20 + activityId: Long, 21 + onComplete: () -> Unit, 22 + ) { 23 + val viewModel = hiltViewModel<TagsViewModel>() 24 + viewModel.init(activityId) 25 + val state = viewModel.state.collectAsState() 26 + 27 + viewModel.effects.collectEffects(key = viewModel) { 28 + when (it) { 29 + is Effect.OnComplete -> onComplete() 30 + } 31 + } 28 32 29 33 TagList( 30 - tags, 31 - mode = if (settings?.recordTagSelectionCloseAfterOne != false) { 32 - TagSelectionMode.SINGLE 33 - } else { 34 - TagSelectionMode.MULTI 35 - }, 36 - onSelectionComplete = { 37 - coroutineScope.launch(Dispatchers.Default) { 38 - currentActivitiesMediator.start(activityId, it) 39 - coroutineScope.launch(Dispatchers.Main) { onComplete() } 40 - } 41 - }, 34 + tags = state.value.tags, 35 + mode = state.value.mode, 36 + onSelectionComplete = { viewModel.onSelectionComplete(it) }, 42 37 ) 43 38 }
+80
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsViewModel.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.presentation.screens 7 + 8 + import androidx.lifecycle.ViewModel 9 + import androidx.lifecycle.viewModelScope 10 + import com.example.util.simpletimetracker.presentation.components.TagSelectionMode 11 + import com.example.util.simpletimetracker.presentation.data.WearRPCClient 12 + import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator 13 + import com.example.util.simpletimetracker.wear_api.WearSettings 14 + import com.example.util.simpletimetracker.wear_api.WearTag 15 + import dagger.hilt.android.lifecycle.HiltViewModel 16 + import kotlinx.coroutines.channels.BufferOverflow 17 + import kotlinx.coroutines.flow.MutableSharedFlow 18 + import kotlinx.coroutines.flow.MutableStateFlow 19 + import kotlinx.coroutines.launch 20 + import javax.inject.Inject 21 + 22 + @HiltViewModel 23 + class TagsViewModel @Inject constructor( 24 + private val rpc: WearRPCClient, 25 + private val currentActivitiesMediator: CurrentActivitiesMediator, 26 + ) : ViewModel() { 27 + 28 + val state = MutableStateFlow(State()) 29 + val effects = MutableSharedFlow<Effect>( 30 + extraBufferCapacity = 1, 31 + onBufferOverflow = BufferOverflow.DROP_OLDEST, 32 + ) 33 + 34 + private var activityId: Long? = null 35 + 36 + fun init(activityId: Long) { 37 + this.activityId = activityId 38 + refresh() 39 + } 40 + 41 + fun onSelectionComplete(tags: List<WearTag>) { 42 + viewModelScope.launch { 43 + val activityId = this@TagsViewModel.activityId ?: return@launch 44 + currentActivitiesMediator.start(activityId, tags) 45 + effects.emit(Effect.OnComplete) 46 + } 47 + } 48 + 49 + private fun refresh() { 50 + viewModelScope.launch { 51 + val activityId = this@TagsViewModel.activityId ?: return@launch 52 + val settings = rpc.querySettings() 53 + val newState = State( 54 + tags = rpc.queryTagsForActivity(activityId), 55 + mode = if (settings.recordTagSelectionCloseAfterOne) { 56 + TagSelectionMode.SINGLE 57 + } else { 58 + TagSelectionMode.MULTI 59 + }, 60 + settings = settings, 61 + ) 62 + state.emit(newState) 63 + } 64 + } 65 + 66 + data class State( 67 + val tags: List<WearTag> = emptyList(), 68 + val mode: TagSelectionMode = TagSelectionMode.SINGLE, 69 + val settings: WearSettings = WearSettings( 70 + allowMultitasking = false, 71 + showRecordTagSelection = false, 72 + recordTagSelectionCloseAfterOne = false, 73 + recordTagSelectionEvenForGeneralTags = false, 74 + ), 75 + ) 76 + 77 + sealed interface Effect { 78 + object OnComplete : Effect 79 + } 80 + }
+24
wear/src/main/java/com/example/util/simpletimetracker/presentation/utils/Extensions.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.presentation.utils 7 + 8 + import android.annotation.SuppressLint 9 + import androidx.compose.runtime.Composable 10 + import androidx.compose.runtime.LaunchedEffect 11 + import kotlinx.coroutines.flow.Flow 12 + import kotlinx.coroutines.flow.collect 13 + import kotlinx.coroutines.flow.onEach 14 + 15 + @SuppressLint("ComposableNaming") 16 + @Composable 17 + fun <T> Flow<T>.collectEffects( 18 + key: Any?, 19 + action: (T) -> Unit, 20 + ) { 21 + LaunchedEffect(key) { 22 + onEach { action(it) }.collect() 23 + } 24 + }