From a9d87883d2c02f98683a22dda9a8282cbd5670d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 18 Dec 2024 20:55:45 -0500 Subject: [PATCH] Integrates with Pokey's Broadcast receiver. --- amethyst/src/main/AndroidManifest.xml | 9 +++ .../com/vitorpamplona/amethyst/Amethyst.kt | 19 +++++ .../EventNotificationConsumer.kt | 79 +++++++++++++++---- .../service/notifications/PokeyReceiver.kt | 60 ++++++++++++++ 4 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index df5e5f8c0..d5249d412 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -144,6 +144,15 @@ android:resource="@xml/file_paths" /> + + + + + + \ No newline at end of file diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 9a88156ad..8e15c295e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst import android.app.Application import android.content.ContentResolver import android.content.Context +import android.content.IntentFilter +import android.os.Build import android.os.Looper import android.os.StrictMode import android.os.StrictMode.ThreadPolicy @@ -34,6 +36,7 @@ import coil3.disk.DiskCache import coil3.memory.MemoryCache import coil3.request.crossfade import com.vitorpamplona.amethyst.service.LocationState +import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver import com.vitorpamplona.amethyst.service.playback.VideoCache import com.vitorpamplona.ammolite.service.HttpClientManager import kotlinx.coroutines.CoroutineScope @@ -55,8 +58,11 @@ class Amethyst : Application() { val serviceManager = ServiceManager(applicationIOScope) val locationManager = LocationState(this, applicationIOScope) + val pokeyReceiver = PokeyReceiver() + override fun onTerminate() { super.onTerminate() + unregisterReceiver(pokeyReceiver) applicationIOScope.cancel() } @@ -125,6 +131,19 @@ class Amethyst : Application() { } Log.d("Rendering Metrics", "VideoCache initialized in $elapsed") } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver( + pokeyReceiver, + IntentFilter(PokeyReceiver.POKEY_ACTION), + RECEIVER_EXPORTED, + ) + } else { + registerReceiver( + pokeyReceiver, + IntentFilter(PokeyReceiver.POKEY_ACTION), + ) + } } fun imageLoaderBuilder(): ImageLoader.Builder = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index aeb11db81..cc9efa88b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -59,7 +59,6 @@ class EventNotificationConsumer( suspend fun consume(event: GiftWrapEvent) { Log.d(TAG, "New Notification Arrived") if (!LocalCache.justVerify(event)) return - if (!notificationManager().areNotificationsEnabled()) return // PushNotification Wraps don't include a receiver. // Test with all logged in accounts @@ -95,22 +94,68 @@ class EventNotificationConsumer( } pushWrappedEvent.unwrapThrowing(signer) { notificationEvent -> - val consumed = LocalCache.hasConsumed(notificationEvent) - val verified = LocalCache.justVerify(notificationEvent) - Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${signer.pubKey} consumed= $consumed && verified= $verified") - if (!consumed && verified) { - Log.d(TAG, "New Notification was verified") - unwrapAndConsume(notificationEvent, signer) { innerEvent -> - Log.d(TAG, "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}") - if (innerEvent is PrivateDmEvent) { - Log.d(TAG, "New Nip-04 DM to Notify") - notify(innerEvent, signer, account) - } else if (innerEvent is LnZapEvent) { - Log.d(TAG, "New Zap to Notify") - notify(innerEvent, signer, account) - } else if (innerEvent is ChatMessageEvent) { - Log.d(TAG, "New ChatMessage to Notify") - notify(innerEvent, signer, account) + consumeNotificationEvent(notificationEvent, signer, account) + } + } + + fun consumeNotificationEvent( + notificationEvent: Event, + signer: NostrSigner, + account: AccountSettings, + ) { + val consumed = LocalCache.hasConsumed(notificationEvent) + val verified = LocalCache.justVerify(notificationEvent) + Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${signer.pubKey} consumed= $consumed && verified= $verified") + if (!consumed && verified) { + Log.d(TAG, "New Notification was verified") + unwrapAndConsume(notificationEvent, signer) { innerEvent -> + if (!notificationManager().areNotificationsEnabled()) return@unwrapAndConsume + + Log.d(TAG, "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}") + if (innerEvent is PrivateDmEvent) { + Log.d(TAG, "New Nip-04 DM to Notify") + notify(innerEvent, signer, account) + } else if (innerEvent is LnZapEvent) { + Log.d(TAG, "New Zap to Notify") + notify(innerEvent, signer, account) + } else if (innerEvent is ChatMessageEvent) { + Log.d(TAG, "New ChatMessage to Notify") + notify(innerEvent, signer, account) + } + } + } + } + + suspend fun findAccountAndConsume(event: Event) { + Log.d(TAG, "New Notification Arrived") + if (!LocalCache.justVerify(event)) return + + val users = event.taggedUsers().map { LocalCache.getOrCreateUser(it) } + val npubs = users.map { it.pubkeyNpub() }.toSet() + + // PushNotification Wraps don't include a receiver. + // Test with all logged in accounts + var matchAccount = false + LocalPreferences.allSavedAccounts().forEach { + if (!matchAccount && (it.hasPrivKey || it.loggedInWithExternalSigner) && it.npub in npubs) { + LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc -> + Log.d(TAG, "New Notification Testing if for ${it.npub}") + try { + // TODO: Modify the external launcher to launch as different users. + // Right now it only registers if Amber has already approved this signature + val signer = acc.createSigner() + if (signer is NostrSignerExternal) { + signer.launcher.registerLauncher( + launcher = { }, + contentResolver = Amethyst.instance::contentResolverFn, + ) + } + + consumeNotificationEvent(event, signer, acc) + matchAccount = true + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.d(TAG, "Message was not for user ${it.npub}: ${e.message}") } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt new file mode 100644 index 000000000..07c54c582 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt @@ -0,0 +1,60 @@ +/** + * 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.vitorpamplona.amethyst.service.notifications + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.vitorpamplona.quartz.events.Event +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +class PokeyReceiver : BroadcastReceiver() { + companion object { + val POKEY_ACTION = "com.shared.NOSTR" + val TAG = "PokeyReceiver" + } + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (intent.action == POKEY_ACTION) { // it's best practice to verify intent action before performing any operation + val eventStr = intent.getStringExtra("EVENT") + Log.d(TAG, "New Pokey Notification Arrived $eventStr") + + if (eventStr == null) return + + scope.launch(Dispatchers.IO) { + try { + EventNotificationConsumer(context.applicationContext).findAccountAndConsume(Event.fromJson(eventStr)) + } catch (e: Exception) { + Log.e(TAG, "Failed to parse Pokey Event", e) + } + } + } + } +}