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
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,14 +12,23 @@ 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() {

val IS_OPTIMIZE = true
Copy link
Member

Choose a reason for hiding this comment

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

Since this property is only used in this class, we can make it private

Suggested change
val IS_OPTIMIZE = true
private val IS_OPTIMIZE = true


private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
Expand Down Expand Up @@ -79,26 +89,24 @@ 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()

if (IS_OPTIMIZE) {
dateRefresh()
} else {
optimizedDateRefresh() // optimized version of dateRefresh() that uses Android SharedPreferences
}
}

private fun askNotificationPermission() {
Expand All @@ -115,8 +123,86 @@ class MainActivity : AppCompatActivity() {
}
}

private suspend fun getAndStoreRegToken(): String {
// [START log_reg_token]
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()
// [END log_reg_token]
Log.d(TAG, "got token: $token")

// As an optimization, store today’s date in Android cache
if (IS_OPTIMIZE) {
val preferences = this.getSharedPreferences("default", Context.MODE_PRIVATE)
preferences.edit().putLong("lastDeviceRefreshDate", Date().time)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
preferences.edit().putLong("lastDeviceRefreshDate", Date().time)
preferences.edit().putLong("lastDeviceRefreshDate", Date().time).apply()

}

return token
}

// Check to see whether this device's registration token was refreshed within the last month. Refresh if not.
private fun dateRefresh() {
lifecycleScope.launch {
val refreshDate = (Firebase.firestore.collection("refresh")
.document("refreshDate").get().await().data!!["lastRefreshDate"] as Timestamp)
val deviceRefreshDate = (Firebase.firestore.collection("fcmTokens")
.document("myuserid").get().await().data!!["timestamp"] as Timestamp)
if (deviceRefreshDate < refreshDate) {
getAndStoreRegToken()
}
}
}

/*
As an optimization to prevent Firestore calls every time the device opens the app, store the last all-devices
refresh date (lastGlobalRefresh) and this particular device's last refresh date (lastDeviceRefresh) into
Android's SharedPreferences.

If lastDeviceRefresh is before lastGlobalRefresh, update the device's registration token, and store it into
Firestore and SharedPreferencs. Also, if today's date is a month after lastGlobalRefresh, sync lastGlobalRefresh
in SharedPreferences with Firestore's lastGlobalRefresh.
*/
private fun optimizedDateRefresh() {
val preferences = this.getPreferences(Context.MODE_PRIVATE)
// Refresh date (stored as milliseconds, SharedPreferences cannot store Date) that ensures token freshness
val lastGlobalRefreshLong = preferences.getLong("lastGlobalRefreshDate", -1)
val lastGlobalRefresh = Date(lastGlobalRefreshLong)
// Date of last refresh of device’s registration token
val lastDeviceRefreshLong = preferences.getLong("lastDeviceRefreshDate", -1)
val lastDeviceRefresh = Date(lastDeviceRefreshLong)
lifecycleScope.launch {
if (lastDeviceRefreshLong == -1L || lastGlobalRefreshLong == -1L
|| lastDeviceRefresh.before(lastGlobalRefresh)) {
// Get token, store into Firestore, and update cache
getAndStoreRegToken()
preferences.edit().putLong("lastGlobalRefreshDate", lastDeviceRefresh.time)
Copy link
Member

Choose a reason for hiding this comment

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

Don't forget to call apply:

Suggested change
preferences.edit().putLong("lastGlobalRefreshDate", lastDeviceRefresh.time)
preferences.edit().putLong("lastGlobalRefreshDate", lastDeviceRefresh.time).apply()

}

// Check if today is more than one month beyond cached global refresh date
// and if so, sync date with Firestore and update cache
val today = Date()
val c = Calendar.getInstance().apply {
time = if (lastGlobalRefreshLong == -1L) today else lastGlobalRefresh
add(Calendar.DATE, 30)
}

if (lastGlobalRefreshLong == -1L || today.after(c.time)) {
val document = Firebase.firestore.collection("refresh").document("refreshDate").get().await()
val updatedTime = (document.data!!["lastRefreshDate"] as Timestamp).seconds * 1000
preferences.edit().putLong("lastGlobalRefreshDate", updatedTime)
Copy link
Member

Choose a reason for hiding this comment

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

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

}
}
}

companion object {

private const val TAG = "MainActivity"
private const val TAG = "MainActivityandreawu"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.os.Build
import android.preference.PreferenceManager
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
import java.util.*
Copy link
Member

Choose a reason for hiding this comment

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

Avoid wildcard imports



class MyFirebaseMessagingService : FirebaseMessagingService() {

Expand Down Expand Up @@ -74,7 +80,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 +109,21 @@ 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)

// As an optimization, store today’s date in Android cache
val preferences = this.getSharedPreferences("default", Context.MODE_PRIVATE)
preferences.edit().putLong("lastDeviceRefreshDate", Date().time)
Copy link
Member

Choose a reason for hiding this comment

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

Don't forget to call apply() after editing your preferences:

Suggested change
preferences.edit().putLong("lastDeviceRefreshDate", Date().time)
preferences.edit().putLong("lastDeviceRefreshDate", Date().time).apply()

}

/**
Expand Down
44 changes: 44 additions & 0 deletions messaging/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'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.
*/
// [START refresh_date_scheduled_function]
exports.scheduledFunction = functions.pubsub.schedule('0 0 1 * *').onRun((context) => {
admin.firestore().doc('refresh/refreshDate').set({ lastRefreshDate : Date.now() });
});
// [END refresh_date_scheduled_function]

/**
* Scheduled function that runs once a day. It retrieves all stale tokens then
* unsubscribes them from 'topic1' then deletes them.
*
* Note: weather is an example topic here. It is up to the developer to unsubscribe
* all topics the token is subscribed to.
*/
// [START remove_stale_tokens]
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, 'weather');

const deletePromises = [];
for (const staleTokenDoc of staleTokensResult.docs) {
deletePromises.push(staleTokenDoc.ref.delete());
}
await Promise.all(deletePromises);
});
// [END remove_stale_tokens]
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