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.

feat: Create WearRPCClient in WearOS

Creates an MVP driver for querying live data from the Mobile phone.

The previous UI has been hidden, though not deleted since much of the code will likely be reused in the new implementation.

Joseph Hale (Feb 26, 2024, 10:23 PM -0700) 1bd90d4a a983e145

+179 -178
-71
wear/build.gradle
··· 1 - plugins { 2 - id 'com.android.application' 3 - id 'org.jetbrains.kotlin.android' 4 - } 5 - 6 - android { 7 - namespace 'com.example.util.simpletimetracker' 8 - compileSdk 34 9 - 10 - defaultConfig { 11 - applicationId "com.razeeman.util.simpletimetracker" 12 - minSdk 26 13 - targetSdk 34 14 - versionCode 1 15 - versionName "1.0" 16 - vectorDrawables { 17 - useSupportLibrary true 18 - } 19 - 20 - } 21 - 22 - buildTypes { 23 - release { 24 - minifyEnabled false 25 - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 - } 27 - } 28 - compileOptions { 29 - sourceCompatibility JavaVersion.VERSION_1_8 30 - targetCompatibility JavaVersion.VERSION_1_8 31 - } 32 - kotlinOptions { 33 - jvmTarget = '1.8' 34 - } 35 - buildFeatures { 36 - compose true 37 - } 38 - composeOptions { 39 - kotlinCompilerExtensionVersion '1.4.0-alpha02' 40 - } 41 - packagingOptions { 42 - resources { 43 - excludes += '/META-INF/{AL2.0,LGPL2.1}' 44 - } 45 - } 46 - } 47 - 48 - dependencies { 49 - var compose_version = "1.3.1" 50 - var wear_compose_version = "1.1.0" 51 - // Custom Dependencies 52 - implementation 'com.google.android.gms:play-services-wearable:18.0.0' 53 - implementation 'com.google.android.horologist:horologist-compose-layout:0.2.7' 54 - 55 - // Default Dependencies 56 - implementation 'androidx.core:core-ktx:1.7.0' 57 - implementation 'com.google.android.gms:play-services-wearable:17.1.0' 58 - implementation 'androidx.percentlayout:percentlayout:1.0.0' 59 - implementation 'androidx.legacy:legacy-support-v4:1.0.0' 60 - implementation 'androidx.recyclerview:recyclerview:1.2.1' 61 - implementation "androidx.compose.ui:ui:$compose_version" 62 - implementation "androidx.wear.compose:compose-material:$wear_compose_version" 63 - implementation "androidx.wear.compose:compose-foundation:$wear_compose_version" 64 - implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 65 - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 66 - implementation 'androidx.activity:activity-compose:1.3.1' 67 - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 68 - debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 69 - debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" 70 - wearApp project(":wear") 71 - }
+77
wear/build.gradle.kts
··· 1 + plugins { 2 + id("com.android.application") 3 + id("kotlin-android") 4 + } 5 + 6 + android { 7 + namespace = "com.example.util.simpletimetracker" 8 + compileSdk = 34 9 + 10 + defaultConfig { 11 + applicationId = "com.razeeman.util.simpletimetracker" 12 + minSdk = 26 13 + targetSdk = 34 14 + versionCode = 1 15 + versionName = "1.0" 16 + vectorDrawables { 17 + useSupportLibrary = true 18 + } 19 + 20 + } 21 + 22 + buildTypes { 23 + getByName("debug") { 24 + applicationIdSuffix = ".debug" 25 + } 26 + release { 27 + isMinifyEnabled = false 28 + proguardFiles( 29 + getDefaultProguardFile("proguard-android-optimize.txt"), 30 + "proguard-rules.pro" 31 + ) 32 + } 33 + } 34 + compileOptions { 35 + sourceCompatibility = JavaVersion.VERSION_1_8 36 + targetCompatibility = JavaVersion.VERSION_1_8 37 + } 38 + kotlinOptions { 39 + jvmTarget = "1.8" 40 + } 41 + buildFeatures { 42 + compose = true 43 + } 44 + composeOptions { 45 + kotlinCompilerExtensionVersion = "1.4.0-alpha02" 46 + } 47 + packagingOptions { 48 + resources { 49 + excludes += "/META-INF/{AL2.0,LGPL2.1}" 50 + } 51 + } 52 + } 53 + 54 + dependencies { 55 + var compose_version = "1.3.1" 56 + var wear_compose_version = "1.1.0" 57 + // Custom Dependencies 58 + implementation("com.google.android.horologist:horologist-compose-layout:0.2.7") 59 + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") 60 + implementation(project(":wearrpc")) 61 + 62 + // Default Dependencies 63 + implementation("androidx.core:core-ktx:1.7.0") 64 + implementation("com.google.android.gms:play-services-wearable:17.1.0") 65 + implementation("androidx.percentlayout:percentlayout:1.0.0") 66 + implementation("androidx.legacy:legacy-support-v4:1.0.0") 67 + implementation("androidx.recyclerview:recyclerview:1.2.1") 68 + implementation("androidx.compose.ui:ui:$compose_version") 69 + implementation("androidx.wear.compose:compose-material:$wear_compose_version") 70 + implementation("androidx.wear.compose:compose-foundation:$wear_compose_version") 71 + implementation("androidx.compose.ui:ui-tooling-preview:$compose_version") 72 + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") 73 + implementation("androidx.activity:activity-compose:1.3.1") 74 + androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") 75 + debugImplementation("androidx.compose.ui:ui-tooling:$compose_version") 76 + debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version") 77 + }
+1 -1
wear/proguard-rules.pro
··· 1 1 # Add project specific ProGuard rules here. 2 2 # You can control the set of applied configuration files using the 3 - # proguardFiles setting in build.gradle. 3 + # proguardFiles setting in build.gradle.kts. 4 4 # 5 5 # For more details, see 6 6 # http://developer.android.com/guide/developing/tools/proguard.html
-81
wear/src/main/java/com/example/util/simpletimetracker/data/Messaging.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.data 7 - 8 - import android.content.Context 9 - import android.util.Log 10 - import com.google.android.gms.tasks.Task 11 - import com.google.android.gms.tasks.Tasks 12 - import com.google.android.gms.wearable.CapabilityClient 13 - import com.google.android.gms.wearable.CapabilityInfo 14 - import com.google.android.gms.wearable.Wearable 15 - import com.example.util.simpletimetracker.presentation.LOG_TAG 16 - 17 - const val START_TIME_TRACKING_ACTIVITY_CAPABILITY_NAME = "start_time_tracking_activity" 18 - 19 - class Messaging { 20 - fun startTimeTracking( 21 - context: Context, 22 - activity: String, 23 - tag: String, 24 - ) { 25 - Thread( 26 - Runnable { 27 - startTimeTrackingTask(context, activity, tag) 28 - }, 29 - ).start() 30 - } 31 - 32 - private fun startTimeTrackingTask( 33 - context: Context, 34 - activity: String, 35 - tag: String, 36 - ) { 37 - // Find all nodes which support the time tracking message 38 - val capabilityInfo: CapabilityInfo = 39 - Tasks.await( 40 - Wearable.getCapabilityClient(context) 41 - .getCapability( 42 - START_TIME_TRACKING_ACTIVITY_CAPABILITY_NAME, 43 - CapabilityClient.FILTER_REACHABLE, 44 - ), 45 - ) 46 - 47 - // Choose the best node (the closest one connected to the watch 48 - val nodes = capabilityInfo.nodes 49 - val bestNode = nodes.firstOrNull { it.isNearby }?.id ?: nodes.firstOrNull()?.id 50 - 51 - // Send the message 52 - val message = "$activity|$tag" 53 - bestNode?.also { nodeId -> 54 - Log.i(LOG_TAG, "Sending message to $bestNode") 55 - val sendTask: Task<*> = 56 - Wearable.getMessageClient(context).sendMessage( 57 - nodeId, 58 - "/$START_TIME_TRACKING_ACTIVITY_CAPABILITY_NAME", 59 - message.toByteArray(), 60 - ).apply { 61 - addOnSuccessListener { 62 - Log.i( 63 - LOG_TAG, 64 - "Sent $START_TIME_TRACKING_ACTIVITY_CAPABILITY_NAME message: $message", 65 - ) 66 - } 67 - addOnFailureListener { 68 - Log.e( 69 - LOG_TAG, 70 - "Failed to send $START_TIME_TRACKING_ACTIVITY_CAPABILITY_NAME message: $message", 71 - ) 72 - } 73 - } 74 - } ?: run { 75 - Log.e( 76 - LOG_TAG, 77 - "No nodes found with the capability $START_TIME_TRACKING_ACTIVITY_CAPABILITY_NAME", 78 - ) 79 - } 80 - } 81 - }
+101 -25
wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt
··· 7 7 8 8 import android.content.Context 9 9 import android.os.Bundle 10 + import android.util.Log 10 11 import androidx.activity.ComponentActivity 11 12 import androidx.activity.compose.setContent 12 13 import androidx.compose.foundation.background ··· 25 26 import com.google.android.horologist.compose.navscaffold.ExperimentalHorologistComposeLayoutApi 26 27 import com.google.android.horologist.compose.rotaryinput.* 27 28 import com.example.util.simpletimetracker.R 28 - import com.example.util.simpletimetracker.data.Messaging 29 29 import com.example.util.simpletimetracker.data.getTimeTrackingActivities 30 30 import com.example.util.simpletimetracker.presentation.theme.SimpleTimeTrackerForWearOSTheme 31 + import com.example.util.simpletimetracker.wearrpc.Messenger 32 + import com.example.util.simpletimetracker.wearrpc.WearRPCClient 33 + //import com.example.util.simpletimetracker.wearrpc.CurrentActivity 34 + import kotlinx.coroutines.* 35 + //import java.util.Date 31 36 32 37 const val LOG_TAG = "com.razeeman.util.simpletimetracker" 33 38 ··· 42 47 43 48 @Composable 44 49 fun WearApp() { 50 + val context = LocalContext.current 51 + val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) 45 52 SimpleTimeTrackerForWearOSTheme { 46 53 val scrollState = rememberScalingLazyListState() 47 54 Scaffold( ··· 55 62 PositionIndicator(scalingLazyListState = scrollState) 56 63 }, 57 64 ) { 58 - ActivityList(scrollState) 65 + ScalingLazyColumn ( 66 + modifier = Modifier.fillMaxSize(), 67 + autoCentering = AutoCenteringParams(itemIndex = 0) 68 + ){ 69 + item { 70 + DebugButton( 71 + label = "Ping", 72 + onClick = { 73 + Log.i(LOG_TAG, "Pinging mobile ...") 74 + scope.async { 75 + val response = WearRPCClient(Messenger(context)).ping("Ping button") 76 + Log.i(LOG_TAG, response) 77 + } 78 + } 79 + ) 80 + } 81 + item { 82 + DebugButton(label="Query Activities", onClick = { 83 + Log.i(LOG_TAG, "Querying activities ...") 84 + scope.async { 85 + val response = WearRPCClient(Messenger(context)).queryActivities() 86 + Log.i(LOG_TAG, "Received ${response.size} activities") 87 + for (activity in response) { 88 + Log.i(LOG_TAG, " - ${activity.id}: ${activity.name}") 89 + } 90 + } 91 + }) 92 + } 93 + } 94 + 95 + // ActivityList(scrollState) 59 96 } 97 + } 98 + } 99 + 100 + @Composable 101 + fun DebugButton(label: String, onClick: () -> Unit) { 102 + Chip( 103 + modifier = Modifier 104 + .fillMaxWidth(0.9f) 105 + .padding(top = 10.dp), 106 + label = { Text(label) }, 107 + onClick = onClick, 108 + ) 109 + } 110 + 111 + @OptIn(ExperimentalHorologistComposeLayoutApi::class) 112 + @Composable 113 + fun ScrollingColumn(scrollState: ScalingLazyListState = rememberScalingLazyListState(), content: @Composable () -> Unit) { 114 + val focusRequester = rememberActiveFocusRequester() 115 + val rotaryHapticFeedback = rememberRotaryHapticFeedback() 116 + ScalingLazyColumn( 117 + modifier = 118 + Modifier 119 + .rotaryWithScroll(focusRequester, scrollState, rotaryHapticFeedback) 120 + .fillMaxSize() 121 + .background(MaterialTheme.colors.background) 122 + .selectableGroup(), 123 + contentPadding = PaddingValues(10.dp), 124 + verticalArrangement = Arrangement.Center, 125 + state = scrollState, 126 + ) { 127 + content 60 128 } 61 129 } 62 130 ··· 68 136 val rotaryHapticFeedback = rememberRotaryHapticFeedback() 69 137 ScalingLazyColumn( 70 138 modifier = 71 - Modifier 72 - .rotaryWithScroll(focusRequester, scrollState, rotaryHapticFeedback) 73 - .fillMaxSize() 74 - .background(MaterialTheme.colors.background) 75 - .selectableGroup(), 139 + Modifier 140 + .rotaryWithScroll(focusRequester, scrollState, rotaryHapticFeedback) 141 + .fillMaxSize() 142 + .background(MaterialTheme.colors.background) 143 + .selectableGroup(), 76 144 contentPadding = PaddingValues(10.dp), 77 145 verticalArrangement = Arrangement.Center, 78 146 state = scrollState, ··· 125 193 icon: Int = R.drawable.baseline_question_mark_24, 126 194 ) { 127 195 val context = LocalContext.current 196 + val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) 128 197 Chip( 129 198 modifier = 130 - Modifier 131 - .fillMaxWidth(0.9f) 132 - .padding(top = 10.dp), 199 + Modifier 200 + .fillMaxWidth(0.9f) 201 + .padding(top = 10.dp), 133 202 icon = { 134 203 ActivityIcon(iconId = icon) 135 204 }, ··· 151 220 ChipDefaults.chipColors( 152 221 backgroundColor = color, 153 222 ), 154 - onClick = { startTimeTracking(context, name, tag) }, 223 + onClick = { startTimeTracking(scope, context) }, 155 224 ) 156 225 } 157 226 ··· 162 231 icon: Int = R.drawable.baseline_question_mark_24, 163 232 ) { 164 233 val context = LocalContext.current 234 + val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) 165 235 Chip( 166 236 modifier = 167 - Modifier 168 - .fillMaxWidth(0.9f) 169 - .padding(top = 10.dp), 237 + Modifier 238 + .fillMaxWidth(0.9f) 239 + .padding(top = 10.dp), 170 240 icon = { 171 241 ActivityIcon(iconId = icon) 172 242 }, ··· 181 251 ChipDefaults.chipColors( 182 252 backgroundColor = color, 183 253 ), 184 - onClick = { startTimeTracking(context, name, "") }, 254 + onClick = { startTimeTracking(scope, context) }, 185 255 ) 186 256 } 187 257 ··· 191 261 painter = painterResource(id = iconId), 192 262 contentDescription = "activity icon", 193 263 modifier = 194 - Modifier 195 - .size(ChipDefaults.IconSize) 196 - .wrapContentSize(align = Alignment.Center), 264 + Modifier 265 + .size(ChipDefaults.IconSize) 266 + .wrapContentSize(align = Alignment.Center), 197 267 ) 198 268 } 199 269 200 - fun startTimeTracking( 201 - context: Context, 202 - activity: String, 203 - tag: String, 204 - ) { 205 - Messaging().startTimeTracking(context, activity, tag) 206 - } 270 + fun startTimeTracking(scope: CoroutineScope, context: Context) { 271 + // val activities = arrayOf( 272 + // CurrentActivity( 273 + // id, 274 + // name, 275 + // startedAt = Date(), 276 + // arrayOf(/* tags */), 277 + // ) 278 + // ) 279 + // scope.async { 280 + // WearRPCClient(context).setCurrentActivities(activities) 281 + // } 282 + }