Skip to content

Commit 1c4bbd1

Browse files
committed
fix some routine launch issues
Signed-off-by: invokevirtual <purwarpranav80@gmail.com>
1 parent 33de6ee commit 1c4bbd1

11 files changed

Lines changed: 198 additions & 48 deletions

File tree

Reef/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@
103103
</intent-filter>
104104
</receiver>
105105

106+
<receiver
107+
android:name=".receivers.RoutineAlarmReceiver"
108+
android:directBootAware="true"
109+
android:exported="false" />
110+
106111
<provider
107112
android:name="androidx.startup.InitializationProvider"
108113
android:authorities="${applicationId}.androidx-startup"

Reef/src/main/java/dev/pranav/reef/App.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.work.*
1010
import com.google.android.material.color.DynamicColors
1111
import dev.pranav.reef.accessibility.BlockerService
1212
import dev.pranav.reef.receivers.DailySummaryScheduler
13+
import dev.pranav.reef.services.routines.RoutineAlarmScheduler
1314
import dev.pranav.reef.services.routines.RoutineSessionManager
1415
import dev.pranav.reef.util.*
1516
import java.util.concurrent.TimeUnit
@@ -36,6 +37,7 @@ class App: Application(), Configuration.Provider {
3637

3738
RoutineSessionManager.evaluateAndSync(this)
3839
NotificationHelper.syncRoutineNotification(this)
40+
RoutineAlarmScheduler.scheduleAll(this, dev.pranav.reef.routine.Routines.getAll())
3941

4042
if (prefs.getBoolean("daily_summary", false)) {
4143
DailySummaryScheduler.scheduleDailySummary(this)

Reef/src/main/java/dev/pranav/reef/receivers/BootReceiver.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.util.Log
77
import androidx.core.content.edit
88
import dev.pranav.reef.accessibility.BlockerService
99
import dev.pranav.reef.accessibility.FocusModeService
10+
import dev.pranav.reef.services.routines.RoutineAlarmScheduler
1011
import dev.pranav.reef.services.routines.RoutineSessionManager
1112
import dev.pranav.reef.util.NotificationHelper
1213
import dev.pranav.reef.util.isAccessibilityServiceEnabledForBlocker
@@ -33,6 +34,10 @@ class BootReceiver: BroadcastReceiver() {
3334

3435
RoutineSessionManager.evaluateAndSync(safeContext)
3536
NotificationHelper.syncRoutineNotification(safeContext)
37+
RoutineAlarmScheduler.scheduleAll(
38+
safeContext,
39+
dev.pranav.reef.routine.Routines.getAll()
40+
)
3641

3742
if (prefs.getBoolean("daily_summary", false)) {
3843
DailySummaryScheduler.scheduleDailySummary(safeContext)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.pranav.reef.receivers
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import dev.pranav.reef.services.routines.RoutineSessionManager
7+
import dev.pranav.reef.util.NotificationHelper
8+
import dev.pranav.reef.util.isPrefsInitialized
9+
import dev.pranav.reef.util.prefs
10+
11+
class RoutineAlarmReceiver: BroadcastReceiver() {
12+
override fun onReceive(context: Context, intent: Intent) {
13+
val safeContext = context.createDeviceProtectedStorageContext()
14+
if (!isPrefsInitialized) {
15+
prefs = safeContext.getSharedPreferences("prefs", Context.MODE_PRIVATE)
16+
}
17+
RoutineSessionManager.evaluateAndSync(safeContext)
18+
NotificationHelper.syncRoutineNotification(safeContext)
19+
}
20+
}
21+

Reef/src/main/java/dev/pranav/reef/routine/Routines.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.util.Log
55
import androidx.core.content.edit
66
import dev.pranav.reef.data.Routine
77
import dev.pranav.reef.data.RoutineSchedule
8+
import dev.pranav.reef.services.routines.RoutineAlarmScheduler
89
import dev.pranav.reef.services.routines.RoutineSessionManager
910
import dev.pranav.reef.util.prefs
1011
import org.json.JSONArray
@@ -72,6 +73,7 @@ object Routines {
7273

7374
if (!updated.isEnabled) {
7475
RoutineSessionManager.stopSession(context, id)
76+
RoutineAlarmScheduler.cancel(context, id)
7577
} else {
7678
when (updated.schedule.type) {
7779
RoutineSchedule.ScheduleType.MANUAL -> {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package dev.pranav.reef.services.routines
2+
3+
import android.app.AlarmManager
4+
import android.app.PendingIntent
5+
import android.content.Context
6+
import android.content.Intent
7+
import dev.pranav.reef.data.Routine
8+
import dev.pranav.reef.data.RoutineSchedule
9+
import dev.pranav.reef.receivers.RoutineAlarmReceiver
10+
import java.time.LocalDateTime
11+
12+
object RoutineAlarmScheduler {
13+
14+
fun scheduleNextStart(context: Context, routine: Routine) {
15+
if (!routine.isEnabled) return
16+
if (routine.schedule.type == RoutineSchedule.ScheduleType.MANUAL) return
17+
val nextStartMs = RoutineTimeCalculator.getNextWindowStartMs(
18+
routine.schedule, LocalDateTime.now()
19+
) ?: return
20+
scheduleAlarm(context, startRequestCode(routine.id), nextStartMs)
21+
}
22+
23+
fun scheduleEnd(context: Context, routineId: String, endTimeMs: Long) {
24+
if (endTimeMs <= 0L) return
25+
scheduleAlarm(context, endRequestCode(routineId), endTimeMs)
26+
}
27+
28+
fun cancel(context: Context, routineId: String) {
29+
cancelAlarm(context, startRequestCode(routineId))
30+
cancelAlarm(context, endRequestCode(routineId))
31+
}
32+
33+
fun scheduleAll(context: Context, routines: List<Routine>) {
34+
routines.filter { it.isEnabled }.forEach { scheduleNextStart(context, it) }
35+
}
36+
37+
private fun scheduleAlarm(context: Context, requestCode: Int, triggerAtMs: Long) {
38+
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
39+
alarmManager.setExactAndAllowWhileIdle(
40+
AlarmManager.RTC_WAKEUP,
41+
triggerAtMs,
42+
buildPendingIntent(context, requestCode)
43+
)
44+
}
45+
46+
private fun cancelAlarm(context: Context, requestCode: Int) {
47+
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
48+
alarmManager.cancel(buildPendingIntent(context, requestCode))
49+
}
50+
51+
private fun buildPendingIntent(context: Context, requestCode: Int): PendingIntent {
52+
val intent = Intent(context, RoutineAlarmReceiver::class.java)
53+
return PendingIntent.getBroadcast(
54+
context,
55+
requestCode,
56+
intent,
57+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
58+
)
59+
}
60+
61+
private fun startRequestCode(routineId: String) = "start:$routineId".hashCode()
62+
private fun endRequestCode(routineId: String) = "end:$routineId".hashCode()
63+
}
64+

Reef/src/main/java/dev/pranav/reef/services/routines/RoutineSessionManager.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ object RoutineSessionManager {
4646
for (session in expired) {
4747
Log.d(TAG, "Expiring session: ${session.routineId}")
4848
stopSessionInternal(context, session.routineId)
49+
routines.find { it.id == session.routineId }?.let { routine ->
50+
if (routine.isEnabled) RoutineAlarmScheduler.scheduleNextStart(context, routine)
51+
}
4952
changed = true
5053
}
5154

@@ -99,8 +102,9 @@ object RoutineSessionManager {
99102
} else {
100103
Log.d(
101104
TAG,
102-
"Routine ${routine.name} is not within schedule window, will activate on schedule"
105+
"Routine ${routine.name} is outside schedule window, scheduling alarm for next start"
103106
)
107+
RoutineAlarmScheduler.scheduleNextStart(context, routine)
104108
}
105109
}
106110

@@ -135,6 +139,9 @@ object RoutineSessionManager {
135139
TAG,
136140
"Started session for ${routine.name}: ${limits.size} limits, ${sharedGroups.size} groups, endTime=${if (endTime == 0L) "never" else endTime}"
137141
)
142+
if (endTime > 0L) {
143+
RoutineAlarmScheduler.scheduleEnd(context, routine.id, endTime)
144+
}
138145
NotificationHelper.syncRoutineNotification(context)
139146
}
140147

@@ -182,7 +189,8 @@ object RoutineSessionManager {
182189
}
183190

184191
fun getLimitMs(packageName: String): Long? {
185-
val sessions = getActiveSessions()
192+
val now = System.currentTimeMillis()
193+
val sessions = getActiveSessions().filter { it.endTime == 0L || now < it.endTime }
186194
if (sessions.isEmpty()) return null
187195

188196
var strictestLimit: Long? = null
@@ -206,7 +214,8 @@ object RoutineSessionManager {
206214
}
207215

208216
fun getUsageMs(context: Context, packageName: String): Long {
209-
val sessions = getActiveSessions()
217+
val now = System.currentTimeMillis()
218+
val sessions = getActiveSessions().filter { it.endTime == 0L || now < it.endTime }
210219
if (sessions.isEmpty()) return 0L
211220

212221
val usm = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager

Reef/src/main/java/dev/pranav/reef/services/routines/RoutineTimeCalculator.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,37 @@ object RoutineTimeCalculator {
4242
return null
4343
}
4444

45+
fun getNextWindowStartMs(schedule: RoutineSchedule, now: LocalDateTime): Long? {
46+
if (schedule.type == RoutineSchedule.ScheduleType.MANUAL) return null
47+
val startTime = schedule.time ?: return null
48+
val today = now.toLocalDate()
49+
50+
return when (schedule.type) {
51+
RoutineSchedule.ScheduleType.DAILY -> {
52+
val todayStart = LocalDateTime.of(today, startTime)
53+
if (now.isBefore(todayStart)) {
54+
todayStart.toEpochMs()
55+
} else {
56+
LocalDateTime.of(today.plusDays(1), startTime).toEpochMs()
57+
}
58+
}
59+
60+
RoutineSchedule.ScheduleType.WEEKLY -> {
61+
if (schedule.daysOfWeek.isEmpty()) return null
62+
for (daysAhead in 0..7) {
63+
val candidate = today.plusDays(daysAhead.toLong())
64+
if (candidate.dayOfWeek !in schedule.daysOfWeek) continue
65+
val candidateStart = LocalDateTime.of(candidate, startTime)
66+
if (daysAhead == 0 && !now.isBefore(candidateStart)) continue
67+
return candidateStart.toEpochMs()
68+
}
69+
null
70+
}
71+
72+
RoutineSchedule.ScheduleType.MANUAL -> null
73+
}
74+
}
75+
4576
fun getDurationMs(schedule: RoutineSchedule): Long {
4677
val startTime = schedule.time ?: return 24 * 60 * 60 * 1000L
4778
val endTime = schedule.endTime ?: return 24 * 60 * 60 * 1000L

Reef/src/main/java/dev/pranav/reef/ui/whitelist/WhitelistScreen.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
44
import android.graphics.Canvas
55
import android.graphics.drawable.BitmapDrawable
66
import android.graphics.drawable.Drawable
7+
import android.os.UserHandle
78
import androidx.compose.animation.*
89
import androidx.compose.animation.core.Spring
910
import androidx.compose.animation.core.spring
@@ -36,7 +37,8 @@ data class WhitelistedApp(
3637
val packageName: String,
3738
val label: String,
3839
val icon: ImageBitmap,
39-
val isWhitelisted: Boolean
40+
val isWhitelisted: Boolean,
41+
val user: UserHandle
4042
)
4143

4244
sealed interface AllowedAppsState {
@@ -138,7 +140,7 @@ fun WhitelistScreen(
138140
) {
139141
itemsIndexed(
140142
items = uiState.apps,
141-
key = { _, app -> app.packageName }
143+
key = { _, app -> app.packageName + app.user.hashCode() }
142144
) { index, app ->
143145
WhitelistItem(
144146
app = app,

Reef/src/main/java/dev/pranav/reef/ui/whitelist/WhitelistViewModel.kt

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package dev.pranav.reef.ui.whitelist
33
import android.content.pm.LauncherApps
44
import android.content.pm.PackageManager
55
import android.os.Build
6-
import android.os.Process
76
import androidx.compose.runtime.State
87
import androidx.compose.runtime.mutableStateOf
98
import androidx.compose.ui.graphics.asImageBitmap
@@ -35,46 +34,52 @@ class WhitelistViewModel(
3534
private fun loadApps() {
3635
viewModelScope.launch {
3736
val apps = withContext(Dispatchers.IO) {
38-
val launcherPackages = launcherApps.getActivityList(null, Process.myUserHandle())
39-
.distinctBy { it.applicationInfo.packageName }
40-
.associate { it.applicationInfo.packageName to it.applicationInfo }
41-
42-
val systemApps =
43-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
44-
launcherApps.getPreInstalledSystemPackages(Process.myUserHandle())
45-
.mapNotNull {
46-
runCatching {
47-
packageManager.getApplicationInfo(
48-
it,
49-
0
50-
)
51-
}.getOrNull()
52-
}.associateBy { it.packageName }
53-
} else {
54-
packageManager
55-
.getInstalledPackages(PackageManager.GET_PERMISSIONS)
56-
.mapNotNull { pkgInfo ->
57-
runCatching {
58-
packageManager.getApplicationInfo(
59-
pkgInfo.packageName,
60-
0
61-
)
62-
}.getOrNull()
63-
}.associateBy { it.packageName }
64-
}
65-
66-
(launcherPackages + systemApps)
67-
.filterKeys { it != currentPackageName }
68-
.values
69-
.map { appInfo ->
70-
WhitelistedApp(
71-
packageName = appInfo.packageName,
72-
label = appInfo.loadLabel(packageManager).toString(),
73-
icon = appInfo.loadIcon(packageManager).toBitmap().asImageBitmap(),
74-
isWhitelisted = Whitelist.isWhitelisted(appInfo.packageName)
75-
)
76-
}
77-
.sortedBy { it.label }
37+
val profiles = launcherApps.profiles
38+
val allAppsList = mutableListOf<WhitelistedApp>()
39+
40+
profiles.forEach { userHandle ->
41+
// Fetch apps for the specific profile (Personal, Work, etc.)
42+
val launcherActivities = launcherApps.getActivityList(null, userHandle)
43+
.distinctBy { it.applicationInfo.packageName }
44+
45+
val profileSystemApps =
46+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
47+
launcherApps.getPreInstalledSystemPackages(userHandle)
48+
.mapNotNull { pkg ->
49+
runCatching {
50+
packageManager.getApplicationInfo(
51+
pkg,
52+
0
53+
)
54+
}.getOrNull()
55+
}
56+
} else {
57+
emptyList()
58+
}
59+
60+
val combined =
61+
(launcherActivities.map { it.applicationInfo } + profileSystemApps)
62+
.distinctBy { it.packageName }
63+
.filter { it.packageName != currentPackageName }
64+
.map { appInfo ->
65+
val originalIcon = appInfo.loadIcon(packageManager)
66+
67+
// Wrap icon with the "Work Badge" if it belongs to a managed profile
68+
val badgedIcon =
69+
packageManager.getUserBadgedIcon(originalIcon, userHandle)
70+
71+
WhitelistedApp(
72+
packageName = appInfo.packageName,
73+
label = appInfo.loadLabel(packageManager).toString(),
74+
icon = badgedIcon.toBitmap().asImageBitmap(),
75+
isWhitelisted = Whitelist.isWhitelisted(appInfo.packageName),
76+
user = userHandle
77+
)
78+
}
79+
allAppsList.addAll(combined)
80+
}
81+
// Sort by label; keep duplicates if they belong to different users
82+
allAppsList.sortedBy { it.label }
7883
}
7984
allApps = apps
8085
updateFilteredList()
@@ -104,7 +109,11 @@ class WhitelistViewModel(
104109
else Whitelist.whitelist(app.packageName)
105110

106111
allApps = allApps.map {
107-
if (it.packageName == app.packageName) it.copy(isWhitelisted = !it.isWhitelisted) else it
112+
if (it.packageName == app.packageName && it.user == app.user) {
113+
it.copy(isWhitelisted = !it.isWhitelisted)
114+
} else {
115+
it
116+
}
108117
}
109118
updateFilteredList()
110119
}

0 commit comments

Comments
 (0)