I’m working with the Samsung Health Sensor API (v1.3.0) to collect Inter-Beat Interval (IBI) data from a Galaxy Watch. My goal is to gather IBI samples periodically (e.g., every hour for 10 minutes) even when the app is not active and the screen is off. The issue I’m facing is that data collection only works when the screen is on or the app is actively running. As soon as the screen turns off or the app moves to the background, the Samsung Health Sensor API stops delivering IBI data, making it impossible to collect the necessary samples as scheduled.
I have tried using Foreground Services to keep the app running in the foreground, but the Samsung Health Sensor API still stops delivering IBI data when the screen turns off. Additionally, I implemented a Wake Lock to prevent the watch from going into a low-power state, but this did not resolve the issue either. Despite these efforts, data collection only works when the screen is on or the app is actively running. I also checked for any relevant permissions or settings that might allow continuous background data collection, but I haven't found a solution yet.
This is the worker:
class IBIDataCollectionWorker(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
private var healthService: HealthTrackingService? = null
private var healthTracker: HealthTracker? = null
private val ibiDataList = mutableListOf<Int>()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault())
private val trackerEventListener = object : HealthTracker.TrackerEventListener {
override fun onDataReceived(dataPoints: List<DataPoint>) {
for (dataPoint in dataPoints) {
val validIbiList = getValidIbiList(dataPoint)
ibiDataList.addAll(validIbiList)
}
}
override fun onError(error: HealthTracker.TrackerError) {
Log.e("IBIWorker", "Error: $error")
}
override fun onFlushCompleted() {}
}
private fun isIBIValid(ibiStatus: Int, ibiValue: Int): Boolean {
return ibiStatus == 0 && ibiValue != 0
}
private fun getValidIbiList(dataPoint: DataPoint): List<Int> {
val ibiValues = dataPoint.getValue(ValueKey.HeartRateSet.IBI_LIST)
val ibiStatuses = dataPoint.getValue(ValueKey.HeartRateSet.IBI_STATUS_LIST)
return ibiValues.filterIndexed { index, value ->
isIBIValid(ibiStatuses[index], value)
}
}
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
return@withContext try {
val connectionDeferred = CompletableDeferred<Boolean>()
healthService = HealthTrackingService(object : ConnectionListener {
override fun onConnectionSuccess() {
connectionDeferred.complete(true)
}
override fun onConnectionFailed(e: HealthTrackerException) {
Log.e("IBIWorker", "Connection Failed: $e")
connectionDeferred.complete(false)
}
override fun onConnectionEnded() {}
}, context)
healthService?.connectService()
if (!connectionDeferred.await()) {
return@withContext Result.failure(workDataOf("error" to "Connection failed"))
}
startTracking()
delay(600000) // collect data for 10 min
stopTracking()
saveDataToFile()
Result.success()
} catch (e: Exception) {
Log.e("IBIWorker", "Error during work execution", e)
Result.failure(workDataOf("error" to e.message.toString()))
} finally {
healthService?.disconnectService()
healthService = null
}
}
private fun startTracking() {
try {
healthTracker = healthService?.getHealthTracker(HealthTrackerType.HEART_RATE_CONTINUOUS)
if (healthTracker == null) {
Log.e("IBIWorker", "Failed to create HealthTracker")
return
}
healthTracker?.setEventListener(trackerEventListener)
Log.d("IBIWorker", "Tracking started")
} catch (e: Exception) {
Log.e("IBIWorker", "Error starting tracking", e)
}
}
private fun stopTracking() {
healthTracker?.unsetEventListener()
}
private fun saveDataToFile() {
val fileName = "ibi_data_worker.txt"
val file = File(context.getExternalFilesDir(null), fileName)
try {
FileOutputStream(file, true).use { outputStream ->
val timestamp = dateFormat.format(Date())
outputStream.write("${timestamp}:\n".toByteArray())
ibiDataList.forEach { ibiValue ->
outputStream.write("${ibiValue},".toByteArray())
}
}
} catch (e: IOException) {
Log.e("IBIWorker", "Error saving to file", e)
} finally {
ibiDataList.clear()
}
}
}
And this is how I run it in the MainActivity
private fun requestPermissions() {
val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
permissionGranted = permissions.all { it.value }
if (permissionGranted) {
scheduleIBIDataCollection()
}
else Toast.makeText(this, "permission denied", Toast.LENGTH_LONG).show()
}
permissionLauncher.launch(arrayOf(android.Manifest.permission.BODY_SENSORS))
}
private fun scheduleIBIDataCollection() {
val ibiWorkRequest = PeriodicWorkRequestBuilder<IBIDataCollectionWorker>(
repeatInterval = 60,
repeatIntervalTimeUnit = TimeUnit.MINUTES
).build()
WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
"ibi_data_collection",
ExistingPeriodicWorkPolicy.KEEP,
ibiWorkRequest
)
}
I contacted Samsung about this and they confirmed that IBI data is still collected when the screen is off, but the delivery method changes to save battery. Instead of streaming in real time, the data is gathered with 1 Hz frequency in the background and delivered in batches every few minutes.
If you need to access the data sooner than the system delivers it, the flush()
method can force an earlier update, though it may impact battery life.
In my own testing, I found that the IBI data collected when the screen was not on, was inaccurate and for my use case, not usable. This might vary depending on the device but it’s something to consider if you’re relying on high-quality data.