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
}