Remove push notifications server

This commit is contained in:
greenart7c3
2024-10-30 17:46:57 -03:00
parent 1b17dc9061
commit 46e2493cf5
33 changed files with 129 additions and 1482 deletions
+1 -10
View File
@@ -4,7 +4,6 @@ plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.gradle.ktlint) version(libs.versions.ktlint)
alias(libs.plugins.google.gms) version(libs.versions.gms)
alias(libs.plugins.kotlin.ksp) version(libs.versions.ksp)
alias(libs.plugins.jetbrainsComposeCompiler)
}
@@ -58,7 +57,6 @@ android {
'ku-rTR',
'lt-rLT',
'ne-rNP',
'night',
'nl',
'nl-rBE',
'pcm-rNG',
@@ -131,11 +129,8 @@ android {
flavorDimensions += "version"
productFlavors {
create("play") {
getIsDefault().set(true)
dimension = "version"
}
create("free") {
getIsDefault().set(true)
dimension = "version"
}
create("offline") {
@@ -241,12 +236,8 @@ dependencies {
// Biometrics
implementation libs.biometric.ktx
playImplementation platform(libs.firebase.bom)
playImplementation(libs.firebase.messaging)
freeImplementation(libs.unfiedpush)
ksp(libs.room.compiler)
implementation(libs.room.ktx)
freeImplementation(libs.okhttp)
playImplementation(libs.okhttp)
}
-48
View File
@@ -1,48 +0,0 @@
{
"project_info": {
"project_number": "575711686692",
"project_id": "amber-c362b",
"storage_bucket": "amber-c362b.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:575711686692:android:11451e08365ea86f6ce8b1",
"android_client_info": {
"package_name": "com.greenart7c3.nostrsigner"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDB2tmjxGmFx_Oz25YhtKkheWbdb_NlnfU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:575711686692:android:fb6ef62b68245d696ce8b1",
"android_client_info": {
"package_name": "com.greenart7c3.nostrsigner.debug"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDB2tmjxGmFx_Oz25YhtKkheWbdb_NlnfU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
-13
View File
@@ -26,19 +26,6 @@
/>
</service>
<receiver
android:exported="true"
android:enabled="true"
android:name=".service.PushMessageReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/>
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
@@ -1,121 +0,0 @@
/**
* 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.LocalPreferences
import com.greenart7c3.nostrsigner.NostrSigner
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.UnifiedPush
interface PushDistributorActions {
fun getSavedDistributor(): String
fun getInstalledDistributors(): List<String>
fun saveDistributor(distributor: String)
fun removeSavedDistributor()
}
object PushDistributorHandler : PushDistributorActions {
private val appContext = NostrSigner.getInstance().applicationContext
private val unifiedPush: UnifiedPush = UnifiedPush
private var endpointInternal = NostrSigner.getInstance().settings.endpoint
fun getSavedEndpoint() = endpointInternal
@OptIn(DelicateCoroutinesApi::class)
fun setEndpoint(newEndpoint: String) {
endpointInternal = newEndpoint
GlobalScope.launch(Dispatchers.IO) {
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(endpoint = newEndpoint)
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
}
Log.d("PushHandler", "New endpoint saved : $endpointInternal")
}
@OptIn(DelicateCoroutinesApi::class)
fun removeEndpoint() {
endpointInternal = ""
GlobalScope.launch(Dispatchers.IO) {
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(endpoint = "")
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
}
}
override fun getSavedDistributor(): String {
return unifiedPush.getSavedDistributor(appContext) ?: ""
}
fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty()
override fun getInstalledDistributors(): List<String> {
return unifiedPush.getDistributors(appContext)
}
fun formattedDistributorNames(): List<String> {
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)
}
}
@@ -1,130 +0,0 @@
/**
* 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.util.Log
import android.util.LruCache
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.database.LogEntity
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.getInstance().applicationContext
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val eventCache = LruCache<String, String>(100)
private val pushHandler = PushDistributorHandler
override fun onMessage(
context: Context,
message: ByteArray,
instance: String,
) {
val messageStr = message.decodeToString()
Log.d(TAG, "New message $messageStr 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 = if (endpoint.endsWith("?up=1")) endpoint.dropLast(5) else endpoint
if (pushHandler.getSavedEndpoint() == sanitizedEndpoint) {
Log.d(TAG, "Endpoint already saved. ")
return
}
pushHandler.setEndpoint(sanitizedEndpoint)
scope.launch(Dispatchers.IO) {
RegisterAccounts(LocalPreferences.allSavedAccounts(context)).go(sanitizedEndpoint)
NotificationUtils.getOrCreateDMChannel(appContext)
}
}
override fun onRegistrationFailed(
context: Context,
instance: String,
) {
Log.d(TAG, "Registration failed for Instance: $instance")
LocalPreferences.allSavedAccounts(context).forEach {
NostrSigner.getInstance().getDatabase(it.npub).applicationDao().insertLog(
LogEntity(
0,
"Push server",
"Push server",
"Registration failed for Instance: $instance",
System.currentTimeMillis(),
),
)
}
scope.cancel()
pushHandler.forceRemoveDistributor(context)
}
override fun onUnregistered(
context: Context,
instance: String,
) {
val removedEndpoint = pushHandler.getSavedEndpoint()
Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance")
Log.d(TAG, "App is unregistered. ")
pushHandler.forceRemoveDistributor(context)
pushHandler.removeEndpoint()
}
}
@@ -1,114 +0,0 @@
/**
* 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.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri
import android.util.Log
import com.greenart7c3.nostrsigner.AccountInfo
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.database.LogEntity
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.NotificationType
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
object PushNotificationUtils {
var hasInit: Boolean = false
private val pushHandler = PushDistributorHandler
var accountState: AccountStateViewModel? = null
@OptIn(DelicateCoroutinesApi::class)
suspend fun init(accounts: List<AccountInfo>) {
if (hasInit || NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
return
}
val savedDistributor = pushHandler.getSavedDistributor()
if (savedDistributor.isBlank()) {
val distributions = pushHandler.getInstalledDistributors()
if (distributions.isNotEmpty()) {
distributions.firstOrNull { it.isNotBlank() }?.let {
pushHandler.saveDistributor(it)
}
} else {
accounts.forEach {
NostrSigner.getInstance().getDatabase(it.npub).applicationDao().insertLog(
LogEntity(
0,
"Push server",
"Push server",
"No distributors found for push notifications",
System.currentTimeMillis(),
),
)
}
if (!NostrSigner.getInstance().settings.pushServerMessage && NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT) {
accountState?.toast(
title = NostrSigner.getInstance().getString(R.string.push_server),
message = NostrSigner.getInstance().getString(R.string.no_distributors_found),
onOk = {
GlobalScope.launch(Dispatchers.IO) {
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(pushServerMessage = true)
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
}
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://ntfy.sh/")
intent.flags = FLAG_ACTIVITY_NEW_TASK
NostrSigner.getInstance().startActivity(intent)
},
)
}
}
}
try {
if (pushHandler.savedDistributorExists()) {
val endpoint = pushHandler.getSavedEndpoint()
if (endpoint.isBlank()) {
pushHandler.saveDistributor(pushHandler.getSavedDistributor())
}
RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint())
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d("Amber-OSSPushUtils", "Failed to get endpoint.")
accounts.forEach {
NostrSigner.getInstance().getDatabase(it.npub).applicationDao().insertLog(
LogEntity(
0,
"Push server",
"Push server",
"Failed to get endpoint. ${e.message}",
System.currentTimeMillis(),
),
)
}
}
}
}
@@ -1,82 +0,0 @@
/**
* 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.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.service.PushDistributorHandler
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, ImmutableList<TitleExplainer>) -> Unit) {
val currentDistributor = PushDistributorHandler.getSavedDistributor().ifBlank { null } ?: "None"
val list =
remember {
PushDistributorHandler.getInstalledDistributors().plus("None").toImmutableList()
}
val readableListWithExplainer =
PushDistributorHandler
.formattedDistributorNames()
.mapIndexed { index, name ->
TitleExplainer(
name,
stringResource(id = R.string.push_server_uses_app_explainer, list[index]),
)
}.plus(
TitleExplainer(
stringResource(id = R.string.push_server_none),
stringResource(id = R.string.push_server_none_explainer),
),
)
.toImmutableList()
onInner(
currentDistributor,
list,
readableListWithExplainer,
)
}
@Composable
fun PushNotificationSettingsRow() {
val context = LocalContext.current
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
SettingsRow(
R.string.push_server_title,
R.string.push_server_explainer,
selectedItems = readableListWithExplainer,
selectedIndex = list.indexOf(currentDistributor),
) { index ->
if (list[index] == "None") {
PushDistributorHandler.forceRemoveDistributor(context)
} else {
PushDistributorHandler.saveDistributor(list[index])
}
}
}
}
@@ -11,7 +11,6 @@ import com.greenart7c3.nostrsigner.database.ApplicationPermissionsEntity
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.AmberSettings
import com.greenart7c3.nostrsigner.ui.parseBiometricsTimeType
import com.greenart7c3.nostrsigner.ui.parseNotificationType
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.ammolite.service.HttpClientManager
@@ -116,7 +115,6 @@ object LocalPreferences {
putStringSet(PrefKeys.DEFAULT_RELAYS, settings.defaultRelays.map { it.url }.toSet())
putLong(PrefKeys.LAST_BIOMETRICS_TIME, settings.lastBiometricsTime)
putBoolean(PrefKeys.USE_AUTH, settings.useAuth)
putInt(PrefKeys.NOTIFICATION_TYPE, settings.notificationType.screenCode)
putInt(PrefKeys.BIOMETRICS_TYPE, settings.biometricsTimeType.screenCode)
putBoolean(PrefKeys.USE_PIN, settings.usePin)
}.apply()
@@ -156,7 +154,6 @@ object LocalPreferences {
} ?: listOf(RelaySetupInfo("wss://relay.nsec.app", read = true, write = true, feedTypes = COMMON_FEED_TYPES)),
lastBiometricsTime = getLong(PrefKeys.LAST_BIOMETRICS_TIME, 0),
useAuth = getBoolean(PrefKeys.USE_AUTH, false),
notificationType = parseNotificationType(getInt(PrefKeys.NOTIFICATION_TYPE, 1)),
biometricsTimeType = parseBiometricsTimeType(getInt(PrefKeys.BIOMETRICS_TYPE, 0)),
usePin = getBoolean(PrefKeys.USE_PIN, false),
)
@@ -12,9 +12,7 @@ import com.greenart7c3.nostrsigner.database.AppDatabase
import com.greenart7c3.nostrsigner.models.AmberSettings
import com.greenart7c3.nostrsigner.service.ConnectivityService
import com.greenart7c3.nostrsigner.service.RelayDisconnectService
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayPool
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect
@@ -122,28 +120,11 @@ class NostrSigner : Application() {
LocalPreferences.loadFromEncryptedStorage(this, it.npub)?.useProxy ?: false
}
if (settings.notificationType != NotificationType.DIRECT) {
RelayPool.register(Client)
savedRelays.forEach { setupInfo ->
if (RelayPool.getRelay(setupInfo.url) == null) {
RelayPool.addRelay(
Relay(
setupInfo.url,
setupInfo.read,
setupInfo.write,
if (isPrivateIp(setupInfo.url)) false else useProxy,
setupInfo.feedTypes,
),
)
}
}
}
if (shouldReconnect) {
checkIfRelaysAreConnected()
}
@Suppress("KotlinConstantConditions")
if (settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline" && savedRelays.isNotEmpty()) {
if (BuildConfig.FLAVOR != "offline" && savedRelays.isNotEmpty()) {
Client.reconnect(
savedRelays.map { RelaySetupInfoToConnect(it.url, if (isPrivateIp(it.url)) false else useProxy, it.read, it.write, it.feedTypes) }.toTypedArray(),
true,
@@ -26,6 +26,9 @@ interface ApplicationDao {
@Query("SELECT * FROM application where pubKey = :pubKey order by name")
fun getAll(pubKey: String): List<ApplicationEntity>
@Query("SELECT * FROM application where pubKey = :pubKey order by name")
fun getAllFlow(pubKey: String): Flow<List<ApplicationEntity>>
@Query("SELECT * FROM application WHERE `key` = :key")
@Transaction
fun getByKey(key: String): ApplicationWithPermissions?
@@ -1,7 +1,6 @@
package com.greenart7c3.nostrsigner.models
import com.greenart7c3.nostrsigner.ui.BiometricsTimeType
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
@@ -11,7 +10,6 @@ data class AmberSettings(
val defaultRelays: List<RelaySetupInfo> = listOf(RelaySetupInfo("wss://relay.nsec.app", read = true, write = true, feedTypes = COMMON_FEED_TYPES)),
val lastBiometricsTime: Long = 0,
val useAuth: Boolean = false,
val notificationType: NotificationType = NotificationType.DIRECT,
val biometricsTimeType: BiometricsTimeType = BiometricsTimeType.EVERY_TIME,
val usePin: Boolean = false,
)
@@ -13,10 +13,8 @@ import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.vitorpamplona.ammolite.relays.RelayPool
import java.util.Timer
import java.util.TimerTask
@@ -40,7 +38,7 @@ class ConnectivityService : Service() {
@Suppress("KotlinConstantConditions")
if (BuildConfig.FLAVOR == "offline") return
if (lastNetwork != null && lastNetwork != network && NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
if (lastNetwork != null && lastNetwork != network) {
scope.launch(Dispatchers.IO) {
NotificationDataSource.stopSync()
delay(1000)
@@ -66,7 +64,7 @@ class ConnectivityService : Service() {
"ServiceManager NetworkCallback",
"onCapabilitiesChanged: ${network.networkHandle} hasMobileData ${NostrSigner.getInstance().isOnMobileDataState.value} hasWifi ${NostrSigner.getInstance().isOnWifiDataState.value}",
)
if (NostrSigner.getInstance().updateNetworkCapabilities(networkCapabilities) && NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
if (NostrSigner.getInstance().updateNetworkCapabilities(networkCapabilities)) {
NotificationDataSource.stopSync()
delay(1000)
NotificationDataSource.start()
@@ -101,11 +99,6 @@ class ConnectivityService : Service() {
isStarted = true
startForeground(1, createNotification())
scope.launch(Dispatchers.IO) {
PushNotificationUtils.hasInit = false
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(this@ConnectivityService))
}
@Suppress("KotlinConstantConditions")
if (BuildConfig.FLAVOR != "offline") {
val connectivityManager =
@@ -119,9 +112,11 @@ class ConnectivityService : Service() {
timer.schedule(
object : TimerTask() {
override fun run() {
if (NostrSigner.getInstance().settings.notificationType == NotificationType.PUSH) return
@Suppress("KotlinConstantConditions")
if (BuildConfig.FLAVOR == "offline") {
return
}
Log.d("ConnectivityService", "Checking connectivity...")
RelayPool.getAll().forEach {
if (!it.isConnected()) {
Log.d(
@@ -42,13 +42,10 @@ import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.service.NotificationUtils.sendNotification
import com.greenart7c3.nostrsigner.service.model.AmberEvent
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.vitorpamplona.quartz.crypto.nip04.Nip04
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
class EventNotificationConsumer(private val applicationContext: Context) {
@@ -64,61 +61,6 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
}
fun consume(event: GiftWrapEvent) {
if (!notificationManager().areNotificationsEnabled()) return
if (!event.hasCorrectIDHash()) return
if (!event.hasVerifiedSignature()) return
// PushNotification Wraps don't include a receiver.
// Test with all logged in accounts
LocalPreferences.allSavedAccounts(applicationContext).forEach {
if (it.hasPrivKey) {
LocalPreferences.loadFromEncryptedStorage(applicationContext, it.npub)?.let { acc ->
if (NostrSigner.getInstance().settings.notificationType == NotificationType.PUSH) {
consumeIfMatchesAccount(event, acc)
}
}
}
}
}
private fun consumeIfMatchesAccount(
pushWrappedEvent: GiftWrapEvent,
account: Account,
) {
pushWrappedEvent.unwrap(account.signer) { notificationEvent ->
unwrapAndConsume(notificationEvent, account) { innerEvent ->
if (innerEvent.kind == 24133) {
if (innerEvent.hasCorrectIDHash()) {
if (innerEvent.hasVerifiedSignature()) {
notify(innerEvent, account)
}
}
}
}
}
}
private fun unwrapAndConsume(
event: Event,
account: Account,
onReady: (Event) -> Unit,
) {
when (event) {
is GiftWrapEvent -> {
event.unwrap(account.signer) { unwrapAndConsume(it, account, onReady) }
}
is SealedGossipEvent -> {
event.unseal(account.signer) {
onReady(it)
}
}
else -> {
onReady(event)
}
}
}
private fun notify(
event: Event,
acc: Account,
@@ -34,7 +34,6 @@ import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.models.containsNip
import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton
import com.greenart7c3.nostrsigner.service.model.AmberEvent
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.RelayPool
@@ -388,15 +387,11 @@ object IntentUtils {
GlobalScope.launch(Dispatchers.IO) {
if (RelayPool.getAll().any { !it.isConnected() }) {
NostrSigner.getInstance().checkForNewRelays(
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
newRelays = relays.toSet(),
)
}
val success = Client.sendAndWaitForResponse(it, relayList = relays)
if (NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT) {
RelayPool.unregister(Client)
}
if (success) {
onDone()
}
@@ -25,7 +25,6 @@ import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.checkNotInMainThread
import com.greenart7c3.nostrsigner.database.LogEntity
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.NostrDataSource
@@ -143,9 +142,7 @@ object NotificationDataSource : NostrDataSource("AccountData") {
init {
scope.launch {
LocalPreferences.loadSettingsFromEncryptedStorage()
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
Client.subscribe(clientListener)
}
Client.subscribe(clientListener)
}
}
@@ -1,228 +0,0 @@
/**
* 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 com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.database.LogEntity
import com.greenart7c3.nostrsigner.models.Account
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.RelayAuthEvent
import java.net.URL
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
class RegisterAccounts(
private val accounts: List<AccountInfo>,
) {
private fun recursiveAuthCreation(
notificationToken: String,
remainingTos: List<Pair<Account, String>>,
output: MutableList<RelayAuthEvent>,
onReady: (List<RelayAuthEvent>) -> Unit,
) {
if (remainingTos.isEmpty()) {
onReady(output)
return
}
val next = remainingTos.first()
next.first.createAuthEvent(next.second, notificationToken) {
output.add(it)
recursiveAuthCreation(notificationToken, remainingTos.filter { remainingTo -> next != remainingTo }, output, onReady)
}
}
private fun isSupportedUrl(url: String): Boolean {
return url.isNotBlank() &&
!url.contains("brb.io") && // no broken relays
!url.contains("echo.websocket.org") && // test relay
!url.contains("127.0") && // no local relays
!url.contains("umbrel.local") && // no local relays
!url.contains("192.168.") && // no local relays
!url.contains(".onion") && // we are not running on Tor
!url.contains("https://") && // not a websocket
!url.contains("http://") && // not a websocket
!url.contains("www://") && // not a websocket
!url.contains("https//") && // not a websocket
!url.contains("http//") && // not a websocket
!url.contains("www//") && // not a websocket
!url.contains("npub1") && // does not allow custom uris
!url.contains("was://") && // common mispellings
!url.contains("ws://umbrel:") && // local domain
!url.contains("\t") && // tab is not allowed
!url.contains(" ") && // space is not allowed
isValidUrl(url)
}
private fun isValidUrl(url: String): Boolean {
try {
URL(url.replace("wss://", "https://").replace("ws://", "http://"))
return true
} catch (e: Exception) {
return false
}
}
// creates proof that it controls all accounts
private fun signEventsToProveControlOfAccounts(
accounts: List<AccountInfo>,
notificationToken: String,
onReady: (List<RelayAuthEvent>) -> Unit,
) {
val readyToSend: MutableSet<Pair<Account, String>> = mutableSetOf()
accounts.forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(NostrSigner.getInstance(), it.npub)
if (acc != null) {
val permissions =
NostrSigner.getInstance().getDatabase(
acc.signer.keyPair.pubKey.toNpub(),
).applicationDao().getAll(acc.signer.keyPair.pubKey.toHexKey())
permissions.forEach { permission ->
permission.relays.forEach { relay ->
if (isSupportedUrl(relay.url)) {
readyToSend.add(
Pair(acc, relay.url),
)
}
}
}
runBlocking {
LocalPreferences.loadSettingsFromEncryptedStorage()
}
val defaultRelays = NostrSigner.getInstance().settings.defaultRelays
defaultRelays.forEach { relay ->
if (isSupportedUrl(relay.url)) {
readyToSend.add(
Pair(acc, relay.url),
)
}
}
}
}
val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
recursiveAuthCreation(
notificationToken,
readyToSend.toList(),
listOfAuthEvents,
onReady,
)
}
private fun postRegistrationEvent(events: List<RelayAuthEvent>, notificationToken: String) {
if (notificationToken.isBlank()) {
Log.d("FirebaseMsgService", "No notification token to register with push server")
return
}
try {
val jsonObject =
"""{
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
}
"""
val mediaType = "application/json; charset=utf-8".toMediaType()
val body = jsonObject.toRequestBody(mediaType)
val request =
Request.Builder()
.header("User-Agent", "Amber/${BuildConfig.VERSION_NAME}")
.url("https://push.greenart7c3.com/register")
.post(body)
.build()
val useProxy = accounts.any {
val account = LocalPreferences.loadFromEncryptedStorage(context = NostrSigner.getInstance(), npub = it.npub)
val useProxy = account?.useProxy ?: false
if (useProxy) {
HttpClientManager.setDefaultProxyOnPort(account?.proxyPort ?: 9050)
}
useProxy
}
val client = HttpClientManager.getHttpClient(useProxy)
var isSuccess = client.newCall(request).execute().use {
it.isSuccessful
}
var tries = 0
while (!isSuccess && tries < 3) {
Log.d("FirebaseMsgService", "Failed to register with push server, retrying...")
isSuccess = client.newCall(request).execute().use {
it.isSuccessful
}
tries++
}
if (isSuccess) {
Log.d("FirebaseMsgService", "Successfully registered with push server")
} else {
accounts.forEach {
NostrSigner.getInstance().getDatabase(it.npub).applicationDao().insertLog(
LogEntity(
0,
"Push server",
"Push server",
"Failed to register with push server",
System.currentTimeMillis(),
),
)
}
}
} catch (e: java.lang.Exception) {
if (e is CancellationException) throw e
val tag = "FirebaseMsgService"
Log.e(tag, "Unable to register with push server", e)
accounts.forEach {
NostrSigner.getInstance().getDatabase(it.npub).applicationDao().insertLog(
LogEntity(
0,
"Push server",
"Push server",
"Unable to register with push server: ${e.message}",
System.currentTimeMillis(),
),
)
}
}
}
suspend fun go(notificationToken: String) =
withContext(Dispatchers.IO) {
signEventsToProveControlOfAccounts(accounts, notificationToken) { postRegistrationEvent(it, notificationToken) }
PushNotificationUtils.hasInit = true
}
}
@@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
@@ -19,10 +17,7 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -49,14 +44,12 @@ import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton
import com.greenart7c3.nostrsigner.service.ConnectivityService
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.service.NotificationDataSource
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
import com.vitorpamplona.quartz.encoders.toNpub
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("StateFlowValueCalledInComposition", "UnrememberedMutableState")
@Composable
fun AccountScreen(
@@ -82,115 +75,69 @@ fun AccountScreen(
MainLoginPage(accountStateViewModel)
}
is AccountState.LoggedIn -> {
var isLoading by remember { mutableStateOf(true) }
var isNotificationsConfigured by remember {
mutableStateOf(false)
var intentData by remember {
mutableStateOf<IntentData?>(null)
}
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
isNotificationsConfigured = LocalPreferences.isNotificationTypeConfigured()
isLoading = false
LaunchedEffect(intent, intents) {
IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) {
intentData = it
}
val data =
intents.firstOrNull {
it.currentAccount.isNotBlank()
}
data?.bunkerRequest?.let {
if (it.currentAccount.isNotBlank()) {
if (LocalPreferences.currentAccount(context) != it.currentAccount) {
accountStateViewModel.switchUser(it.currentAccount, null)
}
}
}
}
if (isLoading) {
Scaffold { innerPadding ->
CenterCircularProgressIndicator(
Modifier
.fillMaxSize()
.padding(innerPadding),
)
if (intentData != null) {
if (intents.none { item -> item.id == intentData!!.id }) {
val oldIntents = intents.toMutableList()
oldIntents.add(intentData!!)
flow.value = oldIntents
}
} else {
if (isNotificationsConfigured) {
var intentData by remember {
mutableStateOf<IntentData?>(null)
}
val newIntents =
intents.ifEmpty {
if (intentData == null) {
listOf()
} else {
listOf(intentData)
}
LaunchedEffect(intent, intents) {
IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) {
intentData = it
}
val data =
intents.firstOrNull {
it.currentAccount.isNotBlank()
}
}.mapNotNull { it }
val database = NostrSigner.getInstance().getDatabase(state.account.signer.keyPair.pubKey.toNpub())
val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route)
data?.bunkerRequest?.let {
if (it.currentAccount.isNotBlank()) {
if (LocalPreferences.currentAccount(context) != it.currentAccount) {
accountStateViewModel.switchUser(it.currentAccount, null)
}
}
}
}
if (intentData != null) {
if (intents.none { item -> item.id == intentData!!.id }) {
val oldIntents = intents.toMutableList()
oldIntents.add(intentData!!)
flow.value = oldIntents
}
}
val newIntents =
intents.ifEmpty {
if (intentData == null) {
listOf()
} else {
listOf(intentData)
}
}.mapNotNull { it }
val database = NostrSigner.getInstance().getDatabase(state.account.signer.keyPair.pubKey.toNpub())
val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route)
SideEffect {
NostrSigner.getInstance().applicationIOScope.launch(Dispatchers.IO) {
PushNotificationUtils.accountState = accountStateViewModel
try {
NostrSigner.getInstance().applicationContext.startForegroundService(
Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java),
)
} catch (e: Exception) {
Log.d("NostrSigner", "Failed to start ConnectivityService", e)
}
@Suppress("KotlinConstantConditions")
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
delay(5000)
}
}
}
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
DisplayErrorMessages(accountStateViewModel)
MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database, navController)
} else {
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
DisplayErrorMessages(accountStateViewModel)
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(stringResource(R.string.notification_type))
},
)
},
) { innerPadding ->
NotificationTypeScreen(
Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(40.dp),
onDone = {
isNotificationsConfigured = true
},
SideEffect {
NostrSigner.getInstance().applicationIOScope.launch(Dispatchers.IO) {
try {
NostrSigner.getInstance().applicationContext.startForegroundService(
Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java),
)
} catch (e: Exception) {
Log.d("NostrSigner", "Failed to start ConnectivityService", e)
}
@Suppress("KotlinConstantConditions")
if (BuildConfig.FLAVOR != "offline") {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
delay(5000)
}
}
}
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
DisplayErrorMessages(accountStateViewModel)
MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database, navController)
}
}
}
@@ -162,9 +162,8 @@ fun EditConfigurationScreen(
it.permissions,
),
)
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
NostrSigner.getInstance().checkForNewRelays()
}
NostrSigner.getInstance().checkForNewRelays()
}
}
},
@@ -203,9 +202,7 @@ fun EditConfigurationScreen(
it.permissions,
),
)
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
NostrSigner.getInstance().checkForNewRelays()
}
NostrSigner.getInstance().checkForNewRelays()
}
}
},
@@ -255,9 +252,7 @@ fun EditConfigurationScreen(
it.permissions,
),
)
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
NostrSigner.getInstance().checkForNewRelays()
}
NostrSigner.getInstance().checkForNewRelays()
}
}
},
@@ -280,9 +275,7 @@ fun EditConfigurationScreen(
scope.launch(Dispatchers.IO) {
database.applicationDao().delete(it.application)
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
NostrSigner.getInstance().checkForNewRelays()
}
NostrSigner.getInstance().checkForNewRelays()
scope.launch(Dispatchers.Main) {
navController.navigate(Route.Applications.route) {
@@ -86,7 +86,6 @@ import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.models.basicPermissions
import com.greenart7c3.nostrsigner.service.EventNotificationConsumer
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupScreen
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
@@ -131,8 +130,7 @@ fun sendResult(
val relays = savedApplication?.application?.relays?.ifEmpty { defaultRelays } ?: (intentData.bunkerRequest?.relays?.ifEmpty { defaultRelays } ?: defaultRelays)
if (intentData.bunkerRequest != null) {
NostrSigner.getInstance().checkForNewRelays(
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
relays.toSet(),
newRelays = relays.toSet(),
)
}
@@ -259,10 +257,6 @@ fun sendResult(
true,
),
)
PushNotificationUtils.hasInit = false
GlobalScope.launch(Dispatchers.IO) {
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(context))
}
EventNotificationConsumer(context).notificationManager().cancelAll()
activity?.intent = null
@@ -375,9 +369,6 @@ private suspend fun askNotificationPermission(
onShouldShowRequestPermissionRationale: () -> Unit,
) {
if (ContextCompat.checkSelfPermission(context, "android.permission.POST_NOTIFICATIONS") == PackageManager.PERMISSION_GRANTED) {
initNotifications(
context = context,
)
return
}
@@ -398,11 +389,6 @@ private suspend fun askNotificationPermission(
}
}
private suspend fun initNotifications(context: Context) {
PushNotificationUtils.hasInit = false
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(context))
}
fun Color.Companion.fromHex(colorString: String) = try {
Color(android.graphics.Color.parseColor("#$colorString"))
} catch (e: Exception) {
@@ -431,13 +417,7 @@ fun MainScreen(
rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
if (isGranted) {
scope.launch(Dispatchers.IO) {
initNotifications(
context = context,
)
}
} else {
if (!isGranted) {
if (LocalPreferences.shouldShowRationale(context) == null) {
LocalPreferences.updateShouldShowRationale(context, true)
}
@@ -682,7 +662,6 @@ fun MainScreen(
.padding(padding)
.padding(24.dp),
account = account,
accountStateViewModel = accountStateViewModel,
navController = navController,
database = database,
)
@@ -779,22 +758,6 @@ fun MainScreen(
},
)
composable(
Route.NotificationType.route,
content = {
NotificationTypeScreen(
modifier =
Modifier
.fillMaxSize()
.padding(padding)
.padding(40.dp),
onDone = {
navController.navigateUp()
},
)
},
)
composable(
Route.DefaultRelays.route,
content = {
@@ -1,133 +0,0 @@
package com.greenart7c3.nostrsigner.ui
import android.content.Intent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton
import com.greenart7c3.nostrsigner.service.ConnectivityService
import com.greenart7c3.nostrsigner.service.NotificationDataSource
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
import com.vitorpamplona.ammolite.relays.RelayPool
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun NotificationTypeScreen(
modifier: Modifier = Modifier,
onDone: () -> Unit,
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
if (!LocalPreferences.isNotificationTypeConfigured()) {
AmberListenerSingleton.accountStateViewModel?.toast(
context.getString(R.string.notification_settings),
context.getString(R.string.please_configure_your_notification_settings_before_continuing),
)
}
}
}
val scope = rememberCoroutineScope()
var notificationItemsIndex by remember {
mutableIntStateOf(NostrSigner.getInstance().settings.notificationType.screenCode)
}
val notificationItems =
persistentListOf(
TitleExplainer(
stringResource(NotificationType.PUSH.resourceId),
stringResource(R.string.push_notifications_explainer),
),
TitleExplainer(
stringResource(NotificationType.DIRECT.resourceId),
stringResource(R.string.direct_notifications_explainer),
),
)
Column(
modifier = modifier
.fillMaxSize(),
) {
Column(
Modifier.weight(1f),
) {
Box(
Modifier
.padding(8.dp),
) {
SettingsRow(
R.string.notification_type,
R.string.select_the_type_of_notification_you_want_to_receive,
notificationItems,
notificationItemsIndex,
) {
notificationItemsIndex = it
}
}
@Suppress("KotlinConstantConditions")
if (BuildConfig.FLAVOR == "free") {
Box(
Modifier
.padding(8.dp),
) {
PushNotificationSettingsRow()
}
}
}
AmberButton(
onClick = {
scope.launch(Dispatchers.IO) {
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
notificationType = parseNotificationType(notificationItemsIndex),
)
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
PushNotificationUtils.hasInit = false
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(context))
if (notificationItemsIndex == 0) {
NotificationDataSource.stopSync()
RelayPool.disconnect()
} else {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
}
NostrSigner.getInstance().applicationContext.startForegroundService(
Intent(
NostrSigner.getInstance().applicationContext,
ConnectivityService::class.java,
),
)
scope.launch(Dispatchers.Main) {
onDone()
}
}
},
content = {
Text(
text = stringResource(R.string.save),
)
},
)
}
}
@@ -12,57 +12,35 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.database.AppDatabase
import com.greenart7c3.nostrsigner.database.ApplicationEntity
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun PermissionsScreen(
modifier: Modifier,
account: Account,
accountStateViewModel: AccountStateViewModel,
navController: NavController,
database: AppDatabase,
) {
val lifecycleEvent = rememberLifecycleEvent()
val context = LocalContext.current
val localAccount = LocalPreferences.loadFromEncryptedStorage(context, account.signer.keyPair.pubKey.toNpub())!!
val applications =
remember {
mutableListOf<ApplicationEntity>()
}
val scope = rememberCoroutineScope()
// val sortBy by remember { mutableStateOf("name") }
LaunchedEffect(lifecycleEvent) {
scope.launch(Dispatchers.IO) {
applications.clear()
applications.addAll(database.applicationDao().getAll(localAccount.signer.keyPair.pubKey.toHexKey()))
}
}
val applications = database.applicationDao().getAllFlow(account.signer.keyPair.pubKey.toHexKey()).collectAsStateWithLifecycle(emptyList())
Column(
modifier,
) {
if (applications.isEmpty()) {
if (applications.value.isEmpty()) {
Column(
Modifier.fillMaxSize(),
Arrangement.Center,
@@ -100,13 +78,13 @@ fun PermissionsScreen(
// )
// }
// }
items(applications.size) {
items(applications.value.size) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 4.dp)
.clickable {
navController.navigate("Permission/${applications.elementAt(it).key}")
navController.navigate("Permission/${applications.value.elementAt(it).key}")
},
verticalAlignment = Alignment.CenterVertically,
) {
@@ -115,7 +93,7 @@ fun PermissionsScreen(
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
) {
val localPermission = applications.elementAt(it)
val localPermission = applications.value.elementAt(it)
Text(
modifier = Modifier.padding(top = 16.dp),
text = localPermission.name.ifBlank { localPermission.key.toShortenHex() },
@@ -18,7 +18,6 @@ import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.Hub
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.SurroundSound
import androidx.compose.material3.AlertDialog
@@ -61,7 +60,6 @@ import com.greenart7c3.nostrsigner.ui.components.IconRow
import com.greenart7c3.nostrsigner.ui.components.TextSpinner
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.vitorpamplona.ammolite.relays.RelayPool
import com.vitorpamplona.quartz.encoders.toNpub
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
@@ -144,20 +142,6 @@ fun SettingsScreen(
@Suppress("KotlinConstantConditions")
if (BuildConfig.FLAVOR != "offline") {
Box(
Modifier
.padding(8.dp),
) {
IconRow(
title = stringResource(R.string.notification_type),
icon = Icons.Default.Notifications,
tint = MaterialTheme.colorScheme.onBackground,
onClick = {
navController.navigate(Route.NotificationType.route)
},
)
}
Box(
Modifier
.padding(8.dp),
@@ -182,43 +166,43 @@ fun SettingsScreen(
},
)
}
}
Box(
Modifier
.padding(8.dp),
) {
IconRow(
title = stringResource(R.string.default_relays),
icon = Icons.Default.Hub,
tint = MaterialTheme.colorScheme.onBackground,
onClick = {
navController.navigate(Route.DefaultRelays.route)
},
)
}
Box(
Modifier
.padding(8.dp),
) {
IconRow(
title = stringResource(R.string.default_relays),
icon = Icons.Default.Hub,
tint = MaterialTheme.colorScheme.onBackground,
onClick = {
navController.navigate(Route.DefaultRelays.route)
},
)
}
Box(
Modifier
.padding(8.dp),
) {
IconRow(
title = if (allowNewConnections) stringResource(R.string.disable_listening_for_new_connections) else stringResource(R.string.enable_listening_for_new_connections),
icon = Icons.Default.SurroundSound,
tint = MaterialTheme.colorScheme.onBackground,
onClick = {
allowNewConnections = !allowNewConnections
account.allowNewConnections = allowNewConnections
LocalPreferences.saveToEncryptedStorage(context, account)
},
onLongClick = {
Toast.makeText(
context,
if (allowNewConnections) context.getString(R.string.disable_listening_for_new_connections) else context.getString(R.string.enable_listening_for_new_connections),
Toast.LENGTH_LONG,
).show()
},
)
Box(
Modifier
.padding(8.dp),
) {
IconRow(
title = if (allowNewConnections) stringResource(R.string.disable_listening_for_new_connections) else stringResource(R.string.enable_listening_for_new_connections),
icon = Icons.Default.SurroundSound,
tint = MaterialTheme.colorScheme.onBackground,
onClick = {
allowNewConnections = !allowNewConnections
account.allowNewConnections = allowNewConnections
LocalPreferences.saveToEncryptedStorage(context, account)
},
onLongClick = {
Toast.makeText(
context,
if (allowNewConnections) context.getString(R.string.disable_listening_for_new_connections) else context.getString(R.string.enable_listening_for_new_connections),
Toast.LENGTH_LONG,
).show()
},
)
}
}
Box(
@@ -307,12 +291,8 @@ fun SettingsScreen(
LocalPreferences.updateProxy(context, true, proxyPort.value.toInt())
scope.launch(Dispatchers.IO) {
NotificationDataSource.stopSync()
if (NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT) {
RelayPool.disconnect()
} else {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
}
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
}
},
onError = {
@@ -341,12 +321,8 @@ fun SettingsScreen(
LocalPreferences.updateProxy(context, false, proxyPort.value.toInt())
scope.launch(Dispatchers.IO) {
NotificationDataSource.stopSync()
if (NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT) {
RelayPool.disconnect()
} else {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
}
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
}
},
) {
@@ -406,11 +382,6 @@ fun SettingsRow(
}
}
enum class NotificationType(val screenCode: Int, val resourceId: Int) {
PUSH(0, R.string.push_notifications),
DIRECT(1, R.string.direct_connection),
}
enum class BiometricsTimeType(val screenCode: Int, val resourceId: Int) {
EVERY_TIME(0, R.string.every_time),
ONE_MINUTE(1, R.string.one_minute),
@@ -428,13 +399,3 @@ fun parseBiometricsTimeType(screenCode: Int): BiometricsTimeType {
}
}
}
fun parseNotificationType(screenCode: Int): NotificationType {
return when (screenCode) {
NotificationType.PUSH.screenCode -> NotificationType.PUSH
NotificationType.DIRECT.screenCode -> NotificationType.DIRECT
else -> {
NotificationType.PUSH
}
}
}
@@ -60,7 +60,6 @@ import com.greenart7c3.nostrsigner.service.Nip11Retriever
import com.greenart7c3.nostrsigner.service.NotificationDataSource
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Client
@@ -149,7 +148,7 @@ fun DefaultRelaysScreen(
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
scope.launch(Dispatchers.IO) {
@Suppress("KotlinConstantConditions")
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
if (BuildConfig.FLAVOR != "offline") {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.stop()
delay(2000)
@@ -191,7 +190,7 @@ fun DefaultRelaysScreen(
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
scope.launch(Dispatchers.IO) {
@Suppress("KotlinConstantConditions")
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
if (BuildConfig.FLAVOR != "offline") {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.stop()
delay(2000)
@@ -238,7 +237,7 @@ fun DefaultRelaysScreen(
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
scope.launch(Dispatchers.IO) {
@Suppress("KotlinConstantConditions")
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
if (BuildConfig.FLAVOR != "offline") {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.stop()
delay(2000)
@@ -61,7 +61,6 @@ import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
import com.greenart7c3.nostrsigner.service.model.AmberEvent
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.greenart7c3.nostrsigner.ui.Result
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.toHexKey
@@ -577,7 +576,7 @@ private fun sendResultIntent(
private suspend fun reconnectToRelays(intents: List<IntentData>) {
if (!intents.any { it.bunkerRequest != null }) return
NostrSigner.getInstance().checkForNewRelays(NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT)
NostrSigner.getInstance().checkForNewRelays()
}
@Composable
@@ -36,7 +36,6 @@ import com.greenart7c3.nostrsigner.models.kindToNip
import com.greenart7c3.nostrsigner.service.AmberUtils
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.NotificationType
import com.greenart7c3.nostrsigner.ui.sendResult
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.bechToBytes
@@ -138,8 +137,7 @@ fun SingleEventHomeScreen(
val relays = savedApplication?.application?.relays?.ifEmpty { defaultRelays } ?: intentData.bunkerRequest.relays.ifEmpty { defaultRelays }
NostrSigner.getInstance().checkForNewRelays(
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
relays.toSet(),
newRelays = relays.toSet(),
)
AmberUtils.sendBunkerError(
@@ -242,8 +240,7 @@ fun SingleEventHomeScreen(
if (intentData.bunkerRequest != null) {
NostrSigner.getInstance().checkForNewRelays(
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
relays.toSet(),
newRelays = relays.toSet(),
)
}
@@ -378,8 +375,7 @@ fun SingleEventHomeScreen(
if (intentData.bunkerRequest != null) {
NostrSigner.getInstance().checkForNewRelays(
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
relays.toSet(),
newRelays = relays.toSet(),
)
}
@@ -528,8 +524,7 @@ fun SingleEventHomeScreen(
if (intentData.bunkerRequest != null) {
NostrSigner.getInstance().checkForNewRelays(
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
relays.toSet(),
newRelays = relays.toSet(),
)
}
@@ -81,13 +81,6 @@ sealed class Route(
selectedIcon = Icons.Default.Settings,
)
data object NotificationType : Route(
title = NostrSigner.getInstance().getString(R.string.notification_type),
route = "NotificationType",
icon = Icons.Outlined.Settings,
selectedIcon = Icons.Default.Settings,
)
data object DefaultRelays : Route(
title = NostrSigner.getInstance().getString(R.string.default_relays),
route = "DefaultRelays",
@@ -182,7 +175,6 @@ val routes = listOf(
Route.Logs,
Route.ActiveRelays,
Route.Language,
Route.NotificationType,
Route.DefaultRelays,
Route.SignPolicy,
Route.Security,
-73
View File
@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<package android:name="org.torproject.android"/>
</queries>
<application
android:name=".NostrSigner">
<service
android:name=".service.ConnectivityService"
android:enabled="true"
android:foregroundServiceType="specialUse"
android:exported="false">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Run a foreground service to keep the connection to the relays active"
/>
</service>
<service
android:name=".service.Notifications"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="com.greenart7c3.nostrsigner.MAIN_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrsigner" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrconnect" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -1,71 +0,0 @@
package com.greenart7c3.nostrsigner.service
import android.content.Intent
import android.util.Log
import android.util.LruCache
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.service.NotificationUtils.getOrCreateDMChannel
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import kotlin.time.measureTimedValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class Notifications : FirebaseMessagingService() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val eventCache = LruCache<String, String>(100)
override fun getStartCommandIntent(originalIntent: Intent?): Intent {
return super.getStartCommandIntent(originalIntent)
}
override fun handleIntent(intent: Intent?) {
Log.d("Time", "Intent received $intent")
super.handleIntent(intent)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
scope.launch(Dispatchers.IO) {
val (_, elapsed) =
measureTimedValue { parseMessage(remoteMessage.data)?.let { receiveIfNew(it) } }
Log.d("Time", "Notification processed in $elapsed")
}
super.onMessageReceived(remoteMessage)
}
private fun parseMessage(params: Map<String, String>): GiftWrapEvent? {
params["encryptedEvent"]?.let { eventStr ->
(Event.fromJson(eventStr) 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)
getOrCreateDMChannel(applicationContext)
EventNotificationConsumer(applicationContext).consume(event)
}
}
override fun onNewToken(token: String) {
scope.launch(Dispatchers.IO) {
RegisterAccounts(LocalPreferences.allSavedAccounts(applicationContext)).go(token)
}
super.onNewToken(token)
}
override fun onDestroy() {
Log.d("Lifetime Event", "PushNotificationReceiverService.onDestroy")
scope.cancel()
super.onDestroy()
}
}
@@ -1,50 +0,0 @@
/**
* 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.google.firebase.messaging.FirebaseMessaging
import com.greenart7c3.nostrsigner.AccountInfo
import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.NotificationType
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.tasks.await
object PushNotificationUtils {
var hasInit: Boolean = false
var accountState: AccountStateViewModel? = null
suspend fun init(accounts: List<AccountInfo>) =
with(Dispatchers.IO) {
if (hasInit || NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT) {
return@with
}
// get user notification token provided by firebase
try {
RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await())
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Firebase token", "failed to get firebase token", e)
}
}
}
@@ -1,6 +0,0 @@
package com.greenart7c3.nostrsigner.ui
import androidx.compose.runtime.Composable
@Composable
fun PushNotificationSettingsRow() {}
-1
View File
@@ -6,7 +6,6 @@ plugins {
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
alias(libs.plugins.serialization) apply false
alias(libs.plugins.google.gms) version(libs.versions.gms) apply false
alias(libs.plugins.kotlin.ksp) version(libs.versions.ksp) apply false
alias(libs.plugins.gradle.ktlint) version(libs.versions.ktlint) apply false
}
+3 -5
View File
@@ -10,15 +10,13 @@ appName=$2
./gradlew clean bundleRelease --stacktrace
./gradlew assembleRelease --stacktrace
rm ~/release/amber-*
rm ~/release/citrine-*
rm ~/release/manifest-*
mv app/build/outputs/bundle/playRelease/app-play-release.aab ~/release/
rm -rf ~/release/amber-*
rm -rf ~/release/citrine-*
rm -rf ~/release/manifest-*
mv app/build/outputs/bundle/offlineRelease/app-offline-release.aab ~/release/
mv app/build/outputs/bundle/freeRelease/app-free-release.aab ~/release/
mv app/build/outputs/apk/offline/release/app-offline-* ~/release/
mv app/build/outputs/apk/free/release/app-free-* ~/release/
mv app/build/outputs/apk/play/release/app-play-* ~/release/
./gradlew --stop
cd ~/release
./generate_manifest.sh ${version} ${appName}
-7
View File
@@ -1,6 +1,5 @@
[versions]
activityCompose = "1.9.3"
unfiedpush = "2.4.0"
appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05"
converterGson = "2.11.0"
@@ -8,8 +7,6 @@ core = "3.5.3"
coreKtx = "1.13.1"
coreSplashscreen = "1.0.1"
espressoCore = "3.6.1"
firebaseBom = "33.5.1"
firebaseMessaging = "24.0.3"
jna = "5.14.0"
junit = "4.13.2"
junitVersion = "1.2.1"
@@ -33,7 +30,6 @@ ksp = "2.0.20-1.0.24"
[libraries]
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
unfiedpush = { module = "com.github.UnifiedPush:android-connector", version.ref = "unfiedpush" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
@@ -42,8 +38,6 @@ core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebaseMessaging" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
junit = { module = "junit:junit", version.ref = "junit" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle_version" }
@@ -76,7 +70,6 @@ jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "k
androidLibrary = { id = "com.android.library", version.ref = "agp" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
gradle_ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "ktlint" }
google_gms = { id = "com.google.gms.google-services", version = "gms" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin_ksp = { id = "com.google.devtools.ksp", version = "ksp" }
jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }