Moves The WIFI/Mobile data connection watcher to the compose.

Moves Relay Connection Manager to the Compose
Adds a 30 second delay to approve events on Amber
This commit is contained in:
Vitor Pamplona
2025-04-01 16:35:46 -04:00
parent cf2959d42e
commit 12235216ff
8 changed files with 170 additions and 183 deletions
@@ -293,6 +293,7 @@ class ServiceManager(
start: Boolean = true,
pause: Boolean = true,
) {
Log.d("ManageRelayServices", "Force Restart $account $start $pause")
if (pause) {
pause()
}
@@ -306,25 +307,25 @@ class ServiceManager(
}
}
fun restartIfDifferentAccount(account: Account) {
if (this.account != account) {
forceRestart(account, true, true)
}
fun setAccountAndRestart(account: Account) {
forceRestart(account, true, true)
}
fun forceRestart() {
forceRestart(null, true, true)
}
fun justStart() {
forceRestart(null, true, false)
fun justStartIfItHasAccount() {
if (account != null) {
forceRestart(null, true, false)
}
}
fun pauseForGood() {
forceRestart(null, false, true)
}
fun pauseForGoodAndClearAccount() {
fun pauseAndLogOff() {
account = null
forceRestart(null, false, true)
}
@@ -20,13 +20,26 @@
*/
package com.vitorpamplona.amethyst.ui
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.util.Log
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
@@ -44,9 +57,68 @@ fun prepareSharedViewModel(act: MainActivity): SharedPreferencesViewModel {
sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures)
}
LaunchedEffect(act.isOnMobileDataState) {
sharedPreferencesViewModel.updateConnectivityStatusState(act.isOnMobileDataState)
}
ManageConnectivity(sharedPreferencesViewModel)
return sharedPreferencesViewModel
}
@Composable
fun ManageConnectivity(sharedPreferencesViewModel: SharedPreferencesViewModel) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var job = remember<Job?> { null }
val networkCallback =
remember {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
sharedPreferencesViewModel.updateNetworkState(network.networkHandle)
}
// Network capabilities have changed for the network
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
super.onCapabilitiesChanged(network, networkCapabilities)
sharedPreferencesViewModel.updateNetworkState(network.networkHandle)
sharedPreferencesViewModel.updateConnectivityStatusState(networkCapabilities)
}
}
}
LifecycleResumeEffect(sharedPreferencesViewModel) {
job?.cancel()
job =
scope.launch(Dispatchers.IO) {
Log.d("ManageConnectivity", "Register network listener from Resume")
val connectivityManager = context.getConnectivityManager()
connectivityManager.registerDefaultNetworkCallback(networkCallback)
connectivityManager.activeNetwork?.let { network ->
sharedPreferencesViewModel.updateNetworkState(network.networkHandle)
connectivityManager.getNetworkCapabilities(network)?.let {
sharedPreferencesViewModel.updateConnectivityStatusState(it)
}
}
}
onPauseOrDispose {
job?.cancel()
job =
scope.launch(Dispatchers.IO) {
delay(30000) // 30 seconds
Log.d("ManageConnectivity", "Unregister network listener from Pause")
context.getConnectivityManager().unregisterNetworkCallback(networkCallback)
}
}
}
}
fun SharedPreferencesViewModel.updateConnectivityStatusState(networkCapabilities: NetworkCapabilities) {
val metered = !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
val mobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
updateConnectivityStatusState(metered || mobileData)
}
fun Context.getConnectivityManager() = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
@@ -20,9 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Bundle
import android.util.Log
@@ -33,11 +30,9 @@ import androidx.appcompat.app.AppCompatActivity
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.debugState
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
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
import com.vitorpamplona.amethyst.ui.navigation.Route
@@ -65,12 +60,9 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Timer
import kotlin.concurrent.schedule
class MainActivity : AppCompatActivity() {
val isOnMobileDataState = mutableStateOf(false)
private val isOnWifiDataState = mutableStateOf(false)
private var shouldPauseService = true
@@ -96,10 +88,6 @@ class MainActivity : AppCompatActivity() {
}
}
fun prepareToLaunchSigner() {
shouldPauseService = false
}
@OptIn(DelicateCoroutinesApi::class)
override fun onResume() {
super.onResume()
@@ -113,22 +101,6 @@ class MainActivity : AppCompatActivity() {
// starts muted every time
DEFAULT_MUTED_SETTING.value = true
// Keep connection alive if it's calling the signer app
Log.d("shouldPauseService", "shouldPauseService onResume: $shouldPauseService")
if (shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.justStart() }
}
val connectivityManager =
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
connectivityManager.registerDefaultNetworkCallback(networkCallback)
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
updateNetworkCapabilities(it)
}
// resets state until next External Signer Call
Timer().schedule(350) { shouldPauseService = true }
}
override fun onPause() {
@@ -137,20 +109,11 @@ class MainActivity : AppCompatActivity() {
GlobalScope.launch(Dispatchers.IO) {
LanguageTranslatorService.clear()
}
Amethyst.instance.serviceManager.cleanObservers()
// if (BuildConfig.DEBUG) {
GlobalScope.launch(Dispatchers.IO) { debugState(this@MainActivity) }
// }
Log.d("shouldPauseService", "shouldPauseService onPause: $shouldPauseService")
if (shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.pauseForGood() }
}
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
.unregisterNetworkCallback(networkCallback)
super.onPause()
}
@@ -178,72 +141,6 @@ class MainActivity : AppCompatActivity() {
super.onDestroy()
}
fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean {
val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
val isOnMobileData = !unmetered || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val isOnWifi = unmetered && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
var changedNetwork = false
if (isOnMobileDataState.value != isOnMobileData) {
isOnMobileDataState.value = isOnMobileData
changedNetwork = true
}
if (isOnWifiDataState.value != isOnWifi) {
isOnWifiDataState.value = isOnWifi
changedNetwork = true
}
if (changedNetwork) {
if (isOnMobileData) {
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
} else {
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
}
}
return changedNetwork
}
@OptIn(DelicateCoroutinesApi::class)
private val networkCallback =
object : ConnectivityManager.NetworkCallback() {
var lastNetwork: Network? = null
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.d("ServiceManager NetworkCallback", "onAvailable: $shouldPauseService")
if (shouldPauseService && lastNetwork != null && lastNetwork != network) {
GlobalScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.forceRestart() }
}
lastNetwork = network
}
// Network capabilities have changed for the network
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
super.onCapabilitiesChanged(network, networkCapabilities)
GlobalScope.launch(Dispatchers.IO) {
Log.d(
"ServiceManager NetworkCallback",
"onCapabilitiesChanged: ${network.networkHandle} hasMobileData ${isOnMobileDataState.value} hasWifi ${isOnWifiDataState.value}",
)
if (updateNetworkCapabilities(networkCapabilities) && shouldPauseService) {
Amethyst.instance.serviceManager.forceRestart()
}
}
}
}
}
fun uriToRoute(uri: String?): String? =
@@ -96,7 +96,7 @@ class AccountStateViewModel : ViewModel() {
private suspend fun requestLoginUI() {
_accountContent.update { AccountState.LoggedOff }
viewModelScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.pauseForGoodAndClearAccount() }
viewModelScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.pauseAndLogOff() }
}
suspend fun loginAndStartUI(
@@ -20,11 +20,11 @@
*/
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -59,7 +59,8 @@ class SettingsState {
var featureSet by mutableStateOf(FeatureSetType.SIMPLIFIED)
var gallerySet by mutableStateOf(ProfileGalleryType.CLASSIC)
var isOnMobileData: State<Boolean> = mutableStateOf(false)
var isOnMobileOrMeteredConnection by mutableStateOf(false)
var currentNetworkId by mutableStateOf(0L)
var windowSizeClass = mutableStateOf<WindowSizeClass?>(null)
var displayFeatures = mutableStateOf<List<DisplayFeature>>(emptyList())
@@ -67,7 +68,7 @@ class SettingsState {
val showProfilePictures =
derivedStateOf {
when (automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !isOnMobileData.value
ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
@@ -84,7 +85,7 @@ class SettingsState {
val showUrlPreview =
derivedStateOf {
when (automaticallyShowUrlPreview) {
ConnectivityType.WIFI_ONLY -> !isOnMobileData.value
ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
@@ -93,7 +94,7 @@ class SettingsState {
val startVideoPlayback =
derivedStateOf {
when (automaticallyStartPlayback) {
ConnectivityType.WIFI_ONLY -> !isOnMobileData.value
ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
@@ -102,7 +103,7 @@ class SettingsState {
val showImages =
derivedStateOf {
when (automaticallyShowImages) {
ConnectivityType.WIFI_ONLY -> !isOnMobileData.value
ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
@@ -223,9 +224,17 @@ class SharedPreferencesViewModel : ViewModel() {
}
}
fun updateConnectivityStatusState(isOnMobileDataState: State<Boolean>) {
if (sharedPrefs.isOnMobileData != isOnMobileDataState) {
sharedPrefs.isOnMobileData = isOnMobileDataState
fun updateConnectivityStatusState(isOnMobileDataState: Boolean) {
if (sharedPrefs.isOnMobileOrMeteredConnection != isOnMobileDataState) {
Log.d("Connectivity", "updateConnectivityStatusState ${sharedPrefs.currentNetworkId}: ${sharedPrefs.isOnMobileOrMeteredConnection} -> $isOnMobileDataState")
sharedPrefs.isOnMobileOrMeteredConnection = isOnMobileDataState
}
}
fun updateNetworkState(networkId: Long) {
if (sharedPrefs.currentNetworkId != networkId) {
Log.d("Connectivity", "updateNetworkState ${sharedPrefs.currentNetworkId} -> $networkId")
sharedPrefs.currentNetworkId = networkId
}
}
@@ -1250,26 +1250,34 @@ class AccountViewModel(
}
}
fun setTorSettings(newTorSettings: TorSettings) {
fun setTorSettings(newTorSettings: TorSettings) =
viewModelScope.launch(Dispatchers.IO) {
// Only restart relay connections if port or type changes
if (account.settings.setTorSettings(newTorSettings)) {
Amethyst.instance.serviceManager.forceRestart()
}
}
}
fun restartServices() {
fun forceRestartServices() =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.restartIfDifferentAccount(account)
Amethyst.instance.serviceManager.setAccountAndRestart(account)
}
}
fun changeProxyPort(port: Int) {
fun justStart() =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.justStartIfItHasAccount()
}
fun justPause() =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.cleanObservers()
Amethyst.instance.serviceManager.pauseForGood()
}
fun changeProxyPort(port: Int) =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.forceRestart()
}
}
class Factory(
val accountSettings: AccountSettings,
@@ -33,6 +33,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -52,6 +53,8 @@ import com.vitorpamplona.amethyst.ui.tor.TorStatus
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -75,9 +78,7 @@ fun LoggedInPage(
accountViewModel.firstRoute = route
LaunchedEffect(key1 = accountViewModel) {
accountViewModel.restartServices()
}
ManageRelayServices(accountViewModel, sharedPreferencesViewModel)
ManageTorInstance(accountViewModel)
@@ -92,40 +93,59 @@ fun LoggedInPage(
)
}
@Composable
fun ManageRelayServices(
accountViewModel: AccountViewModel,
sharedPreferencesViewModel: SharedPreferencesViewModel,
) {
var job = remember<Job?> { null }
LaunchedEffect(
sharedPreferencesViewModel.sharedPrefs.currentNetworkId,
sharedPreferencesViewModel.sharedPrefs.isOnMobileOrMeteredConnection,
) {
Log.d("ManageRelayServices", "Change Network Id/State ${sharedPreferencesViewModel.sharedPrefs.currentNetworkId}, forcing restart")
if (sharedPreferencesViewModel.sharedPrefs.isOnMobileOrMeteredConnection) {
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
} else {
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
}
accountViewModel.forceRestartServices()
}
LifecycleResumeEffect(sharedPreferencesViewModel, accountViewModel) {
job?.cancel()
Log.d("ManageRelayServices", "Starting Relay Services")
job = accountViewModel.justStart()
onPauseOrDispose {
job?.cancel()
job =
GlobalScope.launch(Dispatchers.IO) {
delay(30000) // 30 seconds
Log.d("ManageRelayServices", "Pausing Relay Services")
accountViewModel.justPause()
}
}
}
}
@Composable
fun NotificationRegistration(accountViewModel: AccountViewModel) {
val lifeCycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
var job = remember<Job?> { null }
LaunchedEffect(accountViewModel) {
LifecycleResumeEffect(key1 = accountViewModel) {
Log.d("RegisterAccounts", "Registering for push notifications")
job?.cancel()
job =
launch {
scope.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)
onPauseOrDispose {
job.cancel()
}
}
}
@@ -143,39 +163,21 @@ fun ManageTorInstance(accountViewModel: AccountViewModel) {
@Composable
fun ManageTorInstanceInner(accountViewModel: AccountViewModel) {
val context = LocalContext.current.applicationContext
val lifeCycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
var job = remember<Job?> { null }
DisposableEffect(key1 = accountViewModel) {
LifecycleResumeEffect(key1 = accountViewModel) {
job?.cancel()
job = null
TorManager.startTorIfNotAlreadyOn(context)
val observer =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
job?.cancel()
job = null
TorManager.startTorIfNotAlreadyOn(context)
}
Lifecycle.Event.ON_PAUSE -> {
job =
scope.launch {
delay(5000) // 5 seconds
TorManager.stopTor(context)
}
}
else -> {}
onPauseOrDispose {
job =
scope.launch {
delay(30000) // 5 seconds
TorManager.stopTor(context)
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
TorManager.stopTor(context)
}
}
}
@@ -186,6 +188,7 @@ fun WatchConnection(accountViewModel: AccountViewModel) {
LaunchedEffect(key1 = status, key2 = accountViewModel) {
if (status is TorStatus.Active) {
Log.d("ServiceManager", "Tor is Active, force restart connections")
accountViewModel.changeProxyPort((status as TorStatus.Active).port)
}
}
@@ -224,7 +227,6 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
accountViewModel.account.signer.launcher.registerLauncher(
launcher = {
try {
activity.prepareToLaunchSigner()
launcher.launch(it)
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -244,7 +246,6 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
accountViewModel.account.signer.launcher.registerLauncher(
launcher = {
try {
activity.prepareToLaunchSigner()
launcher.launch(it)
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -644,7 +644,6 @@ private fun PrepareExternalSignerReceiver(onLogin: (pubkey: String, packageName:
externalSignerLauncher.registerLauncher(
launcher = {
try {
activity.prepareToLaunchSigner()
launcher.launch(it)
} catch (e: Exception) {
if (e is CancellationException) throw e