Skip to content

Commit 25cff12

Browse files
Merge pull request #243 from Ayush0Chaudhary/triggers
Triggers :godmode:
2 parents a3176c2 + 67f126f commit 25cff12

19 files changed

+1231
-0
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,7 @@ dependencies {
142142
implementation("com.google.firebase:firebase-crashlytics-ndk")
143143
implementation(libs.firebase.firestore)
144144

145+
implementation("androidx.recyclerview:recyclerview:1.3.2")
146+
147+
145148
}

app/src/main/AndroidManifest.xml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
1414
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
1515
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
16+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
17+
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
1618

1719
<application
1820
android:name=".MyApplication"
@@ -70,6 +72,10 @@
7072
</intent-filter>
7173
</activity>
7274

75+
<activity android:name=".triggers.ui.TriggersActivity" android:exported="false" android:label="Triggers" android:theme="@style/Theme.Blurr" />
76+
<activity android:name=".triggers.ui.CreateTriggerActivity" android:exported="false" android:label="Create Trigger" android:theme="@style/Theme.Blurr" />
77+
<activity android:name=".triggers.ui.ChooseTriggerTypeActivity" android:exported="false" android:label="Choose Trigger Type" android:theme="@style/Theme.Blurr" />
78+
7379

7480
<!-- Your Other Services -->
7581
<service
@@ -116,6 +122,16 @@
116122
android:enabled="true"
117123
android:exported="false" />
118124

125+
<service
126+
android:name=".triggers.PandaNotificationListenerService"
127+
android:label="@string/app_name"
128+
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
129+
android:exported="true">
130+
<intent-filter>
131+
<action android:name="android.service.notification.NotificationListenerService" />
132+
</intent-filter>
133+
</service>
134+
119135
<!-- Your Widget Provider -->
120136
<receiver
121137
android:name=".PandaWidgetProvider"
@@ -128,6 +144,26 @@
128144
android:resource="@xml/panda_widget_info" />
129145
</receiver>
130146

147+
<!-- Central receiver for all app triggers -->
148+
<receiver
149+
android:name=".triggers.TriggerReceiver"
150+
android:enabled="true"
151+
android:exported="false">
152+
<intent-filter>
153+
<action android:name="com.blurr.voice.action.EXECUTE_TASK" />
154+
</intent-filter>
155+
</receiver>
156+
157+
<!-- Receiver to reschedule alarms on device reboot -->
158+
<receiver
159+
android:name=".triggers.BootReceiver"
160+
android:enabled="true"
161+
android:exported="true">
162+
<intent-filter>
163+
<action android:name="android.intent.action.BOOT_COMPLETED" />
164+
</intent-filter>
165+
</receiver>
166+
131167
</application>
132168

133169
</manifest>

app/src/main/java/com/blurr/voice/MainActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ class MainActivity : AppCompatActivity() {
273273
}
274274

275275
private fun setupClickListeners() {
276+
findViewById<TextView>(R.id.triggersButton).setOnClickListener {
277+
startActivity(Intent(this, com.blurr.voice.triggers.ui.TriggersActivity::class.java))
278+
}
276279
findViewById<TextView>(R.id.memoriesButton).setOnClickListener {
277280
startActivity(Intent(this, MemoriesActivity::class.java))
278281
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.blurr.voice.triggers
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.util.Log
7+
import kotlinx.coroutines.CoroutineScope
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.launch
10+
11+
class BootReceiver : BroadcastReceiver() {
12+
13+
private val TAG = "BootReceiver"
14+
15+
override fun onReceive(context: Context, intent: Intent) {
16+
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
17+
Log.d(TAG, "Device boot completed. Rescheduling alarms.")
18+
val triggerManager = TriggerManager.getInstance(context)
19+
20+
// It's good practice to do this work off the main thread
21+
CoroutineScope(Dispatchers.IO).launch {
22+
val triggers = triggerManager.getTriggers()
23+
val scheduledTriggers = triggers.filter { it.isEnabled && it.type == TriggerType.SCHEDULED_TIME }
24+
scheduledTriggers.forEach { trigger ->
25+
// In the future, we might have different logic for rescheduling
26+
// but for now, just calling schedule is fine as it will recreate the alarm.
27+
triggerManager.updateTrigger(trigger)
28+
}
29+
Log.d(TAG, "Finished rescheduling ${scheduledTriggers.size} alarms.")
30+
}
31+
}
32+
}
33+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.blurr.voice.triggers
2+
3+
import android.service.notification.NotificationListenerService
4+
import android.service.notification.StatusBarNotification
5+
import android.util.Log
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.launch
9+
10+
class PandaNotificationListenerService : NotificationListenerService() {
11+
12+
private val TAG = "PandaNotification"
13+
private lateinit var triggerManager: TriggerManager
14+
15+
override fun onCreate() {
16+
super.onCreate()
17+
triggerManager = TriggerManager.getInstance(this)
18+
}
19+
20+
override fun onNotificationPosted(sbn: StatusBarNotification?) {
21+
super.onNotificationPosted(sbn)
22+
if (sbn == null) return
23+
24+
val packageName = sbn.packageName
25+
Log.d(TAG, "Notification posted from package: $packageName")
26+
27+
CoroutineScope(Dispatchers.IO).launch {
28+
val notificationTriggers = triggerManager.getTriggers()
29+
.filter { it.type == TriggerType.NOTIFICATION && it.isEnabled }
30+
31+
val matchingTrigger = notificationTriggers.find { it.packageName == packageName }
32+
33+
if (matchingTrigger != null) {
34+
Log.d(TAG, "Found matching trigger for package: $packageName. Executing instruction: ${matchingTrigger.instruction}")
35+
// Use the TriggerReceiver to start the agent service
36+
val intent = android.content.Intent(this@PandaNotificationListenerService, TriggerReceiver::class.java).apply {
37+
action = TriggerReceiver.ACTION_EXECUTE_TASK
38+
putExtra(TriggerReceiver.EXTRA_TASK_INSTRUCTION, matchingTrigger.instruction)
39+
}
40+
sendBroadcast(intent)
41+
}
42+
}
43+
}
44+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.blurr.voice.triggers
2+
3+
import android.content.Context
4+
import android.provider.Settings
5+
6+
import android.app.AlarmManager
7+
import android.os.Build
8+
9+
object PermissionUtils {
10+
11+
fun isNotificationListenerEnabled(context: Context): Boolean {
12+
val enabledListeners = Settings.Secure.getString(context.contentResolver, "enabled_notification_listeners")
13+
val componentName = PandaNotificationListenerService::class.java.canonicalName
14+
return enabledListeners?.contains(componentName) == true
15+
}
16+
17+
fun canScheduleExactAlarms(context: Context): Boolean {
18+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
19+
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
20+
alarmManager.canScheduleExactAlarms()
21+
} else {
22+
true // Permission is granted by default on older versions
23+
}
24+
}
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.blurr.voice.triggers
2+
3+
import java.util.UUID
4+
5+
enum class TriggerType {
6+
SCHEDULED_TIME,
7+
NOTIFICATION
8+
}
9+
10+
data class Trigger(
11+
val id: String = UUID.randomUUID().toString(),
12+
val type: TriggerType,
13+
val instruction: String,
14+
var isEnabled: Boolean = true,
15+
// For SCHEDULED_TIME triggers
16+
val hour: Int? = null,
17+
val minute: Int? = null,
18+
// For NOTIFICATION triggers
19+
val packageName: String? = null,
20+
val appName: String? = null, // For display purposes
21+
// For SCHEDULED_TIME triggers
22+
val daysOfWeek: Set<Int> = setOf(1, 2, 3, 4, 5, 6, 7) // Default to all days
23+
)
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.blurr.voice.triggers
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import com.google.gson.Gson
6+
import com.google.gson.reflect.TypeToken
7+
8+
import android.app.AlarmManager
9+
import android.app.PendingIntent
10+
import android.content.Intent
11+
import java.util.Calendar
12+
13+
class TriggerManager(private val context: Context) {
14+
15+
private val sharedPreferences: SharedPreferences by lazy {
16+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
17+
}
18+
private val alarmManager: AlarmManager by lazy {
19+
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
20+
}
21+
private val gson = Gson()
22+
23+
fun addTrigger(trigger: Trigger) {
24+
val triggers = loadTriggers()
25+
triggers.add(trigger)
26+
saveTriggers(triggers)
27+
if (trigger.isEnabled) {
28+
scheduleAlarm(trigger)
29+
}
30+
}
31+
32+
fun removeTrigger(trigger: Trigger) {
33+
val triggers = loadTriggers()
34+
val triggerToRemove = triggers.find { it.id == trigger.id }
35+
if (triggerToRemove != null) {
36+
cancelAlarm(triggerToRemove)
37+
triggers.remove(triggerToRemove)
38+
saveTriggers(triggers)
39+
}
40+
}
41+
42+
fun getTriggers(): List<Trigger> {
43+
return loadTriggers()
44+
}
45+
46+
fun rescheduleTrigger(triggerId: String) {
47+
val triggers = loadTriggers()
48+
val trigger = triggers.find { it.id == triggerId }
49+
if (trigger != null && trigger.isEnabled) {
50+
// This will calculate the next day's time and set a new exact alarm
51+
scheduleAlarm(trigger)
52+
android.util.Log.d("TriggerManager", "Rescheduled trigger: ${trigger.id}")
53+
}
54+
}
55+
56+
fun updateTrigger(trigger: Trigger) {
57+
val triggers = loadTriggers()
58+
val index = triggers.indexOfFirst { it.id == trigger.id }
59+
if (index != -1) {
60+
triggers[index] = trigger
61+
saveTriggers(triggers)
62+
if (trigger.isEnabled) {
63+
scheduleAlarm(trigger)
64+
} else {
65+
cancelAlarm(trigger)
66+
}
67+
}
68+
}
69+
70+
private fun scheduleAlarm(trigger: Trigger) {
71+
if (trigger.type != TriggerType.SCHEDULED_TIME) {
72+
android.util.Log.w("TriggerManager", "Attempted to schedule alarm for non-time-based trigger: ${trigger.id}")
73+
return
74+
}
75+
76+
val intent = Intent(context, TriggerReceiver::class.java).apply {
77+
action = TriggerReceiver.ACTION_EXECUTE_TASK
78+
putExtra(TriggerReceiver.EXTRA_TASK_INSTRUCTION, trigger.instruction)
79+
putExtra(TriggerReceiver.EXTRA_TRIGGER_ID, trigger.id)
80+
}
81+
82+
val pendingIntent = PendingIntent.getBroadcast(
83+
context,
84+
trigger.id.hashCode(),
85+
intent,
86+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
87+
)
88+
89+
val nextTriggerTime = getNextTriggerTime(trigger.hour!!, trigger.minute!!, trigger.daysOfWeek)
90+
if (nextTriggerTime == null) {
91+
android.util.Log.w("TriggerManager", "No valid day of week for trigger: ${trigger.id}")
92+
return
93+
}
94+
95+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
96+
if (alarmManager.canScheduleExactAlarms()) {
97+
alarmManager.setExactAndAllowWhileIdle(
98+
AlarmManager.RTC_WAKEUP,
99+
nextTriggerTime.timeInMillis,
100+
pendingIntent
101+
)
102+
} else {
103+
// Handle case where permission is not granted.
104+
// For now, we'll log a warning. The UI part of the plan will handle prompting the user.
105+
android.util.Log.w("TriggerManager", "Cannot schedule exact alarm, permission not granted.")
106+
}
107+
} else {
108+
alarmManager.setExactAndAllowWhileIdle(
109+
AlarmManager.RTC_WAKEUP,
110+
nextTriggerTime.timeInMillis,
111+
pendingIntent
112+
)
113+
}
114+
}
115+
116+
private fun getNextTriggerTime(hour: Int, minute: Int, daysOfWeek: Set<Int>): Calendar? {
117+
val now = Calendar.getInstance()
118+
var nextTrigger = Calendar.getInstance().apply {
119+
set(Calendar.HOUR_OF_DAY, hour)
120+
set(Calendar.MINUTE, minute)
121+
set(Calendar.SECOND, 0)
122+
set(Calendar.MILLISECOND, 0)
123+
}
124+
125+
for (i in 0..7) {
126+
val day = (now.get(Calendar.DAY_OF_WEEK) + i - 1) % 7 + 1
127+
if (day in daysOfWeek) {
128+
nextTrigger.add(Calendar.DAY_OF_YEAR, i)
129+
if (nextTrigger.after(now)) {
130+
return nextTrigger
131+
}
132+
// Reset for next iteration
133+
nextTrigger.add(Calendar.DAY_OF_YEAR, -i)
134+
}
135+
}
136+
return null // Should not happen if at least one day is selected
137+
}
138+
139+
private fun cancelAlarm(trigger: Trigger) {
140+
if (trigger.type != TriggerType.SCHEDULED_TIME) {
141+
return // No alarm to cancel for non-time-based triggers
142+
}
143+
val intent = Intent(context, TriggerReceiver::class.java).apply {
144+
action = TriggerReceiver.ACTION_EXECUTE_TASK
145+
}
146+
val pendingIntent = PendingIntent.getBroadcast(
147+
context,
148+
trigger.id.hashCode(),
149+
intent,
150+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
151+
)
152+
alarmManager.cancel(pendingIntent)
153+
}
154+
155+
private fun saveTriggers(triggers: List<Trigger>) {
156+
val json = gson.toJson(triggers)
157+
sharedPreferences.edit().putString(KEY_TRIGGERS, json).apply()
158+
}
159+
160+
private fun loadTriggers(): MutableList<Trigger> {
161+
val json = sharedPreferences.getString(KEY_TRIGGERS, null)
162+
return if (json != null) {
163+
val type = object : TypeToken<MutableList<Trigger>>() {}.type
164+
gson.fromJson(json, type)
165+
} else {
166+
mutableListOf()
167+
}
168+
}
169+
170+
companion object {
171+
private const val PREFS_NAME = "com.blurr.voice.triggers.prefs"
172+
private const val KEY_TRIGGERS = "triggers_list"
173+
174+
@Volatile
175+
private var INSTANCE: TriggerManager? = null
176+
177+
fun getInstance(context: Context): TriggerManager {
178+
return INSTANCE ?: synchronized(this) {
179+
val instance = TriggerManager(context.applicationContext)
180+
INSTANCE = instance
181+
instance
182+
}
183+
}
184+
}
185+
}

0 commit comments

Comments
 (0)