From bd025152098875c1f382e8c60ac08ac3b0b8af8c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 1 Apr 2025 11:18:15 -0400 Subject: [PATCH] Moves push registration notification into Compose after login to make sure we use the tor configuration of the active user. --- .../service/notifications/RegisterAccounts.kt | 49 +++++------- .../playback/service/PlaybackService.kt | 2 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 6 -- .../amethyst/ui/screen/AccountScreen.kt | 43 +++++++++++ .../PushNotificationReceiverService.kt | 7 +- .../notifications/PushNotificationUtils.kt | 75 +++++++++++++++---- 6 files changed, 131 insertions(+), 51 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 7c145f359..ad84e6e8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -27,21 +27,20 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.launchAndWaitAll import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.tryAndWait -import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import kotlin.coroutines.resume class RegisterAccounts( private val accounts: List, + private val client: OkHttpClient, ) { val tag = if (BuildConfig.FLAVOR == "play") { @@ -134,33 +133,25 @@ class RegisterAccounts( } fun postRegistrationEvent(events: List) { - 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", "Amethyst/${BuildConfig.VERSION_NAME}") - .url("https://push.amethyst.social/register") - .post(body) - .build() - - // Always try via Tor if active. - val client = HttpClientManager.getHttpClient(TorManager.isSocksReady()) - - val isSucess = client.newCall(request).execute().use { it.isSuccessful } - Log.i(tag, "Server registration $isSucess") - } catch (e: java.lang.Exception) { - if (e is CancellationException) throw e - Log.e(tag, "Unable to register with push server", e) + 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", "Amethyst/${BuildConfig.VERSION_NAME}") + .url("https://push.amethyst.social/register") + .post(body) + .build() + + val isSucess = client.newCall(request).execute().use { it.isSuccessful } + Log.i(tag, "Server registration $isSucess") } suspend fun go(notificationToken: String) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt index 0f0f5dfb7..460670804 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt @@ -64,7 +64,7 @@ class PlaybackService : MediaSessionService() { poolWithProxy?.let { pool -> // with proxy, check if the port is the same. val okHttp = HttpClientManager.getHttpClient(true) - if (okHttp.proxy == pool.exoPlayerPool.builder.okHttp.proxy) { + if (okHttp.proxy != null && okHttp.proxy == pool.exoPlayerPool.builder.okHttp.proxy) { return pool } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index b2c77c96c..8428a6404 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -34,11 +34,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService -import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia @@ -122,10 +120,6 @@ class MainActivity : AppCompatActivity() { GlobalScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.justStart() } } - GlobalScope.launch(Dispatchers.IO) { - PushNotificationUtils.init(LocalPreferences.allSavedAccounts()) - } - val connectivityManager = (getSystemService(ConnectivityManager::class.java) as ConnectivityManager) connectivityManager.registerDefaultNetworkCallback(networkCallback) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 083580a78..704443a9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -53,8 +53,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils +import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.AppNavigation @@ -149,6 +152,8 @@ fun LoggedInPage( ListenToExternalSignerIfNeeded(accountViewModel) + NotificationRegistration(accountViewModel) + AppNavigation( accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel, @@ -156,6 +161,44 @@ fun LoggedInPage( ) } +@Composable +fun NotificationRegistration(accountViewModel: AccountViewModel) { + val lifeCycleOwner = LocalLifecycleOwner.current + val scope = rememberCoroutineScope() + var job = remember { null } + + LaunchedEffect(accountViewModel) { + job?.cancel() + job = + launch { + val okHttpClient = HttpClientManager.getHttpClient(accountViewModel.account.shouldUseTorForTrustedRelays()) + PushNotificationUtils.checkAndInit(LocalPreferences.allSavedAccounts(), okHttpClient) + } + } + + DisposableEffect(key1 = accountViewModel) { + val observer = + LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + job?.cancel() + job = + scope.launch { + val okHttpClient = HttpClientManager.getHttpClient(accountViewModel.account.shouldUseTorForTrustedRelays()) + PushNotificationUtils.checkAndInit(LocalPreferences.allSavedAccounts(), okHttpClient) + } + } + else -> {} + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } +} + @Composable fun ManageTorInstance(accountViewModel: AccountViewModel) { val torSettings by accountViewModel.account.settings.torSettings.torType diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt index 0335f8130..0a72ba21e 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt @@ -29,6 +29,8 @@ import com.google.firebase.messaging.RemoteMessage import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel +import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager +import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CoroutineScope @@ -82,7 +84,10 @@ class PushNotificationReceiverService : FirebaseMessagingService() { override fun onNewToken(token: String) { scope.launch(Dispatchers.IO) { - RegisterAccounts(LocalPreferences.allSavedAccounts()).go(token) + Log.d("Lifetime Event", "PushNotificationReceiverService.onNewToken") + // if the app is running, try to get tor. if not, goes open web. + val okHttpClient = HttpClientManager.getHttpClient(TorManager.isSocksReady()) + PushNotificationUtils.checkAndInit(token, LocalPreferences.allSavedAccounts(), okHttpClient) notificationManager().getOrCreateZapChannel(applicationContext) notificationManager().getOrCreateDMChannel(applicationContext) } diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 0c2726702..97c0c9299 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -25,24 +25,71 @@ import com.google.firebase.messaging.FirebaseMessaging import com.vitorpamplona.amethyst.AccountInfo import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.tasks.await +import okhttp3.OkHttpClient object PushNotificationUtils { + var lastToken: String? = null var hasInit: List? = null - suspend fun init(accounts: List) = - with(Dispatchers.IO) { - if (hasInit?.equals(accounts) == true) { - return@with - } - // get user notification token provided by firebase - try { - RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await()) - - hasInit = accounts.toList() - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Firebase token", "failed to get firebase token", e) - } + suspend fun checkAndInit( + accounts: List, + okHttpClient: OkHttpClient, + ) = with(Dispatchers.IO) { + val token = FirebaseMessaging.getInstance().token.await() + if (hasInit?.equals(accounts) == true && lastToken == token) { + return@with } + + registerToken(token, accounts, okHttpClient) + } + + suspend fun checkAndInit( + token: String, + accounts: List, + okHttpClient: OkHttpClient, + ) = with(Dispatchers.IO) { + // initializes if the accounts are different or if the token has changed + if (hasInit?.equals(accounts) == true && lastToken == token) { + return@with + } + registerToken(token, accounts, okHttpClient) + } + + private suspend fun registerToken( + token: String, + accounts: List, + okHttpClient: OkHttpClient, + ) = retryIfException("RegisterAccounts") { + RegisterAccounts(accounts, okHttpClient).go(token) + lastToken = token + hasInit = accounts.toList() + } +} + +suspend fun retryIfException( + debugTag: String = "RetryIfException", + maxRetries: Int = 10, + delayMs: Long = 1000, + func: suspend () -> T, +) { + var tentative = 0 + var currentDelay = delayMs + while (tentative < maxRetries) { + try { + func() + + // if it works, finishes. + return + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e(debugTag, "Tentative $tentative failed", e) + + delay(currentDelay) + tentative++ + currentDelay = currentDelay * 2 + } + } + // gives up }