[Firebase] Implement new custom FCM service.

This commit is contained in:
Kuba Szczodrzyński 2020-01-11 19:07:25 +01:00
parent 92ba7248ef
commit 8e2297359c
6 changed files with 288 additions and 30 deletions

View File

@ -172,13 +172,13 @@
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<receiver android:name=".sync.FirebaseBroadcastReceiver"
<!--<receiver android:name=".sync.FirebaseBroadcastReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</receiver>
</receiver>-->
<receiver android:name=".receivers.SzkolnyReceiver"
android:exported="true">
<intent-filter>
@ -193,16 +193,23 @@
____) | __/ | \ V /| | (_| __/\__ \
|_____/ \___|_| \_/ |_|\___\___||___/
-->
<service android:name=".sync.MyFirebaseMessagingService"
<!--<service android:name=".sync.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</service>-->
<service android:name=".receivers.BootReceiver$NotificationActionService" />
<service android:name=".Notifier$GetDataRetryService" />
<service android:name=".data.api.ApiService" />
<service android:name=".sync.MyFirebaseService"
android:exported="false">
<intent-filter android:priority="10000000">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<!--
_____ _ _

View File

@ -3,6 +3,7 @@ package pl.szczodrzynski.edziennik
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Resources
@ -643,6 +644,28 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
}
}
fun Bundle.toJsonObject(): JsonObject {
val json = JsonObject()
keySet()?.forEach { key ->
get(key)?.let {
when (it) {
is String -> json.addProperty(key, it)
is Char -> json.addProperty(key, it)
is Int -> json.addProperty(key, it)
is Long -> json.addProperty(key, it)
is Float -> json.addProperty(key, it)
is Short -> json.addProperty(key, it)
is Double -> json.addProperty(key, it)
is Boolean -> json.addProperty(key, it)
is Bundle -> json.add(key, it.toJsonObject())
else -> json.addProperty(key, it.toString())
}
}
}
return json
}
fun Intent.toJsonObject() = extras?.toJsonObject()
fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
fun JsonArray.isEmpty(): Boolean = this.size() == 0
operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o)

View File

@ -8,39 +8,28 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.legacy.content.WakefulBroadcastReceiver
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
class FirebaseBroadcastReceiver : WakefulBroadcastReceiver() {
val TAG: String = FirebaseBroadcastReceiver::class.java.simpleName
companion object {
private const val TAG = "FirebaseBroadcast"
}
override fun onReceive(context: Context, intent: Intent) {
val extras = intent.extras
val json = JsonObject()
val dataBundle = intent.extras
if (dataBundle != null)
for (key in dataBundle.keySet()) {
dataBundle.get(key)?.let {
when (it) {
is String -> json.addProperty(key, it)
is Int -> json.addProperty(key, it)
is Long -> json.addProperty(key, it)
is Float -> json.addProperty(key, it)
is Boolean -> json.addProperty(key, it)
else -> json.addProperty(key, it.toString())
}
extras?.keySet()?.forEach { key ->
extras.get(key)?.let {
when (it) {
is String -> json.addProperty(key, it)
is Int -> json.addProperty(key, it)
is Long -> json.addProperty(key, it)
is Float -> json.addProperty(key, it)
is Boolean -> json.addProperty(key, it)
else -> json.addProperty(key, it.toString())
}
}
Log.d(TAG, "Firebase got push from Librus Broadcast ${json}")
val sharedPreferences = context.getSharedPreferences("pushtest_broadcast", Context.MODE_PRIVATE)
sharedPreferences.edit().putString(
System.currentTimeMillis().toString(),
json.toString()
).apply()
}
Log.d(TAG, "Intent(action=${intent?.action}, extras=$json)")
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-11.
*/
package pl.szczodrzynski.edziennik.sync
class FirebaseSendException(reason: String?) : Exception(reason) {
companion object {
const val ERROR_UNKNOWN = 0
const val ERROR_INVALID_PARAMETERS = 1
const val ERROR_SIZE = 2
const val ERROR_TTL_EXCEEDED = 3
const val ERROR_TOO_MANY_MESSAGES = 4
}
val errorCode = when (reason) {
"service_not_available" -> ERROR_TTL_EXCEEDED
"toomanymessages" -> ERROR_TOO_MANY_MESSAGES
"invalid_parameters" -> ERROR_INVALID_PARAMETERS
"messagetoobig" -> ERROR_SIZE
"missing_to" -> ERROR_INVALID_PARAMETERS
else -> ERROR_UNKNOWN
}
}

View File

@ -0,0 +1,190 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-11.
*/
package pl.szczodrzynski.edziennik.sync
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.PendingIntent.CanceledException
import android.content.Intent
import android.util.Log
import com.google.firebase.iid.zzaq
import com.google.firebase.iid.zzv
import com.google.firebase.messaging.MessagingAnalytics
import com.google.firebase.messaging.zzc
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import java.util.*
@SuppressLint("Registered")
open class FirebaseService : zzc() {
companion object {
private const val TAG = "FirebaseService"
}
private val messageQueue = ArrayDeque<String>(10)
open fun onMessageReceived(message: Message) = Unit
open fun onDeletedMessages() = Unit
open fun onMessageSent(messageId: String?) = Unit
open fun onSendError(messageId: String?, exception: Exception) = Unit
open fun onNewToken(token: String?) = Unit
// apparently this gets the correct intent from some
// kind of queue inside Firebase's InstanceID Receiver
final override fun zza(intent: Intent?) = zzaq.zza()?.zzb()
final override fun zzb(intent: Intent?): Boolean {
val action = intent?.action
if (action == "com.google.firebase.messaging.NOTIFICATION_OPEN") {
intent.getParcelableExtra<PendingIntent>("pending_intent")?.let {
try {
it.send()
} catch (e: CanceledException) {
Log.e(TAG, "Notification pending intent canceled")
}
}
if (MessagingAnalytics.shouldUploadMetrics(intent)) {
MessagingAnalytics.logNotificationOpen(intent)
}
return true
}
return false
}
final override fun zzc(intent: Intent?) {
val action = intent?.action
val json = intent?.toJsonObject()
Log.d(TAG, "zzc Intent(action=$action, extras=$json)")
if (action == null || json == null)
return
when (action) {
"com.google.firebase.messaging.NOTIFICATION_DISMISS" -> {
if (MessagingAnalytics.shouldUploadMetrics(intent)) {
MessagingAnalytics.logNotificationDismiss(intent)
}
}
"com.google.firebase.messaging.NEW_TOKEN" -> {
onNewToken(json.getString("token"))
}
"com.google.android.c2dm.intent.RECEIVE",
"com.google.firebase.messaging.RECEIVE_DIRECT_BOOT" -> {
val messageId = json.getString("google.message_id")
if (messageId != null) {
// send back an acknowledgement to Google Play Services
val ackBundle = Bundle(
"google.message_id" to messageId
)
zzv.zza(this).zza(2, ackBundle)
}
// check for duplicate message
// and add it to queue
if (messageId.isNotNullNorEmpty()) {
if (messageQueue.contains(messageId)) {
Log.d(TAG, "Received duplicate message: $messageId")
return
}
if (messageQueue.size >= 10)
messageQueue.remove()
messageQueue += messageId
}
// process the received message
processMessage(messageId, json, intent)
}
else -> {
Log.d(TAG, "Unknown intent action: $action")
}
}
}
private fun processMessage(messageId: String?, json: JsonObject, intent: Intent) {
// remove something that the original FMS removes
json.remove("androidx.contentpager.content.wakelockid")
// get the message type
when (val it = json.getString("message_type") ?: "gcm") {
"gcm" -> { // 0
if (MessagingAnalytics.shouldUploadMetrics(intent)) {
MessagingAnalytics.logNotificationReceived(intent)
}
onMessageReceived(Message(messageId, json))
}
"deleted_messages" -> { // 1
onDeletedMessages()
}
"send_event" -> { // 2
onMessageSent(messageId)
}
"send_error" -> { // 3
onSendError(
messageId ?: json.getString("message_id"),
FirebaseSendException(json.getString("error"))
)
}
else -> {
Log.w(TAG, "Received message with unknown type: $it")
return
}
}
}
data class Message(val messageId: String?, private val json: JsonObject) {
val data = json.deepCopy()
val from by lazy { s("from") ?: "" }
val to by lazy { s("google.to") }
val messageType by lazy { s("message_type") }
val collapseKey by lazy { s("collapse_key") }
val sentTime by lazy { l("google.sent_time") }
val ttl by lazy { i("google.ttl") }
val originalPriority by lazy { getPriority(s("google.original_priority") ?: s("priority")) }
val priority by lazy { getPriority(
s("google.delivered_priority") ?: if (i("google.priority_reduced") == 1)
"normal"
else s("google.priority")
) }
val isNotificationMessage by lazy { isNotificationMessage(json) }
val notificationTitle by lazy { s("gcm.notification.title") }
val notificationText by lazy { s("gcm.notification.body") }
init {
data.also {
val toRemove = mutableListOf<String>()
it.keySet().forEach { key ->
if (key.startsWith("google.")
|| key.startsWith("gcm.")
|| key == "from"
|| key == "message_type"
|| key == "collapse_key")
toRemove += key
}
toRemove.forEach { key ->
it.remove(key)
}
}
}
private fun s(key: String): String? = json.getString(key)
private fun l(key: String): Long = json.getLong(key) ?: 0L
private fun i(key: String): Int = json.getInt(key) ?: 0
private fun isNotificationMessage(json: JsonObject): Boolean {
return json.getInt("gcm.n.e") == 1
|| json.getInt("gcm.notification.e") == 1
|| json.getString("gcm.n.icon") != null
|| json.getString("gcm.notification.icon") != null
}
private fun getPriority(str: String?): Int {
return when (str) {
"high" -> 1
"normal" -> 2
else -> 0
}
}
override fun toString(): String {
return "Message(messageId=$messageId, from=$from, data=$data)"
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-11.
*/
package pl.szczodrzynski.edziennik.sync
import android.util.Log
import pl.szczodrzynski.edziennik.App
class MyFirebaseService : FirebaseService() {
companion object {
private const val TAG = "MyFirebaseService"
}
private val app by lazy { applicationContext as App }
override fun onNewToken(token: String?) {
Log.d(TAG, "Got new token: $token")
app.config.sync.tokenApp = token
}
override fun onMessageReceived(message: Message) {
Log.d(TAG, "Message received from ${message.from}: $message")
}
}