feat: Firebase Cloud Messaging push notifications
Backend: - New user_fcm_tokens table (migration 010) for storing device tokens - FCMService sends push via Firebase HTTP legacy API (server key from .env) - POST /api/fcm/register endpoint for Android to register its FCM token - AdminController::sendNotification() triggers FCM push after DB insert - FCM config added to config.php Android: - Firebase Cloud Messaging service (ServerManagerFirebaseService) receives push notifications and shows them via NotificationHelper - FCM token registered with backend on new token and on login - google-services.json template (must be replaced with real Firebase config) - firebase-setup.sh script with configuration instructions - FCM dependency + google-services plugin added to Gradle - Bump to v1.8.0 (versionCode 12)
This commit is contained in:
@@ -2,6 +2,7 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -65,6 +66,8 @@ dependencies {
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.work:work-runtime-ktx:2.10.0")
|
||||
|
||||
implementation("com.google.firebase:firebase-messaging-ktx:24.1.0")
|
||||
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
}
|
||||
|
||||
35
android/app/google-services.json
Normal file
35
android/app/google-services.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "000000000000",
|
||||
"project_id": "servermanager-placeholder",
|
||||
"storage_bucket": "servermanager-placeholder.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
|
||||
"android_client_info": {
|
||||
"package_name": "com.devlab.app"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "PLACEHOLDER_API_KEY_REPLACE_IN_FIREBASE_CONSOLE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 0
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 0
|
||||
},
|
||||
"google_cast_service": {
|
||||
"status": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -42,6 +42,14 @@
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".service.ServerManagerFirebaseService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@@ -79,4 +79,7 @@ interface ApiService {
|
||||
|
||||
@GET("api/notifications/unread-count")
|
||||
suspend fun getUnreadCount(): UnreadCountResponse
|
||||
|
||||
@POST("api/fcm/register")
|
||||
suspend fun registerFcmToken(@Body request: FcmRegisterRequest): ApiResponse<Map<String, Boolean>>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.devlab.app.data.model
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class FcmRegisterRequest(
|
||||
val token: String
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.devlab.app.service
|
||||
|
||||
import android.util.Log
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import com.devlab.app.data.api.RetrofitClient
|
||||
import com.devlab.app.data.model.AppNotification
|
||||
import com.devlab.app.data.model.FcmRegisterRequest
|
||||
import com.devlab.app.util.NotificationHelper
|
||||
import com.devlab.app.util.PreferencesManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ServerManagerFirebaseService : FirebaseMessagingService() {
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
Log.d(TAG, "New FCM token: $token")
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val prefs = PreferencesManager(this@ServerManagerFirebaseService)
|
||||
prefs.saveFcmToken(token)
|
||||
|
||||
val apiToken = prefs.getToken()
|
||||
if (apiToken.isNotBlank()) {
|
||||
registerTokenWithBackend(token, apiToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
super.onMessageReceived(message)
|
||||
Log.d(TAG, "FCM message received: ${message.data}")
|
||||
|
||||
val title = message.notification?.title
|
||||
?: message.data["title"]
|
||||
?: "Notification"
|
||||
|
||||
val body = message.notification?.body
|
||||
?: message.data["message"]
|
||||
?: ""
|
||||
|
||||
val type = message.data["type"] ?: "info"
|
||||
val noteId = (message.data["notification_id"] ?: "0").toIntOrNull() ?: 0
|
||||
|
||||
showNotification(title, body, type, noteId)
|
||||
}
|
||||
|
||||
private fun showNotification(title: String, body: String, type: String, noteId: Int) {
|
||||
val notification = AppNotification(
|
||||
id = noteId,
|
||||
title = title,
|
||||
message = body,
|
||||
type = type,
|
||||
is_read = 0,
|
||||
created_at = "",
|
||||
)
|
||||
NotificationHelper.showNotification(this, notification)
|
||||
}
|
||||
|
||||
private suspend fun registerTokenWithBackend(token: String, apiToken: String) {
|
||||
try {
|
||||
RetrofitClient.setToken(apiToken)
|
||||
val service = RetrofitClient.getApiService()
|
||||
service.registerFcmToken(FcmRegisterRequest(token))
|
||||
Log.d(TAG, "FCM token registered with backend")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to register FCM token: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ServerManagerFCM"
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.devlab.app.ServerManagerApp
|
||||
import com.devlab.app.data.api.RetrofitClient
|
||||
import com.devlab.app.data.api.SessionManager
|
||||
import com.devlab.app.data.repository.AuthRepository
|
||||
import com.devlab.app.data.model.FcmRegisterRequest
|
||||
import com.devlab.app.util.PreferencesManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -77,6 +78,7 @@ class LoginViewModel : ViewModel() {
|
||||
isConnected = true,
|
||||
error = null
|
||||
)
|
||||
registerFcmToken()
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
@@ -95,4 +97,14 @@ class LoginViewModel : ViewModel() {
|
||||
prefs.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun registerFcmToken() {
|
||||
try {
|
||||
val fcmToken = prefs.getFcmToken()
|
||||
if (fcmToken.isNotBlank()) {
|
||||
val service = RetrofitClient.getApiService()
|
||||
service.registerFcmToken(FcmRegisterRequest(fcmToken))
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class PreferencesManager(private val context: Context) {
|
||||
private val KEY_EMAIL = stringPreferencesKey("email")
|
||||
private val KEY_ROLE = stringPreferencesKey("role")
|
||||
private val KEY_USER_ID = intPreferencesKey("user_id")
|
||||
private val KEY_FCM_TOKEN = stringPreferencesKey("fcm_token")
|
||||
private val KEY_LAST_NOTIFICATION_CHECK = longPreferencesKey("last_notification_check")
|
||||
}
|
||||
|
||||
@@ -80,6 +81,14 @@ class PreferencesManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveFcmToken(token: String) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[KEY_FCM_TOKEN] = token
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFcmToken(): String = context.dataStore.data.first()[KEY_FCM_TOKEN] ?: ""
|
||||
|
||||
suspend fun clear() {
|
||||
context.dataStore.edit { it.clear() }
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ plugins {
|
||||
id("com.android.application") version "9.2.1" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" apply false
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.21" apply false
|
||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user