Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sample implementation of registration tokens for FCM #1453

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
6 changes: 6 additions & 0 deletions messaging/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,14 @@ dependencies {

implementation 'com.google.firebase:firebase-installations-ktx:17.1.0'

// Used to store FCM registration tokens
implementation 'com.google.firebase:firebase-firestore-ktx:24.1.0'
andreaowu marked this conversation as resolved.
Show resolved Hide resolved

implementation 'androidx.work:work-runtime:2.7.1'

// Used for Firestore
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'

// Testing dependencies
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test:runner:1.5.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.google.firebase.quickstart.fcm.kotlin
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
Expand All @@ -11,11 +12,18 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.google.android.gms.tasks.OnCompleteListener
import androidx.lifecycle.lifecycleScope
import com.google.firebase.Timestamp
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.google.firebase.messaging.ktx.messaging
import com.google.firebase.quickstart.fcm.R
import com.google.firebase.quickstart.fcm.databinding.ActivityMainBinding
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import java.util.Calendar
import java.util.Date

class MainActivity : AppCompatActivity() {

Expand Down Expand Up @@ -80,25 +88,40 @@ class MainActivity : AppCompatActivity() {
binding.logTokenButton.setOnClickListener {
// Get token
// [START log_reg_token]
Firebase.messaging.getToken().addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
}

lifecycleScope.launch {
// Get new FCM registration token
val token = task.result
val token = getAndStoreRegToken()

// Log and toast
val msg = getString(R.string.msg_token_fmt, token)
Log.d(TAG, msg)
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
})
}
// [END log_reg_token]
}

Toast.makeText(this, "See README for setup instructions", Toast.LENGTH_SHORT).show()
askNotificationPermission()

// In the app’s first Activity
val preferences = this.getPreferences(Context.MODE_PRIVATE)
val lastRefreshLong = preferences.getLong("lastRefreshDate", -1)
lifecycleScope.launch {
val c = Calendar.getInstance().apply {
time = if (lastRefreshLong == -1L) Date() else Date(lastRefreshLong)
add(Calendar.DATE, 30)
}

val today = Date()
if (today.after(c.time) || lastRefreshLong == -1L) {
// get token and store into Firestore
getAndStoreRegToken()
// sync device cache time with Firestore just in case
val document = Firebase.firestore.collection("refresh").document("refreshDate").get().await()
val updatedTime = (document.data!!["lastRefreshDate"] as Timestamp).seconds * 1000
preferences.edit().putLong("lastRefreshDate", updatedTime)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new values need to be applied to the SharedPreferences after they're edited:

Suggested change
preferences.edit().putLong("lastRefreshDate", updatedTime)
preferences.edit().putLong("lastRefreshDate", updatedTime).apply()

}
}
}

private fun askNotificationPermission() {
Expand All @@ -115,6 +138,20 @@ class MainActivity : AppCompatActivity() {
}
}

private suspend fun getAndStoreRegToken(): String {
var token = Firebase.messaging.token.await()
// Add token and timestamp to Firestore for this user
val deviceToken = hashMapOf(
"token" to token,
"timestamp" to FieldValue.serverTimestamp(),
)

// Get user ID from Firebase Auth or your own server
Firebase.firestore.collection("fcmTokens").document("myuserid")
.set(deviceToken).await()
return token
}

companion object {

private const val TAG = "MainActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.google.firebase.quickstart.fcm.R
Expand Down Expand Up @@ -74,7 +77,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
// If you want to send messages to this application instance or
// manage this apps subscriptions on the server side, send the
// FCM registration token to your app server.
sendRegistrationToServer(token)
sendTokenToServer(token)
}
// [END on_new_token]

Expand Down Expand Up @@ -103,9 +106,17 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
*
* @param token The new token.
*/
private fun sendRegistrationToServer(token: String?) {
private fun sendTokenToServer(token: String?) {
// TODO: Implement this method to send token to your app server.
Log.d(TAG, "sendRegistrationTokenToServer($token)")
// Add token and timestamp to Firestore for this user
val deviceToken = hashMapOf(
"token" to token,
"timestamp" to FieldValue.serverTimestamp(),
)

// Get user ID from Firebase Auth or your own server
Firebase.firestore.collection("fcmTokens").document("myuserid")
.set(deviceToken)
}

/**
Expand Down
41 changes: 41 additions & 0 deletions messaging/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

const EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30; // 30 days

/**
* Scheduled function that runs once a month. It updates the last refresh date for
* tokens so that a client can refresh the token if the last time it did so was
* before the refresh date.
*/

exports.scheduledFunction = functions.pubsub.schedule('0 0 1 * *').onRun((context) => {
admin.firestore().doc('refresh/refreshDate').set({ lastRefreshDate : Date.now() });
});

/**
* Scheduled function that runs once a day. It retrieves all stale tokens then
* unsubscribes them from 'topic1' then deletes them.
*
* Note: topic1 is an example topic here. It is up to the developer to unsubscribe
* all topics the token is subscribed to.
*/
exports.pruneTokens = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
const staleTokensResult = await admin.firestore().collection('fcmTokens')
.where("timestamp", "<", Date.now() - EXPIRATION_TIME)
.get();

const staleTokens = staleTokensResult.docs.map(staleTokenDoc => staleTokenDoc.id);

await admin.messaging().unsubscribeFromTopic(staleTokens, 'topic1');

const deletePromises = [];
for (const staleTokenDoc of staleTokensResult.docs) {
deletePromises.push(staleTokenDoc.ref.delete());
}
await Promise.all(deletePromises);
});
1 change: 1 addition & 0 deletions messaging/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true