Skip to content

Commit c8f11a8

Browse files
committed
Add optional foreground service when watch connection active
Fixes MOB-6883
1 parent 2579397 commit c8f11a8

17 files changed

Lines changed: 173 additions & 152 deletions

File tree

composeApp/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ kotlin {
164164
implementation(libs.play.update)
165165
implementation(libs.play.update.ktx)
166166
implementation(libs.coil.gif)
167+
implementation(libs.coredevices.haversine)
167168
}
168169
androidInstrumentedTest.dependencies {
169170
implementation(libs.androidx.test.runner)

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@
9292
android:enabled="true"
9393
android:exported="false"
9494
android:foregroundServiceType="connectedDevice" />
95+
<service
96+
android:name=".PebbleService"
97+
android:foregroundServiceType="connectedDevice"
98+
android:exported="false" />
9599
<service android:name="coredevices.util.models.ModelDownloadService"
96100
android:permission="android.permission.BIND_JOB_SERVICE"
97101
android:exported="false">

composeApp/src/androidMain/kotlin/coredevices/coreapp/MainApplication.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class MainApplication : Application(), SingletonImageLoader.Factory {
5757
private val experimentalDevices: ExperimentalDevices by inject()
5858
private val fileLogWriter: FileLogWriter by inject()
5959
private val coreConfigHolder: CoreConfigHolder by inject()
60+
private val pebbleBackgroundManager: PebbleBackgroundManager by inject()
6061

6162
override fun onCreate() {
6263
super.onCreate()
@@ -97,6 +98,7 @@ class MainApplication : Application(), SingletonImageLoader.Factory {
9798
)
9899
scheduleBackgroundJob(AppContext(this), coreConfigHolder.config.value)
99100
commonAppDelegate.init()
101+
pebbleBackgroundManager.monitorToStartBackground()
100102
}
101103

102104
private fun dumpPreviousExitInfo() {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package coredevices.coreapp
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import androidx.core.content.ContextCompat
6+
import co.touchlab.kermit.Logger
7+
import coredevices.ring.database.Preferences
8+
import coredevices.util.CoreConfigFlow
9+
import io.rebble.libpebblecommon.connection.ActiveDevice
10+
import io.rebble.libpebblecommon.connection.LibPebble
11+
import kotlinx.coroutines.GlobalScope
12+
import kotlinx.coroutines.flow.MutableStateFlow
13+
import kotlinx.coroutines.flow.StateFlow
14+
import kotlinx.coroutines.flow.asStateFlow
15+
import kotlinx.coroutines.flow.combine
16+
import kotlinx.coroutines.flow.distinctUntilChanged
17+
import kotlinx.coroutines.flow.launchIn
18+
import kotlinx.coroutines.flow.onEach
19+
20+
class PebbleBackgroundManager(
21+
private val context: Context,
22+
private val commonPrefs: Preferences,
23+
private val coreConfigFlow: CoreConfigFlow,
24+
private val libPebble: LibPebble,
25+
) {
26+
companion object {
27+
private val logger = Logger.withTag("PebbleBackgroundManager")
28+
}
29+
30+
private fun startBackground() {
31+
ContextCompat.startForegroundService(context, Intent(context, PebbleService::class.java))
32+
}
33+
34+
private fun stopBackground() {
35+
val serviceIntent = Intent(context, PebbleService::class.java).apply {
36+
action = PebbleService.ACTION_STOP
37+
}
38+
ContextCompat.startForegroundService(context, serviceIntent)
39+
}
40+
41+
fun monitorToStartBackground() {
42+
combine(
43+
commonPrefs.ringPaired,
44+
coreConfigFlow.flow,
45+
libPebble.bluetoothEnabled,
46+
libPebble.watches,
47+
) { ringPaired, config, btState, watches ->
48+
val ringActive = ringPaired != null
49+
val watchKeepAlive = config.androidForegroundServiceForWatchConnection &&
50+
btState.enabled() &&
51+
watches.any { it is ActiveDevice }
52+
ringActive || watchKeepAlive
53+
}
54+
.distinctUntilChanged()
55+
.onEach { shouldRun ->
56+
logger.d { "shouldRun=$shouldRun isRunning=${isRunning.value}" }
57+
if (shouldRun && !isRunning.value) {
58+
startBackground()
59+
} else if (!shouldRun && isRunning.value) {
60+
stopBackground()
61+
}
62+
}
63+
.launchIn(GlobalScope)
64+
}
65+
66+
fun onServiceStarted() {
67+
_isRunning.value = true
68+
}
69+
70+
fun onServiceStopped() {
71+
_isRunning.value = false
72+
}
73+
74+
private val _isRunning: MutableStateFlow<Boolean> = MutableStateFlow(false)
75+
val isRunning: StateFlow<Boolean> = _isRunning.asStateFlow()
76+
}

experimental/src/androidMain/kotlin/coredevices/ring/service/RingService.kt renamed to composeApp/src/androidMain/kotlin/coredevices/coreapp/PebbleService.kt

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,45 @@
1-
package coredevices.ring.service
1+
package coredevices.coreapp
22

3-
import android.Manifest
43
import android.app.NotificationManager
5-
import android.app.PendingIntent
64
import android.app.Service
75
import android.content.Intent
8-
import android.content.pm.PackageManager
96
import android.content.pm.ServiceInfo
107
import android.os.Build
118
import android.os.IBinder
12-
import androidx.core.app.ActivityCompat
139
import androidx.core.app.NotificationChannelCompat
1410
import androidx.core.app.NotificationCompat
1511
import androidx.core.app.NotificationManagerCompat
1612
import androidx.core.app.ServiceCompat
1713
import co.touchlab.kermit.Logger
1814
import coredevices.haversine.KMPHaversineSatelliteManager
15+
import coredevices.ring.database.Preferences
16+
import coredevices.ring.service.IndexNotificationManager
17+
import coredevices.ring.service.PEBBLE_DEBUG_NOTIFICATION_CHANNEL_ID
18+
import coredevices.ring.service.PEBBLE_DEBUG_NOTIFICATION_CHANNEL_NAME
19+
import coredevices.ring.service.RecordingBackgroundScope
20+
import coredevices.ring.service.RingSync
1921
import coredevices.ring.service.recordings.RecordingProcessingQueue
22+
import coredevices.util.R
23+
import kotlinx.coroutines.GlobalScope
2024
import kotlinx.coroutines.Job
2125
import kotlinx.coroutines.cancel
2226
import kotlinx.coroutines.cancelAndJoin
27+
import kotlinx.coroutines.flow.distinctUntilChanged
28+
import kotlinx.coroutines.flow.launchIn
29+
import kotlinx.coroutines.flow.map
30+
import kotlinx.coroutines.flow.onEach
2331
import kotlinx.coroutines.launch
2432
import kotlinx.coroutines.runBlocking
2533
import org.koin.core.component.KoinComponent
2634
import org.koin.core.component.inject
2735

28-
class RingService: Service(), KoinComponent {
36+
class PebbleService: Service(), KoinComponent {
2937
companion object {
30-
const val NOTIFICATION_CHANNEL_ID = "ring"
31-
const val DEBUG_NOTIFICATION_CHANNEL_ID = "ring_debug"
32-
const val NOTIFICATION_CHANNEL_NAME = "Ring Service"
33-
const val DEBUG_NOTIFICATION_CHANNEL_NAME = "Ring Debug"
38+
const val NOTIFICATION_CHANNEL_ID = "pebble"
39+
const val NOTIFICATION_CHANNEL_NAME = "Pebble Service"
3440
const val ACTION_STOP = "STOP"
3541

36-
private val logger = Logger.withTag("RingService")
42+
private val logger = Logger.withTag("PebbleService")
3743
}
3844

3945
private val satelliteManager: KMPHaversineSatelliteManager by inject()
@@ -42,10 +48,12 @@ class RingService: Service(), KoinComponent {
4248
private var recordingDebugNotificationJob: Job? = null
4349
private var ringSyncJob: Job? = null
4450
private val ringSync: RingSync by inject()
45-
private val ringBackgroundManager: RingBackgroundManager by inject()
51+
private val pebbleBackgroundManager: PebbleBackgroundManager by inject()
4652
private val indexNotificationManager: IndexNotificationManager by inject()
4753
private val recordingProcessingQueue: RecordingProcessingQueue by inject()
48-
private var firstRun: Boolean = true
54+
private val commonPrefs: Preferences by inject()
55+
private var ringObserverJob: Job? = null
56+
private var firstRingRun: Boolean = true
4957

5058
private fun handleIntent(intent: Intent) {
5159
when (intent.action) {
@@ -60,9 +68,9 @@ class RingService: Service(), KoinComponent {
6068
recordingDebugNotificationJob?.cancel()
6169
recordingDebugNotificationJob = scope.launch {
6270
val notificationChannel = NotificationChannelCompat.Builder(
63-
DEBUG_NOTIFICATION_CHANNEL_ID,
71+
PEBBLE_DEBUG_NOTIFICATION_CHANNEL_ID,
6472
NotificationManager.IMPORTANCE_DEFAULT)
65-
.setName(DEBUG_NOTIFICATION_CHANNEL_NAME)
73+
.setName(PEBBLE_DEBUG_NOTIFICATION_CHANNEL_NAME)
6674
.build()
6775
notificationManagerCompat.createNotificationChannel(notificationChannel)
6876

@@ -71,9 +79,9 @@ class RingService: Service(), KoinComponent {
7179
}
7280

7381
private fun startRingSyncJob() {
74-
if (firstRun) {
82+
if (firstRingRun) {
7583
logger.i { "Starting ring sync job for the first time, resuming pending recording processing tasks" }
76-
firstRun = false
84+
firstRingRun = false
7785
recordingProcessingQueue.resumePendingTasks()
7886
}
7987
if (ringSyncJob?.isActive == true) {
@@ -85,6 +93,33 @@ class RingService: Service(), KoinComponent {
8593
}
8694
}
8795

96+
private fun stopRingJobs() {
97+
runBlocking {
98+
recordingDebugNotificationJob?.cancelAndJoin()
99+
recordingDebugNotificationJob = null
100+
ringSync.stop()
101+
ringSyncJob?.cancelAndJoin()
102+
ringSyncJob = null
103+
}
104+
}
105+
106+
private fun observeRingPaired() {
107+
if (ringObserverJob?.isActive == true) return
108+
ringObserverJob = commonPrefs.ringPaired
109+
.map { it != null }
110+
.distinctUntilChanged()
111+
.onEach { ringPaired ->
112+
logger.d { "ringPaired changed: $ringPaired" }
113+
if (ringPaired) {
114+
startRingSyncJob()
115+
startRecordingDebugNotificationJob()
116+
} else {
117+
stopRingJobs()
118+
}
119+
}
120+
.launchIn(GlobalScope)
121+
}
122+
88123
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
89124
logger.v { "onStartCommand()" }
90125
if (intent != null) {
@@ -99,10 +134,10 @@ class RingService: Service(), KoinComponent {
99134
notificationManagerCompat.createNotificationChannel(notificationChannel)
100135

101136
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
102-
.setContentTitle("Ring Service")
103-
.setContentText("Ring Service is running")
137+
.setContentTitle("Pebble")
138+
.setContentText("Keeping Pebble connection alive")
104139
.setOngoing(true)
105-
.setSmallIcon(android.R.drawable.ic_dialog_info)
140+
.setSmallIcon(R.mipmap.ic_launcher)
106141
.build()
107142
ServiceCompat.startForeground(
108143
this,
@@ -114,20 +149,16 @@ class RingService: Service(), KoinComponent {
114149
0
115150
}
116151
)
117-
startRingSyncJob()
118-
startRecordingDebugNotificationJob()
119-
ringBackgroundManager.onServiceStarted()
152+
observeRingPaired()
153+
pebbleBackgroundManager.onServiceStarted()
120154
return START_STICKY
121155
}
122156

123-
124-
125157
override fun onDestroy() {
126-
ringBackgroundManager.onServiceStopped()
127-
runBlocking {
128-
recordingDebugNotificationJob?.cancelAndJoin()
129-
ringSync.stop()
130-
}
158+
pebbleBackgroundManager.onServiceStopped()
159+
ringObserverJob?.cancel()
160+
ringObserverJob = null
161+
stopRingJobs()
131162
scope.cancel("Service destroyed")
132163
notificationManagerCompat.cancel(1)
133164
super.onDestroy()
@@ -136,4 +167,4 @@ class RingService: Service(), KoinComponent {
136167
override fun onBind(intent: Intent?): IBinder? {
137168
return null
138169
}
139-
}
170+
}

composeApp/src/androidMain/kotlin/coredevices/coreapp/di/androidDefaultModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import PlatformShareLauncher
66
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
77
import coredevices.analytics.createAndroidAnalytics
88
import coredevices.coreapp.BuildConfig
9+
import coredevices.coreapp.PebbleBackgroundManager
910
import coredevices.coreapp.auth.RealAppleAuthUtil
1011
import coredevices.coreapp.auth.RealGithubAuthUtil
1112
import coredevices.coreapp.auth.RealGoogleAuthUtil
@@ -66,4 +67,5 @@ val androidDefaultModule = module {
6667
}
6768
single { createAndroidAnalytics(get()) }
6869
singleOf(::ModelDownloadManager)
70+
singleOf(::PebbleBackgroundManager)
6971
}

experimental/src/androidMain/AndroidManifest.xml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@
4242
<package android:name="com.beeper.android"/>
4343
</queries>
4444
<application>
45-
<service android:name="coredevices.ring.service.RingService"
46-
android:foregroundServiceType="connectedDevice"
47-
android:exported="false"/>
4845
<receiver android:name=".glance.VoiceWidgetReceiver"
4946
android:exported="true"
5047
android:enabled="false">

experimental/src/androidMain/kotlin/coredevices/ring/RingDelegate.android.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ import coredevices.HackyPermissionRequesterProvider
99
import coredevices.ring.database.firestore.FirestoreKnownRingsSync
1010
import coredevices.ring.database.firestore.dao.FirestoreRecordingsDao
1111
import coredevices.ring.glance.VoiceWidgetReceiver
12-
import coredevices.ring.service.RingBackgroundManager
1312
import coredevices.util.CoreConfigHolder
1413
import coredevices.util.Permission
1514

1615
actual class RingDelegate(
1716
private val context: Context,
18-
private val ringBackgroundManager: RingBackgroundManager,
1917
private val permissionRequester: HackyPermissionRequesterProvider,
2018
private val coreConfigHolder: CoreConfigHolder,
2119
private val recordingsDao: FirestoreRecordingsDao,
@@ -42,7 +40,6 @@ actual class RingDelegate(
4240
*/
4341
actual suspend fun init() {
4442
listenForUserPresent(recordingsDao, coreConfigHolder, settings)
45-
ringBackgroundManager.monitorToStartBackground()
4643
firestoreKnownRingsSync.init()
4744
//enableWidget(context)
4845
}

experimental/src/androidMain/kotlin/coredevices/ring/service/IndexNotificationManager.android.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ import android.content.Intent
77
import android.net.Uri
88
import androidx.core.app.NotificationCompat
99
import androidx.core.app.PendingIntentCompat
10-
import coredevices.ring.service.RingService.Companion.DEBUG_NOTIFICATION_CHANNEL_ID
1110
import androidx.core.net.toUri
1211
import androidx.glance.action.action
1312
import coredevices.ExperimentalDevices
1413
import kotlin.math.roundToInt
1514

15+
const val PEBBLE_DEBUG_NOTIFICATION_CHANNEL_ID = "pebble_debug"
16+
const val PEBBLE_DEBUG_NOTIFICATION_CHANNEL_NAME = "Pebble Debug"
17+
1618
actual class PlatformIndexNotificationManager(
1719
private val context: Context,
1820
) {
1921
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2022
private fun buildDebugNotification() =
21-
NotificationCompat.Builder(context, DEBUG_NOTIFICATION_CHANNEL_ID)
23+
NotificationCompat.Builder(context, PEBBLE_DEBUG_NOTIFICATION_CHANNEL_ID)
2224
.setSmallIcon(android.R.drawable.ic_dialog_info)
2325
.setGroup("ring_debug")
2426
.setCategory(NotificationCompat.CATEGORY_STATUS)

0 commit comments

Comments
 (0)