diff --git a/README.md b/README.md
index b5e076c..9ca8369 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,30 @@ Once created don't forget to update the:
- Issues Template (bug report + feature request).
- Pull Request Template.
+## Firebase Push Demo 📬
+
+- Replace `app/google-services.json` with the config generated from your Firebase project (keep the same package name or update `APP_ID` accordingly).
+- Enable Firebase Cloud Messaging in the Firebase console and download a server key for testing pushes.
+- Build and run the sample app; the home screen shows the current FCM registration token and automatically subscribes the device to the `demo` topic.
+- Allow the runtime notification permission on Android 13+ when prompted so foreground demos can surface notifications.
+- Send a notification from the Firebase console to either the displayed token or the `demo` topic, or trigger it manually:
+
+```bash
+curl -X POST https://fcm.googleapis.com/fcm/send \
+ -H "Authorization: key=${FIREBASE_SERVER_KEY}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "to": "/topics/demo",
+ "notification": {
+ "title": "Hello from FCM",
+ "body": "This push was sent using the legacy HTTP API."
+ },
+ "data": {
+ "message": "You can also handle custom payload data in the demo service."
+ }
+ }'
+```
+
## Troubleshooting
For help with issues which you might encounter when using this template, please refer to [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f1677d8..46da087 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("com.android.application")
kotlin("android")
+ alias(libs.plugins.google.services)
}
val APP_VERSION_NAME : String by project
@@ -71,8 +72,11 @@ dependencies {
implementation(projects.libraryKotlin)
implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.activity)
implementation(libs.androidx.constraint.layout)
implementation(libs.androidx.core.ktx)
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.messaging)
testImplementation(libs.junit)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ddf8326..3a6d3e7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,10 +2,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/ncorti/kotlin/template/app/MainActivity.kt b/app/src/main/java/com/ncorti/kotlin/template/app/MainActivity.kt
index dc8142a..fb29caf 100644
--- a/app/src/main/java/com/ncorti/kotlin/template/app/MainActivity.kt
+++ b/app/src/main/java/com/ncorti/kotlin/template/app/MainActivity.kt
@@ -1,22 +1,48 @@
package com.ncorti.kotlin.template.app
+import android.Manifest
import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
+import android.util.Log
import android.view.View
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import com.google.firebase.messaging.FirebaseMessaging
import com.ncorti.kotlin.template.app.databinding.ActivityMainBinding
+import com.ncorti.kotlin.template.app.firebase.NotificationHelper
import com.ncorti.kotlin.template.library.FactorialCalculator
import com.ncorti.kotlin.template.library.android.ToastUtil
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
+ private var notificationPermissionLauncher: ActivityResultLauncher? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+ setupNotificationPermissionLauncher()
+ NotificationHelper.ensureDefaultChannel(this)
+
+ if (isNotificationPermissionGranted()) {
+ fetchFirebaseToken()
+ } else {
+ binding.textFirebaseToken.text = getString(R.string.firebase_token_permission_required)
+ requestNotificationPermissionIfNeeded()
+ fetchFirebaseToken()
+ }
+
+ subscribeToDemoTopic()
+ setupUiListeners()
+ }
+
+ private fun setupUiListeners() {
binding.buttonCompute.setOnClickListener {
val message = if (binding.editTextFactorial.text.isNotEmpty()) {
val input = binding.editTextFactorial.text.toString().toLong()
@@ -40,4 +66,86 @@ class MainActivity : AppCompatActivity() {
startActivity(intent)
}
}
+
+ private fun setupNotificationPermissionLauncher() {
+ notificationPermissionLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ when {
+ isGranted -> {
+ Log.d(TAG, "Notification permission granted")
+ fetchFirebaseToken()
+ }
+
+ shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> {
+ binding.textFirebaseToken.text =
+ getString(R.string.firebase_token_permission_rationale)
+ }
+
+ else -> {
+ binding.textFirebaseToken.text =
+ getString(R.string.firebase_token_permission_denied)
+ }
+ }
+ }
+ }
+
+ private fun requestNotificationPermissionIfNeeded() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ return
+ }
+
+ if (isNotificationPermissionGranted()) {
+ return
+ }
+
+ notificationPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+
+ private fun isNotificationPermissionGranted(): Boolean {
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
+ ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun fetchFirebaseToken() {
+ binding.textFirebaseToken.text = getString(R.string.firebase_token_loading)
+ FirebaseMessaging.getInstance().token
+ .addOnCompleteListener { task ->
+ if (!task.isSuccessful) {
+ val errorMessage =
+ task.exception?.localizedMessage ?: getString(R.string.firebase_token_error_generic)
+ binding.textFirebaseToken.text =
+ getString(R.string.firebase_token_error, errorMessage)
+ Log.w(TAG, "Fetching FCM registration token failed", task.exception)
+ return@addOnCompleteListener
+ }
+
+ val token = task.result
+ binding.textFirebaseToken.text = token
+ Log.d(TAG, "FCM registration token: $token")
+ }
+ }
+
+ private fun subscribeToDemoTopic() {
+ FirebaseMessaging.getInstance().subscribeToTopic(DEMO_TOPIC)
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ Log.d(TAG, "Subscribed to '$DEMO_TOPIC' topic for demo pushes.")
+ } else {
+ Log.w(TAG, "Topic subscription failed", task.exception)
+ }
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ }
+
+ companion object {
+ private const val TAG = "MainActivity"
+ private const val DEMO_TOPIC = "demo"
+ }
}
diff --git a/app/src/main/java/com/ncorti/kotlin/template/app/firebase/DemoFirebaseMessagingService.kt b/app/src/main/java/com/ncorti/kotlin/template/app/firebase/DemoFirebaseMessagingService.kt
new file mode 100644
index 0000000..4ac8460
--- /dev/null
+++ b/app/src/main/java/com/ncorti/kotlin/template/app/firebase/DemoFirebaseMessagingService.kt
@@ -0,0 +1,68 @@
+package com.ncorti.kotlin.template.app.firebase
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import com.ncorti.kotlin.template.app.R
+import kotlin.random.Random
+
+class DemoFirebaseMessagingService : FirebaseMessagingService() {
+
+ override fun onMessageReceived(message: RemoteMessage) {
+ super.onMessageReceived(message)
+
+ val channelId = NotificationHelper.ensureDefaultChannel(this)
+
+ val title = message.notification?.title
+ ?: message.data[KEY_TITLE]
+ ?: getString(R.string.firebase_default_notification_title)
+
+ val body = message.notification?.body
+ ?: message.data[KEY_BODY]
+ ?: message.data[KEY_MESSAGE]
+ ?: getString(R.string.firebase_default_notification_body)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ Log.w(TAG, "Notification permission not granted; dropping push message.")
+ return
+ }
+
+ Log.d(TAG, "Message received from ${message.from}: $message")
+
+ val notification = NotificationCompat.Builder(this, channelId)
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentTitle(title)
+ .setContentText(body)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(body))
+ .setAutoCancel(true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .build()
+
+ with(NotificationManagerCompat.from(this)) {
+ notify(Random.nextInt(0, Int.MAX_VALUE), notification)
+ }
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ Log.i(TAG, "FCM token refreshed: $token")
+ }
+
+ private companion object {
+ private const val TAG = "DemoFirebaseMessaging"
+ private const val KEY_TITLE = "title"
+ private const val KEY_BODY = "body"
+ private const val KEY_MESSAGE = "message"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ncorti/kotlin/template/app/firebase/NotificationHelper.kt b/app/src/main/java/com/ncorti/kotlin/template/app/firebase/NotificationHelper.kt
new file mode 100644
index 0000000..1350c78
--- /dev/null
+++ b/app/src/main/java/com/ncorti/kotlin/template/app/firebase/NotificationHelper.kt
@@ -0,0 +1,33 @@
+package com.ncorti.kotlin.template.app.firebase
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.core.content.getSystemService
+import com.ncorti.kotlin.template.app.R
+
+object NotificationHelper {
+
+ fun ensureDefaultChannel(context: Context): String {
+ val channelId = context.getString(R.string.firebase_notification_channel_id)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channelName = context.getString(R.string.firebase_notification_channel_name)
+ val channelDescription =
+ context.getString(R.string.firebase_notification_channel_description)
+
+ val notificationManager: NotificationManager? = context.getSystemService()
+ val channel = NotificationChannel(
+ channelId,
+ channelName,
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = channelDescription
+ }
+ notificationManager?.createNotificationChannel(channel)
+ }
+
+ return channelId
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index cc49eb8..6c47300 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -24,9 +24,32 @@
android:layout_marginVertical="@dimen/base_grid_size"
android:text="@string/description"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
- app:layout_constraintBottom_toTopOf="@id/edit_text_factorial"
+ app:layout_constraintBottom_toTopOf="@id/text_firebase_token_label"
app:layout_constraintStart_toStartOf="parent" />
+
+
+
+
The Result is: %s
Please Enter a Number
In Compose
+ FCM registration token
+ Fetching token…
+ Unable to fetch token (%1$s)
+ unknown error
+ Grant notification permission to preview incoming pushes.
+ Enable notifications so we can display Firebase push messages.
+ Notification permission denied. You can enable it later from system settings.
+ firebase_demo_channel
+ Demo push notifications
+ Shows Firebase Cloud Messaging demo alerts.
+ Firebase demo message
+ Send yourself a push from the Firebase console or REST API to see it here.
diff --git a/build.gradle.kts b/build.gradle.kts
index 54e16ca..bd2b4e9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,6 +3,7 @@ plugins {
id("com.android.library") apply false
kotlin("android") apply false
alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.google.services) apply false
alias(libs.plugins.detekt)
alias(libs.plugins.nexus.publish)
cleanup
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 41b3901..90b5e90 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,10 +16,12 @@ kotlin = "2.2.21"
min_sdk_version = "23"
nexus_publish = "2.0.0"
target_sdk_version = "36"
+firebase_bom = "33.7.0"
[libraries]
junit = { module = "junit:junit", version.ref = "junit" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "androidx_activity_compose" }
+androidx_activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx_activity_compose" }
androidx_appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx_constraint_layout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraint_layout" }
androidx_core_ktx = { module = "androidx.core:core-ktx", version.ref = "core_ktx" }
@@ -38,8 +40,11 @@ detekt_formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting",
espresso_core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso_core" }
agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
kgp = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase_bom" }
+firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx" }
[plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus_publish" }
\ No newline at end of file
+nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus_publish" }
+google-services = { id = "com.google.gms.google-services", version = "4.4.2" }
\ No newline at end of file