Skip to content

Commit dd5a653

Browse files
Rawakl
andcommitted
Migrate from SharedPreferences to DataStore
Co-authored-by: Kalle Lindström <karl.lindstrom@mullvad.net>
1 parent 1f1a55e commit dd5a653

8 files changed

+113
-39
lines changed

android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt

+15-11
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package net.mullvad.mullvadvpn.di
22

33
import android.content.ComponentName
44
import android.content.Context
5-
import android.content.SharedPreferences
65
import android.content.pm.PackageManager
6+
import androidx.datastore.core.DataStore
7+
import androidx.datastore.dataStore
78
import kotlinx.coroutines.Dispatchers
89
import kotlinx.coroutines.MainScope
910
import net.mullvad.mullvadvpn.BuildConfig
@@ -20,14 +21,17 @@ import net.mullvad.mullvadvpn.repository.ChangelogRepository
2021
import net.mullvad.mullvadvpn.repository.CustomListsRepository
2122
import net.mullvad.mullvadvpn.repository.InAppNotificationController
2223
import net.mullvad.mullvadvpn.repository.NewDeviceRepository
23-
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
2424
import net.mullvad.mullvadvpn.repository.ProblemReportRepository
2525
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
2626
import net.mullvad.mullvadvpn.repository.RelayListRepository
2727
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
2828
import net.mullvad.mullvadvpn.repository.SettingsRepository
2929
import net.mullvad.mullvadvpn.repository.SplashCompleteRepository
3030
import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository
31+
import net.mullvad.mullvadvpn.repository.UserPreferences
32+
import net.mullvad.mullvadvpn.repository.UserPreferencesMigration
33+
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository
34+
import net.mullvad.mullvadvpn.repository.UserPreferencesSerializer
3135
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
3236
import net.mullvad.mullvadvpn.ui.MainActivity
3337
import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
@@ -99,16 +103,13 @@ import net.mullvad.mullvadvpn.viewmodel.location.SearchLocationViewModel
99103
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel
100104
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationViewModel
101105
import org.apache.commons.validator.routines.InetAddressValidator
102-
import org.koin.android.ext.koin.androidApplication
103106
import org.koin.android.ext.koin.androidContext
104107
import org.koin.core.module.dsl.viewModel
105108
import org.koin.core.qualifier.named
106109
import org.koin.dsl.module
107110

108111
val uiModule = module {
109-
single<SharedPreferences>(named(APP_PREFERENCES_NAME)) {
110-
androidApplication().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE)
111-
}
112+
single<DataStore<UserPreferences>> { androidContext().userPreferencesStore }
112113

113114
single<PackageManager> { androidContext().packageManager }
114115
single<String>(named(SELF_PACKAGE_NAME)) { androidContext().packageName }
@@ -126,11 +127,7 @@ val uiModule = module {
126127
single { androidContext().contentResolver }
127128

128129
single { ChangelogRepository(get()) }
129-
single {
130-
PrivacyDisclaimerRepository(
131-
androidContext().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE)
132-
)
133-
}
130+
single { UserPreferencesRepository(get()) }
134131
single { SettingsRepository(get()) }
135132
single { MullvadProblemReport(get()) }
136133
single { RelayOverridesRepository(get()) }
@@ -272,3 +269,10 @@ val uiModule = module {
272269
const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
273270
const val APP_PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}.app_preferences"
274271
const val BOOT_COMPLETED_RECEIVER_COMPONENT_NAME = "BOOT_COMPLETED_RECEIVER_COMPONENT_NAME"
272+
273+
private val Context.userPreferencesStore: DataStore<UserPreferences> by
274+
dataStore(
275+
fileName = APP_PREFERENCES_NAME,
276+
serializer = UserPreferencesSerializer,
277+
produceMigrations = UserPreferencesMigration::migrations,
278+
)

android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/PrivacyDisclaimerRepository.kt

-15
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package net.mullvad.mullvadvpn.repository
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataMigration
5+
import androidx.datastore.migrations.SharedPreferencesMigration
6+
import androidx.datastore.migrations.SharedPreferencesView
7+
import net.mullvad.mullvadvpn.di.APP_PREFERENCES_NAME
8+
9+
private const val IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY_SHARED_PREF_KEY =
10+
"is_privacy_disclosure_accepted"
11+
12+
data object UserPreferencesMigration {
13+
fun migrations(context: Context): List<DataMigration<UserPreferences>> =
14+
listOf(
15+
SharedPreferencesMigration(
16+
context,
17+
sharedPreferencesName = APP_PREFERENCES_NAME,
18+
keysToMigrate = setOf(IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY_SHARED_PREF_KEY),
19+
) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
20+
val privacyDisclosureAccepted =
21+
sharedPrefs.getBoolean(
22+
IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY_SHARED_PREF_KEY,
23+
false,
24+
)
25+
currentData
26+
.toBuilder()
27+
.setIsPrivacyDisclosureAccepted(privacyDisclosureAccepted)
28+
.build()
29+
}
30+
)
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package net.mullvad.mullvadvpn.repository
2+
3+
import androidx.datastore.core.DataStore
4+
import co.touchlab.kermit.Logger
5+
import java.io.IOException
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.catch
8+
import kotlinx.coroutines.flow.first
9+
10+
class UserPreferencesRepository(private val userPreferences: DataStore<UserPreferences>) {
11+
12+
// Note: this should not be made into a StateFlow. See:
13+
// https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore#data()
14+
val preferencesFlow: Flow<UserPreferences> =
15+
userPreferences.data.catch { exception ->
16+
// dataStore.data throws an IOException when an error is encountered when reading data
17+
if (exception is IOException) {
18+
Logger.e("Error reading user preferences file, falling back to default.", exception)
19+
emit(UserPreferences.getDefaultInstance())
20+
} else {
21+
throw exception
22+
}
23+
}
24+
25+
suspend fun preferences(): UserPreferences = preferencesFlow.first()
26+
27+
suspend fun setPrivacyDisclosureAccepted() {
28+
userPreferences.updateData { prefs ->
29+
prefs.toBuilder().setIsPrivacyDisclosureAccepted(true).build()
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.mullvad.mullvadvpn.repository
2+
3+
import androidx.datastore.core.CorruptionException
4+
import androidx.datastore.core.Serializer
5+
import com.google.protobuf.InvalidProtocolBufferException
6+
import java.io.InputStream
7+
import java.io.OutputStream
8+
9+
object UserPreferencesSerializer : Serializer<UserPreferences> {
10+
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
11+
12+
override suspend fun readFrom(input: InputStream): UserPreferences {
13+
try {
14+
return UserPreferences.parseFrom(input)
15+
} catch (exception: InvalidProtocolBufferException) {
16+
throw CorruptionException("Cannot read proto", exception)
17+
}
18+
}
19+
20+
override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
21+
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt

+8-6
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import net.mullvad.mullvadvpn.lib.endpoint.getApiEndpointConfigurationExtras
3333
import net.mullvad.mullvadvpn.lib.model.PrepareError
3434
import net.mullvad.mullvadvpn.lib.model.Prepared
3535
import net.mullvad.mullvadvpn.lib.theme.AppTheme
36-
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
3736
import net.mullvad.mullvadvpn.repository.SplashCompleteRepository
37+
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository
3838
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
3939
import net.mullvad.mullvadvpn.viewmodel.MullvadAppViewModel
4040
import org.koin.android.ext.android.inject
@@ -55,7 +55,7 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
5555

5656
private val apiEndpointFromIntentHolder by inject<ApiEndpointFromIntentHolder>()
5757
private val mullvadAppViewModel by inject<MullvadAppViewModel>()
58-
private val privacyDisclaimerRepository by inject<PrivacyDisclaimerRepository>()
58+
private val userPreferencesRepository by inject<UserPreferencesRepository>()
5959
private val serviceConnectionManager by inject<ServiceConnectionManager>()
6060
private val splashCompleteRepository by inject<SplashCompleteRepository>()
6161
private val managementService by inject<ManagementService>()
@@ -93,7 +93,7 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
9393
// https://medium.com/@lepicekmichal/android-background-service-without-hiccup-501e4479110f
9494
lifecycleScope.launch {
9595
repeatOnLifecycle(Lifecycle.State.STARTED) {
96-
if (privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
96+
if (userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
9797
bindService()
9898
}
9999
}
@@ -103,7 +103,7 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
103103
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
104104
super.onRestoreInstanceState(savedInstanceState)
105105
lifecycleScope.launch {
106-
if (privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
106+
if (userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
107107
// If service is to be started wait for it to be connected before dismissing Splash
108108
// screen
109109
managementService.connectionState
@@ -121,8 +121,10 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
121121

122122
override fun onStop() {
123123
super.onStop()
124-
if (privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
125-
serviceConnectionManager.unbind()
124+
lifecycleScope.launch {
125+
if (userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
126+
serviceConnectionManager.unbind()
127+
}
126128
}
127129
}
128130

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@ import androidx.lifecycle.viewModelScope
55
import kotlinx.coroutines.channels.Channel
66
import kotlinx.coroutines.flow.MutableStateFlow
77
import kotlinx.coroutines.flow.SharingStarted
8-
import kotlinx.coroutines.flow.WhileSubscribed
98
import kotlinx.coroutines.flow.map
109
import kotlinx.coroutines.flow.receiveAsFlow
1110
import kotlinx.coroutines.flow.stateIn
1211
import kotlinx.coroutines.flow.update
1312
import kotlinx.coroutines.launch
14-
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
13+
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository
1514

1615
data class PrivacyDisclaimerViewState(val isStartingService: Boolean, val isPlayBuild: Boolean)
1716

1817
class PrivacyDisclaimerViewModel(
19-
private val privacyDisclaimerRepository: PrivacyDisclaimerRepository,
18+
private val userPreferencesRepository: UserPreferencesRepository,
2019
isPlayBuild: Boolean,
2120
) : ViewModel() {
2221

@@ -40,8 +39,8 @@ class PrivacyDisclaimerViewModel(
4039
val uiSideEffect = _uiSideEffect.receiveAsFlow()
4140

4241
fun setPrivacyDisclosureAccepted() {
43-
privacyDisclaimerRepository.setPrivacyDisclosureAccepted()
4442
viewModelScope.launch {
43+
userPreferencesRepository.setPrivacyDisclosureAccepted()
4544
if (!_isStartingService.value) {
4645
_isStartingService.update { true }
4746
_uiSideEffect.send(PrivacyDisclaimerUiSideEffect.StartService)

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_TIMEOUT_MS
1616
import net.mullvad.mullvadvpn.lib.model.DeviceState
1717
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
1818
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
19-
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
2019
import net.mullvad.mullvadvpn.repository.SplashCompleteRepository
20+
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository
2121

2222
data class SplashScreenState(val splashComplete: Boolean = false)
2323

2424
class SplashViewModel(
25-
private val privacyDisclaimerRepository: PrivacyDisclaimerRepository,
25+
private val userPreferencesRepository: UserPreferencesRepository,
2626
private val accountRepository: AccountRepository,
2727
private val deviceRepository: DeviceRepository,
2828
private val splashCompleteRepository: SplashCompleteRepository,
@@ -37,7 +37,7 @@ class SplashViewModel(
3737
val uiState: StateFlow<SplashScreenState> = _uiState
3838

3939
private suspend fun getStartDestination(): SplashUiSideEffect {
40-
if (!privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
40+
if (!userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
4141
return SplashUiSideEffect.NavigateToPrivacyDisclaimer
4242
}
4343

0 commit comments

Comments
 (0)