Skip to content

Commit 2928b15

Browse files
CopilotPranavPurwar
andcommitted
Add reminder and grace period functionality for app limits
Co-authored-by: PranavPurwar <75154889+PranavPurwar@users.noreply.github.com>
1 parent 1b180e6 commit 2928b15

5 files changed

Lines changed: 243 additions & 2 deletions

File tree

Reef/src/main/java/dev/pranav/reef/accessibility/BlockerService.kt

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import dev.pranav.reef.AppUsageActivity
1616
import dev.pranav.reef.R
1717
import dev.pranav.reef.util.AppLimits
1818
import dev.pranav.reef.util.CHANNEL_ID
19+
import dev.pranav.reef.util.GRACE_PERIOD_MS
20+
import dev.pranav.reef.util.NotificationHelper
1921
import dev.pranav.reef.util.NotificationHelper.createNotificationChannel
22+
import dev.pranav.reef.util.REMINDER_TIME_MS
2023
import dev.pranav.reef.util.RoutineLimits
2124
import dev.pranav.reef.util.RoutineManager
2225
import dev.pranav.reef.util.Whitelist
@@ -78,10 +81,35 @@ class BlockerService : AccessibilityService() {
7881
"Routine check for $packageName - UsageTime: $routineUsageTime ms, Limit: $routineLimit ms"
7982
)
8083

84+
// Check if we should send a reminder (10 minutes before limit)
85+
val timeUntilLimit = routineLimit - routineUsageTime
86+
if (timeUntilLimit in 1..REMINDER_TIME_MS && !RoutineLimits.hasRoutineReminderBeenSent(packageName)) {
87+
Log.d("BlockerService", "Sending reminder for $packageName - ${timeUntilLimit / 60000} minutes remaining")
88+
NotificationHelper.showReminderNotification(this, packageName, timeUntilLimit)
89+
RoutineLimits.markRoutineReminderSent(packageName)
90+
}
91+
8192
if (routineUsageTime >= routineLimit) {
93+
// Check if grace period is enabled and active
94+
if (!RoutineLimits.hasRoutineGracePeriodStarted(packageName)) {
95+
// Start grace period and show notification
96+
Log.d("BlockerService", "Starting grace period for $packageName")
97+
RoutineLimits.startRoutineGracePeriod(packageName)
98+
NotificationHelper.showGracePeriodNotification(this, packageName)
99+
return
100+
}
101+
102+
// Check if still in grace period
103+
if (RoutineLimits.isInRoutineGracePeriod(packageName)) {
104+
val remaining = RoutineLimits.getRemainingRoutineGracePeriod(packageName)
105+
Log.d("BlockerService", "Grace period active for $packageName - ${remaining / 1000}s remaining")
106+
return
107+
}
108+
109+
// Grace period expired, block the app
82110
Log.d(
83111
"BlockerService",
84-
"BLOCKING $packageName - routine usage ($routineUsageTime) >= limit ($routineLimit)"
112+
"BLOCKING $packageName - routine usage ($routineUsageTime) >= limit ($routineLimit) and grace period expired"
85113
)
86114
showTimeLimitNotification(packageName, "routine")
87115
performGlobalAction(GLOBAL_ACTION_HOME)
@@ -105,10 +133,35 @@ class BlockerService : AccessibilityService() {
105133
"UsageTime: $usageTime ms, Limit: $limit ms, LimitType: regular"
106134
)
107135

136+
// Check if we should send a reminder (10 minutes before limit)
137+
val timeUntilLimit = limit - usageTime
138+
if (timeUntilLimit in 1..REMINDER_TIME_MS && !AppLimits.hasReminderBeenSent(packageName)) {
139+
Log.d("BlockerService", "Sending reminder for $packageName - ${timeUntilLimit / 60000} minutes remaining")
140+
NotificationHelper.showReminderNotification(this, packageName, timeUntilLimit)
141+
AppLimits.markReminderSent(packageName)
142+
}
143+
108144
if (usageTime >= limit) {
145+
// Check if grace period is enabled and active
146+
if (!AppLimits.hasGracePeriodStarted(packageName)) {
147+
// Start grace period and show notification
148+
Log.d("BlockerService", "Starting grace period for $packageName")
149+
AppLimits.startGracePeriod(packageName)
150+
NotificationHelper.showGracePeriodNotification(this, packageName)
151+
return
152+
}
153+
154+
// Check if still in grace period
155+
if (AppLimits.isInGracePeriod(packageName)) {
156+
val remaining = AppLimits.getRemainingGracePeriod(packageName)
157+
Log.d("BlockerService", "Grace period active for $packageName - ${remaining / 1000}s remaining")
158+
return
159+
}
160+
161+
// Grace period expired, block the app
109162
Log.d(
110163
"BlockerService",
111-
"BLOCKING $packageName - usage ($usageTime) >= limit ($limit)"
164+
"BLOCKING $packageName - usage ($usageTime) >= limit ($limit) and grace period expired"
112165
)
113166
showTimeLimitNotification(packageName, "regular")
114167
performGlobalAction(GLOBAL_ACTION_HOME)

Reef/src/main/java/dev/pranav/reef/util/AppLimits.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ object AppLimits {
1414
private lateinit var sharedPreferences: SharedPreferences
1515
private val appLimits = mutableMapOf<String, Long>()
1616
private lateinit var usageStatsManager: UsageStatsManager
17+
18+
// Track when reminders were sent to avoid duplicate notifications
19+
private val reminderSentMap = mutableMapOf<String, Long>()
20+
private val gracePeriodStartMap = mutableMapOf<String, Long>()
1721

1822
fun setLimit(packageName: String, limit: Int) {
1923
// limit is in minutes, convert to milliseconds
@@ -147,6 +151,63 @@ object AppLimits {
147151
}
148152
}
149153
}
154+
155+
fun hasReminderBeenSent(packageName: String): Boolean {
156+
val lastSent = reminderSentMap[packageName] ?: return false
157+
val startOfDay = java.time.ZonedDateTime.ofInstant(
158+
java.time.Instant.now(),
159+
java.time.ZoneId.systemDefault()
160+
)
161+
.toLocalDate()
162+
.atStartOfDay(java.time.ZoneOffset.systemDefault())
163+
.toInstant()
164+
.toEpochMilli()
165+
// Reminder is valid if sent today
166+
return lastSent >= startOfDay
167+
}
168+
169+
fun markReminderSent(packageName: String) {
170+
reminderSentMap[packageName] = System.currentTimeMillis()
171+
}
172+
173+
fun clearReminderSent(packageName: String) {
174+
reminderSentMap.remove(packageName)
175+
}
176+
177+
fun isInGracePeriod(packageName: String): Boolean {
178+
val graceStart = gracePeriodStartMap[packageName] ?: return false
179+
val elapsed = System.currentTimeMillis() - graceStart
180+
return elapsed < GRACE_PERIOD_MS
181+
}
182+
183+
fun startGracePeriod(packageName: String) {
184+
gracePeriodStartMap[packageName] = System.currentTimeMillis()
185+
}
186+
187+
fun hasGracePeriodStarted(packageName: String): Boolean {
188+
val graceStart = gracePeriodStartMap[packageName] ?: return false
189+
val startOfDay = java.time.ZonedDateTime.ofInstant(
190+
java.time.Instant.now(),
191+
java.time.ZoneId.systemDefault()
192+
)
193+
.toLocalDate()
194+
.atStartOfDay(java.time.ZoneOffset.systemDefault())
195+
.toInstant()
196+
.toEpochMilli()
197+
// Grace period is valid if started today
198+
return graceStart >= startOfDay
199+
}
200+
201+
fun clearGracePeriod(packageName: String) {
202+
gracePeriodStartMap.remove(packageName)
203+
}
204+
205+
fun getRemainingGracePeriod(packageName: String): Long {
206+
val graceStart = gracePeriodStartMap[packageName] ?: return 0L
207+
val elapsed = System.currentTimeMillis() - graceStart
208+
val remaining = GRACE_PERIOD_MS - elapsed
209+
return if (remaining > 0) remaining else 0L
210+
}
150211
}
151212

152213
object Whitelist {

Reef/src/main/java/dev/pranav/reef/util/Constants.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ lateinit var prefs: SharedPreferences
77

88
val isPrefsInitialized: Boolean
99
get() = ::prefs.isInitialized
10+
11+
// Reminder and grace period constants
12+
const val REMINDER_TIME_MS = 10 * 60 * 1000L // 10 minutes before limit
13+
const val GRACE_PERIOD_MS = 5 * 60 * 1000L // 5 minutes grace period

Reef/src/main/java/dev/pranav/reef/util/NotificationHelper.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import dev.pranav.reef.data.Routine
1616

1717
object NotificationHelper {
1818
private const val ROUTINE_NOTIFICATION_ID = 100
19+
private const val REMINDER_NOTIFICATION_ID = 200
20+
private const val GRACE_PERIOD_NOTIFICATION_ID = 300
1921

2022
fun Context.createNotificationChannel() {
2123
val descriptionText = "Shows reminders for screen time and when apps are blocked."
@@ -81,4 +83,78 @@ object NotificationHelper {
8183
.notify(ROUTINE_NOTIFICATION_ID + 1, builder.build())
8284
}
8385
}
86+
87+
fun showReminderNotification(context: Context, packageName: String, timeRemaining: Long) {
88+
val appName = try {
89+
context.packageManager.getApplicationLabel(
90+
context.packageManager.getApplicationInfo(packageName, 0)
91+
)
92+
} catch (_: PackageManager.NameNotFoundException) {
93+
packageName
94+
}
95+
96+
val minutes = (timeRemaining / 60000).toInt()
97+
98+
val intent = Intent(context, dev.pranav.reef.AppUsageActivity::class.java)
99+
val pendingIntent = PendingIntent.getActivity(
100+
context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
101+
)
102+
103+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
104+
.setContentTitle("Time Limit Reminder")
105+
.setContentText("$appName will be blocked in $minutes minutes")
106+
.setSmallIcon(R.drawable.round_hourglass_disabled_24)
107+
.setAutoCancel(true)
108+
.setContentIntent(pendingIntent)
109+
.setPriority(NotificationCompat.PRIORITY_HIGH)
110+
111+
if (ActivityCompat.checkSelfPermission(
112+
context,
113+
Manifest.permission.POST_NOTIFICATIONS
114+
) == PackageManager.PERMISSION_GRANTED
115+
) {
116+
NotificationManagerCompat.from(context).notify(
117+
REMINDER_NOTIFICATION_ID + packageName.hashCode(),
118+
builder.build()
119+
)
120+
}
121+
}
122+
123+
fun showGracePeriodNotification(context: Context, packageName: String) {
124+
val appName = try {
125+
context.packageManager.getApplicationLabel(
126+
context.packageManager.getApplicationInfo(packageName, 0)
127+
)
128+
} catch (_: PackageManager.NameNotFoundException) {
129+
packageName
130+
}
131+
132+
val intent = Intent(context, dev.pranav.reef.AppUsageActivity::class.java)
133+
val pendingIntent = PendingIntent.getActivity(
134+
context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
135+
)
136+
137+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
138+
.setContentTitle("Time Limit Reached")
139+
.setContentText("$appName will be blocked in 5 minutes. Please finish up.")
140+
.setStyle(
141+
NotificationCompat.BigTextStyle()
142+
.bigText("You've reached your time limit for $appName. The app will be blocked in 5 minutes. Please wrap up your current activity.")
143+
)
144+
.setSmallIcon(R.drawable.round_hourglass_disabled_24)
145+
.setAutoCancel(true)
146+
.setContentIntent(pendingIntent)
147+
.setPriority(NotificationCompat.PRIORITY_MAX)
148+
149+
if (ActivityCompat.checkSelfPermission(
150+
context,
151+
Manifest.permission.POST_NOTIFICATIONS
152+
) == PackageManager.PERMISSION_GRANTED
153+
) {
154+
NotificationManagerCompat.from(context).notify(
155+
GRACE_PERIOD_NOTIFICATION_ID + packageName.hashCode(),
156+
builder.build()
157+
)
158+
}
159+
}
84160
}

Reef/src/main/java/dev/pranav/reef/util/RoutineLimits.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ object RoutineLimits {
99
private const val ACTIVE_ROUTINE_KEY = "active_routine_id"
1010
private const val ROUTINE_START_TIME_KEY = "routine_start_time"
1111
private val routineLimits = mutableMapOf<String, Long>()
12+
13+
// Track when reminders were sent for routine limits
14+
private val routineReminderSentMap = mutableMapOf<String, Long>()
15+
private val routineGracePeriodStartMap = mutableMapOf<String, Long>()
1216

1317
fun setRoutineLimits(limits: Map<String, Int>, routineId: String) {
1418
// Clear existing routine limits
@@ -138,4 +142,47 @@ object RoutineLimits {
138142
}
139143
}
140144
}
145+
146+
fun hasRoutineReminderBeenSent(packageName: String): Boolean {
147+
val lastSent = routineReminderSentMap[packageName] ?: return false
148+
val routineStartTime = prefs.getLong(ROUTINE_START_TIME_KEY, 0L)
149+
// Reminder is valid if sent during this routine session
150+
return lastSent >= routineStartTime
151+
}
152+
153+
fun markRoutineReminderSent(packageName: String) {
154+
routineReminderSentMap[packageName] = System.currentTimeMillis()
155+
}
156+
157+
fun clearRoutineReminderSent(packageName: String) {
158+
routineReminderSentMap.remove(packageName)
159+
}
160+
161+
fun isInRoutineGracePeriod(packageName: String): Boolean {
162+
val graceStart = routineGracePeriodStartMap[packageName] ?: return false
163+
val elapsed = System.currentTimeMillis() - graceStart
164+
return elapsed < GRACE_PERIOD_MS
165+
}
166+
167+
fun startRoutineGracePeriod(packageName: String) {
168+
routineGracePeriodStartMap[packageName] = System.currentTimeMillis()
169+
}
170+
171+
fun hasRoutineGracePeriodStarted(packageName: String): Boolean {
172+
val graceStart = routineGracePeriodStartMap[packageName] ?: return false
173+
val routineStartTime = prefs.getLong(ROUTINE_START_TIME_KEY, 0L)
174+
// Grace period is valid if started during this routine session
175+
return graceStart >= routineStartTime
176+
}
177+
178+
fun clearRoutineGracePeriod(packageName: String) {
179+
routineGracePeriodStartMap.remove(packageName)
180+
}
181+
182+
fun getRemainingRoutineGracePeriod(packageName: String): Long {
183+
val graceStart = routineGracePeriodStartMap[packageName] ?: return 0L
184+
val elapsed = System.currentTimeMillis() - graceStart
185+
val remaining = GRACE_PERIOD_MS - elapsed
186+
return if (remaining > 0) remaining else 0L
187+
}
141188
}

0 commit comments

Comments
 (0)