Skip to main content

Notification Service β€” Data Layer

Retrofit API definitions, DTOs, repository implementation, data-only repositories, and the reactive data store for notification state.


NotificationApi​

com.visla.vislagps.data.remote.api.NotificationApi

Retrofit interface with suspend endpoints for notification operations. Base path: api/notifications/.

Notification History​

HTTPPathMethodSignature
GETapi/notificationsgetNotificationHistorysuspend fun getNotificationHistory(@Query("user_id") userId: Int, @Query("limit") limit: Int = 50, @Query("offset") offset: Int = 0, @Query("unread_only") unreadOnly: Boolean = true, @Query("read_only") readOnly: Boolean = false): NotificationHistoryResponse
PUTapi/notifications/{notificationId}/readmarkNotificationReadsuspend fun markNotificationRead(@Path("notificationId") notificationId: Int)
PUTapi/notifications/user/{userId}/read-allmarkAllNotificationsReadsuspend fun markAllNotificationsRead(@Path("userId") userId: Int)
DELETEapi/notifications/{notificationId}deleteNotificationsuspend fun deleteNotification(@Path("notificationId") notificationId: Int)
DELETEapi/notifications/user/{userId}/read-alldeleteAllReadNotificationssuspend fun deleteAllReadNotifications(@Path("userId") userId: Int)

Push Tokens​

HTTPPathMethodSignature
POSTapi/notifications/tokensregisterPushTokensuspend fun registerPushToken(@Body request: RegisterTokenRequest): RegisterTokenResponse
DELETEapi/notifications/tokens/{tokenId}deleteTokensuspend fun deleteToken(@Path("tokenId") tokenId: Int)

User Notification Settings​

HTTPPathMethodSignature
GETapi/notifications/settings/{userId}getSettingssuspend fun getSettings(@Path("userId") userId: Int): NotificationSettingsDto
PUTapi/notifications/settings/{userId}updateSettingssuspend fun updateSettings(@Path("userId") userId: Int, @Body request: UpdateSettingsRequest): NotificationSettingsDto

Device Notification Settings​

HTTPPathMethodSignature
GETapi/notifications/devices/{deviceId}/settingsgetDeviceSettingssuspend fun getDeviceSettings(@Path("deviceId") deviceId: Int): DeviceNotificationSettingsDto
PUTapi/notifications/devices/{deviceId}/settingsupdateDeviceSettingssuspend fun updateDeviceSettings(@Path("deviceId") deviceId: Int, @Body request: UpdateDeviceSettingsRequest): DeviceNotificationSettingsDto

Device Contacts​

HTTPPathMethodSignature
GETapi/notifications/devices/{deviceId}/contactsgetDeviceContactssuspend fun getDeviceContacts(@Path("deviceId") deviceId: Int): ContactsResponseDto
POSTapi/notifications/devices/{deviceId}/contactsaddDeviceContactsuspend fun addDeviceContact(@Path("deviceId") deviceId: Int, @Body request: AddContactRequest): DeviceContactDto
DELETEapi/notifications/contacts/{contactId}deleteDeviceContactsuspend fun deleteDeviceContact(@Path("contactId") contactId: Int): DeleteContactResponseDto

Alarm Categories & Sounds​

HTTPPathMethodSignature
GETapi/notifications/alarm-categoriesgetAlarmCategoriessuspend fun getAlarmCategories(): AlarmCategoriesResponse
GETapi/notifications/soundsgetAvailableSoundssuspend fun getAvailableSounds(): SoundsResponse
GETapi/notifications/users/{userId}/sound-preferencesgetSoundPreferencessuspend fun getSoundPreferences(@Path("userId") userId: Int): SoundPreferencesResponse
PUTapi/notifications/users/{userId}/sound-preferences/{category}updateSoundPreferencesuspend fun updateSoundPreference(@Path("userId") userId: Int, @Path("category") category: String, @Body request: UpdateSoundPreferenceRequest)
DELETEapi/notifications/users/{userId}/sound-preferences/{category}resetSoundPreferencesuspend fun resetSoundPreference(@Path("userId") userId: Int, @Path("category") category: String)

Notification DTOs​

com.visla.vislagps.data.remote.dto.NotificationDtos

Request Models​

data class RegisterTokenRequest(
@SerializedName("user_id") val userId: Int,
val token: String,
val platform: String = "android",
@SerializedName("device_name") val deviceName: String? = null
)

data class UpdateSettingsRequest(
@SerializedName("email_enabled") val emailEnabled: Boolean? = null,
@SerializedName("push_enabled") val pushEnabled: Boolean? = null,
@SerializedName("call_enabled") val callEnabled: Boolean? = null,
@SerializedName("default_phone_number") val defaultPhoneNumber: String? = null,
val events: Map<String, Any>? = null
)

data class UpdateDeviceSettingsRequest(
@SerializedName("device_id") val deviceId: Int,
@SerializedName("push_enabled") val pushEnabled: Boolean? = null,
@SerializedName("email_enabled") val emailEnabled: Boolean? = null,
@SerializedName("call_enabled") val callEnabled: Boolean? = null,
val events: Map<String, Any>? = null
)

data class AddContactRequest(
@SerializedName("device_id") val deviceId: Int,
@SerializedName("contact_type") val contactType: String,
@SerializedName("contact_value") val contactValue: String,
val label: String? = null,
val priority: Int = 0
)

data class UpdateSoundPreferenceRequest(
@SerializedName("sound_id") val soundId: String
)

Response / DTO Models​

data class RegisterTokenResponse(
val status: String,
@SerializedName("token_id") val tokenId: Int? = null
)

data class PushTokenDto(
val id: Int,
@SerializedName("user_id") val userId: Int,
val token: String,
val platform: String,
@SerializedName("device_name") val deviceName: String?
)

data class NotificationSettingsDto(
val id: Int?,
@SerializedName("userId") val userId: Int,
@SerializedName("emailEnabled") val emailEnabled: Boolean = true,
@SerializedName("pushEnabled") val pushEnabled: Boolean = true,
@SerializedName("callEnabled") val callEnabled: Boolean = false,
@SerializedName("phoneNumber") val defaultPhoneNumber: String? = null,
val events: Map<String, Any>? = null
)

data class DeviceNotificationSettingsDto(
val id: Int?,
@SerializedName("userId") val userId: Int,
@SerializedName("deviceId") val deviceId: Int,
@SerializedName("pushEnabled") val pushEnabled: Boolean = true,
@SerializedName("emailEnabled") val emailEnabled: Boolean = false,
@SerializedName("callEnabled") val callEnabled: Boolean = false,
val events: Map<String, Any>? = null
)

data class NotificationHistoryDto(
val id: Int?,
@SerializedName("userId") val userId: Int,
val title: String,
val body: String,
val type: String? = null,
val channel: String? = null,
val sent: Boolean? = null,
@SerializedName("sentAt") val sentAt: String? = null,
@SerializedName("readAt") val readAt: String? = null,
@SerializedName("deviceId") val deviceId: Int? = null,
@SerializedName("eventId") val eventId: Int? = null,
val data: Map<String, Any>? = null,
@SerializedName("createdAt") val createdAt: String? = null
)

data class NotificationHistoryResponse(
val notifications: List<NotificationHistoryDto>,
val count: Int
)

data class ContactsResponseDto(
@SerializedName("device_id") val deviceId: Int,
val contacts: List<DeviceContactDto>,
val count: Int
)

data class DeviceContactDto(
val id: Int,
@SerializedName("device_id") val deviceId: Int,
@SerializedName("user_id") val userId: Int,
@SerializedName("contact_type") val contactType: String,
@SerializedName("contact_value") val contactValue: String,
val label: String? = null,
val priority: Int? = null,
@SerializedName("is_active") val isActive: Boolean? = true
)

data class DeleteContactResponseDto(
val status: String,
@SerializedName("contact_id") val contactId: Int
)

data class AlarmCategoriesResponse(val categories: List<AlarmCategoryDto>)
data class AlarmCategoryDto(val value: String, val label: String)

data class SoundsResponse(val sounds: List<SoundDto>)
data class SoundDto(
val soundId: String,
val name: String,
val file: String,
val description: String? = null
)

data class SoundPreferencesResponse(val preferences: List<SoundPreferenceDto>)
data class SoundPreferenceDto(
val alarmCategory: String,
val soundId: String,
val soundName: String,
val soundFile: String,
val vibrationEnabled: Boolean = true,
val enabled: Boolean = true,
val isCustom: Boolean = false
)

NotificationRepository (Domain Interface)​

com.visla.vislagps.domain.repositories.NotificationRepository

interface NotificationRepository {
suspend fun getNotificationHistory(
userId: Int, limit: Int = 50, offset: Int = 0,
unreadOnly: Boolean = true, readOnly: Boolean = false
): List<NotificationHistoryItem>

suspend fun markNotificationRead(notificationId: Int)
suspend fun markAllNotificationsRead(userId: Int)

suspend fun registerPushToken(
userId: Int, token: String, platform: String = "android",
deviceName: String? = null
): PushToken

suspend fun deletePushToken(tokenId: Int)

suspend fun getSettings(userId: Int): NotificationSettings
suspend fun updateSettings(
userId: Int, emailEnabled: Boolean? = null,
pushEnabled: Boolean? = null, callEnabled: Boolean? = null,
defaultPhoneNumber: String? = null, events: Map<String, Any>? = null
): NotificationSettings

suspend fun getDeviceSettings(deviceId: Int): DeviceNotificationSettings
suspend fun updateDeviceSettings(
deviceId: Int, pushEnabled: Boolean? = null,
emailEnabled: Boolean? = null, callEnabled: Boolean? = null,
events: Map<String, Any>? = null
): DeviceNotificationSettings

suspend fun getDeviceContacts(deviceId: Int): List<DeviceContact>
suspend fun addDeviceContact(
deviceId: Int, contactType: String, contactValue: String,
label: String? = null, priority: Int = 0
): DeviceContact

suspend fun deleteDeviceContact(contactId: Int)
}

NotificationRepositoryImpl​

com.visla.vislagps.data.repositories.NotificationRepositoryImpl

@Singleton
class NotificationRepositoryImpl @Inject constructor(
private val notificationApi: NotificationApi
) : NotificationRepository

Mapping & Error Handling​

Every override follows the same pattern:

  1. Call notificationApi.* with DTO request objects.
  2. Map the DTO response into a domain entity (inline β€” no separate mapper class).
  3. Catch HttpException β†’ mapException() converts status codes (401 β†’ "Unauthorized", 403 β†’ "Forbidden", 404 β†’ "Not found", else β†’ "Server error: {code}").
  4. Catch IOException β†’ wraps in NetworkException with a descriptive message.

Method Implementations​

MethodAPI CallReturns
getNotificationHistorynotificationApi.getNotificationHistory(...)Maps NotificationHistoryDto list β†’ NotificationHistoryItem list, filtering out DTOs with null id
markNotificationReadnotificationApi.markNotificationRead(notificationId)Unit
markAllNotificationsReadnotificationApi.markAllNotificationsRead(userId)Unit
registerPushTokennotificationApi.registerPushToken(RegisterTokenRequest(...))Constructs PushToken from request params + response.tokenId
deletePushTokennotificationApi.deleteToken(tokenId)Unit
getSettingsnotificationApi.getSettings(userId)Maps NotificationSettingsDto β†’ NotificationSettings
updateSettingsnotificationApi.updateSettings(userId, UpdateSettingsRequest(...))Maps NotificationSettingsDto β†’ NotificationSettings
getDeviceSettingsnotificationApi.getDeviceSettings(deviceId)Maps DeviceNotificationSettingsDto β†’ DeviceNotificationSettings
updateDeviceSettingsnotificationApi.updateDeviceSettings(deviceId, UpdateDeviceSettingsRequest(...))Maps DeviceNotificationSettingsDto β†’ DeviceNotificationSettings
getDeviceContactsnotificationApi.getDeviceContacts(deviceId)Maps DeviceContactDto list β†’ DeviceContact list
addDeviceContactnotificationApi.addDeviceContact(deviceId, AddContactRequest(...))Maps DeviceContactDto β†’ DeviceContact
deleteDeviceContactnotificationApi.deleteDeviceContact(contactId)Unit

No separate NotificationMapper exists. All DTO β†’ domain mapping is done inline in the repository.


NotificationDataStore​

com.visla.vislagps.core.data.NotificationDataStore

Single source of truth for notification history. Bridges HTTP API initial loads, FCM push real-time additions, and (future) WebSocket events. All mutable operations are serialised through a Mutex.

@Singleton
class NotificationDataStore @Inject constructor()

Constants​

NameValue
DEFAULT_PAGE_SIZE10

Observable State​

PropertyTypeDescription
notificationsStateFlow<List<NotificationHistoryItem>>Sorted newest-first notification list
isLoadingStateFlow<Boolean>Whether a load is in progress
hasMoreStateFlow<Boolean>Whether more pages are available
unreadCountStateFlow<Int>Count of notifications where readAt == null
newNotificationEventSharedFlow<NotificationHistoryItem>Emitted when a new notification is added (buffer capacity 16)

Initialisation (HTTP API)​

MethodSignatureDescription
setNotificationssuspend fun setNotifications(notifications: List<NotificationHistoryItem>, hasMore: Boolean)Replace all data with initial API load
appendNotificationssuspend fun appendNotifications(notifications: List<NotificationHistoryItem>, hasMore: Boolean)Append paginated results

Real-Time Updates​

MethodSignatureDescription
addNotificationsuspend fun addNotification(notification: NotificationHistoryItem, emitEvent: Boolean = true)Add at top; skips duplicates by id; optionally emits newNotificationEvent
addFromPushsuspend fun addFromPush(userId: Int, title: String, body: String, deviceId: Int?, eventType: String?, channel: String = "push")Create a NotificationHistoryItem from FCM payload with a temporary negative ID

Mark as Read / Remove​

MethodSignatureDescription
markAsReadsuspend fun markAsRead(notificationId: Int)Set readAt to now, update unread count
removeNotificationsuspend fun removeNotification(notificationId: Int)Remove by id, update unread count
markAllAsReadsuspend fun markAllAsRead()Set readAt on all items, reset unread count to 0
clearAllsuspend fun clearAll()Empty list, reset unread count and hasMore

Loading State​

MethodSignatureDescription
setLoadingfun setLoading(loading: Boolean)Toggle loading indicator

Query Methods​

MethodSignatureDescription
getByTypefun getByType(eventType: String): List<NotificationHistoryItem>Filter current list by type
getByDevicefun getByDevice(deviceId: Int): List<NotificationHistoryItem>Filter current list by deviceId
getUnreadfun getUnread(): List<NotificationHistoryItem>All items where readAt == null

State Management​

MethodSignatureDescription
clearsuspend fun clear()Reset all state (used on logout)
invalidatefun invalidate()Set hasMore = true to force refresh on next load

Data-Only Repositories​

These repositories have no domain-layer interface β€” they live entirely in the data layer.

NotificationPermissionRepository​

com.visla.vislagps.data.repositories.NotificationPermissionRepository

Wraps Android NotificationManagerCompat for runtime notification permission checks.

@Singleton
class NotificationPermissionRepository @Inject constructor(
@ApplicationContext private val context: Context
)
MethodSignatureDescription
areNotificationsEnabledfun areNotificationsEnabled(): BooleanDelegates to NotificationManagerCompat.from(context).areNotificationsEnabled()
createNotificationSettingsIntentfun createNotificationSettingsIntent(): IntentReturns an Intent targeting Settings.ACTION_APP_NOTIFICATION_SETTINGS for the app package

NotificationSettingsRepository​

com.visla.vislagps.data.repositories.NotificationSettingsRepository

Local SharedPreferences store for notification sound selection. Prefs file: notification_settings_prefs.

@Singleton
class NotificationSettingsRepository @Inject constructor(
@ApplicationContext private val context: Context
)
MethodSignatureDescription
getSelectedSoundfun getSelectedSound(): StringRead selected_sound key, defaults to "default"
saveSelectedSoundfun saveSelectedSound(soundId: String)Write selected_sound key

NotificationStateRepository​

com.visla.vislagps.data.repositories.NotificationStateRepository

In-memory reactive state for real-time push notifications, FCM token, dismissed events, and pending navigation from notification taps. Replaces a legacy global NotificationState singleton.

@Singleton
class NotificationStateRepository @Inject constructor()

Observable State​

PropertyTypeDescription
fcmTokenStateFlow<String?>Current FCM push token
notificationCountStateFlow<Int>Running count of received notifications
notificationsStateFlow<List<PushNotification>>In-memory list of push notifications (newest first)
dismissedEventIdsStateFlow<Set<Int>>Set of event IDs the user has dismissed
pendingNavigationStateFlow<NotificationClickData?>Navigation data from a tapped notification

Methods​

MethodSignatureDescription
setTokenfun setToken(token: String)Store FCM token
addNotificationfun addNotification(title: String, body: String, data: Map<String, String>)Prepend a PushNotification, increment count
dismissNotificationfun dismissNotification(id: String)Mark notification as dismissed by id
removeNotificationfun removeNotification(id: String)Remove notification from list by id
dismissEventfun dismissEvent(eventId: Int)Add event ID to dismissed set
isEventDismissedfun isEventDismissed(eventId: Int): BooleanCheck if event ID is in dismissed set
clearNotificationsfun clearNotifications()Empty notification list
clearAllfun clearAll()Empty notifications and dismissed events
setPendingNavigationfun setPendingNavigation(deviceId: Int, title: String, body: String, eventType: String?, timestamp: Long, latitude: Double? = null, longitude: Double? = null)Store navigation data from a notification tap
consumePendingNavigationfun consumePendingNavigation(): NotificationClickData?Return and clear pending navigation (one-shot)
clearPendingNavigationfun clearPendingNavigation()Clear pending navigation without consuming