Skip to content

Commit 1c97520

Browse files
committed
feat: make Media Button configurable and clean
1 parent fb7b2b5 commit 1c97520

File tree

7 files changed

+243
-53
lines changed

7 files changed

+243
-53
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ android {
4747
applicationId = "com.ilseon"
4848
minSdk = 24
4949
targetSdk = 36
50-
versionCode = 120
51-
versionName = "0.39.0"
50+
versionCode = 121
51+
versionName = "0.40.0"
5252

5353
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
5454
}

app/src/main/AndroidManifest.xml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
<uses-permission android:name="android.permission.RECORD_AUDIO" />
1818
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
1919
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
20-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
2120

2221
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
2322

@@ -80,10 +79,14 @@
8079
<receiver
8180
android:name=".notifications.ReminderBroadcastReceiver"
8281
android:enabled="true"
83-
android:exported="false">
82+
android:exported="true">
8483
<intent-filter>
8584
<action android:name="android.intent.action.BOOT_COMPLETED" />
8685
</intent-filter>
86+
<intent-filter>
87+
<action android:name="com.ilseon.REMINDER_NOTIFICATION" />
88+
<action android:name="com.ilseon.REMINDER_HAPTIC" />
89+
</intent-filter>
8790
</receiver>
8891

8992
<receiver
@@ -106,8 +109,29 @@
106109
android:name=".service.RecordingService"
107110
android:enabled="true"
108111
android:exported="false"
109-
android:foregroundServiceType="microphone" />
112+
android:foregroundServiceType="microphone">
113+
<intent-filter>
114+
<action android:name="android.intent.action.MEDIA_BUTTON" />
115+
</intent-filter>
116+
</service>
117+
118+
<!-- Both receivers must remain: the system persists the last known media button
119+
receiver ComponentName across reboots. Different builds registered different
120+
receivers, so we keep both to ensure delivery regardless of which one the
121+
system has cached. Both forward to RecordingService. -->
122+
<receiver android:name=".service.MediaButtonBroadcastReceiver"
123+
android:exported="true">
124+
<intent-filter>
125+
<action android:name="android.intent.action.MEDIA_BUTTON" />
126+
</intent-filter>
127+
</receiver>
110128

129+
<receiver android:name="androidx.media.session.MediaButtonReceiver"
130+
android:exported="true">
131+
<intent-filter>
132+
<action android:name="android.intent.action.MEDIA_BUTTON" />
133+
</intent-filter>
134+
</receiver>
111135

112136
</application>
113137

app/src/main/java/com/ilseon/IlseonApplication.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package com.ilseon
22

33
import android.app.Application
4+
import android.content.Intent
5+
import android.os.Build
46
import androidx.work.Configuration
57
import androidx.work.ExistingPeriodicWorkPolicy
68
import androidx.work.PeriodicWorkRequestBuilder
79
import androidx.work.WorkManager
10+
import com.ilseon.data.task.SettingsRepository
811
import com.ilseon.notifications.NotificationHelper
912
import com.ilseon.service.HapticWorker
13+
import com.ilseon.service.RecordingService
1014
import dagger.hilt.android.HiltAndroidApp
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.flow.first
18+
import kotlinx.coroutines.launch
1119
import java.util.concurrent.TimeUnit
1220
import javax.inject.Inject
1321

@@ -17,13 +25,31 @@ class IlseonApplication : Application(), Configuration.Provider {
1725
@Inject
1826
lateinit var notificationHelper: NotificationHelper
1927

28+
@Inject
29+
lateinit var settingsRepository: SettingsRepository
30+
2031
override val workManagerConfiguration: Configuration
2132
get() = Configuration.Builder().build()
2233

2334
override fun onCreate() {
2435
super.onCreate()
2536
notificationHelper.createNotificationChannels()
2637
setupHapticWorker()
38+
startRecordingServiceIfNeeded()
39+
}
40+
41+
private fun startRecordingServiceIfNeeded() {
42+
CoroutineScope(Dispatchers.Main).launch {
43+
val enabled = settingsRepository.mediaButtonTriggerEnabled.first()
44+
if (enabled) {
45+
val intent = Intent(this@IlseonApplication, RecordingService::class.java)
46+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
47+
startForegroundService(intent)
48+
} else {
49+
startService(intent)
50+
}
51+
}
52+
}
2753
}
2854

2955
private fun setupHapticWorker() {

app/src/main/java/com/ilseon/SettingsViewModel.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.ilseon
22

3+
import android.content.Context
4+
import android.content.Intent
5+
import android.os.Build
36
import android.util.Log
47
import androidx.lifecycle.ViewModel
58
import androidx.lifecycle.viewModelScope
@@ -12,7 +15,9 @@ import com.ilseon.data.task.ReflectionExporter
1215
import com.ilseon.data.task.ReflectionImporter
1316
import com.ilseon.data.task.SettingsRepository
1417
import com.ilseon.data.task.TaskRepository
18+
import com.ilseon.service.RecordingService
1519
import dagger.hilt.android.lifecycle.HiltViewModel
20+
import dagger.hilt.android.qualifiers.ApplicationContext
1621
import kotlinx.coroutines.flow.SharingStarted
1722
import kotlinx.coroutines.flow.first
1823
import kotlinx.coroutines.flow.stateIn
@@ -22,6 +27,7 @@ import kotlin.onFailure
2227

2328
@HiltViewModel
2429
class SettingsViewModel @Inject constructor(
30+
@ApplicationContext private val context: Context,
2531
private val settingsRepository: SettingsRepository,
2632
private val taskRepository: TaskRepository,
2733
private val reflectionExporter: ReflectionExporter,
@@ -85,6 +91,17 @@ class SettingsViewModel @Inject constructor(
8591
viewModelScope.launch {
8692
settingsRepository.setMediaButtonTriggerEnabled(enabled)
8793
}
94+
// Start/stop the RecordingService so the MediaSession is alive to receive events
95+
val serviceIntent = Intent(context, RecordingService::class.java)
96+
if (enabled) {
97+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
98+
context.startForegroundService(serviceIntent)
99+
} else {
100+
context.startService(serviceIntent)
101+
}
102+
} else {
103+
context.stopService(serviceIntent)
104+
}
88105
}
89106

90107
fun setSstLanguage(language: String) {

app/src/main/java/com/ilseon/notifications/ReminderBroadcastReceiver.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import android.content.pm.PackageManager
88
import android.os.Build
99
import androidx.core.content.ContextCompat
1010
import com.ilseon.data.task.SchedulingType
11+
import com.ilseon.data.task.SettingsRepository
1112
import com.ilseon.data.task.TimerState
13+
import com.ilseon.service.RecordingService
1214
import dagger.hilt.android.AndroidEntryPoint
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.flow.first
18+
import kotlinx.coroutines.launch
1319
import javax.inject.Inject
1420

1521
@AndroidEntryPoint
@@ -18,9 +24,33 @@ class ReminderBroadcastReceiver : BroadcastReceiver() {
1824
@Inject
1925
lateinit var notificationHelper: NotificationHelper
2026

27+
@Inject
28+
lateinit var settingsRepository: SettingsRepository
29+
2130
override fun onReceive(context: Context, intent: Intent) {
22-
if (intent.action == "com.ilseon.REMINDER_NOTIFICATION") {
23-
handleNotification(context, intent)
31+
when (intent.action) {
32+
Intent.ACTION_BOOT_COMPLETED -> handleBootCompleted(context)
33+
"com.ilseon.REMINDER_NOTIFICATION" -> handleNotification(context, intent)
34+
}
35+
}
36+
37+
private fun handleBootCompleted(context: Context) {
38+
// Start RecordingService on boot if media button trigger is enabled
39+
val pendingResult = goAsync()
40+
CoroutineScope(Dispatchers.Main).launch {
41+
try {
42+
val enabled = settingsRepository.mediaButtonTriggerEnabled.first()
43+
if (enabled) {
44+
val serviceIntent = Intent(context, RecordingService::class.java)
45+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
46+
context.startForegroundService(serviceIntent)
47+
} else {
48+
context.startService(serviceIntent)
49+
}
50+
}
51+
} finally {
52+
pendingResult.finish()
53+
}
2454
}
2555
}
2656

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.ilseon.service
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.os.Build
7+
import android.util.Log
8+
import android.view.KeyEvent
9+
10+
/**
11+
* Receives MEDIA_BUTTON broadcasts from the system and forwards them to RecordingService.
12+
* Only forwards if the media button trigger setting is enabled — otherwise does nothing
13+
* so other apps (e.g. Spotify) can handle the event.
14+
*/
15+
class MediaButtonBroadcastReceiver : BroadcastReceiver() {
16+
17+
override fun onReceive(context: Context, intent: Intent) {
18+
if (intent.action != Intent.ACTION_MEDIA_BUTTON) return
19+
20+
// Check if the user has enabled the media button trigger
21+
val prefs = context.getSharedPreferences("app_settings", Context.MODE_PRIVATE)
22+
val enabled = prefs.getBoolean("media_button_trigger_enabled", false)
23+
if (!enabled) {
24+
Log.d("MediaButtonReceiver", "Media button trigger disabled, ignoring")
25+
return
26+
}
27+
28+
Log.d("MediaButtonReceiver", "onReceive: forwarding to RecordingService")
29+
30+
val serviceIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
31+
setClass(context, RecordingService::class.java)
32+
intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let {
33+
putExtra(Intent.EXTRA_KEY_EVENT, it)
34+
}
35+
}
36+
37+
try {
38+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
39+
context.startForegroundService(serviceIntent)
40+
} else {
41+
context.startService(serviceIntent)
42+
}
43+
} catch (e: Exception) {
44+
Log.e("MediaButtonReceiver", "Failed to start RecordingService", e)
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)