nfty: Remove firebase support and rating

This commit is contained in:
althafvly 2024-07-02 16:17:41 +05:30
parent 31eadd9768
commit 3486333c2a
13 changed files with 10 additions and 353 deletions

View file

@ -41,14 +41,7 @@ android {
flavorDimensions "store" flavorDimensions "store"
productFlavors { productFlavors {
play {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
}
fdroid { fdroid {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true' buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
} }
} }
@ -110,9 +103,6 @@ dependencies {
// OkHttp (HTTP library) // OkHttp (HTTP library)
implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:okhttp:4.10.0'
// Firebase, sigh ... (only Google Play)
playImplementation 'com.google.firebase:firebase-messaging:23.1.2'
// RecyclerView // RecyclerView
implementation "androidx.recyclerview:recyclerview:1.3.0" implementation "androidx.recyclerview:recyclerview:1.3.0"

View file

@ -1,12 +0,0 @@
package io.heckel.ntfy.firebase
@Suppress("UNUSED_PARAMETER")
class FirebaseMessenger {
fun subscribe(topic: String) {
// Dummy to keep F-Droid flavor happy
}
fun unsubscribe(topic: String) {
// Dummy to keep F-Droid flavor happy
}
}

View file

@ -1,12 +0,0 @@
package io.heckel.ntfy.firebase
import android.app.Service
import android.content.Intent
import android.os.IBinder
// Dummy to keep F-Droid flavor happy
class FirebaseService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

View file

@ -8,7 +8,6 @@ import com.google.gson.stream.JsonReader
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.topicUrl import io.heckel.ntfy.util.topicUrl
@ -18,7 +17,6 @@ class Backuper(val context: Context) {
private val gson = Gson() private val gson = Gson()
private val resolver = context.applicationContext.contentResolver private val resolver = context.applicationContext.contentResolver
private val repository = (context.applicationContext as Application).repository private val repository = (context.applicationContext as Application).repository
private val messenger = FirebaseMessenger()
private val notifier = NotificationService(context) private val notifier = NotificationService(context)
suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) { suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) {
@ -114,11 +112,6 @@ class Backuper(val context: Context) {
) )
repository.addSubscription(subscription) repository.addSubscription(subscription)
// Subscribe to Firebase topics
if (s.baseUrl == appBaseUrl) {
messenger.subscribe(s.topic)
}
// Create dedicated channels // Create dedicated channels
if (s.dedicatedChannels) { if (s.dedicatedChannels) {
notifier.createSubscriptionNotificationChannels(subscription) notifier.createSubscriptionNotificationChannels(subscription)

View file

@ -90,11 +90,7 @@ class SubscriberService : Service() {
Log.d(TAG, "Subscriber service has been created") Log.d(TAG, "Subscriber service has been created")
val title = getString(R.string.channel_subscriber_notification_title) val title = getString(R.string.channel_subscriber_notification_title)
val text = if (BuildConfig.FIREBASE_AVAILABLE) { val text = getString(R.string.channel_subscriber_notification_noinstant_text)
getString(R.string.channel_subscriber_notification_instant_text)
} else {
getString(R.string.channel_subscriber_notification_noinstant_text)
}
notificationManager = createNotificationChannel() notificationManager = createNotificationChannel()
serviceNotification = createNotification(title, text) serviceNotification = createNotification(title, text)
@ -221,18 +217,7 @@ class SubscriberService : Service() {
// Update foreground service notification popup // Update foreground service notification popup
if (connections.size > 0) { if (connections.size > 0) {
val title = getString(R.string.channel_subscriber_notification_title) val title = getString(R.string.channel_subscriber_notification_title)
val text = if (BuildConfig.FIREBASE_AVAILABLE) { val text = when (instantSubscriptions.size) {
when (instantSubscriptions.size) {
1 -> getString(R.string.channel_subscriber_notification_instant_text_one)
2 -> getString(R.string.channel_subscriber_notification_instant_text_two)
3 -> getString(R.string.channel_subscriber_notification_instant_text_three)
4 -> getString(R.string.channel_subscriber_notification_instant_text_four)
5 -> getString(R.string.channel_subscriber_notification_instant_text_five)
6 -> getString(R.string.channel_subscriber_notification_instant_text_six)
else -> getString(R.string.channel_subscriber_notification_instant_text_more, instantSubscriptions.size)
}
} else {
when (instantSubscriptions.size) {
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one) 1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two) 2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three) 3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
@ -241,7 +226,6 @@ class SubscriberService : Service() {
6 -> getString(R.string.channel_subscriber_notification_noinstant_text_six) 6 -> getString(R.string.channel_subscriber_notification_noinstant_text_six)
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size) else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
} }
}
serviceNotification = createNotification(title, text) serviceNotification = createNotification(title, text)
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification) notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
} }

View file

@ -114,10 +114,7 @@ class AddFragment : DialogFragment() {
// Set foreground description text // Set foreground description text
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl)) subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
// Show/hide based on flavor (faster shortcut for validateInputSubscribeView, which can only run onShow)
if (!BuildConfig.FIREBASE_AVAILABLE) {
subscribeInstantDeliveryBox.visibility = View.GONE subscribeInstantDeliveryBox.visibility = View.GONE
}
// Add baseUrl auto-complete behavior // Add baseUrl auto-complete behavior
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -291,16 +288,6 @@ class AddFragment : DialogFragment() {
private fun validateInputSubscribeView() { private fun validateInputSubscribeView() {
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
// Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty".
val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) {
false
} else if (subscribeUseAnotherServerCheckbox.isChecked && subscribeBaseUrlText.text.toString() == appBaseUrl) {
true
} else if (!subscribeUseAnotherServerCheckbox.isChecked && defaultBaseUrl == null) {
true
} else {
false
}
if (subscribeUseAnotherServerCheckbox.isChecked) { if (subscribeUseAnotherServerCheckbox.isChecked) {
subscribeUseAnotherServerDescription.visibility = View.VISIBLE subscribeUseAnotherServerDescription.visibility = View.VISIBLE
subscribeBaseUrlLayout.visibility = View.VISIBLE subscribeBaseUrlLayout.visibility = View.VISIBLE
@ -308,15 +295,9 @@ class AddFragment : DialogFragment() {
subscribeUseAnotherServerDescription.visibility = View.GONE subscribeUseAnotherServerDescription.visibility = View.GONE
subscribeBaseUrlLayout.visibility = View.GONE subscribeBaseUrlLayout.visibility = View.GONE
} }
if (instantToggleAllowed) {
subscribeInstantDeliveryBox.visibility = View.VISIBLE
subscribeInstantDeliveryDescription.visibility = if (subscribeInstantDeliveryCheckbox.isChecked) View.VISIBLE else View.GONE
subscribeForegroundDescription.visibility = View.GONE
} else {
subscribeInstantDeliveryBox.visibility = View.GONE subscribeInstantDeliveryBox.visibility = View.GONE
subscribeInstantDeliveryDescription.visibility = View.GONE subscribeInstantDeliveryDescription.visibility = View.GONE
subscribeForegroundDescription.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE subscribeForegroundDescription.visibility = View.GONE
}
// Enable/disable "Subscribe" button // Enable/disable "Subscribe" button
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -356,7 +337,7 @@ class AddFragment : DialogFragment() {
activity.runOnUiThread { activity.runOnUiThread {
val topic = subscribeTopicText.text.toString() val topic = subscribeTopicText.text.toString()
val baseUrl = getBaseUrl() val baseUrl = getBaseUrl()
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked val instant = baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
subscribeListener.onSubscribe(topic, baseUrl, instant) subscribeListener.onSubscribe(topic, baseUrl, instant)
dialog?.dismiss() dialog?.dismiss()
} }

View file

@ -30,7 +30,6 @@ import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.msg.NotificationService
@ -47,7 +46,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
} }
private val repository by lazy { (application as Application).repository } private val repository by lazy { (application as Application).repository }
private val api = ApiService() private val api = ApiService()
private val messenger = FirebaseMessenger()
private var notifier: NotificationService? = null // Context-dependent private var notifier: NotificationService? = null // Context-dependent
private var appBaseUrl: String? = null // Context-dependent private var appBaseUrl: String? = null // Context-dependent
@ -127,12 +125,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
) )
repository.addSubscription(subscription) repository.addSubscription(subscription)
// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
if (baseUrl == appBaseUrl) {
Log.d(TAG, "Subscribing to Firebase topic $topic")
messenger.subscribe(topic)
}
// Fetch cached messages // Fetch cached messages
try { try {
val user = repository.getUser(subscription.baseUrl) // May be null val user = repository.getUser(subscription.baseUrl) // May be null
@ -529,16 +521,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
val appBaseUrl = getString(R.string.app_base_url) val appBaseUrl = getString(R.string.app_base_url)
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant) val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant) val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
val allowToggleInstant = BuildConfig.FIREBASE_AVAILABLE && subscriptionBaseUrl == appBaseUrl
if (allowToggleInstant) {
enableInstantItem?.isVisible = !subscriptionInstant
disableInstantItem?.isVisible = subscriptionInstant
} else {
enableInstantItem?.isVisible = false enableInstantItem?.isVisible = false
disableInstantItem?.isVisible = false disableInstantItem?.isVisible = false
} }
} }
}
private fun showHideMutedUntilMenuItems(mutedUntilTimestamp: Long) { private fun showHideMutedUntilMenuItems(mutedUntilTimestamp: Long) {
if (!this::menu.isInitialized) { if (!this::menu.isInitialized) {
@ -608,9 +594,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
repository.removeAllNotifications(subscriptionId) repository.removeAllNotifications(subscriptionId)
repository.removeSubscription(subscriptionId) repository.removeSubscription(subscriptionId)
if (subscriptionBaseUrl == appBaseUrl) {
messenger.unsubscribe(subscriptionTopic)
}
} }
finish() finish()
} }

View file

@ -114,7 +114,6 @@ class DetailSettingsActivity : AppCompatActivity() {
private fun loadView() { private fun loadView() {
if (subscription.upAppId == null) { if (subscription.upAppId == null) {
loadInstantPref()
loadMutedUntilPref() loadMutedUntilPref()
loadMinPriorityPref() loadMinPriorityPref()
loadAutoDeletePref() loadAutoDeletePref()
@ -134,29 +133,6 @@ class DetailSettingsActivity : AppCompatActivity() {
loadTopicUrlPref() loadTopicUrlPref()
} }
private fun loadInstantPref() {
val appBaseUrl = getString(R.string.app_base_url)
val prefId = context?.getString(R.string.detail_settings_notifications_instant_key) ?: return
val pref: SwitchPreference? = findPreference(prefId)
pref?.isVisible = BuildConfig.FIREBASE_AVAILABLE && subscription.baseUrl == appBaseUrl
pref?.isChecked = subscription.instant
pref?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
save(subscription.copy(instant = value), refresh = true)
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return subscription.instant
}
}
pref?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { preference ->
if (preference.isChecked) {
getString(R.string.detail_settings_notifications_instant_summary_on)
} else {
getString(R.string.detail_settings_notifications_instant_summary_off)
}
}
}
private fun loadDedicatedChannelsPrefs() { private fun loadDedicatedChannelsPrefs() {
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
val pref: SwitchPreference? = findPreference(prefId) val pref: SwitchPreference? = findPreference(prefId)

View file

@ -34,7 +34,6 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.DownloadManager import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType import io.heckel.ntfy.msg.DownloadType
@ -57,7 +56,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
} }
private val repository by lazy { (application as Application).repository } private val repository by lazy { (application as Application).repository }
private val api = ApiService() private val api = ApiService()
private val messenger = FirebaseMessenger()
// UI elements // UI elements
private lateinit var menu: Menu private lateinit var menu: Menu
@ -199,9 +197,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Create notification channels right away, so we can configure them immediately after installing the app // Create notification channels right away, so we can configure them immediately after installing the app
dispatcher?.init() dispatcher?.init()
// Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
messenger.subscribe(ApiService.CONTROL_TOPIC)
// Darrkkkk mode // Darrkkkk mode
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode()) AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
@ -349,10 +344,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
} }
val mutedUntilSeconds = repository.getGlobalMutedUntil() val mutedUntilSeconds = repository.getGlobalMutedUntil()
runOnUiThread { runOnUiThread {
// Show/hide in-app rate widget
val rateAppItem = menu.findItem(R.id.main_menu_rate)
rateAppItem.isVisible = BuildConfig.RATE_APP_AVAILABLE
// Pause notification icons // Pause notification icons
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled) val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until) val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
@ -389,14 +380,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_report_bug_url)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_report_bug_url))))
true true
} }
R.id.main_menu_rate -> {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")))
} catch (e: ActivityNotFoundException) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName")))
}
true
}
R.id.main_menu_donate -> { R.id.main_menu_donate -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_donate_url)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_donate_url))))
true true
@ -466,12 +449,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
) )
viewModel.add(subscription) viewModel.add(subscription)
// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
if (baseUrl == appBaseUrl) {
Log.d(TAG, "Subscribing to Firebase topic $topic")
messenger.subscribe(topic)
}
// Fetch cached messages // Fetch cached messages
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {

View file

@ -105,7 +105,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE
notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE
instantImageView.visibility = if (subscription.instant && BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE instantImageView.visibility = View.GONE
if (isUnifiedPush || subscription.newCount == 0) { if (isUnifiedPush || subscription.newCount == 0) {
newItemsView.visibility = View.GONE newItemsView.visibility = View.GONE
} else { } else {

View file

@ -7,7 +7,6 @@
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/> app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/>
<item android:id="@+id/main_menu_settings" android:title="@string/main_menu_settings_title"/> <item android:id="@+id/main_menu_settings" android:title="@string/main_menu_settings_title"/>
<item android:id="@+id/main_menu_docs" android:title="@string/main_menu_docs_title"/> <item android:id="@+id/main_menu_docs" android:title="@string/main_menu_docs_title"/>
<item android:id="@+id/main_menu_rate" android:title="@string/main_menu_rate_title"/>
<item android:id="@+id/main_menu_donate" android:title="@string/main_menu_donate_title"/> <item android:id="@+id/main_menu_donate" android:title="@string/main_menu_donate_title"/>
<item android:id="@+id/main_menu_report_bug" android:title="@string/main_menu_report_bug_title"/> <item android:id="@+id/main_menu_report_bug" android:title="@string/main_menu_report_bug_title"/>
</menu> </menu>

View file

@ -1,36 +0,0 @@
package io.heckel.ntfy.firebase
import com.google.firebase.messaging.FirebaseMessaging
import io.heckel.ntfy.util.Log
class FirebaseMessenger {
fun subscribe(topic: String) {
val firebase = maybeInstance() ?: return
firebase
.subscribeToTopic(topic)
.addOnCompleteListener {
Log.d(TAG, "Subscribing to topic $topic complete: result=${it.result}, exception=${it.exception}, successful=${it.isSuccessful}")
}
.addOnFailureListener { e ->
Log.e(TAG, "Subscribing to topic $topic failed: ${e.message}", e)
}
}
fun unsubscribe(topic: String) {
val firebase = maybeInstance() ?: return
firebase.unsubscribeFromTopic(topic)
}
private fun maybeInstance(): FirebaseMessaging? {
return try {
FirebaseMessaging.getInstance()
} catch (e: Exception) {
Log.e(TAG, "Firebase instance unavailable: ${e.message}", e)
null
}
}
companion object {
private const val TAG = "NtfyFirebase"
}
}

View file

@ -1,166 +0,0 @@
package io.heckel.ntfy.firebase
import android.content.Intent
import androidx.work.*
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Attachment
import io.heckel.ntfy.db.Icon
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.msg.NotificationParser
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.util.nullIfZero
import io.heckel.ntfy.util.toPriority
import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.random.Random
class FirebaseService : FirebaseMessagingService() {
private val repository by lazy { (application as Application).repository }
private val dispatcher by lazy { NotificationDispatcher(this, repository) }
private val job = SupervisorJob()
private val messenger = FirebaseMessenger()
private val parser = NotificationParser()
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// Init log (this is done in all entrypoints)
Log.init(this)
// We only process data messages
if (remoteMessage.data.isEmpty()) {
Log.d(TAG, "Discarding unexpected message (1): from=${remoteMessage.from}")
return
}
// Dispatch event
val data = remoteMessage.data
when (data["event"]) {
ApiService.EVENT_MESSAGE -> handleMessage(remoteMessage)
ApiService.EVENT_KEEPALIVE -> handleKeepalive(remoteMessage)
ApiService.EVENT_POLL_REQUEST -> handlePollRequest(remoteMessage)
else -> Log.d(TAG, "Discarding unexpected message (2): from=${remoteMessage.from}, data=${data}")
}
}
private fun handleKeepalive(remoteMessage: RemoteMessage) {
Log.d(TAG, "Keepalive received, sending auto restart broadcast for foregrounds service")
sendBroadcast(Intent(this, SubscriberService.AutoRestartReceiver::class.java)) // Restart it if necessary!
val topic = remoteMessage.data["topic"]
if (topic != ApiService.CONTROL_TOPIC) {
Log.d(TAG, "Keepalive on non-control topic $topic received, subscribing to control topic ${ApiService.CONTROL_TOPIC}")
messenger.subscribe(ApiService.CONTROL_TOPIC)
}
}
private fun handlePollRequest(remoteMessage: RemoteMessage) {
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
val topic = remoteMessage.data["topic"] ?: return
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val workName = "${PollWorker.WORK_NAME_ONCE_SINGE_PREFIX}_${baseUrl}_${topic}"
val workManager = WorkManager.getInstance(this)
val workRequest = OneTimeWorkRequest.Builder(PollWorker::class.java)
.setInputData(workDataOf(
PollWorker.INPUT_DATA_BASE_URL to baseUrl,
PollWorker.INPUT_DATA_TOPIC to topic
))
.setConstraints(constraints)
.build()
Log.d(TAG, "Poll request for ${topicShortUrl(baseUrl, topic)} received, scheduling unique poll worker with name $workName")
workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest)
}
private fun handleMessage(remoteMessage: RemoteMessage) {
val data = remoteMessage.data
val id = data["id"]
val timestamp = data["time"]?.toLongOrNull()
val topic = data["topic"]
val title = data["title"]
val message = data["message"]
val priority = data["priority"]?.toIntOrNull()
val tags = data["tags"]
val click = data["click"]
val iconUrl = data["icon"]
val actions = data["actions"] // JSON array as string, sigh ...
val encoding = data["encoding"]
val attachmentName = data["attachment_name"] ?: "attachment.bin"
val attachmentType = data["attachment_type"]
val attachmentSize = data["attachment_size"]?.toLongOrNull()?.nullIfZero()
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()?.nullIfZero()
val attachmentUrl = data["attachment_url"]
val truncated = (data["truncated"] ?: "") == "1"
if (id == null || topic == null || message == null || timestamp == null) {
Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
return
}
Log.d(TAG, "Received message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
CoroutineScope(job).launch {
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
// Check if notification was truncated and discard if it will (or likely already did) arrive via instant delivery
val subscription = repository.getSubscription(baseUrl, topic) ?: return@launch
if (truncated && subscription.instant) {
Log.d(TAG, "Discarding truncated message that did/will arrive via instant delivery: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
return@launch
}
// Add notification
val attachment = if (attachmentUrl != null) {
Attachment(
name = attachmentName,
type = attachmentType,
size = attachmentSize,
expires = attachmentExpires,
url = attachmentUrl,
)
} else null
val icon: Icon? = if (iconUrl != null && iconUrl != "") Icon(url = iconUrl) else null
val notification = Notification(
id = id,
subscriptionId = subscription.id,
timestamp = timestamp,
title = title ?: "",
message = message,
encoding = encoding ?: "",
priority = toPriority(priority),
tags = tags ?: "",
click = click ?: "",
icon = icon,
actions = parser.parseActions(actions),
attachment = attachment,
notificationId = Random.nextInt(),
deleted = false
)
if (repository.addNotification(notification)) {
Log.d(TAG, "Dispatching notification: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
dispatcher.dispatch(subscription, notification)
}
}
}
override fun onNewToken(token: String) {
// Called if the FCM registration token is updated
// We don't actually use or care about the token, since we're using topics
Log.d(TAG, "Registration token was updated: $token")
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
companion object {
private const val TAG = "NtfyFirebase"
}
}