From f0af307a1fd1f8c44370f779eb01f8d508d08479 Mon Sep 17 00:00:00 2001 From: av1m <36456709+av1m@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:10:51 +0300 Subject: [PATCH] api for webapp that re-use wear api #103 --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 2 + .../util/simpletimetracker/TimeTrackerApp.kt | 23 + .../simpletimetracker/api/WebApiAdapter.kt | 158 +++ .../simpletimetracker/api/WebApiModule.kt | 19 + gradle/libs.versions.toml | 2 + web/index.html | 1160 +++++++++++++++++ 7 files changed, 1366 insertions(+) create mode 100644 app/src/play/java/com/example/util/simpletimetracker/api/WebApiAdapter.kt create mode 100644 app/src/play/java/com/example/util/simpletimetracker/api/WebApiModule.kt create mode 100644 web/index.html diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a35c10075..048aad635 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -128,12 +128,14 @@ dependencies { implementation(project(":feature_change_goals")) implementation(project(":feature_change_goals:api")) implementation(project(":feature_change_goals:views")) + implementation(project(":wear_api")) "playImplementation"(project(":feature_wear")) implementation(libs.androidx.room) implementation(libs.ktx.navigationFragment) implementation(libs.ktx.navigationUi) implementation(libs.google.dagger) + implementation(libs.nanohttpd) ksp(libs.kapt.dagger) kspAndroidTest(libs.kapt.dagger) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d021899c2..b8cde7fd0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + addHeader(k, v) } } + } + + return try { + when { + // GET /api/activities + session.uri == "/api/activities" && session.method == Method.GET -> { + getAllActivities(headers) + } + // GET /api/running + session.uri == "/api/running" && session.method == Method.GET -> { + getRunningActivities(headers) + } + // POST /api/start/:id + session.uri.startsWith("/api/start/") && session.method == Method.POST -> { + val id = session.uri.substringAfterLast("/").toLongOrNull() + startActivity(id, headers) + } + // POST /api/stop/:id + session.uri.startsWith("/api/stop/") && session.method == Method.POST -> { + val id = session.uri.substringAfterLast("/").toLongOrNull() + stopActivity(id, headers) + } + else -> { + newFixedLengthResponse( + Response.Status.NOT_FOUND, + "application/json", + """{"error": "Not found"}""" + ).apply { headers.forEach { (k, v) -> addHeader(k, v) } } + } + } + } catch (e: Exception) { + newFixedLengthResponse( + Response.Status.INTERNAL_ERROR, + "application/json", + """{"error": "${e.message}"}""" + ).apply { headers.forEach { (k, v) -> addHeader(k, v) } } + } + } + + private fun getAllActivities(headers: Map): Response = runBlocking { + // Reuse the EXACT same method Wear OS uses! + val activities = wearApi.queryActivities() + val currentState = wearApi.queryCurrentActivities() + val runningIds = currentState.currentActivities.map { it.id }.toSet() + + val json = JSONArray() + activities.forEach { activity -> + json.put(JSONObject().apply { + put("id", activity.id) + put("name", activity.name) + put("icon", activity.icon) + put("color", activity.color) + put("isRunning", runningIds.contains(activity.id)) + }) + } + + newFixedLengthResponse( + Response.Status.OK, + "application/json", + json.toString() + ).apply { + headers.forEach { (k, v) -> addHeader(k, v) } + } + } + + private fun getRunningActivities(headers: Map): Response = runBlocking { + // Reuse the EXACT same method Wear OS uses! + val currentState = wearApi.queryCurrentActivities() + val activities = wearApi.queryActivities().associateBy { it.id } + + val json = JSONArray() + currentState.currentActivities.forEach { current -> + val activity = activities[current.id] + json.put(JSONObject().apply { + put("id", current.id) + put("name", activity?.name ?: "Unknown") + put("timeStarted", current.startedAt) + put("duration", System.currentTimeMillis() - current.startedAt) + }) + } + + newFixedLengthResponse( + Response.Status.OK, + "application/json", + json.toString() + ).apply { + headers.forEach { (k, v) -> addHeader(k, v) } + } + } + + private fun startActivity(id: Long?, headers: Map): Response = runBlocking { + if (id == null) { + return@runBlocking newFixedLengthResponse( + Response.Status.BAD_REQUEST, + "application/json", + """{"error": "Invalid ID"}""" + ) + } + + // Reuse the EXACT same method Wear OS uses! + wearApi.startActivity(WearStartActivityRequest(id = id, tags = null)) + + newFixedLengthResponse( + Response.Status.OK, + "application/json", + """{"success": true}""" + ).apply { + headers.forEach { (k, v) -> addHeader(k, v) } + } + } + + private fun stopActivity(id: Long?, headers: Map): Response = runBlocking { + if (id == null) { + return@runBlocking newFixedLengthResponse( + Response.Status.BAD_REQUEST, + "application/json", + """{"error": "Invalid ID"}""" + ) + } + + // Reuse the EXACT same method Wear OS uses! + wearApi.stopActivity(WearStopActivityRequest(id = id)) + + newFixedLengthResponse( + Response.Status.OK, + "application/json", + """{"success": true}""" + ).apply { + headers.forEach { (k, v) -> addHeader(k, v) } + } + } +} \ No newline at end of file diff --git a/app/src/play/java/com/example/util/simpletimetracker/api/WebApiModule.kt b/app/src/play/java/com/example/util/simpletimetracker/api/WebApiModule.kt new file mode 100644 index 000000000..4b9ab4280 --- /dev/null +++ b/app/src/play/java/com/example/util/simpletimetracker/api/WebApiModule.kt @@ -0,0 +1,19 @@ +package com.example.util.simpletimetracker.api + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object WebApiModule { + @Provides + @Singleton + fun provideWebApiAdapter( + wearApi: com.example.util.simpletimetracker.wear_api.WearCommunicationAPI + ): WebApiAdapter { + return WebApiAdapter(wearApi) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e3ad8dee9..d1f0d4260 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ ksp = "1.9.25-1.0.20" # same as kotlin marathon = "0.10.3" coroutines = "1.6.4" +nanohttpd = "2.3.1" timber = "4.7.1" javax = "1" @@ -55,6 +56,7 @@ rules = "1.5.0" kotlin = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } javax = { group = "javax.inject", name = "javax.inject", version.ref = "javax" } +nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } # Androidx diff --git a/web/index.html b/web/index.html new file mode 100644 index 000000000..9644db23b --- /dev/null +++ b/web/index.html @@ -0,0 +1,1160 @@ + + + + + + Simple Time Tracker - Web + + + +
+ +
+
+ +
+

Simple Time Tracker

+
Web Interface
+
+
+
+
+
+ Disconnected +
+ +
+
+ + +
+
+ + + + +
+
+ + + + + +
+ + + + +
+ + +
+ +
+
+

Loading activities...

+
+
+
+ + +
+
+
+

Loading running activities...

+
+ + +
+ + +
+
+
+

Loading recent records...

+
+
+
+ + +
+
+
+

Loading statistics...

+
+

📊 Today's Activity

+
+
+
+ + + + \ No newline at end of file