From eee093f4cb3414c02e68e0c77d8b1634006b96b1 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Sun, 26 May 2024 13:47:25 -0700 Subject: [PATCH] Allow use upstream-base-url config for Android app This mirrors the logic used for instant notifications in the iOS app: * When a user subscribes to a topic, the app will subscribe to Firebase using sha256(baseUrl + topic). * When a "poll_request" call is received from Firebase, the app will look up the sha256(baseUrl + topic) of all subscriptions in the database and see if the poll_request topic matches that. If it does, it will poll the original server using the stored baseUrl from the subscriptions database. --- .../java/io/heckel/ntfy/backup/Backuper.kt | 7 ++-- .../main/java/io/heckel/ntfy/db/Database.kt | 5 ++- .../main/java/io/heckel/ntfy/db/Repository.kt | 10 +++++ .../java/io/heckel/ntfy/ui/DetailActivity.kt | 16 ++++---- .../java/io/heckel/ntfy/ui/MainActivity.kt | 9 ++--- app/src/main/java/io/heckel/ntfy/util/Util.kt | 1 + .../heckel/ntfy/firebase/FirebaseService.kt | 39 +++++++++++-------- 7 files changed, 53 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt index 89460c4bb..c88cf2242 100644 --- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt +++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt @@ -11,6 +11,7 @@ import io.heckel.ntfy.db.Repository import io.heckel.ntfy.firebase.FirebaseMessenger import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.util.Log +import io.heckel.ntfy.util.topicHash import io.heckel.ntfy.util.topicUrl import java.io.InputStreamReader @@ -115,9 +116,9 @@ class Backuper(val context: Context) { repository.addSubscription(subscription) // Subscribe to Firebase topics - if (s.baseUrl == appBaseUrl) { - messenger.subscribe(s.topic) - } + val firebaseTopic = if (s.baseUrl == appBaseUrl) s.topic else topicHash(s.baseUrl, s.topic) + Log.d(TAG, "Subscribing to Firebase topic $firebaseTopic") + messenger.subscribe(firebaseTopic) // Create dedicated channels if (s.dedicatedChannels) { diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index 5f397969e..08ac78dc4 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -6,6 +6,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import io.heckel.ntfy.util.topicHash import kotlinx.coroutines.flow.Flow import java.lang.reflect.Type @@ -90,7 +91,9 @@ data class SubscriptionWithMetadata( val totalCount: Int, val newCount: Int, val lastActive: Long -) +) { + fun urlHash() = topicHash(baseUrl, topic) +} @Entity(primaryKeys = ["id", "subscriptionId"]) data class Notification( diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index 71b030920..e47400b9e 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -9,6 +9,9 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.lifecycle.* import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.validUrl +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong @@ -44,6 +47,13 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas .map { list -> list.map { Pair(it.id, it.instant) }.toSet() } } + suspend fun getSubscriptionByHash(topicHash: String): Subscription? { + return toSubscription( + subscriptionDao.listFlow().map { subs -> subs.first { topicHash == it.urlHash() } } + .first() + ) + } + suspend fun getSubscriptions(): List { return toSubscriptionList(subscriptionDao.list()) } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 851f43ac2..4de970ac9 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -127,11 +127,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra ) 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) - } + // Subscribe to Firebase topic (even if instant, just to be sure!) + val firebaseTopic = if (baseUrl == appBaseUrl) topic else topicHash(baseUrl, topic) + Log.d(TAG, "Subscribing to Firebase topic $topic") + messenger.subscribe(firebaseTopic) // Fetch cached messages try { @@ -608,9 +607,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra GlobalScope.launch(Dispatchers.IO) { repository.removeAllNotifications(subscriptionId) repository.removeSubscription(subscriptionId) - if (subscriptionBaseUrl == appBaseUrl) { - messenger.unsubscribe(subscriptionTopic) - } + + val firebaseTopic = if (subscriptionBaseUrl == appBaseUrl) subscriptionTopic else topicHash(subscriptionBaseUrl, subscriptionTopic) + Log.d(TAG, "Unsubscribing from Firebase topic $firebaseTopic") + messenger.unsubscribe(firebaseTopic) } finish() } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index e131f32c7..f1041d08f 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -466,11 +466,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc ) 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) - } + // Subscribe to Firebase topic (even if instant, just to be sure!) + val firebaseTopic = if (baseUrl == appBaseUrl) topic else topicHash(baseUrl, topic) + Log.d(TAG, "Subscribing to Firebase topic $firebaseTopic") + messenger.subscribe(firebaseTopic) // Fetch cached messages lifecycleScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index 2ffbb7666..84619b8f3 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -59,6 +59,7 @@ fun topicUrlWs(baseUrl: String, topic: String, since: String) = "${topicUrl(base fun topicUrlAuth(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/auth" fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?poll=1&since=$since" fun topicShortUrl(baseUrl: String, topic: String) = shortUrl(topicUrl(baseUrl, topic)) +fun topicHash(baseUrl: String, topic: String) = topicUrl(baseUrl, topic).sha256() fun subscriptionTopicShortUrl(subscription: Subscription) : String { return topicShortUrl(subscription.baseUrl, subscription.topic) diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt index 830b92d47..76169ae99 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -61,23 +61,28 @@ class FirebaseService : FirebaseMessagingService() { } 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) + CoroutineScope(job).launch { + val pollTopic = remoteMessage.data["topic"] ?: return@launch + val subscription = repository.getSubscriptionByHash(pollTopic) + val baseUrl = subscription?.baseUrl ?: getString(R.string.app_base_url) + val topic = subscription?.topic ?: pollTopic + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val workName = "${PollWorker.WORK_NAME_ONCE_SINGE_PREFIX}_${baseUrl}_${topic}" + val workManager = WorkManager.getInstance(this@FirebaseService) + 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) {