Sonde Health API Platform Documentation
Developer guide - Background Handling, Notifications and Reminder Scheduling.
Overview
This guide helps developers understand how the Sonde Passive SDK manages:
✅ Background Voice Analysis
✅ User Notifications
✅ Reminder Scheduling
so you can integrate seamlessly into your existing Android app.
🚀 App Running in the Background
Background Handling Using Foreground Service
The Sonde Passive SDK uses a foreground service for background voice analysis while respecting user privacy via persistent notifications.
Voice analysis runs continuously in the background.
Users see real-time progress in the notification tray.
Scores can be generated even while the app is in background.
Required Permissions and Service Declaration
Add the following in your AndroidManifest.xml:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<service
android:name=".service.VoiceAnalysisService"
android:enabled="true"
android:foregroundServiceType="microphone"
android:stopWithTask="true" />
Foreground Service Setup (VoiceAnalysisService.kt)
Create one kotlin class as VoiceAnalysisService.kt extends Service.
Binder for Fragment/Activity Communication
private val binder: IBinder = VoiceAnalysisBinder()
inner class VoiceAnalysisBinder : Binder() {
val service: VoiceAnalysisService = this@VoiceAnalysisService
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
Starting Foreground Service with Notification
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = CHANNEL_DESCRIPTION
}
notificationManager.createNotificationChannel(channel)
}
private fun startForegroundServiceWithNotification() {
notificationManager =
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
createNotificationChannel()
startForeground(
NOTIFICATION_ID,
createNotification().build()
)
}
private fun createNotification(): NotificationCompat.Builder {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setChannelId(CHANNEL_ID)
.setContentTitle(NOTIFICATION_TITLE)
.setContentText(NOTIFICATION_CONTENT)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setSound(null)
.setStyle(NotificationCompat.BigTextStyle())
.setSmallIcon(R.drawable.ic_mic_filled)
}
Event Handling Using Sealed Interfaces
Create VoiceAnalysisServiceEvent:
sealed interface VoiceAnalysisServiceEvent {
object StartService : VoiceAnalysisServiceEvent
object StopService : VoiceAnalysisServiceEvent
}
Handling Events:
suspend fun voiceAnalysisServiceEvent(event: VoiceAnalysisServiceEvent) {
when (event) {
VoiceAnalysisServiceEvent.StartService -> {
// Start or restart voice analysis
startForegroundServiceWithNotification()
}
VoiceAnalysisServiceEvent.StopService -> {
// Stop voice analysis
}
}
}
Based on the above event, we have to start or restart the service by calling
startVoiceAnalysis
& to stop the service callstopVoiceAnalysis
.To send updates back to fragment we have used the
StateFlow
.Create on sealed interface as VoiceAnalysisServiceState in same package of VoiceAnalysisService.kt means parallel to service for send updates back from service to Fragment
sealed interface VoiceAnalysisServiceState { object ServiceStarted : VoiceAnalysisServiceState object ServiceStopped : VoiceAnalysisServiceState object ServiceRestarted : VoiceAnalysisServiceState object SessionCompleted : VoiceAnalysisServiceState data class SegmentAnalysed(val voiceSegmentData: VoiceSegmentData) : VoiceAnalysisServiceState data class SessionAnalysed(val score: VoiceScore, val cfScore: VoiceScore?, val sessionNumber: Int) : VoiceAnalysisServiceState data class Error(val errorMsg: String) : VoiceAnalysisServiceState }
Add below code to declare state in Fragment,
private val _serviceState = MutableStateFlow<VoiceAnalysisServiceState?>(null) val serviceState: StateFlow<VoiceAnalysisServiceState?> = _serviceState
Fragment changes to update the UI and handle the service states
Refer the code in VerificationFragment.kt
Binding the service in onViewCreated
val intent = Intent(requireActivity(), VoiceAnalysisService::class.java) requireActivity().bindService( intent, voiceAnalysisServiceConnection, Context.BIND_AUTO_CREATE )
Create service connection in the fragment.
private var serviceBound = false private lateinit var voiceAnalysisService: VoiceAnalysisService
private var serviceBound = false private lateinit var voiceAnalysisService: VoiceAnalysisService private val voiceAnalysisServiceConnection = object : ServiceConnection { override fun onServiceConnected(p0: ComponentName?, service: IBinder?) { val binder = service as VoiceAnalysisService.VoiceAnalysisBinder voiceAnalysisService = binder.service serviceBound = true // Observe service state updates lifecycleScope.launch { voiceAnalysisService.serviceState.collectLatest { state -> // Update UI based on state } } } override fun onServiceDisconnected(p0: ComponentName?) { serviceBound = false } }
Once the service is connected, you can start communication with the service by sending the events.
Perform the required actions based on the state change of service which can be listened to using flow inside the onServiceConnected of the voiceAnalysisServiceConnection.
voiceAnalysisService.serviceState.collectLatest
{state ->
}
Destroy Service
override fun onDestroy() {
stopContinuousVoiceAnalysis()
super.onDestroy()
}
private fun stopContinuousVoiceAnalysis() {
voiceAnalysisEngine.stopVoiceAnalysis()
}
⏰ Reminder Scheduling
Refer the code in
SettingFragment
.ktTake 4 parameter from user as maintained in following ReminderItem Data Class from UI/Fragment.
data class ReminderItem( val id: String, // Unique ID for each reminder val time: String, // e.g., "10:30" val hours: Int, val sessions: Int, val notificationMessage: String = "Your voice matters! Complete your check-in." )
Create one method as
scheduleDailyNotifications
to schedule notification on exact time. We are using AlarmManager here for exact time. You can use alternate for this is WorkManager for inexact time.fun scheduleDailyNotifications( context: Context, reminderItem: ReminderItem ) { val timeParts = reminderItem.time.split(":") val hour = timeParts[0].toInt() val minute = timeParts[1].toInt() val calendar = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, hour) set(Calendar.MINUTE, minute) set(Calendar.SECOND, 0) set(Calendar.MILLISECOND, 0) if (before(Calendar.getInstance())) add(Calendar.DATE, 1) } val intent = Intent(context, DailyNotificationReceiver::class.java).apply { putExtra("title", "Sonde Mental Fitness:") putExtra("message", reminderItem.notificationMessage) putExtra("workId", reminderItem.id) putExtra("time", reminderItem.time) putExtra("hours", reminderItem.hours) putExtra("sessions", reminderItem.sessions) } val pendingIntent = PendingIntent.getBroadcast( context, reminderItem.id.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager try { alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent ) } catch (exception: SecurityException) { Log.d("Notification", "Exception thrown: ${exception.message}") } Log.d("Notification", "${reminderItem.id}: AlarmManager task scheduled at ${calendar.time}") }
To receive notification on exact time create one BroadcastReceiver as
DailyNotificationReceiver
class DailyNotificationReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val title = intent.getStringExtra("title") ?: "Sonde Mental Fitness:" val message = intent.getStringExtra("message") ?: "Your voice matters! Complete your check-in." val workId = intent.getStringExtra("workId") ?: "daily_notification" val time = intent.getStringExtra("time") ?: "09:00" val hours = intent.getIntExtra("hours",4) val sessions = intent.getIntExtra("sessions",10) showNotification(context, title, message, workId) // 🔁 Reschedule for next day! val reminderItem=ReminderItem( id = workId, time = time, hours = hours, sessions = sessions, notificationMessage = message) scheduleDailyNotifications(context, reminderItem) } private fun showNotification(context: Context, title: String, message: String, workId: String) { val channelId = "multi_notification_channel" val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val channel = NotificationChannel( channelId, "Multiple Daily Notifications", NotificationManager.IMPORTANCE_HIGH ) notificationManager.createNotificationChannel(channel) val args = Bundle().apply { putBoolean("fromNotification", true) putString("title", title) putString("message", message) putString("workId", workId) } val pendingIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_graph) .setDestination(R.id.VerificationFragment) .setArguments(args) .createPendingIntent() val notification = NotificationCompat.Builder(context, channelId) .setContentTitle(title) .setContentText(message) .setSmallIcon(R.drawable.ic_mic_filled) .setContentIntent(pendingIntent) .setAutoCancel(true) .build() notificationManager.notify(System.currentTimeMillis().toInt(), notification) }
Declare this in your AndroidManifest.xml
<receiver android:name=".data.framework.DailyNotificationReceiver" android:exported="false" />
Once you have received notification on your device’s notification, you can handle notification click as per your requirement. Please refer
VerificationFragment.kt
fragment andonViewCreated
method.
📝 Summary
✅ Background Handling: Using a foreground service with notifications for privacy and persistent analysis.
✅ Notification System: Enables users to track progress and receive reminders.
✅ Reminder Scheduling: Uses AlarmManager for exact time, recurring daily notifications. You can use WorkManager for inexact notification timing.
Sonde Health