Skip to content

Commit

Permalink
Migrate from SharedPreferences to DataStore
Browse files Browse the repository at this point in the history
  • Loading branch information
Rawa committed Dec 19, 2024
1 parent cb6cb1f commit d2f5f94
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 39 deletions.
26 changes: 15 additions & 11 deletions android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package net.mullvad.mullvadvpn.di

import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import net.mullvad.mullvadvpn.BuildConfig
Expand All @@ -20,14 +21,17 @@ import net.mullvad.mullvadvpn.repository.ChangelogRepository
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.InAppNotificationController
import net.mullvad.mullvadvpn.repository.NewDeviceRepository
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.repository.ProblemReportRepository
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.SplashCompleteRepository
import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository
import net.mullvad.mullvadvpn.repository.UserPreferences
import net.mullvad.mullvadvpn.repository.UserPreferencesMigration
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository
import net.mullvad.mullvadvpn.repository.UserPreferencesSerializer
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.ui.MainActivity
import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
Expand Down Expand Up @@ -99,16 +103,13 @@ import net.mullvad.mullvadvpn.viewmodel.location.SearchLocationViewModel
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationViewModel
import org.apache.commons.validator.routines.InetAddressValidator
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module

val uiModule = module {
single<SharedPreferences>(named(APP_PREFERENCES_NAME)) {
androidApplication().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE)
}
single<DataStore<UserPreferences>> { androidContext().userPreferencesStore }

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

single { ChangelogRepository(get()) }
single {
PrivacyDisclaimerRepository(
androidContext().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE)
)
}
single { UserPreferencesRepository(get()) }
single { SettingsRepository(get()) }
single { MullvadProblemReport(get()) }
single { RelayOverridesRepository(get()) }
Expand Down Expand Up @@ -272,3 +269,10 @@ val uiModule = module {
const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
const val APP_PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}.app_preferences"
const val BOOT_COMPLETED_RECEIVER_COMPONENT_NAME = "BOOT_COMPLETED_RECEIVER_COMPONENT_NAME"

private val Context.userPreferencesStore: DataStore<UserPreferences> by
dataStore(
fileName = APP_PREFERENCES_NAME,
serializer = UserPreferencesSerializer,
produceMigrations = UserPreferencesMigration::migrations,
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.mullvad.mullvadvpn.repository

import android.content.Context
import androidx.datastore.core.DataMigration
import androidx.datastore.migrations.SharedPreferencesMigration
import androidx.datastore.migrations.SharedPreferencesView
import net.mullvad.mullvadvpn.di.APP_PREFERENCES_NAME

private const val IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY_SHARED_PREF_KEY =
"is_privacy_disclosure_accepted"

data object UserPreferencesMigration {
fun migrations(context: Context): List<DataMigration<UserPreferences>> =
listOf(
SharedPreferencesMigration(
context,
sharedPreferencesName = APP_PREFERENCES_NAME,
keysToMigrate = setOf(IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY_SHARED_PREF_KEY),
) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
val privacyDisclosureAccepted =
sharedPrefs.getBoolean(
IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY_SHARED_PREF_KEY,
false,
)
currentData
.toBuilder()
.setIsPrivacyDisclosureAccepted(privacyDisclosureAccepted)
.build()
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.mullvad.mullvadvpn.repository

import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import java.io.IOException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first

class UserPreferencesRepository(private val userPreferences: DataStore<UserPreferences>) {

// Note: this should not be made into a StateFlow. See:
// https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore#data()
val preferencesFlow: Flow<UserPreferences> =
userPreferences.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Logger.e("Error reading user preferences file, falling back to default.", exception)
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}

suspend fun preferences(): UserPreferences = preferencesFlow.first()

suspend fun setPrivacyDisclosureAccepted() {
userPreferences.updateData { prefs ->
prefs.toBuilder().setIsPrivacyDisclosureAccepted(true).build()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.mullvad.mullvadvpn.repository

import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream
import java.io.OutputStream

object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()

override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto", exception)
}
}

override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import net.mullvad.mullvadvpn.lib.endpoint.getApiEndpointConfigurationExtras
import net.mullvad.mullvadvpn.lib.model.PrepareError
import net.mullvad.mullvadvpn.lib.model.Prepared
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.repository.SplashCompleteRepository
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.viewmodel.MullvadAppViewModel
import org.koin.android.ext.android.inject
Expand All @@ -55,7 +55,7 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {

private val apiEndpointFromIntentHolder by inject<ApiEndpointFromIntentHolder>()
private val mullvadAppViewModel by inject<MullvadAppViewModel>()
private val privacyDisclaimerRepository by inject<PrivacyDisclaimerRepository>()
private val userPreferencesRepository by inject<UserPreferencesRepository>()
private val serviceConnectionManager by inject<ServiceConnectionManager>()
private val splashCompleteRepository by inject<SplashCompleteRepository>()
private val managementService by inject<ManagementService>()
Expand Down Expand Up @@ -93,7 +93,7 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
// https://medium.com/@lepicekmichal/android-background-service-without-hiccup-501e4479110f
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
if (userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
bindService()
}
}
Expand All @@ -103,7 +103,7 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
lifecycleScope.launch {
if (privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
if (userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
// If service is to be started wait for it to be connected before dismissing Splash
// screen
managementService.connectionState
Expand All @@ -121,8 +121,10 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {

override fun onStop() {
super.onStop()
if (privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
serviceConnectionManager.unbind()
lifecycleScope.launch {
if (userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
serviceConnectionManager.unbind()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ 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.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository

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

class PrivacyDisclaimerViewModel(
private val privacyDisclaimerRepository: PrivacyDisclaimerRepository,
private val userPreferencesRepository: UserPreferencesRepository,
isPlayBuild: Boolean,
) : ViewModel() {

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

fun setPrivacyDisclosureAccepted() {
privacyDisclaimerRepository.setPrivacyDisclosureAccepted()
viewModelScope.launch {
userPreferencesRepository.setPrivacyDisclosureAccepted()
if (!_isStartingService.value) {
_isStartingService.update { true }
_uiSideEffect.send(PrivacyDisclaimerUiSideEffect.StartService)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_TIMEOUT_MS
import net.mullvad.mullvadvpn.lib.model.DeviceState
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.repository.SplashCompleteRepository
import net.mullvad.mullvadvpn.repository.UserPreferencesRepository

data class SplashScreenState(val splashComplete: Boolean = false)

class SplashViewModel(
private val privacyDisclaimerRepository: PrivacyDisclaimerRepository,
private val userPreferencesRepository: UserPreferencesRepository,
private val accountRepository: AccountRepository,
private val deviceRepository: DeviceRepository,
private val splashCompleteRepository: SplashCompleteRepository,
Expand All @@ -37,7 +37,7 @@ class SplashViewModel(
val uiState: StateFlow<SplashScreenState> = _uiState

private suspend fun getStartDestination(): SplashUiSideEffect {
if (!privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
if (!userPreferencesRepository.preferences().isPrivacyDisclosureAccepted) {
return SplashUiSideEffect.NavigateToPrivacyDisclaimer
}

Expand Down

0 comments on commit d2f5f94

Please sign in to comment.