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: Implement most Wear <-> Mobile methods

This commit completes full, end-to-end implementations for
nearly all RPC methods.

The WearRPCClient and WearRPCServer are effectively complete
(all that's left is some error handling that might become needed).

As a result, all that's left is the DomainAPI

Joseph Hale (Feb 26, 2024, 10:23 PM -0700) 4673dd17 c99ee595

+301 -154
+8 -5
app/src/main/java/com/example/util/simpletimetracker/wear/DomainAPI.kt
··· 34 34 } 35 35 36 36 override suspend fun queryCurrentActivities(): Array<CurrentActivity> { 37 - TODO("Not yet implemented") 38 - // 1. Pull the current activities from the runningRecordInteractor 39 - // 2. Return as an `Array<CurrentActivity>` 37 + return runningRecordInteractor.getAll().map { 38 + CurrentActivity( 39 + it.id, 40 + it.timeStarted, 41 + arrayOf() // TODO - Pull actual list of active tags 42 + ) 43 + }.toTypedArray() 40 44 } 41 45 42 46 override suspend fun setCurrentActivities(activities: Array<CurrentActivity>) { ··· 62 66 } 63 67 64 68 override suspend fun querySettings(): Settings { 65 - TODO("Not yet implemented") 66 - // Obtain the desired Settings from the prefsInteractor and return them. 69 + return Settings(prefsInteractor.getAllowMultitasking()) 67 70 } 68 71 69 72 }
+2 -2
wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt
··· 48 48 import androidx.wear.compose.material.scrollAway 49 49 import com.example.util.simpletimetracker.presentation.theme.SimpleTimeTrackerForWearOSTheme 50 50 import com.example.util.simpletimetracker.wearrpc.Activity 51 - import com.example.util.simpletimetracker.wearrpc.Messenger 51 + import com.example.util.simpletimetracker.wearrpc.ContextMessenger 52 52 import com.example.util.simpletimetracker.wearrpc.WearRPCClient 53 53 import com.google.android.horologist.compose.focus.rememberActiveFocusRequester 54 54 import com.google.android.horologist.compose.navscaffold.ExperimentalHorologistComposeLayoutApi ··· 97 97 key1 = queryCount, 98 98 block = { 99 99 async(Dispatchers.Default) { 100 - activities = WearRPCClient(Messenger(context)).queryActivities() 100 + activities = WearRPCClient(ContextMessenger(context)).queryActivities() 101 101 } 102 102 }, 103 103 )
+1 -3
wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/DTO.kt
··· 15 15 16 16 data class Activity(val id: Long, val name: String, val icon: String, val color: String) 17 17 18 - data class CurrentActivity(val id: Long, val name: String, val startedAt: Date, val tags: Array<Tag>) { 18 + data class CurrentActivity(val id: Long, val startedAt: Long, val tags: Array<Tag>) { 19 19 override fun equals(other: Any?): Boolean { 20 20 // autogenerated 21 21 if (this === other) return true ··· 24 24 other as CurrentActivity 25 25 26 26 if (id != other.id) return false 27 - if (name != other.name) return false 28 27 if (startedAt != other.startedAt) return false 29 28 return tags.contentEquals(other.tags) 30 29 } ··· 32 31 override fun hashCode(): Int { 33 32 // autogenerated 34 33 var result = id.toInt() 35 - result = 31 * result + name.hashCode() 36 34 result = 31 * result + startedAt.hashCode() 37 35 result = 31 * result + tags.contentHashCode() 38 36 return result
+9 -6
wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/Messenger.kt
··· 15 15 import kotlinx.coroutines.CompletableDeferred 16 16 import java.util.concurrent.CancellationException 17 17 18 - class Messenger(private val context: Context) { 19 - private val TAG: String = Messenger::class.java.name 20 - 21 - suspend fun sendMessage(capability: String): ByteArray? { 22 - return sendMessage(capability, ByteArray(0)) 18 + interface Messenger { 19 + suspend fun send(capability: String): ByteArray? { 20 + return this.send(capability, ByteArray(0)) 23 21 } 22 + suspend fun send(capability: String, message: ByteArray): ByteArray? 23 + } 24 24 25 - suspend fun sendMessage(capability: String, message: ByteArray): ByteArray? { 25 + class ContextMessenger(private val context: Context): Messenger { 26 + private val TAG: String = ContextMessenger::class.java.name 27 + 28 + override suspend fun send(capability: String, message: ByteArray): ByteArray? { 26 29 val def = CompletableDeferred<ByteArray?>() 27 30 val bestNode = findNearestNode(capability) 28 31
+22 -20
wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCClient.kt
··· 5 5 */ 6 6 package com.example.util.simpletimetracker.wearrpc 7 7 8 - import android.util.Log 9 8 import com.google.gson.Gson 10 9 import com.google.gson.reflect.TypeToken 11 10 12 11 13 - class WearRPCClient(private val messenger: Messenger): SimpleTimeTrackerAPI { 14 - private val TAG: String = WearRPCClient::class.java.name 12 + class WearRPCClient(private val messenger: Messenger) : SimpleTimeTrackerAPI { 15 13 16 14 override suspend fun ping(message: String): String { 17 - val response = messenger.sendMessage(Request.PING, message.toByteArray()) 15 + val response = messenger.send(Request.PING, message.toByteArray()) 18 16 if (response != null) return String(response) 19 17 else throw WearRPCException("No response") 20 18 } 21 19 22 20 override suspend fun queryActivities(): Array<Activity> { 23 - val response = messenger.sendMessage(Request.QUERY_ACTIVITIES) 21 + val response = messenger.send(Request.QUERY_ACTIVITIES) 24 22 if (response != null) { 25 - Log.i(TAG, String(response)) 26 23 val collectionType = object : TypeToken<Array<Activity>>() {}.type 27 24 return Gson().fromJson(String(response), collectionType) 28 25 } else throw WearRPCException("No response") 29 26 } 30 27 31 28 override suspend fun queryCurrentActivities(): Array<CurrentActivity> { 32 - TODO("Not yet implemented") 33 - // 1. Send a Request.QUERY_CURRENT_ACTIVITIES message 34 - // 2. Parse the resulting JSON bytes and return it 29 + val response = messenger.send(Request.QUERY_CURRENT_ACTIVITIES) 30 + if (response != null) { 31 + val collectionType = object : TypeToken<Array<CurrentActivity>>() {}.type 32 + return Gson().fromJson(String(response), collectionType) 33 + } else throw WearRPCException("No response") 35 34 } 36 35 37 36 override suspend fun setCurrentActivities(activities: Array<CurrentActivity>) { 38 - TODO("Not yet implemented") 39 - // 1. Serialize the given activities to JSON 40 - // 2. Send a Request.SET_CURRENT_ACTIVITIES message 41 - // 3. Wait for response (successful/error) 37 + messenger.send(Request.SET_CURRENT_ACTIVITIES, Gson().toJson(activities).toByteArray()) 42 38 } 43 39 44 40 override suspend fun queryTagsForActivity(activityId: Long): Array<Tag> { 45 - TODO("Not yet implemented") 46 - // 1. Serialize the given activityID to bytes 47 - // 2. Send a Request.QUERY_TAGS_FOR_ACTIVITY message 48 - // 3. Deserialize the JSON response and return. 41 + val response = messenger.send( 42 + Request.QUERY_TAGS_FOR_ACTIVITY, 43 + Gson().toJson(activityId).toByteArray(), 44 + ) 45 + if (response != null) { 46 + val collectionType = object : TypeToken<Array<Tag>>() {}.type 47 + return Gson().fromJson(String(response), collectionType) 48 + } else throw WearRPCException("No response") 49 49 } 50 50 51 51 override suspend fun querySettings(): Settings { 52 - TODO("Not yet implemented") 53 - // 1. Send a Request.QUERY_SETTINGS message 54 - // 2. Deserialize the JSON response and return 52 + val response = messenger.send(Request.QUERY_SETTINGS) 53 + if (response != null) { 54 + val jsonType = object : TypeToken<Settings>() {}.type 55 + return Gson().fromJson(String(response), jsonType) 56 + } else throw WearRPCException("No response") 55 57 } 56 58 }
+8 -11
wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCServer.kt
··· 6 6 package com.example.util.simpletimetracker.wearrpc 7 7 8 8 import com.google.gson.Gson 9 + import com.google.gson.reflect.TypeToken 9 10 10 11 11 12 class WearRPCServer(private val api: SimpleTimeTrackerAPI) { ··· 25 26 } 26 27 27 28 private suspend fun onQueryTagsForActivity(request: ByteArray): ByteArray? { 28 - TODO("Not yet implemented") 29 - // 1. Parse the activity id (Long) from the request (throw an exception if not there) 30 - // 2. Ask this.api for the associated tags 31 - // 3. JSON-serialize the tag list into a ByteArray to return as a response. 29 + val activityId = Gson().fromJson(String(request), Long::class.java) 30 + return Gson().toJson(api.queryTagsForActivity(activityId)).toByteArray() 32 31 } 33 32 34 33 private suspend fun onSetCurrentActivities(request: ByteArray): ByteArray? { 35 - TODO("Not yet implemented") 36 - // 1. Deserialize the JSON CurrentActivity records from the request 37 - // 2. Delegate to this.api 38 - // 3. Return a success message (let unresolvable exceptions from this.api bubble up) 34 + val collectionType = object : TypeToken<Array<CurrentActivity>>() {}.type 35 + val activities: Array<CurrentActivity> = Gson().fromJson(String(request), collectionType) 36 + api.setCurrentActivities(activities) 37 + return ByteArray(0) 39 38 } 40 39 41 40 private suspend fun onPing(request: ByteArray): ByteArray? { ··· 51 50 } 52 51 53 52 private suspend fun onQuerySettings(): ByteArray? { 54 - TODO("Not yet implemented") 55 - // 1. Obtain the settings from this.api 56 - // 2. Serialize to JSON and return the bytes 53 + return Gson().toJson(api.querySettings()).toByteArray() 57 54 } 58 55 }
-107
wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCServerTest.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.wearrpc 7 - 8 - import org.junit.Test 9 - import org.junit.Assert.* 10 - import org.junit.Before 11 - import kotlinx.coroutines.test.runTest 12 - 13 - 14 - open class WearRPCServerTestBase { 15 - lateinit var api: MockSimpleTimeTrackerAPI 16 - lateinit var rpc: WearRPCServer 17 - 18 - 19 - @Before fun setup() { 20 - api = MockSimpleTimeTrackerAPI() 21 - rpc = WearRPCServer(api) 22 - api.mockReset() 23 - } 24 - 25 - fun assertResponseEquals(expected: String, response: ByteArray?) { 26 - if (response != null) assertEquals(expected, String(response)) 27 - else fail("response was unexpectedly null") 28 - } 29 - } 30 - 31 - class WearRPCServerTest: WearRPCServerTestBase() { 32 - @Test fun returns_null_for_unsupported_request() = runTest { 33 - val response = rpc.onRequest("/fake/path", "fake data".toByteArray()) 34 - assertNull(response) 35 - } 36 - 37 - @Test(expected = WearRPCException::class) 38 - fun raises_for_invalid_SimpleTimeTracker_request() = runTest { 39 - rpc.onRequest("/stt//GET/fake/path", "fake data".toByteArray()) 40 - } 41 - } 42 - 43 - class PingEndpointTest: WearRPCServerTestBase() { 44 - 45 - @Test fun responds_to_empty_request_with_empty_string() = runTest { 46 - val response = rpc.onRequest(Request.PING, ByteArray(0)) 47 - assertResponseEquals("", response) 48 - } 49 - 50 - @Test fun echoes_response() = runTest { 51 - val response = rpc.onRequest(Request.PING, "Hello World!".toByteArray()) 52 - assertResponseEquals("Hello World!", response) 53 - } 54 - } 55 - 56 - class GetActivitiesTest: WearRPCServerTestBase() { 57 - @Test fun returns_no_activities_when_none_are_available() = runTest { 58 - val response = rpc.onRequest(Request.QUERY_ACTIVITIES, ByteArray(0)) 59 - assertResponseEquals("[]", response) 60 - } 61 - 62 - @Test fun returns_one_activity_when_one_exists() = runTest { 63 - api.mock_queryActivities(arrayOf(Activity(42, "Chores"))) 64 - val response = rpc.onRequest(Request.QUERY_ACTIVITIES, ByteArray(0)) 65 - assertResponseEquals("[{\"id\":42,\"name\":\"Chores\"}]", response) 66 - } 67 - 68 - @Test fun returns_all_existing_activities() = runTest { 69 - api.mock_queryActivities(arrayOf(Activity(13, "Singing"), Activity(24, "Homework"))) 70 - val response = rpc.onRequest(Request.QUERY_ACTIVITIES, ByteArray(0)) 71 - assertResponseEquals("[{\"id\":13,\"name\":\"Singing\"},{\"id\":24,\"name\":\"Homework\"}]", response) 72 - } 73 - } 74 - 75 - 76 - class MockSimpleTimeTrackerAPI: SimpleTimeTrackerAPI { 77 - 78 - lateinit var activities: Array<Activity> 79 - override suspend fun queryActivities(): Array<Activity> { 80 - return activities 81 - } 82 - 83 - fun mock_queryActivities(activities: Array<Activity>) { 84 - this.activities = activities 85 - } 86 - 87 - override suspend fun queryCurrentActivities(): Array<CurrentActivity> { 88 - TODO("Not yet implemented") 89 - } 90 - 91 - override suspend fun setCurrentActivities(activities: Array<CurrentActivity>): Unit { 92 - TODO("Not yet implemented") 93 - } 94 - 95 - override suspend fun queryTagsForActivity(activityId: Long): Array<Tag> { 96 - TODO("Not yet implemented") 97 - } 98 - 99 - override suspend fun querySettings(): Settings { 100 - TODO("Not yet implemented") 101 - } 102 - 103 - fun mockReset() { 104 - this.activities = arrayOf() 105 - } 106 - 107 - }
+251
wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.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.wearrpc 7 + 8 + import kotlinx.coroutines.test.runTest 9 + import org.junit.Assert.assertArrayEquals 10 + import org.junit.Assert.assertEquals 11 + import org.junit.Assert.assertFalse 12 + import org.junit.Assert.assertNull 13 + import org.junit.Assert.assertTrue 14 + import org.junit.Before 15 + import org.junit.Test 16 + 17 + 18 + open class WearRPCServerTestBase { 19 + lateinit var api: MockSimpleTimeTrackerAPI 20 + lateinit var rpc: WearRPCServer 21 + lateinit var messenger: Messenger 22 + lateinit var client: WearRPCClient 23 + 24 + @Before 25 + fun setup() { 26 + api = MockSimpleTimeTrackerAPI() 27 + rpc = WearRPCServer(api) 28 + messenger = MockMessenger(rpc) 29 + client = WearRPCClient(messenger) 30 + api.mockReset() 31 + } 32 + } 33 + 34 + class WearRPCServerTest : WearRPCServerTestBase() { 35 + @Test 36 + fun returns_null_for_unsupported_request() = runTest { 37 + val response = rpc.onRequest("/fake/path", "fake data".toByteArray()) 38 + assertNull(response) 39 + } 40 + 41 + @Test(expected = WearRPCException::class) 42 + fun raises_for_invalid_SimpleTimeTracker_request() = runTest { 43 + rpc.onRequest("/stt//GET/fake/path", "fake data".toByteArray()) 44 + } 45 + } 46 + 47 + class PingTest : WearRPCServerTestBase() { 48 + 49 + @Test 50 + fun responds_to_empty_request_with_empty_string() = runTest { 51 + val response = client.ping("") 52 + assertEquals("", response) 53 + } 54 + 55 + @Test 56 + fun echoes_response() = runTest { 57 + val response = client.ping("Hello World!") 58 + assertEquals("Hello World!", response) 59 + } 60 + } 61 + 62 + class GetActivitiesTest : WearRPCServerTestBase() { 63 + @Test 64 + fun returns_no_activities_when_none_are_available() = runTest { 65 + val response = client.queryActivities() 66 + assertArrayEquals(arrayOf<Activity>(), response) 67 + } 68 + 69 + @Test 70 + fun returns_one_activity_when_one_exists() = runTest { 71 + val activities = arrayOf(Activity(42, "Chores", "🎉", "#00FF00")) 72 + api.mock_queryActivities(activities) 73 + val response = client.queryActivities() 74 + assertArrayEquals(activities, response) 75 + } 76 + 77 + @Test 78 + fun returns_all_existing_activities() = runTest { 79 + val activities = arrayOf( 80 + Activity(13, "Singing", "🎶", "#123456"), 81 + Activity(24, "Homework", "📝", "#ABCDEF"), 82 + ) 83 + api.mock_queryActivities(activities) 84 + val response = client.queryActivities() 85 + assertArrayEquals(activities, response) 86 + } 87 + } 88 + 89 + class QueryCurrentActivitiesTest : WearRPCServerTestBase() { 90 + @Test 91 + fun returns_no_activities_when_none_are_running() = runTest { 92 + val activities = arrayOf<CurrentActivity>() 93 + val response = client.queryCurrentActivities() 94 + assertArrayEquals(activities, response) 95 + } 96 + @Test 97 + fun returns_one_activity_when_one_exists() = runTest { 98 + val jan_31_2024_afternoon = 1706704801L 99 + val activities = arrayOf( 100 + CurrentActivity( 101 + 42, 102 + "Activities", 103 + jan_31_2024_afternoon, 104 + arrayOf(Tag(1, "Friends"), Tag(2, "Family")), 105 + ), 106 + ) 107 + api.mock_queryCurrentActivities(activities) 108 + val response = client.queryCurrentActivities() 109 + assertArrayEquals(activities, response) 110 + } 111 + 112 + @Test 113 + fun returns_all_running_activities() = runTest { 114 + val jan_31_2024_afternoon = 1706704801L 115 + val jan_31_2024_evening = 1706751601L 116 + val activities = arrayOf( 117 + CurrentActivity( 118 + 42, 119 + "Activities", 120 + jan_31_2024_afternoon, 121 + arrayOf(Tag(1, "Friends"), Tag(2, "Family")), 122 + ), 123 + CurrentActivity( 124 + 42, 125 + "Chores", 126 + jan_31_2024_evening, 127 + arrayOf(Tag(5, "Shopping")), 128 + ), 129 + ) 130 + api.mock_queryCurrentActivities(activities) 131 + val response = client.queryCurrentActivities() 132 + assertArrayEquals(activities, response) 133 + } 134 + } 135 + 136 + class QueryTagsForActivityTest: WearRPCServerTestBase() { 137 + @Test 138 + fun returns_no_tags_if_activity_has_none() = runTest { 139 + api.mock_queryTagsForActivity(mapOf(13L to arrayOf())) 140 + val response = client.queryTagsForActivity(13) 141 + assertArrayEquals(arrayOf(), response) 142 + } 143 + 144 + @Test 145 + fun returns_no_tags_if_activity_doesnt_exist() = runTest { 146 + val response = client.queryTagsForActivity(42) 147 + assertArrayEquals(arrayOf(), response) 148 + } 149 + 150 + @Test 151 + fun returns_one_tag_associated_with_activity() = runTest { 152 + val tags = arrayOf(Tag(5, "Shopping")) 153 + api.mock_queryTagsForActivity(mapOf(13L to tags)) 154 + val response = client.queryTagsForActivity(13) 155 + assertArrayEquals(tags, response) 156 + } 157 + 158 + @Test 159 + fun returns_all_tags_associated_with_activity() = runTest { 160 + val tags = arrayOf(Tag(5, "Shopping"), Tag(14, "Work")) 161 + api.mock_queryTagsForActivity(mapOf(13L to tags)) 162 + val response = client.queryTagsForActivity(13L) 163 + assertArrayEquals(tags, response) 164 + } 165 + 166 + @Test 167 + fun returns_only_tags_associated_with_requested_activity() = runTest { 168 + val tags = arrayOf(Tag(5, "Shopping"), Tag(14, "Work")) 169 + val otherTags = arrayOf(Tag(7, "Chores"), Tag(13, "Sleep")) 170 + api.mock_queryTagsForActivity(mapOf(10L to tags, 17L to otherTags)) 171 + val response = client.queryTagsForActivity(10L) 172 + assertArrayEquals(tags, response) 173 + val responseOther = client.queryTagsForActivity(17L) 174 + assertArrayEquals(otherTags, responseOther) 175 + } 176 + } 177 + 178 + class QuerySettingsTest: WearRPCServerTestBase() { 179 + @Test 180 + fun returns_settings_with_multitasking_enabled() = runTest { 181 + api.mock_querySettings(Settings(multitasking = true)) 182 + val response = client.querySettings() 183 + assertTrue(response.multitasking) 184 + } 185 + 186 + @Test 187 + fun returns_settings_with_multitasking_disabled() = runTest { 188 + api.mock_querySettings(Settings(multitasking = false)) 189 + val response = client.querySettings() 190 + assertFalse(response.multitasking) 191 + } 192 + } 193 + 194 + 195 + class MockSimpleTimeTrackerAPI : SimpleTimeTrackerAPI { 196 + 197 + var activities: Array<Activity> = arrayOf() 198 + var currentActivities: Array<CurrentActivity> = arrayOf() 199 + var tags: Map<Long, Array<Tag>> = mapOf() 200 + lateinit var settings: Settings 201 + 202 + override suspend fun queryActivities(): Array<Activity> { 203 + return activities 204 + } 205 + 206 + fun mock_queryActivities(activities: Array<Activity>) { 207 + this.activities = activities 208 + } 209 + 210 + override suspend fun queryCurrentActivities(): Array<CurrentActivity> { 211 + return currentActivities 212 + } 213 + 214 + fun mock_queryCurrentActivities(activities: Array<CurrentActivity>) { 215 + this.currentActivities = activities 216 + } 217 + 218 + override suspend fun setCurrentActivities(activities: Array<CurrentActivity>): Unit { 219 + TODO("Not yet implemented") 220 + } 221 + 222 + override suspend fun queryTagsForActivity(activityId: Long): Array<Tag> { 223 + return this.tags.getOrDefault(activityId, arrayOf()) 224 + } 225 + fun mock_queryTagsForActivity(tags: Map<Long, Array<Tag>>) { 226 + this.tags = tags 227 + } 228 + 229 + override suspend fun querySettings(): Settings { 230 + return settings 231 + } 232 + 233 + fun mock_querySettings(settings: Settings) { 234 + this.settings = settings 235 + } 236 + 237 + fun mockReset() { 238 + this.activities = arrayOf() 239 + this.currentActivities = arrayOf() 240 + this.tags = mapOf() 241 + } 242 + 243 + } 244 + 245 + class MockMessenger(private val rpc: WearRPCServer) : Messenger { 246 + 247 + override suspend fun send(capability: String, message: ByteArray): ByteArray? { 248 + return rpc.onRequest(capability, message) 249 + } 250 + 251 + }