11package net.activitywatch.android.widget
22
3+ import android.app.AlarmManager
34import android.app.PendingIntent
45import android.appwidget.AppWidgetManager
56import android.appwidget.AppWidgetProvider
67import android.content.ComponentName
78import android.content.Context
89import android.content.Intent
10+ import android.os.SystemClock
911import android.util.Log
1012import android.view.View
1113import android.widget.RemoteViews
14+ import kotlinx.coroutines.CoroutineScope
15+ import kotlinx.coroutines.Dispatchers
16+ import kotlinx.coroutines.launch
1217import net.activitywatch.android.R
1318import net.activitywatch.android.watcher.UsageStatsWatcher
1419
1520private const val TAG = " CategoryTimeWidget"
1621private const val ACTION_REFRESH = " net.activitywatch.android.widget.ACTION_REFRESH"
22+ const val ACTION_PERIODIC_UPDATE = " net.activitywatch.android.widget.ACTION_PERIODIC_UPDATE"
23+ private const val UPDATE_INTERVAL_MS = 5 * 60 * 1000L // 5 minutes
1724
1825/* *
1926 * Widget provider for displaying category time stats.
@@ -36,43 +43,61 @@ class CategoryTimeWidgetProvider : AppWidgetProvider() {
3643
3744 override fun onReceive (context : Context , intent : Intent ) {
3845 super .onReceive(context, intent)
39-
40- if (intent.action == ACTION_REFRESH ) {
41- Log .d(TAG , " Refresh button clicked - re-parsing usage events and updating widgets" )
42-
43- val appWidgetManager = AppWidgetManager .getInstance(context)
44- val componentName = ComponentName (context, CategoryTimeWidgetProvider ::class .java)
45- val appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
46-
47- // Show loading indicator first
48- for (appWidgetId in appWidgetIds) {
49- showLoadingState(context, appWidgetManager, appWidgetId)
50- }
51-
52- // Re-parse usage events
53- try {
54- val usageStatsWatcher = UsageStatsWatcher (context)
55- usageStatsWatcher.sendHeartbeats()
56- Log .d(TAG , " Triggered usage events re-parsing" )
57- } catch (e: Exception ) {
58- Log .e(TAG , " Error re-parsing usage events" , e)
46+ when (intent.action) {
47+ ACTION_REFRESH -> {
48+ Log .d(TAG , " Refresh button clicked - re-parsing usage events and updating widgets" )
49+
50+ val appWidgetManager = AppWidgetManager .getInstance(context)
51+ val componentName = ComponentName (context, CategoryTimeWidgetProvider ::class .java)
52+ val appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
53+
54+ // Show loading indicator first
55+ for (appWidgetId in appWidgetIds) {
56+ showLoadingState(context, appWidgetManager, appWidgetId)
57+ }
58+
59+ // Re-parse usage events
60+ try {
61+ val usageStatsWatcher = UsageStatsWatcher (context)
62+ usageStatsWatcher.sendHeartbeats()
63+ Log .d(TAG , " Triggered usage events re-parsing" )
64+ } catch (e: Exception ) {
65+ Log .e(TAG , " Error re-parsing usage events" , e)
66+ }
67+
68+ // Then update with fresh data
69+ for (appWidgetId in appWidgetIds) {
70+ updateWidgetWithRefreshButton(context, appWidgetManager, appWidgetId)
71+ }
5972 }
60-
61- // Then update with fresh data
62- for (appWidgetId in appWidgetIds) {
63- updateWidgetWithRefreshButton(context, appWidgetManager, appWidgetId)
73+ ACTION_PERIODIC_UPDATE -> {
74+ Log .d(TAG , " Periodic update triggered by AlarmManager" )
75+ val pendingResult = goAsync()
76+ CoroutineScope (Dispatchers .IO ).launch {
77+ try {
78+ CategoryTimeWidgetUpdater .updateAllWidgets(context)
79+ } catch (e: Exception ) {
80+ Log .e(TAG , " Error during periodic update" , e)
81+ } finally {
82+ // Reschedule the next exact alarm
83+ schedulePeriodicUpdates(context)
84+ pendingResult.finish()
85+ }
86+ }
6487 }
6588 }
6689 }
6790
6891 override fun onEnabled (context : Context ) {
69- Log .d(TAG , " Widget enabled - scheduling 30-minute background updates" )
70- CategoryTimeWidgetWorker .schedulePeriodicUpdates(context)
92+ super .onEnabled(context)
93+ Log .d(TAG , " Widget enabled - scheduling 5-minute background updates" )
94+ schedulePeriodicUpdates(context)
7195 }
7296
7397 override fun onDisabled (context : Context ) {
98+ super .onDisabled(context)
7499 Log .d(TAG , " Widget disabled - cancelling background updates" )
75- CategoryTimeWidgetWorker . cancelPeriodicUpdates(context)
100+ cancelPeriodicUpdates(context)
76101 }
77102
78103 companion object {
@@ -99,7 +124,50 @@ class CategoryTimeWidgetProvider : AppWidgetProvider() {
99124 appWidgetId : Int
100125 ) {
101126 // updateSingleWidget handles data, click handler, and loading state
102- CategoryTimeWidgetWorker .updateSingleWidget(context, appWidgetManager, appWidgetId)
127+ CategoryTimeWidgetUpdater .updateSingleWidget(context, appWidgetManager, appWidgetId)
128+ }
129+
130+ private fun getUpdateIntent (context : Context ): PendingIntent {
131+ val intent = Intent (context, CategoryTimeWidgetProvider ::class .java).apply {
132+ action = ACTION_PERIODIC_UPDATE
133+ }
134+ return PendingIntent .getBroadcast(
135+ context,
136+ 1 ,
137+ intent,
138+ PendingIntent .FLAG_UPDATE_CURRENT or PendingIntent .FLAG_IMMUTABLE
139+ )
140+ }
141+
142+ fun schedulePeriodicUpdates (context : Context ) {
143+ val alarmManager = context.getSystemService(Context .ALARM_SERVICE ) as AlarmManager
144+ val pendingIntent = getUpdateIntent(context)
145+
146+ // Start exact alarm that fires once
147+ try {
148+ alarmManager.setExactAndAllowWhileIdle(
149+ AlarmManager .ELAPSED_REALTIME_WAKEUP ,
150+ SystemClock .elapsedRealtime() + UPDATE_INTERVAL_MS ,
151+ pendingIntent
152+ )
153+ Log .d(TAG , " Scheduled exact AlarmManager update in 5 minutes" )
154+ } catch (e: SecurityException ) {
155+ // In Android 14+, SCHEDULE_EXACT_ALARM might be revoked by user
156+ Log .e(TAG , " Cannot schedule exact alarm without permission, falling back to inexact" , e)
157+ alarmManager.setInexactRepeating(
158+ AlarmManager .ELAPSED_REALTIME_WAKEUP ,
159+ SystemClock .elapsedRealtime() + UPDATE_INTERVAL_MS ,
160+ UPDATE_INTERVAL_MS ,
161+ pendingIntent
162+ )
163+ }
164+ }
165+
166+ fun cancelPeriodicUpdates (context : Context ) {
167+ val alarmManager = context.getSystemService(Context .ALARM_SERVICE ) as AlarmManager
168+ val pendingIntent = getUpdateIntent(context)
169+ alarmManager.cancel(pendingIntent)
170+ Log .d(TAG , " Cancelled AlarmManager periodic updates" )
103171 }
104172 }
105173}
0 commit comments