mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-10-01 03:45:36 -04:00
demo: include SOCKS proxy support
This commit is contained in:
parent
7d39842402
commit
08de472199
@ -69,6 +69,7 @@ dependencies {
|
|||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion"
|
||||||
implementation "androidx.activity:activity-compose:1.6.1"
|
implementation "androidx.activity:activity-compose:1.6.1"
|
||||||
|
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||||
|
|
||||||
implementation "androidx.room:room-runtime:$roomVersion"
|
implementation "androidx.room:room-runtime:$roomVersion"
|
||||||
implementation "androidx.room:room-ktx:$roomVersion"
|
implementation "androidx.room:room-ktx:$roomVersion"
|
||||||
|
@ -3,7 +3,6 @@ package im.molly.monero.demo
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import im.molly.monero.demo.data.*
|
import im.molly.monero.demo.data.*
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Naive container of global instances.
|
* Naive container of global instances.
|
||||||
@ -11,7 +10,7 @@ import okhttp3.OkHttpClient
|
|||||||
* A complex app should use Koin or Hilt for dependencies.
|
* A complex app should use Koin or Hilt for dependencies.
|
||||||
*/
|
*/
|
||||||
object AppModule {
|
object AppModule {
|
||||||
lateinit var application: Application
|
private lateinit var application: Application
|
||||||
|
|
||||||
private val applicationScope = kotlinx.coroutines.MainScope()
|
private val applicationScope = kotlinx.coroutines.MainScope()
|
||||||
|
|
||||||
@ -21,10 +20,6 @@ object AppModule {
|
|||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val walletHttpClient: OkHttpClient by lazy {
|
|
||||||
OkHttpClient.Builder().build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val walletDataSource: WalletDataSource by lazy {
|
private val walletDataSource: WalletDataSource by lazy {
|
||||||
WalletDataSource(database.walletDao())
|
WalletDataSource(database.walletDao())
|
||||||
}
|
}
|
||||||
@ -34,7 +29,11 @@ object AppModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val moneroSdkClient: MoneroSdkClient by lazy {
|
private val moneroSdkClient: MoneroSdkClient by lazy {
|
||||||
MoneroSdkClient(application, walletDataFileStorage, walletHttpClient)
|
MoneroSdkClient(application, walletDataFileStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
val settingsRepository: SettingsRepository by lazy {
|
||||||
|
SettingsRepository(application.preferencesDataStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
val remoteNodeRepository: RemoteNodeRepository by lazy {
|
val remoteNodeRepository: RemoteNodeRepository by lazy {
|
||||||
@ -42,7 +41,7 @@ object AppModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val walletRepository: WalletRepository by lazy {
|
val walletRepository: WalletRepository by lazy {
|
||||||
WalletRepository(moneroSdkClient, walletDataSource, applicationScope)
|
WalletRepository(moneroSdkClient, walletDataSource, settingsRepository, applicationScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun provide(application: Application) {
|
fun provide(application: Application) {
|
||||||
|
@ -6,8 +6,6 @@ import android.content.Intent
|
|||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.StrictMode
|
|
||||||
import android.os.StrictMode.VmPolicy
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
package im.molly.monero.demo
|
package im.molly.monero.demo
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import im.molly.monero.demo.service.SyncService
|
import im.molly.monero.demo.service.SyncService
|
||||||
import im.molly.monero.isIsolatedProcess
|
import im.molly.monero.isIsolatedProcess
|
||||||
|
|
||||||
|
val Context.preferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
|
|
||||||
class MainApplication : Application() {
|
class MainApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
@ -10,7 +10,6 @@ import okhttp3.OkHttpClient
|
|||||||
class MoneroSdkClient(
|
class MoneroSdkClient(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val walletDataFileStorage: WalletDataFileStorage,
|
private val walletDataFileStorage: WalletDataFileStorage,
|
||||||
private val httpClient: OkHttpClient,
|
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
private val providerDeferred = CoroutineScope(ioDispatcher).async {
|
private val providerDeferred = CoroutineScope(ioDispatcher).async {
|
||||||
@ -31,6 +30,7 @@ class MoneroSdkClient(
|
|||||||
suspend fun openWallet(
|
suspend fun openWallet(
|
||||||
publicAddress: String,
|
publicAddress: String,
|
||||||
remoteNodes: Flow<List<RemoteNode>>,
|
remoteNodes: Flow<List<RemoteNode>>,
|
||||||
|
httpClient: OkHttpClient,
|
||||||
): MoneroWallet {
|
): MoneroWallet {
|
||||||
val provider = providerDeferred.await()
|
val provider = providerDeferred.await()
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
|
@ -4,19 +4,27 @@ import im.molly.monero.*
|
|||||||
import im.molly.monero.demo.data.model.WalletConfig
|
import im.molly.monero.demo.data.model.WalletConfig
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class WalletRepository(
|
class WalletRepository(
|
||||||
private val moneroSdkClient: MoneroSdkClient,
|
private val moneroSdkClient: MoneroSdkClient,
|
||||||
private val walletDataSource: WalletDataSource,
|
private val walletDataSource: WalletDataSource,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
private val externalScope: CoroutineScope,
|
private val externalScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
private val walletIdMap = ConcurrentHashMap<Long, Deferred<MoneroWallet>>()
|
private val walletIdMap = ConcurrentHashMap<Long, Deferred<MoneroWallet>>()
|
||||||
|
|
||||||
|
private val sharedHttpClient = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
suspend fun getWallet(walletId: Long): MoneroWallet {
|
suspend fun getWallet(walletId: Long): MoneroWallet {
|
||||||
return walletIdMap.computeIfAbsent(walletId) {
|
return walletIdMap.computeIfAbsent(walletId) {
|
||||||
externalScope.async {
|
externalScope.async {
|
||||||
val config = getWalletConfig(walletId)
|
val config = getWalletConfig(walletId)
|
||||||
|
val userSettings = settingsRepository.getUserSettings().first()
|
||||||
|
val httpClient = sharedHttpClient.newBuilder()
|
||||||
|
.proxy(userSettings.activeProxy)
|
||||||
|
.build()
|
||||||
val wallet = moneroSdkClient.openWallet(
|
val wallet = moneroSdkClient.openWallet(
|
||||||
publicAddress = config.first().publicAddress,
|
publicAddress = config.first().publicAddress,
|
||||||
remoteNodes = config.map {
|
remoteNodes = config.map {
|
||||||
@ -27,7 +35,8 @@ class WalletRepository(
|
|||||||
password = node.password,
|
password = node.password,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
httpClient = httpClient,
|
||||||
)
|
)
|
||||||
wallet
|
wallet
|
||||||
}
|
}
|
||||||
@ -36,6 +45,9 @@ class WalletRepository(
|
|||||||
|
|
||||||
fun getWalletIdList() = walletDataSource.readWalletIdList()
|
fun getWalletIdList() = walletDataSource.readWalletIdList()
|
||||||
|
|
||||||
|
fun getRemoteClients(): Flow<List<RemoteNodeClient>> =
|
||||||
|
getWalletIdList().map { it.mapNotNull { walletId -> getWallet(walletId).remoteNodeClient } }
|
||||||
|
|
||||||
fun getWalletConfig(walletId: Long) = walletDataSource.readWalletConfig(walletId)
|
fun getWalletConfig(walletId: Long) = walletDataSource.readWalletConfig(walletId)
|
||||||
|
|
||||||
fun getLedger(walletId: Long): Flow<Ledger> = flow {
|
fun getLedger(walletId: Long): Flow<Ledger> = flow {
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package im.molly.monero.demo.ui
|
package im.molly.monero.demo.ui
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.StringRes
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -15,8 +14,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import im.molly.monero.demo.R
|
|
||||||
import im.molly.monero.demo.data.model.RemoteNode
|
import im.molly.monero.demo.data.model.RemoteNode
|
||||||
|
import im.molly.monero.demo.data.model.UserSettings
|
||||||
|
import im.molly.monero.demo.data.model.toSocketAddress
|
||||||
import im.molly.monero.demo.ui.theme.AppIcons
|
import im.molly.monero.demo.ui.theme.AppIcons
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
import im.molly.monero.demo.ui.theme.AppTheme
|
||||||
|
|
||||||
@ -26,46 +26,72 @@ fun SettingsRoute(
|
|||||||
viewModel: SettingsViewModel = viewModel(),
|
viewModel: SettingsViewModel = viewModel(),
|
||||||
navigateToEditRemoteNode: (Long?) -> Unit,
|
navigateToEditRemoteNode: (Long?) -> Unit,
|
||||||
) {
|
) {
|
||||||
val remoteNodes by viewModel.remoteNodes.collectAsStateWithLifecycle()
|
val settingsUiState by viewModel.settingsUiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
remoteNodes = remoteNodes,
|
settingsUiState = settingsUiState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onAddRemoteNode = { navigateToEditRemoteNode(RemoteNode.EMPTY.id) },
|
onAddRemoteNode = { navigateToEditRemoteNode(RemoteNode.EMPTY.id) },
|
||||||
onEditRemoteNode = { navigateToEditRemoteNode(it.id) },
|
onEditRemoteNode = { navigateToEditRemoteNode(it.id) },
|
||||||
onDeleteRemoteNode = { viewModel.forgetRemoteNodeDetails(it) },
|
onDeleteRemoteNode = { viewModel.forgetRemoteNodeDetails(it) },
|
||||||
|
onChangeSocksProxy = { viewModel.setSocksProxyAddress(it) },
|
||||||
|
onValidateSocksProxy = { viewModel.isSocksProxyAddressCorrect(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(
|
private fun SettingsScreen(
|
||||||
remoteNodes: List<RemoteNode>,
|
settingsUiState: SettingsUiState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onAddRemoteNode: () -> Unit = {},
|
onAddRemoteNode: () -> Unit = {},
|
||||||
onEditRemoteNode: (RemoteNode) -> Unit = {},
|
onEditRemoteNode: (RemoteNode) -> Unit = {},
|
||||||
onDeleteRemoteNode: (RemoteNode) -> Unit = {},
|
onDeleteRemoteNode: (RemoteNode) -> Unit = {},
|
||||||
|
onChangeSocksProxy: (String) -> Unit = {},
|
||||||
|
onValidateSocksProxy: (String) -> Boolean = { true },
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
SettingsSection(
|
when (settingsUiState) {
|
||||||
header = {
|
SettingsUiState.Loading -> {
|
||||||
SettingsSectionTitle(R.string.remote_nodes)
|
// TODO: Add loading wheel
|
||||||
IconButton(onClick = onAddRemoteNode) {
|
}
|
||||||
Icon(
|
is SettingsUiState.Success -> {
|
||||||
imageVector = AppIcons.AddRemoteWallet,
|
SettingsSection(
|
||||||
contentDescription = stringResource(R.string.add_remote_node),
|
header = {
|
||||||
|
SettingsSectionTitle("Remote nodes")
|
||||||
|
IconButton(onClick = onAddRemoteNode) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.AddRemoteWallet,
|
||||||
|
contentDescription = "Add remote node",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
RemoteNodeEditableList(
|
||||||
|
remoteNodes = settingsUiState.remoteNodes,
|
||||||
|
onEditRemoteNode = onEditRemoteNode,
|
||||||
|
onDeleteRemoteNode = onDeleteRemoteNode,
|
||||||
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
|
)
|
||||||
|
SettingsSection(
|
||||||
|
header = {
|
||||||
|
SettingsSectionTitle("Network")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
EditTextSettingsItem(
|
||||||
|
title = "SOCKS proxy server",
|
||||||
|
summary = "Connect via proxy.",
|
||||||
|
value = settingsUiState.socksProxyAddress,
|
||||||
|
onValueChange = onChangeSocksProxy,
|
||||||
|
inputHeading = "Provide proxy address or leave it blank for no proxy.",
|
||||||
|
inputLabel = "host:port",
|
||||||
|
inputChecker = onValidateSocksProxy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
RemoteNodeEditableList(
|
|
||||||
remoteNodes = remoteNodes,
|
|
||||||
onEditRemoteNode = onEditRemoteNode,
|
|
||||||
onDeleteRemoteNode = onDeleteRemoteNode,
|
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +104,7 @@ private fun SettingsSection(
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier.padding(horizontal = 24.dp),
|
modifier = modifier.padding(horizontal = 24.dp),
|
||||||
) {
|
) {
|
||||||
Divider(Modifier.padding(top = 24.dp))
|
Divider(Modifier.padding(bottom = 24.dp))
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@ -92,20 +118,86 @@ private fun SettingsSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsSectionTitle(@StringRes titleRes: Int) {
|
private fun SettingsSectionTitle(title: String) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(titleRes),
|
text = title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun EditTextSettingsItem(
|
||||||
|
title: String,
|
||||||
|
summary: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
inputLabel: String,
|
||||||
|
inputHeading: String = title,
|
||||||
|
inputChecker: (String) -> Boolean = { true },
|
||||||
|
) {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineText = { Text(title) },
|
||||||
|
supportingText = { Text(summary) },
|
||||||
|
trailingContent = { Text(value) },
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { showDialog = true }),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
var input by remember { mutableStateOf(value) }
|
||||||
|
val valid = inputChecker(input)
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = inputHeading,
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(inputLabel) },
|
||||||
|
value = input,
|
||||||
|
onValueChange = { input = it },
|
||||||
|
singleLine = true,
|
||||||
|
isError = !valid,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = { showDialog = false },
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDialog = false }) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
enabled = valid,
|
||||||
|
onClick = {
|
||||||
|
onValueChange(input)
|
||||||
|
showDialog = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreenPreview() {
|
private fun SettingsScreenPopulatedPreview() {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
val aNode = RemoteNode.EMPTY.copy(uri = Uri.parse("http://node.monero"))
|
val aNode = RemoteNode.EMPTY.copy(uri = Uri.parse("http://node.monero"))
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
remoteNodes = listOf(aNode),
|
SettingsUiState.Success(
|
||||||
|
socksProxyAddress = "localhost:9050",
|
||||||
|
remoteNodes = listOf(aNode),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,29 +3,44 @@ package im.molly.monero.demo.ui
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import im.molly.monero.demo.AppModule
|
import im.molly.monero.demo.AppModule
|
||||||
|
|
||||||
import im.molly.monero.demo.data.RemoteNodeRepository
|
import im.molly.monero.demo.data.RemoteNodeRepository
|
||||||
|
import im.molly.monero.demo.data.SettingsRepository
|
||||||
|
import im.molly.monero.demo.data.WalletRepository
|
||||||
import im.molly.monero.demo.data.model.RemoteNode
|
import im.molly.monero.demo.data.model.RemoteNode
|
||||||
|
import im.molly.monero.demo.data.model.SocksProxy
|
||||||
|
import im.molly.monero.demo.data.model.toSocketAddress
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
|
private val settingsRepository: SettingsRepository = AppModule.settingsRepository,
|
||||||
private val remoteNodeRepository: RemoteNodeRepository = AppModule.remoteNodeRepository,
|
private val remoteNodeRepository: RemoteNodeRepository = AppModule.remoteNodeRepository,
|
||||||
|
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val remoteNodes: StateFlow<List<RemoteNode>> =
|
val settingsUiState: StateFlow<SettingsUiState> =
|
||||||
remoteNodeRepository.getAllRemoteNodes()
|
combine(
|
||||||
.stateIn(
|
settingsRepository.getUserSettings(),
|
||||||
scope = viewModelScope,
|
remoteNodeRepository.getAllRemoteNodes(),
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
::Pair,
|
||||||
initialValue = listOf(RemoteNode.EMPTY),
|
).map {
|
||||||
|
SettingsUiState.Success(
|
||||||
|
socksProxyAddress = it.first.socksProxy?.address()?.toString().orEmpty(),
|
||||||
|
remoteNodes = it.second,
|
||||||
)
|
)
|
||||||
|
}.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
|
initialValue = SettingsUiState.Loading,
|
||||||
|
)
|
||||||
|
|
||||||
fun remoteNode(remoteNodeId: Long): Flow<RemoteNode> =
|
fun remoteNode(remoteNodeId: Long): Flow<RemoteNode> =
|
||||||
remoteNodeRepository.getRemoteNode(remoteNodeId)
|
remoteNodeRepository.getRemoteNode(remoteNodeId)
|
||||||
|
|
||||||
fun isRemoteNodeCorrect(remoteNode: RemoteNode): Boolean {
|
fun isRemoteNodeCorrect(remoteNode: RemoteNode): Boolean =
|
||||||
return remoteNode.uri.host?.isEmpty() == false
|
remoteNode.uri.host?.isEmpty() == false
|
||||||
}
|
|
||||||
|
|
||||||
fun saveRemoteNodeDetails(remoteNode: RemoteNode) {
|
fun saveRemoteNodeDetails(remoteNode: RemoteNode) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -38,4 +53,50 @@ class SettingsViewModel(
|
|||||||
remoteNodeRepository.deleteRemoteNode(remoteNode)
|
remoteNodeRepository.deleteRemoteNode(remoteNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isSocksProxyAddressCorrect(address: String) =
|
||||||
|
try {
|
||||||
|
if (address.isNotEmpty()) address.toSocketAddress()
|
||||||
|
true
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSocksProxyAddress(address: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val socksProxy =
|
||||||
|
if (address.isNotEmpty()) SocksProxy(address.toSocketAddress()) else null
|
||||||
|
settingsRepository.setSocksProxy(socksProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Consider using a ProxySelector in OkHttpClient instead once this bug is resolved:
|
||||||
|
// https://github.com/square/okhttp/issues/7698
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.getUserSettings()
|
||||||
|
.distinctUntilChangedBy { it.activeProxy }
|
||||||
|
.map { it.activeProxy }
|
||||||
|
.collect { onProxyChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onProxyChanged(newProxy: Proxy) {
|
||||||
|
walletRepository.getRemoteClients().first().forEach {
|
||||||
|
val currentProxy = it.httpClient.proxy()
|
||||||
|
if (currentProxy != newProxy) {
|
||||||
|
val builder = it.httpClient.newBuilder()
|
||||||
|
it.httpClient = builder.proxy(newProxy).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SettingsUiState {
|
||||||
|
data class Success(
|
||||||
|
val socksProxyAddress: String,
|
||||||
|
val remoteNodes: List<RemoteNode>,
|
||||||
|
) : SettingsUiState
|
||||||
|
|
||||||
|
object Loading : SettingsUiState
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||||||
|
|
||||||
class MoneroWallet internal constructor(
|
class MoneroWallet internal constructor(
|
||||||
private val wallet: IWallet,
|
private val wallet: IWallet,
|
||||||
client: RemoteNodeClient?,
|
val remoteNodeClient: RemoteNodeClient?,
|
||||||
) : IWallet by wallet, AutoCloseable {
|
) : IWallet by wallet, AutoCloseable {
|
||||||
|
|
||||||
val publicAddress: String = wallet.primaryAccountAddress
|
val publicAddress: String = wallet.primaryAccountAddress
|
||||||
|
@ -19,7 +19,7 @@ class RemoteNodeClient private constructor(
|
|||||||
val network: MoneroNetwork,
|
val network: MoneroNetwork,
|
||||||
private val loadBalancer: LoadBalancer,
|
private val loadBalancer: LoadBalancer,
|
||||||
private val loadBalancerRule: Rule,
|
private val loadBalancerRule: Rule,
|
||||||
private val httpClient: OkHttpClient,
|
var httpClient: OkHttpClient,
|
||||||
private val retryBackoff: BackoffPolicy,
|
private val retryBackoff: BackoffPolicy,
|
||||||
private val requestsScope: CoroutineScope,
|
private val requestsScope: CoroutineScope,
|
||||||
) : IRemoteNodeClient.Stub(), AutoCloseable {
|
) : IRemoteNodeClient.Stub(), AutoCloseable {
|
||||||
|
Loading…
Reference in New Issue
Block a user