From c9a4f807d69f40e68c822582c15bc89eadc72301 Mon Sep 17 00:00:00 2001 From: razeeman Date: Fri, 01 Mar 2024 19:54:21 +0000 Subject: [PATCH] add injection of wear classes --- wearrpc/build.gradle.kts | 15 ++++++--------- .github/workflows/github_actions.yml | 2 +- features/feature_wear/build.gradle.kts | 4 ++++ buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt | 4 ++++ buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt | 2 ++ wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/MockWearCommunicationAPI.kt | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/Request.kt | 14 ++++++++------ wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/SimpleTimeTrackerAPI.kt | 54 ------------------------------------------------------ wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediator.kt | 2 +- wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearCommunicationAPI.kt | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCClient.kt | 2 +- wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCServer.kt | 55 ------------------------------------------------------- wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediatorTest.kt | 63 +++++++++++++++++++++++++++++---------------------------------- wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt | 261 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/DomainAPI.kt | 117 --------------------------------------------------------------------------------------------------------------------- features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearCommunicationInteractor.kt | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearRPCServer.kt | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearService.kt | 45 ++++++++++----------------------------------- features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt | 15 +++++++++++++++ features/feature_wear/src/test/java/com/example/util/simpletimetracker/feature_wear/WearRPCServerTest.kt | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 20 file(s) changed, 591 insertion(s)(+), 574 deletion(s)(-) diff --git a/wearrpc/build.gradle.kts b/wearrpc/build.gradle.kts --- a/wearrpc/build.gradle.kts +++ b/wearrpc/build.gradle.kts @@ -1,4 +1,5 @@ import com.example.util.simpletimetracker.Base +import com.example.util.simpletimetracker.Deps /* * This Source Code Form is subject to the terms of the Mozilla Public @@ -40,16 +41,12 @@ } dependencies { - // Custom Dependencies - implementation("com.google.android.gms:play-services-wearable:18.0.0") - implementation("com.google.code.gson:gson:2.10.1") - - // Auto-generated dependencies + implementation(Deps.Google.services) + implementation(Deps.Google.gson) implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") - testImplementation("junit:junit:4.13.2") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + testImplementation(Deps.Test.junit) + testImplementation(Deps.Test.coroutines) } \ No newline at end of file diff --git a/.github/workflows/github_actions.yml b/.github/workflows/github_actions.yml --- a/.github/workflows/github_actions.yml +++ b/.github/workflows/github_actions.yml @@ -23,7 +23,7 @@ run: ./gradlew ktlintCheck - name: Run tests - run: ./gradlew testDebug --stacktrace + run: ./gradlew testDebugUnitTest --stacktrace android_tests: runs-on: macos-latest diff --git a/features/feature_wear/build.gradle.kts b/features/feature_wear/build.gradle.kts --- a/features/feature_wear/build.gradle.kts +++ b/features/feature_wear/build.gradle.kts @@ -19,6 +19,10 @@ implementation(project(":core")) implementation(project(":wearrpc")) implementation(Deps.Google.services) + implementation(Deps.Google.gson) implementation(Deps.Google.dagger) kapt(Deps.Kapt.dagger) + + testImplementation(Deps.Test.junit) + testImplementation(Deps.Test.coroutines) } diff --git a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt --- a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt +++ b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt @@ -36,6 +36,8 @@ "com.google.android:flexbox:${Versions.flexBox}" const val services = "com.google.android.gms:play-services-wearable:${Versions.services}" + const val gson = + "com.google.code.gson:gson:${Versions.gson}" } object Emoji { @@ -76,6 +78,8 @@ "org.mockito:mockito-core:${Versions.mockito}" const val mockitoInline = "org.mockito:mockito-inline:${Versions.mockito}" + const val coroutines = + "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutinesTest}" } object UiTest { diff --git a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt --- a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt +++ b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt @@ -18,6 +18,7 @@ const val viewpager2 = "1.0.0" const val flexBox = "2.0.1" const val services = "18.0.0" + const val gson = "2.10.1" const val cardView = "1.0.0" const val material = "1.2.0" const val emoji = "1.4.0" @@ -33,4 +34,5 @@ const val junitUi = "1.1.4" const val espresso = "3.5.0" const val mockito = "5.2.0" + const val coroutinesTest = "1.7.1" } \ No newline at end of file diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/MockWearCommunicationAPI.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/MockWearCommunicationAPI.kt new file mode 100644 --- /dev/null +++ b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/MockWearCommunicationAPI.kt @@ -0,0 +1,52 @@ +package com.example.util.simpletimetracker.wearrpc + +class MockWearCommunicationAPI : WearCommunicationAPI { + + var activities: Array = arrayOf() + var currentActivities: Array = arrayOf() + var tags: Map> = mapOf() + lateinit var settings: Settings + + override suspend fun queryActivities(): Array { + return activities + } + + fun mock_queryActivities(activities: Array) { + this.activities = activities + } + + override suspend fun queryCurrentActivities(): Array { + return currentActivities + } + + fun mock_queryCurrentActivities(activities: Array) { + this.currentActivities = activities + } + + override suspend fun setCurrentActivities(activities: Array) { + TODO("Not yet implemented") + } + + override suspend fun queryTagsForActivity(activityId: Long): Array { + return this.tags[activityId] ?: arrayOf() + } + + fun mock_queryTagsForActivity(tags: Map>) { + this.tags = tags + } + + override suspend fun querySettings(): Settings { + return settings + } + + fun mock_querySettings(settings: Settings) { + this.settings = settings + } + + fun mockReset() { + this.activities = arrayOf() + this.currentActivities = arrayOf() + this.tags = mapOf() + } + +} \ No newline at end of file diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/Request.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/Request.kt --- a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/Request.kt +++ b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/Request.kt @@ -6,10 +6,12 @@ package com.example.util.simpletimetracker.wearrpc object Request { - val PING = "/stt//GET/ping" - val QUERY_ACTIVITIES = "/stt//GET/activities" - val QUERY_CURRENT_ACTIVITIES = "/stt//GET/activities/current" - val SET_CURRENT_ACTIVITIES = "/stt//PUT/activities/current" - val QUERY_TAGS_FOR_ACTIVITY = "/stt//GET/activities/:ID/tags" - val QUERY_SETTINGS = "/stt//GET/settings" + const val PATH = "/stt" + + const val PING = "$PATH//GET/ping" + const val QUERY_ACTIVITIES = "$PATH//GET/activities" + const val QUERY_CURRENT_ACTIVITIES = "$PATH//GET/activities/current" + const val SET_CURRENT_ACTIVITIES = "$PATH//PUT/activities/current" + const val QUERY_TAGS_FOR_ACTIVITY = "$PATH//GET/activities/:ID/tags" + const val QUERY_SETTINGS = "$PATH//GET/settings" } \ No newline at end of file diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/SimpleTimeTrackerAPI.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/SimpleTimeTrackerAPI.kt deleted file mode 100644 --- a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/SimpleTimeTrackerAPI.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.wearrpc - -interface SimpleTimeTrackerAPI { - /** - * /stt//GET/ping - * - * Echos the message it receives - * - * Primarily used to test request/response functionality - */ - suspend fun ping(str: String): String { - return str - } - - /** - * /stt//GET/activities - * - * Retrieves a list of all the time-tracking activities available for selection - */ - suspend fun queryActivities(): Array - - /** - * /stt//GET/activities/current - * - * Retrieves a list of the currently running activity/activities - */ - suspend fun queryCurrentActivities(): Array - - /** - * /stt//PUT/activities/current - * - * Replaces the currently running activity/activities with the given activities - */ - suspend fun setCurrentActivities(activities: Array): Unit - - /** - * /stt//GET/activities/:ID/tags - * - * Retrieves the tags available for association with the activity with the given ID - */ - suspend fun queryTagsForActivity(activityId: Long): Array - - /** - * /stt//GET/settings - * - * Retrieves the settings relevant to time tracking behavior - */ - suspend fun querySettings(): Settings -} diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediator.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediator.kt --- a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediator.kt +++ b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediator.kt @@ -6,7 +6,7 @@ package com.example.util.simpletimetracker.wearrpc class StartActivityMediator( - private val api: SimpleTimeTrackerAPI, + private val api: WearCommunicationAPI, private val onRequestStartActivity: suspend (activity: Activity) -> Unit, private val onRequestTagSelection: suspend (activity: Activity) -> Unit, ) { diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearCommunicationAPI.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearCommunicationAPI.kt new file mode 100644 --- /dev/null +++ b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearCommunicationAPI.kt @@ -0,0 +1,54 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.wearrpc + +interface WearCommunicationAPI { + /** + * [Request.PING] + * + * Echos the message it receives + * + * Primarily used to test request/response functionality + */ + suspend fun ping(message: String): String { + return message + } + + /** + * [Request.QUERY_ACTIVITIES] + * + * Retrieves a list of all the time-tracking activities available for selection + */ + suspend fun queryActivities(): Array + + /** + * [Request.QUERY_CURRENT_ACTIVITIES] + * + * Retrieves a list of the currently running activity/activities + */ + suspend fun queryCurrentActivities(): Array + + /** + * [Request.SET_CURRENT_ACTIVITIES] + * + * Replaces the currently running activity/activities with the given activities + */ + suspend fun setCurrentActivities(activities: Array) + + /** + * [Request.QUERY_TAGS_FOR_ACTIVITY] + * + * Retrieves the tags available for association with the activity with the given ID + */ + suspend fun queryTagsForActivity(activityId: Long): Array + + /** + * [Request.QUERY_SETTINGS] + * + * Retrieves the settings relevant to time tracking behavior + */ + suspend fun querySettings(): Settings +} diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCClient.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCClient.kt --- a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCClient.kt +++ b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCClient.kt @@ -9,7 +9,7 @@ import com.google.gson.reflect.TypeToken -class WearRPCClient(private val messenger: Messenger) : SimpleTimeTrackerAPI { +class WearRPCClient(private val messenger: Messenger) : WearCommunicationAPI { override suspend fun ping(message: String): String { val response = messenger.send(Request.PING, message.toByteArray()) diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCServer.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCServer.kt deleted file mode 100644 --- a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/WearRPCServer.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.wearrpc - -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken - - -class WearRPCServer(private val api: SimpleTimeTrackerAPI) { - - suspend fun onRequest(path: String, request: ByteArray): ByteArray? { - if (path.startsWith("/stt")) { - when (path) { - Request.PING -> return onPing(request) - Request.QUERY_ACTIVITIES -> return onQueryActivities() - Request.QUERY_CURRENT_ACTIVITIES -> return onQueryCurrentActivities() - Request.SET_CURRENT_ACTIVITIES -> return onSetCurrentActivities(request) - Request.QUERY_TAGS_FOR_ACTIVITY -> return onQueryTagsForActivity(request) - Request.QUERY_SETTINGS -> return onQuerySettings() - else -> throw WearRPCException("$path is an invalid RPC call") - } - } else return null - } - - private suspend fun onQueryTagsForActivity(request: ByteArray): ByteArray? { - val activityId = Gson().fromJson(String(request), Long::class.java) - return Gson().toJson(api.queryTagsForActivity(activityId)).toByteArray() - } - - private suspend fun onSetCurrentActivities(request: ByteArray): ByteArray? { - val collectionType = object : TypeToken>() {}.type - val activities: Array = Gson().fromJson(String(request), collectionType) - api.setCurrentActivities(activities) - return ByteArray(0) - } - - private suspend fun onPing(request: ByteArray): ByteArray? { - return request - } - - private suspend fun onQueryActivities(): ByteArray? { - return Gson().toJson(api.queryActivities()).toByteArray() - } - - private suspend fun onQueryCurrentActivities(): ByteArray? { - return Gson().toJson(api.queryCurrentActivities()).toByteArray() - } - - private suspend fun onQuerySettings(): ByteArray? { - return Gson().toJson(api.querySettings()).toByteArray() - } -} \ No newline at end of file diff --git a/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediatorTest.kt b/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediatorTest.kt --- a/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediatorTest.kt +++ b/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/StartActivityMediatorTest.kt @@ -10,29 +10,30 @@ import org.junit.Before import org.junit.Test - -open class StartActivityMediatorTestBase { - - protected val api = MockSimpleTimeTrackerAPI() - protected val startCallback = MockMediatorCallback() - protected val requestTagCallback = MockMediatorCallback() - protected val mediator = StartActivityMediator( +// TODO use mockito +class StartActivityMediatorTest { + private val api = MockWearCommunicationAPI() + private val startCallback = MockMediatorCallback() + private val requestTagCallback = MockMediatorCallback() + private val mediator = StartActivityMediator( api = api, onRequestStartActivity = startCallback, onRequestTagSelection = requestTagCallback, ) - protected val sampleActivity = Activity(id = 1, name = "Sleep", icon = "🛏️", color = 0xFF123456) - protected val sampleGeneralTag = + private val sampleActivity = Activity(id = 1, name = "Sleep", icon = "🛏️", color = 0xFF123456) + private val sampleGeneralTag = Tag(id = 13, name = "Sleep", isGeneral = true, color = 0xFF654321) - protected val sampleNonGeneralTag = + private val sampleNonGeneralTag = Tag(id = 14, name = "Work", isGeneral = false, color = 0xFF654321) - protected val settings = Settings( + private val settings = Settings( allowMultitasking = false, showRecordTagSelection = false, recordTagSelectionCloseAfterOne = false, recordTagSelectionEvenForGeneralTags = false, ) + + private val sampleSettings = settings.copy(showRecordTagSelection = true) @Before fun setup() { @@ -40,9 +41,7 @@ startCallback.reset() requestTagCallback.reset() } -} -class `Starts Activity When` : StartActivityMediatorTestBase() { @Test fun `tag selection disabled`() = runTest { api.mock_querySettings(settings.copy(showRecordTagSelection = false)) @@ -77,10 +76,6 @@ startCallback.assertCallsMade(1) requestTagCallback.assertCallsMade(0) } -} - -class `Requests tags when tag selection enabled and` : StartActivityMediatorTestBase() { - private val sampleSettings = settings.copy(showRecordTagSelection = true) @Test fun `activity has non-general tags`() = runTest { @@ -103,26 +98,26 @@ requestTagCallback.assertCallsMade(1) startCallback.assertCallsMade(0) } -} -class MockMediatorCallback : (Activity) -> Unit { - private var calledWith: Activity? = null - private var callCount: Int = 0 - override fun invoke(activity: Activity) { - calledWith = activity - callCount++ - } + class MockMediatorCallback : (Activity) -> Unit { + private var calledWith: Activity? = null + private var callCount: Int = 0 + override fun invoke(activity: Activity) { + calledWith = activity + callCount++ + } - fun assertCalledWith(activity: Activity) { - assertEquals(activity, calledWith) - } + fun assertCalledWith(activity: Activity) { + assertEquals(activity, calledWith) + } - fun assertCallsMade(count: Int) { - assertEquals(count, callCount) - } + fun assertCallsMade(count: Int) { + assertEquals(count, callCount) + } - fun reset() { - calledWith = null - callCount = 0 + fun reset() { + calledWith = null + callCount = 0 + } } } \ No newline at end of file diff --git a/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt b/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt deleted file mode 100644 --- a/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt +++ /dev/null @@ -1,261 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.wearrpc - -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - - -open class WearRPCServerTestBase { - lateinit var api: MockSimpleTimeTrackerAPI - lateinit var rpc: WearRPCServer - lateinit var messenger: Messenger - lateinit var client: WearRPCClient - - val tagFriends = Tag(1, "Friends", isGeneral = false, 0xFFFD3251) - val tagFamily = Tag(2, "Family", isGeneral = true, 0xFFFD3251) - val tagShopping = Tag(3, "Shopping", isGeneral = false, 0xFFFF0000) - val tagWork = Tag(14, "Work", isGeneral = false, 0xFF00FF00) - - @Before - fun setup() { - api = MockSimpleTimeTrackerAPI() - rpc = WearRPCServer(api) - messenger = MockMessenger(rpc) - client = WearRPCClient(messenger) - api.mockReset() - } -} - -class WearRPCServerTest : WearRPCServerTestBase() { - @Test - fun returns_null_for_unsupported_request() = runTest { - val response = rpc.onRequest("/fake/path", "fake data".toByteArray()) - assertNull(response) - } - - @Test(expected = WearRPCException::class) - fun raises_for_invalid_SimpleTimeTracker_request() = runTest { - rpc.onRequest("/stt//GET/fake/path", "fake data".toByteArray()) - } -} - -class PingTest : WearRPCServerTestBase() { - - @Test - fun responds_to_empty_request_with_empty_string() = runTest { - val response = client.ping("") - assertEquals("", response) - } - - @Test - fun echoes_response() = runTest { - val response = client.ping("Hello World!") - assertEquals("Hello World!", response) - } -} - -class GetActivitiesTest : WearRPCServerTestBase() { - @Test - fun returns_no_activities_when_none_are_available() = runTest { - val response = client.queryActivities() - assertArrayEquals(arrayOf(), response) - } - - @Test - fun returns_one_activity_when_one_exists() = runTest { - val activities = arrayOf(Activity(42, "Chores", "🎉", 0xFF00FF00)) - api.mock_queryActivities(activities) - val response = client.queryActivities() - assertArrayEquals(activities, response) - } - - @Test - fun returns_all_existing_activities() = runTest { - val activities = arrayOf( - Activity(13, "Singing", "🎶", 0xFF123456), - Activity(24, "Homework", "📝", 0xFFABCDEF), - ) - api.mock_queryActivities(activities) - val response = client.queryActivities() - assertArrayEquals(activities, response) - } -} - -class QueryCurrentActivitiesTest : WearRPCServerTestBase() { - @Test - fun returns_no_activities_when_none_are_running() = runTest { - val activities = arrayOf() - val response = client.queryCurrentActivities() - assertArrayEquals(activities, response) - } - - @Test - fun returns_one_activity_when_one_exists() = runTest { - val jan_31_2024_afternoon = 1706704801L - val activities = arrayOf( - CurrentActivity( - 42, - jan_31_2024_afternoon, - arrayOf(tagFriends, tagFamily), - ), - ) - api.mock_queryCurrentActivities(activities) - val response = client.queryCurrentActivities() - assertArrayEquals(activities, response) - } - - @Test - fun returns_all_running_activities() = runTest { - val jan_31_2024_afternoon = 1706704801L - val jan_31_2024_evening = 1706751601L - val activities = arrayOf( - CurrentActivity( - 42, - jan_31_2024_afternoon, - arrayOf(tagFriends, tagFamily), - ), - CurrentActivity( - 42, - jan_31_2024_evening, - arrayOf(tagShopping), - ), - ) - api.mock_queryCurrentActivities(activities) - val response = client.queryCurrentActivities() - assertArrayEquals(activities, response) - } -} - -class QueryTagsForActivityTest : WearRPCServerTestBase() { - @Test - fun returns_no_tags_if_activity_has_none() = runTest { - api.mock_queryTagsForActivity(mapOf(13L to arrayOf())) - val response = client.queryTagsForActivity(13) - assertArrayEquals(arrayOf(), response) - } - - @Test - fun returns_no_tags_if_activity_doesnt_exist() = runTest { - val response = client.queryTagsForActivity(42) - assertArrayEquals(arrayOf(), response) - } - - @Test - fun returns_one_tag_associated_with_activity() = runTest { - val tags = arrayOf(tagShopping) - api.mock_queryTagsForActivity(mapOf(13L to tags)) - val response = client.queryTagsForActivity(13) - assertArrayEquals(tags, response) - } - - @Test - fun returns_all_tags_associated_with_activity() = runTest { - val tags = arrayOf(tagShopping, tagWork) - api.mock_queryTagsForActivity(mapOf(13L to tags)) - val response = client.queryTagsForActivity(13L) - assertArrayEquals(tags, response) - } - - @Test - fun returns_only_tags_associated_with_requested_activity() = runTest { - val tags = arrayOf(tagShopping, tagWork) - val otherTags = arrayOf(tagFriends, tagFamily) - api.mock_queryTagsForActivity(mapOf(10L to tags, 17L to otherTags)) - val response = client.queryTagsForActivity(10L) - assertArrayEquals(tags, response) - val responseOther = client.queryTagsForActivity(17L) - assertArrayEquals(otherTags, responseOther) - } -} - -class QuerySettingsTest : WearRPCServerTestBase() { - private val sampleSettings = Settings( - allowMultitasking = true, - showRecordTagSelection = false, - recordTagSelectionCloseAfterOne = false, - recordTagSelectionEvenForGeneralTags = false, - ) - @Test - fun returns_settings_with_multitasking_enabled() = runTest { - api.mock_querySettings(sampleSettings) - val response = client.querySettings() - assertTrue(response.allowMultitasking) - } - - @Test - fun returns_settings_with_multitasking_disabled() = runTest { - api.mock_querySettings(sampleSettings.copy(allowMultitasking = false)) - val response = client.querySettings() - assertFalse(response.allowMultitasking) - } -} - - -class MockSimpleTimeTrackerAPI : SimpleTimeTrackerAPI { - - var activities: Array = arrayOf() - var currentActivities: Array = arrayOf() - var tags: Map> = mapOf() - lateinit var settings: Settings - - override suspend fun queryActivities(): Array { - return activities - } - - fun mock_queryActivities(activities: Array) { - this.activities = activities - } - - override suspend fun queryCurrentActivities(): Array { - return currentActivities - } - - fun mock_queryCurrentActivities(activities: Array) { - this.currentActivities = activities - } - - override suspend fun setCurrentActivities(activities: Array) { - TODO("Not yet implemented") - } - - override suspend fun queryTagsForActivity(activityId: Long): Array { - return this.tags.getOrDefault(activityId, arrayOf()) - } - - fun mock_queryTagsForActivity(tags: Map>) { - this.tags = tags - } - - override suspend fun querySettings(): Settings { - return settings - } - - fun mock_querySettings(settings: Settings) { - this.settings = settings - } - - fun mockReset() { - this.activities = arrayOf() - this.currentActivities = arrayOf() - this.tags = mapOf() - } - -} - -class MockMessenger(private val rpc: WearRPCServer) : Messenger { - - override suspend fun send(capability: String, message: ByteArray): ByteArray? { - return rpc.onRequest(capability, message) - } - -} \ No newline at end of file diff --git a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/DomainAPI.kt b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/DomainAPI.kt deleted file mode 100644 --- a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/DomainAPI.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.feature_wear - -import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor -import com.example.util.simpletimetracker.domain.interactor.RecordTagInteractor -import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor -import com.example.util.simpletimetracker.domain.interactor.RemoveRunningRecordMediator -import com.example.util.simpletimetracker.domain.interactor.RunningRecordInteractor -import com.example.util.simpletimetracker.domain.mapper.AppColorMapper -import com.example.util.simpletimetracker.domain.model.AppColor -import com.example.util.simpletimetracker.domain.model.RecordTag -import com.example.util.simpletimetracker.domain.model.RunningRecord -import com.example.util.simpletimetracker.wearrpc.Activity -import com.example.util.simpletimetracker.wearrpc.CurrentActivity -import com.example.util.simpletimetracker.wearrpc.Settings -import com.example.util.simpletimetracker.wearrpc.SimpleTimeTrackerAPI -import com.example.util.simpletimetracker.wearrpc.Tag - -class DomainAPI( - private val prefsInteractor: PrefsInteractor, - private val recordTypeInteractor: RecordTypeInteractor, - private val recordTagInteractor: RecordTagInteractor, - private val runningRecordInteractor: RunningRecordInteractor, - private val removeRunningRecordMediator: RemoveRunningRecordMediator, - private val appColorMapper: AppColorMapper, -) : SimpleTimeTrackerAPI { - - override suspend fun queryActivities(): Array { - return recordTypeInteractor.getAll().filter { recordType -> !recordType.hidden } - .map { recordType -> - Activity( - id = recordType.id, - name = recordType.name, - icon = recordType.icon, - color = asColor(recordType.color), - ) - }.toTypedArray() - } - - override suspend fun queryCurrentActivities(): Array { - return runningRecordInteractor.getAll().map { record -> - CurrentActivity( - record.id, - record.timeStarted, - record.tagIds.map { tagId -> - asTag(recordTagInteractor.get(tagId)) - }.filter { it.id > 0 }.toTypedArray(), - ) - }.toTypedArray() - } - - override suspend fun setCurrentActivities(activities: Array) { - val currents = queryCurrentActivities() - val unchanged = currents.filter { c -> activities.any { a -> a == c } } - val stopped = currents.filter { c -> unchanged.none { u -> u == c } } - val started = activities.filter { a -> currents.none { c -> a == c } } - stopped.forEach { removeRunningRecordMediator.removeWithRecordAdd(asRunningRecord(it)) } - started.forEach { runningRecordInteractor.add(asRunningRecord(it)) } - } - - private fun asRunningRecord(currentActivity: CurrentActivity): RunningRecord { - return RunningRecord( - id = currentActivity.id, - timeStarted = currentActivity.startedAt, - comment = "", - tagIds = currentActivity.tags.map { t -> t.id }, - ) - } - - override suspend fun queryTagsForActivity(activityId: Long): Array { - val activityColor = recordTypeInteractor.get(activityId)?.color - return recordTagInteractor.getByTypeOrUntyped(activityId).filter { !it.archived } - .map { asTag(it, asColor(activityColor)) }.sortedBy { it.name } - .sortedBy { it.isGeneral }.toTypedArray() - } - - private fun asTag(recordTag: RecordTag?, activityColor: Long = 0x00000000): Tag { - return if (recordTag != null) { - val isGeneral = recordTag.typeId == 0L - val tagColor = if (isGeneral) { - asColor(recordTag.color) - } else { - activityColor - } - Tag( - id = recordTag.id, - name = recordTag.name, - isGeneral = isGeneral, - color = tagColor, - ) - } else { - Tag(id = -1, name = "", isGeneral = true, color = 0xFF555555) - } - } - - private fun asColor(appColor: AppColor?): Long { - return if (appColor == null) { - 0x00000000 - } else { - appColorMapper.mapToColorInt(appColor).toLong() - } - } - - override suspend fun querySettings(): Settings { - return Settings( - allowMultitasking = prefsInteractor.getAllowMultitasking(), - showRecordTagSelection = prefsInteractor.getShowRecordTagSelection(), - recordTagSelectionCloseAfterOne = prefsInteractor.getRecordTagSelectionCloseAfterOne(), - recordTagSelectionEvenForGeneralTags = prefsInteractor.getRecordTagSelectionEvenForGeneralTags(), - ) - } - -} \ No newline at end of file diff --git a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearCommunicationInteractor.kt b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearCommunicationInteractor.kt new file mode 100644 --- /dev/null +++ b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearCommunicationInteractor.kt @@ -0,0 +1,121 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.feature_wear + +import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor +import com.example.util.simpletimetracker.domain.interactor.RecordTagInteractor +import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor +import com.example.util.simpletimetracker.domain.interactor.RemoveRunningRecordMediator +import com.example.util.simpletimetracker.domain.interactor.RunningRecordInteractor +import com.example.util.simpletimetracker.domain.mapper.AppColorMapper +import com.example.util.simpletimetracker.domain.model.AppColor +import com.example.util.simpletimetracker.domain.model.RecordTag +import com.example.util.simpletimetracker.domain.model.RunningRecord +import com.example.util.simpletimetracker.wearrpc.Activity +import com.example.util.simpletimetracker.wearrpc.CurrentActivity +import com.example.util.simpletimetracker.wearrpc.Settings +import com.example.util.simpletimetracker.wearrpc.WearCommunicationAPI +import com.example.util.simpletimetracker.wearrpc.Tag +import javax.inject.Inject + +class WearCommunicationInteractor @Inject constructor( + private val prefsInteractor: PrefsInteractor, + private val recordTypeInteractor: RecordTypeInteractor, + private val recordTagInteractor: RecordTagInteractor, + private val runningRecordInteractor: RunningRecordInteractor, + private val removeRunningRecordMediator: RemoveRunningRecordMediator, + private val appColorMapper: AppColorMapper, +) : WearCommunicationAPI { + + override suspend fun queryActivities(): Array { + return recordTypeInteractor.getAll() + .filter { recordType -> !recordType.hidden } + .map { recordType -> + Activity( + id = recordType.id, + name = recordType.name, + icon = recordType.icon, + color = asColor(recordType.color), + ) + }.toTypedArray() + } + + override suspend fun queryCurrentActivities(): Array { + return runningRecordInteractor.getAll().map { record -> + CurrentActivity( + record.id, + record.timeStarted, + record.tagIds.map { tagId -> + asTag(recordTagInteractor.get(tagId)) + }.filter { it.id > 0 }.toTypedArray(), + ) + }.toTypedArray() + } + + override suspend fun setCurrentActivities(activities: Array) { + val currents = queryCurrentActivities() + val unchanged = currents.filter { c -> activities.any { a -> a == c } } + val stopped = currents.filter { c -> unchanged.none { u -> u == c } } + val started = activities.filter { a -> currents.none { c -> a == c } } + stopped.forEach { removeRunningRecordMediator.removeWithRecordAdd(asRunningRecord(it)) } + started.forEach { runningRecordInteractor.add(asRunningRecord(it)) } + } + + private fun asRunningRecord(currentActivity: CurrentActivity): RunningRecord { + return RunningRecord( + id = currentActivity.id, + timeStarted = currentActivity.startedAt, + comment = "", + tagIds = currentActivity.tags.map(Tag::id), + ) + } + + override suspend fun queryTagsForActivity(activityId: Long): Array { + val activityColor = recordTypeInteractor.get(activityId)?.color + return recordTagInteractor.getByTypeOrUntyped(activityId) + .filter { !it.archived } + .map { asTag(it, asColor(activityColor)) } + .sortedBy { it.name } + .sortedBy { it.isGeneral } + .toTypedArray() + } + + private fun asTag(recordTag: RecordTag?, activityColor: Long = 0x00000000): Tag { + return if (recordTag != null) { + val isGeneral = recordTag.typeId == 0L + val tagColor = if (isGeneral) { + asColor(recordTag.color) + } else { + activityColor + } + Tag( + id = recordTag.id, + name = recordTag.name, + isGeneral = isGeneral, + color = tagColor, + ) + } else { + Tag(id = -1, name = "", isGeneral = true, color = 0xFF555555) + } + } + + private fun asColor(appColor: AppColor?): Long { + return if (appColor == null) { + 0x00000000 + } else { + appColorMapper.mapToColorInt(appColor).toLong() + } + } + + override suspend fun querySettings(): Settings { + return Settings( + allowMultitasking = prefsInteractor.getAllowMultitasking(), + showRecordTagSelection = prefsInteractor.getShowRecordTagSelection(), + recordTagSelectionCloseAfterOne = prefsInteractor.getRecordTagSelectionCloseAfterOne(), + recordTagSelectionEvenForGeneralTags = prefsInteractor.getRecordTagSelectionEvenForGeneralTags(), + ) + } +} \ No newline at end of file diff --git a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearRPCServer.kt b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearRPCServer.kt new file mode 100644 --- /dev/null +++ b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearRPCServer.kt @@ -0,0 +1,78 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.feature_wear + +import com.example.util.simpletimetracker.wearrpc.CurrentActivity +import com.example.util.simpletimetracker.wearrpc.Request +import com.example.util.simpletimetracker.wearrpc.WearCommunicationAPI +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import timber.log.Timber +import javax.inject.Inject + +class WearRPCServer @Inject constructor( + private val api: WearCommunicationAPI, +) { + + private val gson = Gson() + + suspend fun onRequest(path: String, request: ByteArray): ByteArray? { + return if (path.startsWith(Request.PATH)) { + when (path) { + Request.PING -> onPing(request) + Request.QUERY_ACTIVITIES -> onQueryActivities() + Request.QUERY_CURRENT_ACTIVITIES -> onQueryCurrentActivities() + Request.SET_CURRENT_ACTIVITIES -> onSetCurrentActivities(request) + Request.QUERY_TAGS_FOR_ACTIVITY -> onQueryTagsForActivity(request) + Request.QUERY_SETTINGS -> onQuerySettings() + else -> { + Timber.d("$path is an invalid RPC call") + null + } + } + } else { + null + } + } + + private suspend fun onQueryTagsForActivity(request: ByteArray): ByteArray? { + val activityId: Long = mapRequest(request) ?: return null + return mapToResponse(api.queryTagsForActivity(activityId)) + } + + private suspend fun onSetCurrentActivities(request: ByteArray): ByteArray? { + val activities: Array = mapRequest(request) ?: return null + api.setCurrentActivities(activities) + return ByteArray(0) + } + + private fun onPing(request: ByteArray): ByteArray { + return request + } + + private suspend fun onQueryActivities(): ByteArray { + return mapToResponse(api.queryActivities()) + } + + private suspend fun onQueryCurrentActivities(): ByteArray { + return mapToResponse(api.queryCurrentActivities()) + } + + private suspend fun onQuerySettings(): ByteArray { + return mapToResponse(api.querySettings()) + } + + private fun mapToResponse(data: T): ByteArray { + return gson.toJson(data).toByteArray() + } + + private inline fun mapRequest(data: ByteArray): T? { + return runCatching { + val collectionType = object : TypeToken() {}.type + gson.fromJson(String(data), collectionType) + }.getOrNull() + } +} \ No newline at end of file diff --git a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearService.kt b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearService.kt --- a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearService.kt +++ b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearService.kt @@ -5,13 +5,6 @@ */ package com.example.util.simpletimetracker.feature_wear -import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor -import com.example.util.simpletimetracker.domain.interactor.RecordTagInteractor -import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor -import com.example.util.simpletimetracker.domain.interactor.RemoveRunningRecordMediator -import com.example.util.simpletimetracker.domain.interactor.RunningRecordInteractor -import com.example.util.simpletimetracker.domain.mapper.AppColorMapper -import com.example.util.simpletimetracker.wearrpc.WearRPCServer import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.android.gms.wearable.WearableListenerService @@ -29,34 +22,16 @@ class WearService : WearableListenerService() { @Inject - lateinit var prefsInteractor: PrefsInteractor + lateinit var wearRPCServer: WearRPCServer - @Inject - lateinit var recordTypeInteractor: RecordTypeInteractor - - @Inject - lateinit var recordTagInteractor: RecordTagInteractor - - @Inject - lateinit var runningRecordInteractor: RunningRecordInteractor - - @Inject - lateinit var removeRunningRecordMediator: RemoveRunningRecordMediator - - @Inject - lateinit var appColorMapper: AppColorMapper - - override fun onRequest(nodeId: String, path: String, request: ByteArray): Task? { - val rpc = WearRPCServer( - DomainAPI( - prefsInteractor, - recordTypeInteractor, - recordTagInteractor, - runningRecordInteractor, - removeRunningRecordMediator, - appColorMapper, - ), - ) - return runBlocking { Tasks.forResult(rpc.onRequest(path, request)) } + override fun onRequest( + nodeId: String, + path: String, + request: ByteArray, + ): Task? { + // Can block because service is on separate thread. + return runBlocking { + Tasks.forResult(wearRPCServer.onRequest(path, request)) + } } } diff --git a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt new file mode 100644 --- /dev/null +++ b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt @@ -0,0 +1,15 @@ +package com.example.util.simpletimetracker.feature_wear + +import com.example.util.simpletimetracker.wearrpc.WearCommunicationAPI +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface WidgetModule { + + @Binds + fun WearCommunicationInteractor.bindWearCommunicationInteractor(): WearCommunicationAPI +} \ No newline at end of file diff --git a/features/feature_wear/src/test/java/com/example/util/simpletimetracker/feature_wear/WearRPCServerTest.kt b/features/feature_wear/src/test/java/com/example/util/simpletimetracker/feature_wear/WearRPCServerTest.kt new file mode 100644 --- /dev/null +++ b/features/feature_wear/src/test/java/com/example/util/simpletimetracker/feature_wear/WearRPCServerTest.kt @@ -0,0 +1,205 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.feature_wear + +import com.example.util.simpletimetracker.wearrpc.Activity +import com.example.util.simpletimetracker.wearrpc.CurrentActivity +import com.example.util.simpletimetracker.wearrpc.Messenger +import com.example.util.simpletimetracker.wearrpc.MockWearCommunicationAPI +import com.example.util.simpletimetracker.wearrpc.Settings +import com.example.util.simpletimetracker.wearrpc.Tag +import com.example.util.simpletimetracker.wearrpc.WearRPCClient +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +// TODO use mockito +@Suppress("LocalVariableName") +class WearRPCServerTest { + private lateinit var api: MockWearCommunicationAPI + private lateinit var rpc: WearRPCServer + private lateinit var messenger: Messenger + private lateinit var client: WearRPCClient + + private val sampleSettings = Settings( + allowMultitasking = true, + showRecordTagSelection = false, + recordTagSelectionCloseAfterOne = false, + recordTagSelectionEvenForGeneralTags = false, + ) + + private val tagFriends = Tag(1, "Friends", isGeneral = false, 0xFFFD3251) + private val tagFamily = Tag(2, "Family", isGeneral = true, 0xFFFD3251) + private val tagShopping = Tag(3, "Shopping", isGeneral = false, 0xFFFF0000) + private val tagWork = Tag(14, "Work", isGeneral = false, 0xFF00FF00) + + @Before + fun setup() { + api = MockWearCommunicationAPI() + rpc = WearRPCServer(api) + messenger = MockMessenger(rpc) + client = WearRPCClient(messenger) + api.mockReset() + } + + @Test + fun returns_null_for_unsupported_request() = runTest { + val response = rpc.onRequest("/fake/path", "fake data".toByteArray()) + assertNull(response) + } + + @Test + fun returns_null_for_invalid_request() = runTest { + val response = rpc.onRequest("/stt//GET/fake/path", "fake data".toByteArray()) + assertNull(response) + } + + @Test + fun responds_to_empty_request_with_empty_string() = runTest { + val response = client.ping("") + assertEquals("", response) + } + + @Test + fun echoes_response() = runTest { + val response = client.ping("Hello World!") + assertEquals("Hello World!", response) + } + + @Test + fun returns_no_activities_when_none_are_available() = runTest { + val response = client.queryActivities() + assertArrayEquals(arrayOf(), response) + } + + @Test + fun returns_one_activity_when_one_exists() = runTest { + val activities = arrayOf(Activity(42, "Chores", "🎉", 0xFF00FF00)) + api.mock_queryActivities(activities) + val response = client.queryActivities() + assertArrayEquals(activities, response) + } + + @Test + fun returns_all_existing_activities() = runTest { + val activities = arrayOf( + Activity(13, "Singing", "🎶", 0xFF123456), + Activity(24, "Homework", "📝", 0xFFABCDEF), + ) + api.mock_queryActivities(activities) + val response = client.queryActivities() + assertArrayEquals(activities, response) + } + + @Test + fun returns_no_activities_when_none_are_running() = runTest { + val activities = arrayOf() + val response = client.queryCurrentActivities() + assertArrayEquals(activities, response) + } + + @Test + fun returns_one_current_activity_when_one_exists() = runTest { + val jan_31_2024_afternoon = 1706704801L + val activities = arrayOf( + CurrentActivity( + 42, + jan_31_2024_afternoon, + arrayOf(tagFriends, tagFamily), + ), + ) + api.mock_queryCurrentActivities(activities) + val response = client.queryCurrentActivities() + assertArrayEquals(activities, response) + } + + @Test + fun returns_all_running_activities() = runTest { + val jan_31_2024_afternoon = 1706704801L + val jan_31_2024_evening = 1706751601L + val activities = arrayOf( + CurrentActivity( + 42, + jan_31_2024_afternoon, + arrayOf(tagFriends, tagFamily), + ), + CurrentActivity( + 42, + jan_31_2024_evening, + arrayOf(tagShopping), + ), + ) + api.mock_queryCurrentActivities(activities) + val response = client.queryCurrentActivities() + assertArrayEquals(activities, response) + } + + @Test + fun returns_no_tags_if_activity_has_none() = runTest { + api.mock_queryTagsForActivity(mapOf(13L to arrayOf())) + val response = client.queryTagsForActivity(13) + assertArrayEquals(arrayOf(), response) + } + + @Test + fun returns_no_tags_if_activity_does_not_exist() = runTest { + val response = client.queryTagsForActivity(42) + assertArrayEquals(arrayOf(), response) + } + + @Test + fun returns_one_tag_associated_with_activity() = runTest { + val tags = arrayOf(tagShopping) + api.mock_queryTagsForActivity(mapOf(13L to tags)) + val response = client.queryTagsForActivity(13) + assertArrayEquals(tags, response) + } + + @Test + fun returns_all_tags_associated_with_activity() = runTest { + val tags = arrayOf(tagShopping, tagWork) + api.mock_queryTagsForActivity(mapOf(13L to tags)) + val response = client.queryTagsForActivity(13L) + assertArrayEquals(tags, response) + } + + @Test + fun returns_only_tags_associated_with_requested_activity() = runTest { + val tags = arrayOf(tagShopping, tagWork) + val otherTags = arrayOf(tagFriends, tagFamily) + api.mock_queryTagsForActivity(mapOf(10L to tags, 17L to otherTags)) + val response = client.queryTagsForActivity(10L) + assertArrayEquals(tags, response) + val responseOther = client.queryTagsForActivity(17L) + assertArrayEquals(otherTags, responseOther) + } + + @Test + fun returns_settings_with_multitasking_enabled() = runTest { + api.mock_querySettings(sampleSettings) + val response = client.querySettings() + assertTrue(response.allowMultitasking) + } + + @Test + fun returns_settings_with_multitasking_disabled() = runTest { + api.mock_querySettings(sampleSettings.copy(allowMultitasking = false)) + val response = client.querySettings() + assertFalse(response.allowMultitasking) + } + + class MockMessenger(private val rpc: WearRPCServer) : Messenger { + + override suspend fun send(capability: String, message: ByteArray): ByteArray? { + return rpc.onRequest(capability, message) + } + } +} \ No newline at end of file -- tangled.sh