Moves push registration notification into Compose after login to make sure we use the tor configuration of the active user.

This commit is contained in:
Vitor Pamplona
2025-04-01 11:18:15 -04:00
parent b3f1b093ed
commit bd02515209
6 changed files with 131 additions and 51 deletions
@@ -27,21 +27,20 @@ import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.launchAndWaitAll import com.vitorpamplona.amethyst.launchAndWaitAll
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.tryAndWait
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import kotlin.coroutines.resume import kotlin.coroutines.resume
class RegisterAccounts( class RegisterAccounts(
private val accounts: List<AccountInfo>, private val accounts: List<AccountInfo>,
private val client: OkHttpClient,
) { ) {
val tag = val tag =
if (BuildConfig.FLAVOR == "play") { if (BuildConfig.FLAVOR == "play") {
@@ -134,33 +133,25 @@ class RegisterAccounts(
} }
fun postRegistrationEvent(events: List<RelayAuthEvent>) { fun postRegistrationEvent(events: List<RelayAuthEvent>) {
try { val jsonObject =
val jsonObject = """{
"""{ "events": [ ${events.joinToString(", ") { it.toJson() }} ]
"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 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) { suspend fun go(notificationToken: String) {
@@ -64,7 +64,7 @@ class PlaybackService : MediaSessionService() {
poolWithProxy?.let { pool -> poolWithProxy?.let { pool ->
// with proxy, check if the port is the same. // with proxy, check if the port is the same.
val okHttp = HttpClientManager.getHttpClient(true) 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 return pool
} }
@@ -34,11 +34,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.debugState
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService 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.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia 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) { Amethyst.instance.serviceManager.justStart() }
} }
GlobalScope.launch(Dispatchers.IO) {
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
}
val connectivityManager = val connectivityManager =
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager) (getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
connectivityManager.registerDefaultNetworkCallback(networkCallback) connectivityManager.registerDefaultNetworkCallback(networkCallback)
@@ -53,8 +53,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AccountSettings 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.MainActivity
import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
@@ -149,6 +152,8 @@ fun LoggedInPage(
ListenToExternalSignerIfNeeded(accountViewModel) ListenToExternalSignerIfNeeded(accountViewModel)
NotificationRegistration(accountViewModel)
AppNavigation( AppNavigation(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
accountStateViewModel = accountStateViewModel, accountStateViewModel = accountStateViewModel,
@@ -156,6 +161,44 @@ fun LoggedInPage(
) )
} }
@Composable
fun NotificationRegistration(accountViewModel: AccountViewModel) {
val lifeCycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
var job = remember<Job?> { 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 @Composable
fun ManageTorInstance(accountViewModel: AccountViewModel) { fun ManageTorInstance(accountViewModel: AccountViewModel) {
val torSettings by accountViewModel.account.settings.torSettings.torType val torSettings by accountViewModel.account.settings.torSettings.torType
@@ -29,6 +29,8 @@ import com.google.firebase.messaging.RemoteMessage
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel 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.nip01Core.core.Event
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -82,7 +84,10 @@ class PushNotificationReceiverService : FirebaseMessagingService() {
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
scope.launch(Dispatchers.IO) { 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().getOrCreateZapChannel(applicationContext)
notificationManager().getOrCreateDMChannel(applicationContext) notificationManager().getOrCreateDMChannel(applicationContext)
} }
@@ -25,24 +25,71 @@ import com.google.firebase.messaging.FirebaseMessaging
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import okhttp3.OkHttpClient
object PushNotificationUtils { object PushNotificationUtils {
var lastToken: String? = null
var hasInit: List<AccountInfo>? = null var hasInit: List<AccountInfo>? = null
suspend fun init(accounts: List<AccountInfo>) = suspend fun checkAndInit(
with(Dispatchers.IO) { accounts: List<AccountInfo>,
if (hasInit?.equals(accounts) == true) { okHttpClient: OkHttpClient,
return@with ) = with(Dispatchers.IO) {
} val token = FirebaseMessaging.getInstance().token.await()
// get user notification token provided by firebase if (hasInit?.equals(accounts) == true && lastToken == token) {
try { return@with
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)
}
} }
registerToken(token, accounts, okHttpClient)
}
suspend fun checkAndInit(
token: String,
accounts: List<AccountInfo>,
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<AccountInfo>,
okHttpClient: OkHttpClient,
) = retryIfException("RegisterAccounts") {
RegisterAccounts(accounts, okHttpClient).go(token)
lastToken = token
hasInit = accounts.toList()
}
}
suspend fun <T> 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
} }