diff --git a/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt b/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt index 6dd4a7e5..fc59f757 100644 --- a/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt +++ b/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt @@ -300,11 +300,11 @@ object CategoryTimeWidgetUpdater { /** * Parse category events from the androidQuery response. * - * Groups by the full category path, matching the Activity view: aw-webui builds - * cat_events with `merge_events_by_keys(events, ["$category"])` and labels them - * with `$category.join(" > ")`. Rolling up to the top-level category here would - * make per-category times disagree with the Activity view even though the totals - * match (ActivityWatch/aw-android#142). + * Groups by the top-level category only (`$category[0]`), so subcategories roll + * up into a single row (e.g. "Work > Programming" and "Work > Planning" both + * contribute to "Work") and every "Uncategorized > *" subcategory collapses into + * a single "Uncategorized" row. This matches the widget's compact display; the + * Activity view's finer full-path grouping lives elsewhere. */ internal fun parseCategories(jsonResult: String): List> { val categories = mutableMapOf() @@ -325,11 +325,11 @@ object CategoryTimeWidgetUpdater { val categoryArray = data.optJSONArray("\$category") if (categoryArray == null || categoryArray.length() == 0) continue - // Full category path, e.g. ["Work", "Programming"] -> "Work > Programming" - val categoryPath = (0 until categoryArray.length()) - .joinToString(" > ") { categoryArray.optString(it, "Uncategorized") } + // Get top-level category (first element only) + val topLevelCategory = categoryArray.optString(0, "Uncategorized") - categories[categoryPath] = categories.getOrDefault(categoryPath, 0.0) + duration + // Aggregate time by top-level category + categories[topLevelCategory] = categories.getOrDefault(topLevelCategory, 0.0) + duration } } catch (e: Exception) { Log.e(TAG, "Error parsing categories", e) diff --git a/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt b/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt index 17ce9a76..d298ee28 100644 --- a/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt +++ b/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt @@ -14,13 +14,12 @@ class CategoryTimeWidgetUpdaterTest { """[{"cat_events":[${events.joinToString(",")}]}]""" /** - * Regression for ActivityWatch/aw-android#142: the widget rolled every event up to its - * top-level category, so subcategories collapsed together and per-category times - * disagreed with the Activity view (which groups by the full `$category` path) even - * though the totals matched. + * The widget groups by top-level category ($category[0]), so subcategories collapse + * into a single row. This is the widget's compact display; see + * ActivityWatch/aw-android#142 for the discussion. */ @Test - fun parseCategories_keepsSubcategoriesSeparate() { + fun parseCategories_collapsesSubcategoriesIntoTopLevel() { val result = CategoryTimeWidgetUpdater.parseCategories( response( catEvent(60.0, "Work", "Programming"), @@ -28,10 +27,7 @@ class CategoryTimeWidgetUpdaterTest { ) ) - assertEquals( - listOf("Work > Programming" to 60_000L, "Work > Planning" to 30_000L), - result - ) + assertEquals(listOf("Work" to 90_000L), result) } @Test @@ -48,15 +44,15 @@ class CategoryTimeWidgetUpdaterTest { } @Test - fun parseCategories_mergesRepeatsOfTheSamePath() { + fun parseCategories_mergesRepeatsOfTheSameTopLevel() { val result = CategoryTimeWidgetUpdater.parseCategories( response( catEvent(60.0, "Work", "Programming"), - catEvent(15.0, "Work", "Programming") + catEvent(15.0, "Work", "Planning") ) ) - assertEquals(listOf("Work > Programming" to 75_000L), result) + assertEquals(listOf("Work" to 75_000L), result) } @Test @@ -70,11 +66,29 @@ class CategoryTimeWidgetUpdaterTest { ) assertEquals( - listOf("Work > Programming", "Comms", "Media > Video"), + listOf("Work", "Comms", "Media"), result.map { it.first } ) } + /** + * Regression for ActivityWatch/aw-android#142: with full-path grouping, + * ["Uncategorized", "Browser"] and ["Uncategorized", "Games"] became separate rows. + * Top-level grouping collapses all "Uncategorized > *" subcategories back into a + * single "Uncategorized" row. + */ + @Test + fun parseCategories_collapsesUncategorizedSubcategories() { + val result = CategoryTimeWidgetUpdater.parseCategories( + response( + catEvent(20.0, "Uncategorized", "Browser"), + catEvent(10.0, "Uncategorized", "Games") + ) + ) + + assertEquals(listOf("Uncategorized" to 30_000L), result) + } + @Test fun parseCategories_keepsSingleLevelCategoriesUnprefixed() { val result = CategoryTimeWidgetUpdater.parseCategories(