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β
| HTTP | Path | Method | Signature |
|---|---|---|---|
GET | api/notifications | getNotificationHistory | suspend 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 |
PUT | api/notifications/{notificationId}/read | markNotificationRead | suspend fun markNotificationRead(@Path("notificationId") notificationId: Int) |
PUT | api/notifications/user/{userId}/read-all | markAllNotificationsRead | suspend fun markAllNotificationsRead(@Path("userId") userId: Int) |
DELETE | api/notifications/{notificationId} | deleteNotification | suspend fun deleteNotification(@Path("notificationId") notificationId: Int) |
DELETE | api/notifications/user/{userId}/read-all | deleteAllReadNotifications | suspend fun deleteAllReadNotifications(@Path("userId") userId: Int) |
Push Tokensβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
POST | api/notifications/tokens | registerPushToken | suspend fun registerPushToken(@Body request: RegisterTokenRequest): RegisterTokenResponse |
DELETE | api/notifications/tokens/{tokenId} | deleteToken | suspend fun deleteToken(@Path("tokenId") tokenId: Int) |
User Notification Settingsβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
GET | api/notifications/settings/{userId} | getSettings | suspend fun getSettings(@Path("userId") userId: Int): NotificationSettingsDto |
PUT | api/notifications/settings/{userId} | updateSettings | suspend fun updateSettings(@Path("userId") userId: Int, @Body request: UpdateSettingsRequest): NotificationSettingsDto |
Device Notification Settingsβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
GET | api/notifications/devices/{deviceId}/settings | getDeviceSettings | suspend fun getDeviceSettings(@Path("deviceId") deviceId: Int): DeviceNotificationSettingsDto |
PUT | api/notifications/devices/{deviceId}/settings | updateDeviceSettings | suspend fun updateDeviceSettings(@Path("deviceId") deviceId: Int, @Body request: UpdateDeviceSettingsRequest): DeviceNotificationSettingsDto |
Device Contactsβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
GET | api/notifications/devices/{deviceId}/contacts | getDeviceContacts | suspend fun getDeviceContacts(@Path("deviceId") deviceId: Int): ContactsResponseDto |
POST | api/notifications/devices/{deviceId}/contacts | addDeviceContact | suspend fun addDeviceContact(@Path("deviceId") deviceId: Int, @Body request: AddContactRequest): DeviceContactDto |
DELETE | api/notifications/contacts/{contactId} | deleteDeviceContact | suspend fun deleteDeviceContact(@Path("contactId") contactId: Int): DeleteContactResponseDto |
Alarm Categories & Soundsβ
| HTTP | Path | Method | Signature |
|---|---|---|---|
GET | api/notifications/alarm-categories | getAlarmCategories | suspend fun getAlarmCategories(): AlarmCategoriesResponse |
GET | api/notifications/sounds | getAvailableSounds | suspend fun getAvailableSounds(): SoundsResponse |
GET | api/notifications/users/{userId}/sound-preferences | getSoundPreferences | suspend fun getSoundPreferences(@Path("userId") userId: Int): SoundPreferencesResponse |
PUT | api/notifications/users/{userId}/sound-preferences/{category} | updateSoundPreference | suspend fun updateSoundPreference(@Path("userId") userId: Int, @Path("category") category: String, @Body request: UpdateSoundPreferenceRequest) |
DELETE | api/notifications/users/{userId}/sound-preferences/{category} | resetSoundPreference | suspend 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:
- Call
notificationApi.*with DTO request objects. - Map the DTO response into a domain entity (inline β no separate mapper class).
- Catch
HttpExceptionβmapException()converts status codes (401β "Unauthorized",403β "Forbidden",404β "Not found", else β "Server error:{code}"). - Catch
IOExceptionβ wraps inNetworkExceptionwith a descriptive message.
Method Implementationsβ
| Method | API Call | Returns |
|---|---|---|
getNotificationHistory | notificationApi.getNotificationHistory(...) | Maps NotificationHistoryDto list β NotificationHistoryItem list, filtering out DTOs with null id |
markNotificationRead | notificationApi.markNotificationRead(notificationId) | Unit |
markAllNotificationsRead | notificationApi.markAllNotificationsRead(userId) | Unit |
registerPushToken | notificationApi.registerPushToken(RegisterTokenRequest(...)) | Constructs PushToken from request params + response.tokenId |
deletePushToken | notificationApi.deleteToken(tokenId) | Unit |
getSettings | notificationApi.getSettings(userId) | Maps NotificationSettingsDto β NotificationSettings |
updateSettings | notificationApi.updateSettings(userId, UpdateSettingsRequest(...)) | Maps NotificationSettingsDto β NotificationSettings |
getDeviceSettings | notificationApi.getDeviceSettings(deviceId) | Maps DeviceNotificationSettingsDto β DeviceNotificationSettings |
updateDeviceSettings | notificationApi.updateDeviceSettings(deviceId, UpdateDeviceSettingsRequest(...)) | Maps DeviceNotificationSettingsDto β DeviceNotificationSettings |
getDeviceContacts | notificationApi.getDeviceContacts(deviceId) | Maps DeviceContactDto list β DeviceContact list |
addDeviceContact | notificationApi.addDeviceContact(deviceId, AddContactRequest(...)) | Maps DeviceContactDto β DeviceContact |
deleteDeviceContact | notificationApi.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β
| Name | Value |
|---|---|
DEFAULT_PAGE_SIZE | 10 |
Observable Stateβ
| Property | Type | Description |
|---|---|---|
notifications | StateFlow<List<NotificationHistoryItem>> | Sorted newest-first notification list |
isLoading | StateFlow<Boolean> | Whether a load is in progress |
hasMore | StateFlow<Boolean> | Whether more pages are available |
unreadCount | StateFlow<Int> | Count of notifications where readAt == null |
newNotificationEvent | SharedFlow<NotificationHistoryItem> | Emitted when a new notification is added (buffer capacity 16) |
Initialisation (HTTP API)β
| Method | Signature | Description |
|---|---|---|
setNotifications | suspend fun setNotifications(notifications: List<NotificationHistoryItem>, hasMore: Boolean) | Replace all data with initial API load |
appendNotifications | suspend fun appendNotifications(notifications: List<NotificationHistoryItem>, hasMore: Boolean) | Append paginated results |
Real-Time Updatesβ
| Method | Signature | Description |
|---|---|---|
addNotification | suspend fun addNotification(notification: NotificationHistoryItem, emitEvent: Boolean = true) | Add at top; skips duplicates by id; optionally emits newNotificationEvent |
addFromPush | suspend 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β
| Method | Signature | Description |
|---|---|---|
markAsRead | suspend fun markAsRead(notificationId: Int) | Set readAt to now, update unread count |
removeNotification | suspend fun removeNotification(notificationId: Int) | Remove by id, update unread count |
markAllAsRead | suspend fun markAllAsRead() | Set readAt on all items, reset unread count to 0 |
clearAll | suspend fun clearAll() | Empty list, reset unread count and hasMore |
Loading Stateβ
| Method | Signature | Description |
|---|---|---|
setLoading | fun setLoading(loading: Boolean) | Toggle loading indicator |
Query Methodsβ
| Method | Signature | Description |
|---|---|---|
getByType | fun getByType(eventType: String): List<NotificationHistoryItem> | Filter current list by type |
getByDevice | fun getByDevice(deviceId: Int): List<NotificationHistoryItem> | Filter current list by deviceId |
getUnread | fun getUnread(): List<NotificationHistoryItem> | All items where readAt == null |
State Managementβ
| Method | Signature | Description |
|---|---|---|
clear | suspend fun clear() | Reset all state (used on logout) |
invalidate | fun 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
)
| Method | Signature | Description |
|---|---|---|
areNotificationsEnabled | fun areNotificationsEnabled(): Boolean | Delegates to NotificationManagerCompat.from(context).areNotificationsEnabled() |
createNotificationSettingsIntent | fun createNotificationSettingsIntent(): Intent | Returns 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
)
| Method | Signature | Description |
|---|---|---|
getSelectedSound | fun getSelectedSound(): String | Read selected_sound key, defaults to "default" |
saveSelectedSound | fun 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β
| Property | Type | Description |
|---|---|---|
fcmToken | StateFlow<String?> | Current FCM push token |
notificationCount | StateFlow<Int> | Running count of received notifications |
notifications | StateFlow<List<PushNotification>> | In-memory list of push notifications (newest first) |
dismissedEventIds | StateFlow<Set<Int>> | Set of event IDs the user has dismissed |
pendingNavigation | StateFlow<NotificationClickData?> | Navigation data from a tapped notification |
Methodsβ
| Method | Signature | Description |
|---|---|---|
setToken | fun setToken(token: String) | Store FCM token |
addNotification | fun addNotification(title: String, body: String, data: Map<String, String>) | Prepend a PushNotification, increment count |
dismissNotification | fun dismissNotification(id: String) | Mark notification as dismissed by id |
removeNotification | fun removeNotification(id: String) | Remove notification from list by id |
dismissEvent | fun dismissEvent(eventId: Int) | Add event ID to dismissed set |
isEventDismissed | fun isEventDismissed(eventId: Int): Boolean | Check if event ID is in dismissed set |
clearNotifications | fun clearNotifications() | Empty notification list |
clearAll | fun clearAll() | Empty notifications and dismissed events |
setPendingNavigation | fun setPendingNavigation(deviceId: Int, title: String, body: String, eventType: String?, timestamp: Long, latitude: Double? = null, longitude: Double? = null) | Store navigation data from a notification tap |
consumePendingNavigation | fun consumePendingNavigation(): NotificationClickData? | Return and clear pending navigation (one-shot) |
clearPendingNavigation | fun clearPendingNavigation() | Clear pending navigation without consuming |