-
Notifications
You must be signed in to change notification settings - Fork 7.4k
sample implementation of registration tokens for FCM #1453
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
base: master
Are you sure you want to change the base?
Changes from 12 commits
fca8b50
32c4b65
88f4bf5
4ef6bba
61a1767
5692eb5
1ff3190
cf9f113
7f876a4
baa8132
defd24c
248b3e6
d9cd34b
8057452
0b77e46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 | ||||||
|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
|
||||||
private val requestPermissionLauncher = registerForActivityResult( | ||||||
ActivityResultContracts.RequestPermission() | ||||||
) { isGranted: Boolean -> | ||||||
|
@@ -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() { | ||||||
|
@@ -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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forget to call apply:
Suggested change
|
||||||
} | ||||||
|
||||||
// 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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
companion object { | ||||||
|
||||||
private const val TAG = "MainActivity" | ||||||
private const val TAG = "MainActivityandreawu" | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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.* | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid wildcard imports |
||||||
|
||||||
|
||||||
class MyFirebaseMessagingService : FirebaseMessagingService() { | ||||||
|
||||||
|
@@ -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] | ||||||
|
||||||
|
@@ -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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forget to call apply() after editing your preferences:
Suggested change
|
||||||
} | ||||||
|
||||||
/** | ||||||
|
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] |
Uh oh!
There was an error while loading. Please reload this page.