Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pair<String, Long>> {
val categories = mutableMapOf<String, Double>()
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,20 @@ 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"),
catEvent(30.0, "Work", "Planning")
)
)

assertEquals(
listOf("Work > Programming" to 60_000L, "Work > Planning" to 30_000L),
result
)
assertEquals(listOf("Work" to 90_000L), result)
}

@Test
Expand All @@ -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
Expand All @@ -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(
Expand Down
Loading