summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-09-30 15:09:22 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-09-30 15:09:22 +0200
commitf98be8c0ffeacef02f748c86ae1cc750d2ad8e76 (patch)
treeeb242949de8741b5884c44a57532def613955f01 /android
parent7221f569af139c76d0848af2eb064ef3859cb94b (diff)
parent175d837d129ddf3c62b59f2ce6d66439f573c69a (diff)
downloadmullvadvpn-f98be8c0ffeacef02f748c86ae1cc750d2ad8e76.tar.xz
mullvadvpn-f98be8c0ffeacef02f748c86ae1cc750d2ad8e76.zip
Merge branch 'polling-for-account-expiry-is-slow-if-logged-in-droid-1916'
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/ViewModelConstant.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessListViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt8
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt4
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt4
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt36
-rw-r--r--android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt78
44 files changed, 325 insertions, 58 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/ViewModelConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/ViewModelConstant.kt
new file mode 100644
index 0000000000..3d95be115f
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/ViewModelConstant.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.constant
+
+import kotlin.time.Duration.Companion.seconds
+
+val VIEW_MODEL_STOP_TIMEOUT = 5.seconds
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
index 08a99a8aef..2db89a9ef9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.AccountData
import net.mullvad.mullvadvpn.lib.model.AccountNumber
import net.mullvad.mullvadvpn.lib.model.DeviceState
@@ -54,10 +56,14 @@ class AccountViewModel(
)
.toLc<Unit, AccountUiState>()
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(Unit))
+ .onStart { viewModelScope.launch { updateAccountExpiry() } }
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(Unit),
+ )
init {
- updateAccountExpiry()
verifyPurchases()
}
@@ -88,7 +94,7 @@ class AccountViewModel(
}
private fun updateAccountExpiry() {
- viewModelScope.launch { accountRepository.getAccountData() }
+ viewModelScope.launch { accountRepository.refreshAccountData() }
}
private fun verifyPurchases() {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt
index 36e6864a80..19637aeabe 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
@@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.AddTimeUiState
import net.mullvad.mullvadvpn.compose.state.PurchaseState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
@@ -51,7 +53,7 @@ class AddTimeViewModel(
}
.stateIn(
scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
+ started = SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
initialValue = Lc.Loading(Unit),
)
@@ -105,7 +107,7 @@ class AddTimeViewModel(
}
private fun updateAccountExpiry() {
- viewModelScope.launch { accountRepository.getAccountData() }
+ viewModelScope.launch { accountRepository.refreshAccountData() }
}
private fun PurchaseResult.toPurchaseState() =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessListViewModel.kt
index 86d6069c16..68777ab9a4 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessListViewModel.kt
@@ -3,9 +3,11 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.compose.state.ApiAccessListUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.repository.ApiAccessRepository
class ApiAccessListViewModel(apiAccessRepository: ApiAccessRepository) : ViewModel() {
@@ -19,5 +21,9 @@ class ApiAccessListViewModel(apiAccessRepository: ApiAccessRepository) : ViewMod
apiAccessMethodSettings = apiAccessMethods ?: emptyList(),
)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ApiAccessListUiState())
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ ApiAccessListUiState(),
+ )
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt
index 1e617817af..08a1ba4b61 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt
@@ -11,12 +11,14 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.ApiAccessMethodDetailsUiState
import net.mullvad.mullvadvpn.constant.MINIMUM_LOADING_TIME_MILLIS
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId
import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodError
import net.mullvad.mullvadvpn.repository.ApiAccessRepository
@@ -54,7 +56,7 @@ class ApiAccessMethodDetailsViewModel(
}
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
ApiAccessMethodDetailsUiState.Loading(apiAccessMethodId = apiAccessMethodId),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt
index db5f499479..d7783502eb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt
@@ -8,11 +8,13 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.VersionInfo
import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
import net.mullvad.mullvadvpn.util.Lc
@@ -31,7 +33,11 @@ class AppInfoViewModel(
val uiState: StateFlow<Lc<Unit, AppInfoUiState>> =
appVersionInfoRepository.versionInfo
.map { versionInfo -> Lc.Content(AppInfoUiState(versionInfo, isPlayBuild)) }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(Unit))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(Unit),
+ )
fun openAppListing() =
viewModelScope.launch {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
index e836acb844..4b26fde420 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
@@ -9,15 +9,18 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.state.ConnectUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.common.util.daysFromNow
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
import net.mullvad.mullvadvpn.lib.model.ConnectError
@@ -110,12 +113,21 @@ class ConnectViewModel(
isPlayBuild = isPlayBuild,
)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ConnectUiState.INITIAL)
+ .onStart {
+ viewModelScope.launch {
+ accountRepository.refreshAccountData(ignoreTimeout = false)
+ }
+ }
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ ConnectUiState.INITIAL,
+ )
init {
viewModelScope.launch {
if (paymentUseCase.verifyPurchases().isSuccess()) {
- accountRepository.getAccountData()
+ accountRepository.refreshAccountData()
}
}
viewModelScope.launch { deviceRepository.updateDevice() }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt
index 8cb8cfb012..1a127a5929 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
@@ -15,6 +16,7 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
@@ -38,7 +40,11 @@ class CreateCustomListDialogViewModel(
val uiState =
_error
.map { CreateCustomListUiState(it) }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CreateCustomListUiState())
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ CreateCustomListUiState(),
+ )
fun createCustomList(name: String) {
viewModelScope.launch {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
index 9627dd4c90..26188d625c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
@@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
@@ -21,6 +22,7 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
@@ -101,7 +103,7 @@ class CustomListLocationsViewModel(
}
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
CustomListLocationsUiState(newList = navArgs.newList, content = Lce.Loading(Unit)),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
index c30b8ca637..d6a7bdba03 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
@@ -3,12 +3,14 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.state.CustomListsUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
@@ -23,7 +25,7 @@ class CustomListsViewModel(
.map(CustomListsUiState::Content)
.stateIn(
viewModelScope,
- started = SharingStarted.WhileSubscribed(),
+ started = SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
CustomListsUiState.Loading,
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt
index fdaa9c7eb6..13e05ad091 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt
@@ -5,11 +5,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.DaitaDestination
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.DaitaUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.util.Lc
import net.mullvad.mullvadvpn.util.isDaitaDirectOnly
@@ -36,7 +38,7 @@ class DaitaViewModel(
}
.stateIn(
scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
+ started = SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
initialValue = Lc.Loading(navArgs.isModal),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt
index 6e7d99dc39..96c476db6e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt
@@ -7,11 +7,13 @@ import com.ramcosta.composedestinations.generated.destinations.DeleteApiAccessMe
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.DeleteApiAccessMethodUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId
import net.mullvad.mullvadvpn.lib.model.RemoveApiAccessMethodError
import net.mullvad.mullvadvpn.repository.ApiAccessRepository
@@ -34,7 +36,7 @@ class DeleteApiAccessMethodConfirmationViewModel(
.map { DeleteApiAccessMethodUiState(it) }
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
DeleteApiAccessMethodUiState(null),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt
index 398325d7ed..89a28a652a 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt
@@ -7,6 +7,7 @@ import com.ramcosta.composedestinations.generated.destinations.DeleteCustomListD
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
@@ -14,6 +15,7 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.state.DeleteCustomListUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
@@ -37,7 +39,7 @@ class DeleteCustomListConfirmationViewModel(
.map { DeleteCustomListUiState(name, it) }
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
DeleteCustomListUiState(name, null),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
index fb94965532..84a2eecef9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
@@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.DeviceItemUiState
import net.mullvad.mullvadvpn.compose.state.DeviceListUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.AccountNumber
import net.mullvad.mullvadvpn.lib.model.Device
import net.mullvad.mullvadvpn.lib.model.DeviceId
@@ -58,7 +60,11 @@ class DeviceListViewModel(
}
}
.onStart { fetchDevices() }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DeviceListUiState.Loading)
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ DeviceListUiState.Loading,
+ )
fun fetchDevices() =
viewModelScope.launch {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
index ca503f3ee3..020a384c6c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
@@ -7,11 +7,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.DeviceRevokedUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
@@ -32,7 +34,7 @@ class DeviceRevokedViewModel(
}
.stateIn(
scope = CoroutineScope(dispatcher),
- started = SharingStarted.WhileSubscribed(),
+ started = SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
initialValue = DeviceRevokedUiState.UNKNOWN,
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
index d5d733e8da..d3d16fc89b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
@@ -15,12 +15,14 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.DeleteCustomDnsUseCase
@@ -91,7 +93,7 @@ class DnsDialogViewModel(
}
.stateIn(
viewModelScope,
- SharingStarted.Lazily,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
DnsDialogViewState(
input = _ipAddressInput.value,
validationError = null,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt
index 48e4ac85ac..197fe80ad3 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt
@@ -16,6 +16,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.receiveAsFlow
@@ -26,6 +27,7 @@ import net.mullvad.mullvadvpn.compose.state.ApiAccessMethodTypes
import net.mullvad.mullvadvpn.compose.state.EditApiAccessFormData
import net.mullvad.mullvadvpn.compose.state.EditApiAccessMethodUiState
import net.mullvad.mullvadvpn.constant.MINIMUM_LOADING_TIME_MILLIS
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.ApiAccessMethod
import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId
import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName
@@ -66,7 +68,7 @@ class EditApiAccessMethodViewModel(
}
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
EditApiAccessMethodUiState.Loading(editMode = apiAccessMethodId != null),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt
index b57d3f3a61..0ef6396114 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
@@ -15,6 +16,7 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.state.EditCustomListNameUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.usecase.customlists.RenameError
@@ -37,7 +39,7 @@ class EditCustomListNameDialogViewModel(
combine(inputName, _error) { name, error -> EditCustomListNameUiState(name = name, error) }
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
EditCustomListNameUiState(name = navArgs.initialName.value),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt
index c736781fcc..3e4d97eb63 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt
@@ -5,9 +5,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.EditCustomListDestination
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.compose.state.EditCustomListUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.repository.CustomListsRepository
@@ -33,7 +35,7 @@ class EditCustomListViewModel(
}
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
EditCustomListUiState.Loading,
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt
index 41d98b40fb..dce94490f9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt
@@ -7,6 +7,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
@@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.RelayFilterUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.ProviderId
@@ -40,7 +42,11 @@ class FilterViewModel(
val uiState: StateFlow<RelayFilterUiState> =
combine(providerToOwnershipsUseCase(), selectedOwnership, selectedProviders, ::createState)
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), RelayFilterUiState())
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ RelayFilterUiState(),
+ )
private fun createState(
providerToOwnerships: Map<ProviderId, Set<Ownership>>,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
index 32004e2852..f1213ea6f6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
@@ -25,6 +26,7 @@ import net.mullvad.mullvadvpn.compose.state.LoginState.Idle
import net.mullvad.mullvadvpn.compose.state.LoginState.Loading
import net.mullvad.mullvadvpn.compose.state.LoginState.Success
import net.mullvad.mullvadvpn.compose.state.LoginUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.common.util.isBeforeNowInstant
import net.mullvad.mullvadvpn.lib.model.AccountNumber
import net.mullvad.mullvadvpn.lib.model.LoginAccountError
@@ -73,7 +75,11 @@ class LoginViewModel(
val uiState: StateFlow<LoginUiState> =
_uiState
.onStart { viewModelScope.launch { accountRepository.fetchAccountHistory() } }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), LoginUiState.INITIAL)
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ LoginUiState.INITIAL,
+ )
fun clearAccountHistory() =
viewModelScope.launch {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt
index b92fd9845b..4e17038fa6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
@@ -13,6 +14,7 @@ import net.mullvad.mullvadvpn.compose.state.DeviceItemUiState
import net.mullvad.mullvadvpn.compose.state.DeviceListUiState
import net.mullvad.mullvadvpn.compose.state.ManageDevicesItemUiState
import net.mullvad.mullvadvpn.compose.state.ManageDevicesUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Device
import net.mullvad.mullvadvpn.lib.model.DeviceId
import net.mullvad.mullvadvpn.lib.model.DeviceState
@@ -49,7 +51,11 @@ class ManageDevicesViewModel(
}
}
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lce.Loading(Unit))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lce.Loading(Unit),
+ )
fun fetchDevices() = deviceListViewModel.fetchDevices()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt
index 292ceec717..91e6a25498 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt
@@ -10,10 +10,12 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.repository.SettingsRepository
@@ -31,7 +33,7 @@ class MtuDialogViewModel(
combine(_mtuInput, _isValidMtu, ::createState)
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
createState(_mtuInput.value, _isValidMtu.value),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt
index 7b5b08a088..187131b848 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt
@@ -6,10 +6,12 @@ import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.MultihopDestination
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.util.Lc
@@ -23,7 +25,11 @@ class MultihopViewModel(
wireguardConstraintsRepository.wireguardConstraints
.filterNotNull()
.map { Lc.Content(MultihopUiState(it.isMultihopEnabled, isModal = navArgs.isModal)) }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(navArgs.isModal))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(navArgs.isModal),
+ )
fun setMultihop(enable: Boolean) {
viewModelScope.launch { wireguardConstraintsRepository.setMultihop(enable) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
index ace068304d..0fecbb65e2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
@@ -49,7 +51,11 @@ class OutOfTimeViewModel(
verificationPending = paymentAvailability.hasPendingPayment(),
)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), OutOfTimeUiState())
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ OutOfTimeUiState(),
+ )
init {
viewModelScope.launch {
@@ -84,7 +90,7 @@ class OutOfTimeViewModel(
}
private suspend fun updateAccountExpiry() {
- accountRepository.getAccountData()
+ accountRepository.refreshAccountData()
}
private fun notOutOfTimeEffect() =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt
index 11800791d2..ea23e7e34f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt
@@ -5,11 +5,13 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository
data class PrivacyDisclaimerViewState(val isStartingService: Boolean, val isPlayBuild: Boolean)
@@ -31,7 +33,7 @@ class PrivacyDisclaimerViewModel(
}
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
PrivacyDisclaimerViewState(false, isPlayBuild),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt
index 3b1ce59a70..0f27ea1516 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt
@@ -7,11 +7,13 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.constant.MINIMUM_LOADING_TIME_MILLIS
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.dataproxy.SendProblemReportResult
import net.mullvad.mullvadvpn.dataproxy.UserReport
@@ -50,7 +52,11 @@ class ReportProblemViewModel(
description = userReport.description,
)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ReportProblemUiState())
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ ReportProblemUiState(),
+ )
private val _uiSideEffect = Channel<ReportProblemSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt
index 9d9d0380b3..a0555b6e48 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt
@@ -10,12 +10,14 @@ import java.io.InputStreamReader
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.SettingsPatchError
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
import net.mullvad.mullvadvpn.util.Lc
@@ -41,7 +43,11 @@ class ServerIpOverridesViewModel(
)
.toLc<Boolean, ServerIpOverridesUiState>()
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(navArgs.isModal))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(navArgs.isModal),
+ )
fun importFile(uri: Uri) =
viewModelScope.launch {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt
index b3b09889c3..dad4f1f677 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt
@@ -4,9 +4,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.compose.state.SettingsUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.DeviceState
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
@@ -41,5 +43,9 @@ class SettingsViewModel(
)
.toLc<Unit, SettingsUiState>()
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(Unit))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(Unit),
+ )
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt
index a3ce03428f..da38dfe201 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt
@@ -10,10 +10,12 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.util.inAnyOf
@@ -31,7 +33,7 @@ class ShadowsocksCustomPortDialogViewModel(
combine(_portInput, _isValidPort, ::createState)
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
createState(_portInput.value, _isValidPort.value),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt
index c7c9e8d900..c1f95713bd 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt
@@ -6,6 +6,7 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
@@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsUiState
import net.mullvad.mullvadvpn.constant.SHADOWSOCKS_PRESET_PORTS
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.repository.SettingsRepository
@@ -38,7 +40,7 @@ class ShadowsocksSettingsViewModel(private val settingsRepository: SettingsRepos
}
.stateIn(
scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
+ started = SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
initialValue = Lc.Loading(Unit),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
index 8743afc308..679f9316f6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
@@ -8,12 +8,14 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.applist.AppData
import net.mullvad.mullvadvpn.applist.ApplicationsProvider
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.AppId
import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository
import net.mullvad.mullvadvpn.util.Lc
@@ -45,7 +47,7 @@ class SplitTunnelingViewModel(
}
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
SplitTunnelingViewModelState(),
)
@@ -54,7 +56,7 @@ class SplitTunnelingViewModel(
.map { it.toUiState(navArgs.isModal) }
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
Lc.Loading(Loading(enabled = false, isModal = navArgs.isModal)),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt
index 0d7d1293b5..798bf82341 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt
@@ -5,11 +5,13 @@ import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.repository.SettingsRepository
@@ -26,7 +28,7 @@ class Udp2TcpSettingsViewModel(private val repository: SettingsRepository) : Vie
}
.stateIn(
scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
+ started = SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
initialValue = Lc.Loading(Unit),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt
index 23fe12130e..25cac3f132 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt
@@ -5,12 +5,14 @@ import androidx.lifecycle.viewModelScope
import arrow.core.raise.either
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
import net.mullvad.mullvadvpn.lib.model.ParseVoucherCodeError
import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
@@ -27,7 +29,11 @@ class VoucherDialogViewModel(private val voucherRepository: VoucherRepository) :
combine(vmState, voucherInput) { state, input ->
VoucherDialogUiState(voucherInput = input, voucherState = state)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), VoucherDialogUiState.INITIAL)
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ VoucherDialogUiState.INITIAL,
+ )
fun onRedeem(voucherInput: String) {
vmState.update { VoucherDialogState.Verifying }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
index 9ba3e00995..19e9ae12ea 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
@@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
@@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.CustomDnsItem
import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
@@ -126,7 +128,11 @@ class VpnSettingsViewModel(
)
.toLc<Boolean, VpnSettingsUiState>()
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(navArgs.isModal))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(navArgs.isModal),
+ )
fun onToggleLocalNetworkSharing(isEnabled: Boolean) {
viewModelScope.launch(dispatcher) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
index 334a360cfd..e2a2fbb6f5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.common.util.isAfterNowInstant
import net.mullvad.mullvadvpn.lib.model.AccountNumber
import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
@@ -55,7 +57,11 @@ class WelcomeViewModel(
)
)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(Unit))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(Unit),
+ )
init {
viewModelScope.launch {
@@ -105,7 +111,7 @@ class WelcomeViewModel(
}
private suspend fun updateAccountExpiry() {
- accountRepository.getAccountData()
+ accountRepository.refreshAccountData()
}
sealed interface UiSideEffect {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt
index b98801612e..a3fcfa97f9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt
@@ -10,10 +10,12 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.util.inAnyOf
@@ -31,7 +33,7 @@ class WireguardCustomPortDialogViewModel(
combine(_portInput, _isValidPort, ::createState)
.stateIn(
viewModelScope,
- SharingStarted.WhileSubscribed(),
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
createState(_portInput.value, _isValidPort.value),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
index fde0b3b94d..4c20dd424b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
@@ -17,6 +18,7 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.Hop
@@ -120,7 +122,11 @@ class SearchLocationViewModel(
)
)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lce.Loading(Unit))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lce.Loading(Unit),
+ )
private val _uiSideEffect = Channel<SearchLocationSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
index bd9929c87d..fa0174ce32 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
@@ -5,11 +5,13 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItemId
@@ -55,7 +57,11 @@ class SelectLocationListViewModel(
)
}
}
- .stateIn(viewModelScope, SharingStarted.Lazily, Lce.Loading(Unit))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lce.Loading(Unit),
+ )
fun onToggleExpand(item: RelayItemId, parent: CustomListId? = null, expand: Boolean) {
_expandedItems.onToggleExpandSet(item, parent, expand)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
index 0ddb20efa7..8db8da3c35 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
@@ -6,6 +6,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
@@ -17,6 +18,7 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
+import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.Hop
@@ -86,7 +88,11 @@ class SelectLocationViewModel(
)
)
}
- .stateIn(viewModelScope, SharingStarted.Lazily, Lc.Loading(Unit))
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
+ Lc.Loading(Unit),
+ )
private val _uiSideEffect = Channel<SelectLocationSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
index 7a6b756bf5..26fc228729 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
@@ -2,9 +2,11 @@ package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
import arrow.core.right
+import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
+import io.mockk.just
import io.mockk.mockk
import io.mockk.unmockkAll
import java.time.ZonedDateTime
@@ -61,7 +63,7 @@ class AccountViewModelTest {
every { mockAccountRepository.accountData } returns accountExpiryState
every { mockDeviceRepository.deviceState } returns deviceState
coEvery { mockPaymentUseCase.paymentAvailability } returns paymentAvailability
- coEvery { mockAccountRepository.getAccountData() } returns null
+ coEvery { mockAccountRepository.refreshAccountData(any()) } just Runs
viewModel =
AccountViewModel(
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt
index 7e3f966dbb..d3c95690bf 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt
@@ -56,7 +56,7 @@ class AddTimeViewModelTest {
VerificationResult.NothingToVerify.right()
coEvery { mockPaymentUseCase.queryPaymentAvailability() } just Runs
coEvery { mockPaymentUseCase.resetPurchaseResult() } just Runs
- coEvery { mockAccountRepository.getAccountData() } returns null
+ coEvery { mockAccountRepository.refreshAccountData(any()) } just Runs
viewModel =
AddTimeViewModel(
@@ -151,7 +151,7 @@ class AddTimeViewModelTest {
purchaseResult.emit(purchaseResultData)
// Assert
- coVerify { mockAccountRepository.getAccountData() }
+ coVerify { mockAccountRepository.refreshAccountData(ignoreTimeout = true) }
}
@Test
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
index 2e922a0895..bfb1918875 100644
--- a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
@@ -1,7 +1,6 @@
package net.mullvad.mullvadvpn.lib.shared
import arrow.core.Either
-import arrow.core.raise.nullable
import java.time.ZonedDateTime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -27,6 +26,7 @@ class AccountRepository(
private val deviceRepository: DeviceRepository,
val scope: CoroutineScope,
) {
+ private var lastSuccessfulAccountDataFetch: ZonedDateTime? = null
private val _mutableAccountDataCache: MutableSharedFlow<AccountData> = MutableSharedFlow()
@@ -43,7 +43,10 @@ class AccountRepository(
managementService.deviceState.map { deviceState ->
when (deviceState) {
is DeviceState.LoggedIn -> {
- managementService.getAccountData(deviceState.accountNumber).getOrNull()
+ managementService
+ .getAccountData(deviceState.accountNumber)
+ .getOrNull()
+ ?.also { lastSuccessfulAccountDataFetch = ZonedDateTime.now() }
}
DeviceState.LoggedOut,
DeviceState.Revoked -> null
@@ -72,15 +75,27 @@ class AccountRepository(
suspend fun clearAccountHistory(): Either<ClearAccountHistoryError, Unit> =
managementService.clearAccountHistory().onRight { _mutableAccountHistory.value = null }
- suspend fun getAccountData(): AccountData? = nullable {
- val deviceState = ensureNotNull(deviceRepository.deviceState.value as? DeviceState.LoggedIn)
+ /*
+ * Fetches the account data from the server, and updates the cache.
+ * Unless force is true, it will only fetch if no fetch was made in the last minute.
+ */
+ suspend fun refreshAccountData(ignoreTimeout: Boolean = true) {
+ // Only refresh if logged in
+ val deviceState = deviceRepository.deviceState.value as? DeviceState.LoggedIn ?: return
- val accountData =
- managementService.getAccountData(deviceState.accountNumber).getOrNull().bind()
+ if (ignoreTimeout || lastSuccessfulAccountDataFetch.canFetchAccountData()) {
+ val accountData =
+ managementService.getAccountData(deviceState.accountNumber).getOrNull()
+ lastSuccessfulAccountDataFetch = ZonedDateTime.now()
- // Update stateflow cache
- _mutableAccountDataCache.emit(accountData)
- accountData
+ // Update stateflow cache, only update if device state is still logged in and using the
+ // same account number
+ deviceRepository.deviceState.value?.let {
+ if (it is DeviceState.LoggedIn && it.accountNumber == accountData?.accountNumber) {
+ _mutableAccountDataCache.emit(accountData)
+ }
+ }
+ }
}
suspend fun getWebsiteAuthToken(): WebsiteAuthToken? =
@@ -93,4 +108,7 @@ class AccountRepository(
fun resetIsNewAccount() {
_isNewAccount.value = false
}
+
+ private fun ZonedDateTime?.canFetchAccountData(): Boolean =
+ this == null || this.isBefore(ZonedDateTime.now().minusMinutes(1))
}
diff --git a/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt
new file mode 100644
index 0000000000..ff2ecb88b2
--- /dev/null
+++ b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt
@@ -0,0 +1,78 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import arrow.core.right
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.AccountNumber
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(TestCoroutineRule::class)
+class AccountRepositoryTest {
+
+ private val mockManagementService: ManagementService = mockk()
+ private val mockDeviceRepository: DeviceRepository = mockk()
+
+ private val mockDeviceStateFlow = MutableStateFlow<DeviceState>(DeviceState.LoggedOut)
+
+ private lateinit var accountRepository: AccountRepository
+
+ @BeforeEach
+ fun setup() {
+ every { mockDeviceRepository.deviceState } returns mockDeviceStateFlow
+ every { mockManagementService.deviceState } returns mockDeviceStateFlow
+
+ accountRepository =
+ AccountRepository(
+ managementService = mockManagementService,
+ deviceRepository = mockDeviceRepository,
+ scope = MainScope(),
+ )
+ }
+
+ @Test
+ fun `given force is true should always call managementService getAccountData`() = runTest {
+ // Arrange
+ val accountData: AccountData = mockk()
+ val accountNumber = AccountNumber("1234567890")
+ every { accountData.accountNumber } returns accountNumber
+ coEvery { mockManagementService.getAccountData(accountNumber) } returns accountData.right()
+
+ // Act
+ mockDeviceStateFlow.emit(DeviceState.LoggedIn(accountNumber, mockk(relaxed = true)))
+ accountRepository.refreshAccountData(ignoreTimeout = true)
+
+ // Assert
+ coVerify { mockManagementService.getAccountData(accountNumber) }
+ }
+
+ @Test
+ fun `given last latestAccountDataFetch null should always call managementService getAccountData`() =
+ runTest {
+ // Arrange
+ val accountData: AccountData = mockk()
+ val accountNumber = AccountNumber("1234567890")
+ every { accountData.accountNumber } returns accountNumber
+ coEvery { mockManagementService.getAccountData(accountNumber) } returns
+ accountData.right()
+
+ // Act
+ mockDeviceStateFlow.emit(DeviceState.LoggedIn(accountNumber, mockk(relaxed = true)))
+ accountRepository.refreshAccountData(ignoreTimeout = false)
+
+ // Assert
+ coVerify { mockManagementService.getAccountData(accountNumber) }
+ }
+}