From b198fae2eb055fd25a6b935422280a3ea412df43 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 10 Apr 2024 11:45:46 -0300 Subject: [PATCH] support for ntfy --- app/build.gradle | 4 + app/src/free/AndroidManifest.xml | 24 ++++ .../service/PushDistributorHandler.kt | 107 ++++++++++++++ .../service/PushMessageReceiver.kt | 130 ++++++++++++++++++ .../service/PushNotificationUtils.kt | 46 +++++++ .../service/EventNotificationConsumer.kt | 4 +- 6 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 app/src/free/AndroidManifest.xml create mode 100644 app/src/free/java/com/greenart7c3/nostrsigner/service/PushDistributorHandler.kt create mode 100644 app/src/free/java/com/greenart7c3/nostrsigner/service/PushMessageReceiver.kt create mode 100644 app/src/free/java/com/greenart7c3/nostrsigner/service/PushNotificationUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 89ad91fa..2f9cb822 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,6 +46,9 @@ android { create("play") { dimension = "version" } + create("free") { + dimension = "version" + } create("offline") { dimension = "version" } @@ -142,6 +145,7 @@ dependencies { playImplementation platform('com.google.firebase:firebase-bom:32.8.0') playImplementation('com.google.firebase:firebase-messaging:23.4.1') + freeImplementation('com.github.UnifiedPush:android-connector:2.2.0') ksp("androidx.room:room-compiler:$room_version") implementation("androidx.room:room-ktx:$room_version") } \ No newline at end of file diff --git a/app/src/free/AndroidManifest.xml b/app/src/free/AndroidManifest.xml new file mode 100644 index 00000000..c5e4d0de --- /dev/null +++ b/app/src/free/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/free/java/com/greenart7c3/nostrsigner/service/PushDistributorHandler.kt b/app/src/free/java/com/greenart7c3/nostrsigner/service/PushDistributorHandler.kt new file mode 100644 index 00000000..7528f137 --- /dev/null +++ b/app/src/free/java/com/greenart7c3/nostrsigner/service/PushDistributorHandler.kt @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.greenart7c3.nostrsigner.service + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import com.greenart7c3.nostrsigner.nostrsigner +import org.unifiedpush.android.connector.UnifiedPush + +interface PushDistributorActions { + fun getSavedDistributor(): String + + fun getInstalledDistributors(): List + + fun saveDistributor(distributor: String) + + fun removeSavedDistributor() +} + +object PushDistributorHandler : PushDistributorActions { + private val appContext = nostrsigner.instance.applicationContext + private val unifiedPush: UnifiedPush = UnifiedPush + + private var endpointInternal = "" + val endpoint = endpointInternal + + fun getSavedEndpoint() = endpoint + + fun setEndpoint(newEndpoint: String) { + endpointInternal = newEndpoint + Log.d("PushHandler", "New endpoint saved : $endpointInternal") + } + + fun removeEndpoint() { + endpointInternal = "" + } + + override fun getSavedDistributor(): String { + return unifiedPush.getDistributor(appContext) + } + + fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty() + + override fun getInstalledDistributors(): List { + return unifiedPush.getDistributors(appContext) + } + + fun formattedDistributorNames(): List { + val distributorsArray = getInstalledDistributors().toTypedArray() + val distributorsNameArray = + distributorsArray + .map { + try { + val ai = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + appContext.packageManager.getApplicationInfo( + it, + PackageManager.ApplicationInfoFlags.of( + PackageManager.GET_META_DATA.toLong() + ) + ) + } else { + appContext.packageManager.getApplicationInfo(it, 0) + } + appContext.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it + } + as String + } + .toTypedArray() + return distributorsNameArray.toList() + } + + override fun saveDistributor(distributor: String) { + unifiedPush.saveDistributor(appContext, distributor) + unifiedPush.registerApp(appContext) + } + + override fun removeSavedDistributor() { + unifiedPush.safeRemoveDistributor(appContext) + } + + fun forceRemoveDistributor(context: Context) { + unifiedPush.forceRemoveDistributor(context) + } +} diff --git a/app/src/free/java/com/greenart7c3/nostrsigner/service/PushMessageReceiver.kt b/app/src/free/java/com/greenart7c3/nostrsigner/service/PushMessageReceiver.kt new file mode 100644 index 00000000..28039840 --- /dev/null +++ b/app/src/free/java/com/greenart7c3/nostrsigner/service/PushMessageReceiver.kt @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.greenart7c3.nostrsigner.service + +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.util.Log +import android.util.LruCache +import androidx.core.content.ContextCompat +import com.greenart7c3.nostrsigner.LocalPreferences +import com.greenart7c3.nostrsigner.nostrsigner +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.GiftWrapEvent +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.unifiedpush.android.connector.MessagingReceiver + +class PushMessageReceiver : MessagingReceiver() { + companion object { + private val TAG = "Amber-OSSPushReceiver" + } + + private val appContext = nostrsigner.instance.applicationContext + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val eventCache = LruCache(100) + private val pushHandler = PushDistributorHandler + + override fun onMessage( + context: Context, + message: ByteArray, + instance: String + ) { + val messageStr = String(message) + Log.d(TAG, "New message ${message.decodeToString()} for Instance: $instance") + scope.launch { + try { + parseMessage(messageStr)?.let { receiveIfNew(it) } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.d(TAG, "Message could not be parsed: ${e.message}") + } + } + } + + private suspend fun parseMessage(message: String): GiftWrapEvent? { + (Event.fromJson(message) as? GiftWrapEvent)?.let { + return it + } + return null + } + + private suspend fun receiveIfNew(event: GiftWrapEvent) { + if (eventCache.get(event.id) == null) { + eventCache.put(event.id, event.id) + EventNotificationConsumer(appContext).consume(event) + } + } + + override fun onNewEndpoint( + context: Context, + endpoint: String, + instance: String + ) { + Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance") + val sanitizedEndpoint = endpoint.dropLast(5) + PushDistributorHandler.setEndpoint(sanitizedEndpoint) + scope.launch(Dispatchers.IO) { + RegisterAccounts(LocalPreferences.allSavedAccounts()).go(sanitizedEndpoint) + NotificationUtils.getOrCreateDMChannel(appContext) + } + } + + override fun onReceive( + context: Context, + intent: Intent + ) { + val intentData = intent.dataString + val intentAction = intent.action.toString() + Log.d(TAG, "Intent Data:- $intentData Intent Action: $intentAction") + super.onReceive(context, intent) + } + + override fun onRegistrationFailed( + context: Context, + instance: String + ) { + Log.d(TAG, "Registration failed for Instance: $instance") + scope.cancel() + PushDistributorHandler.forceRemoveDistributor(context) + } + + override fun onUnregistered( + context: Context, + instance: String + ) { + val removedEndpoint = PushDistributorHandler.endpoint + Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance") + Log.d(TAG, "App is unregistered. ") + PushDistributorHandler.forceRemoveDistributor(context) + PushDistributorHandler.removeEndpoint() + } + + fun notificationManager(): NotificationManager { + return ContextCompat.getSystemService(appContext, NotificationManager::class.java) + as NotificationManager + } +} diff --git a/app/src/free/java/com/greenart7c3/nostrsigner/service/PushNotificationUtils.kt b/app/src/free/java/com/greenart7c3/nostrsigner/service/PushNotificationUtils.kt new file mode 100644 index 00000000..76b6d9e9 --- /dev/null +++ b/app/src/free/java/com/greenart7c3/nostrsigner/service/PushNotificationUtils.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.greenart7c3.nostrsigner.service + +import android.util.Log +import com.greenart7c3.nostrsigner.AccountInfo +import kotlinx.coroutines.CancellationException + +object PushNotificationUtils { + var hasInit: Boolean = false + private val pushHandler = PushDistributorHandler + + suspend fun init(accounts: List) { + if (hasInit) { + return + } + val distributions = pushHandler.getInstalledDistributors() + if (distributions.isNotEmpty()) pushHandler.saveDistributor(distributions.first()) + try { + if (pushHandler.savedDistributorExists()) { + RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt index e1d7e36e..60bfe37c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt @@ -244,9 +244,9 @@ class EventNotificationConsumer(private val applicationContext: Context) { ) var name = event.pubKey.toShortenHex() - if (permission != null) { + if (permission != null && permission.application.name.isNotBlank()) { name = permission.application.name - } else if (applicationWithSecret != null) { + } else if (applicationWithSecret != null && applicationWithSecret.application.name.isNotBlank()) { name = applicationWithSecret.application.name }