diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-09-30 15:09:22 +0200 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-09-30 15:09:22 +0200 |
| commit | f98be8c0ffeacef02f748c86ae1cc750d2ad8e76 (patch) | |
| tree | eb242949de8741b5884c44a57532def613955f01 /android | |
| parent | 7221f569af139c76d0848af2eb064ef3859cb94b (diff) | |
| parent | 175d837d129ddf3c62b59f2ce6d66439f573c69a (diff) | |
| download | mullvadvpn-f98be8c0ffeacef02f748c86ae1cc750d2ad8e76.tar.xz mullvadvpn-f98be8c0ffeacef02f748c86ae1cc750d2ad8e76.zip | |
Merge branch 'polling-for-account-expiry-is-slow-if-logged-in-droid-1916'
Diffstat (limited to 'android')
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) } + } +} |
