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.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<AccountInfo>,
private val client: OkHttpClient,
) {
val tag =
if (BuildConfig.FLAVOR == "play") {
@@ -134,33 +133,25 @@ class RegisterAccounts(
}
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
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) {
@@ -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
}
@@ -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)
@@ -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<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
fun ManageTorInstance(accountViewModel: AccountViewModel) {
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.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)
}
@@ -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<AccountInfo>? = null
suspend fun init(accounts: List<AccountInfo>) =
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<AccountInfo>,
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<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
}