summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2024-12-19 13:49:20 +0100
committerDavid Göransson <david.goransson@mullvad.net>2024-12-19 13:49:20 +0100
commitccd7525a21676967574e0469ece35ec236bed200 (patch)
tree632677c4c72b389aa543c1e44909a2e961c29c93 /android/app/src
parent83b08bf485a753f63d8dbe6101740f42efda6bc4 (diff)
parent3c259a8001ce5725e4953c7901628a21839afd93 (diff)
downloadmullvadvpn-ccd7525a21676967574e0469ece35ec236bed200.tar.xz
mullvadvpn-ccd7525a21676967574e0469ece35ec236bed200.zip
Merge branch 'migrate-from-sharedpreferences-to-datastore-droid-1688'
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt26
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/PrivacyDisclaimerRepository.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesMigration.kt31
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt32
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesSerializer.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt6
-rw-r--r--android/app/src/main/proto/user_prefs.proto6
9 files changed, 119 insertions, 39 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index b111db10b2..650ee67eaa 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -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
@@ -20,7 +21,6 @@ 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
@@ -28,6 +28,10 @@ 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
@@ -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 }
@@ -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()) }
@@ -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,
+ )
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/PrivacyDisclaimerRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/PrivacyDisclaimerRepository.kt
deleted file mode 100644
index db1ad220e3..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/PrivacyDisclaimerRepository.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package net.mullvad.mullvadvpn.repository
-
-import android.content.SharedPreferences
-
-private const val IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY = "is_privacy_disclosure_accepted"
-
-class PrivacyDisclaimerRepository(private val sharedPreferences: SharedPreferences) {
- fun hasAcceptedPrivacyDisclosure(): Boolean {
- return sharedPreferences.getBoolean(IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY, false)
- }
-
- fun setPrivacyDisclosureAccepted() {
- sharedPreferences.edit().putBoolean(IS_PRIVACY_DISCLOSURE_ACCEPTED_KEY, true).apply()
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesMigration.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesMigration.kt
new file mode 100644
index 0000000000..c92d9d393a
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesMigration.kt
@@ -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()
+ }
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt
new file mode 100644
index 0000000000..f3e6a72b64
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesRepository.kt
@@ -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()
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesSerializer.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesSerializer.kt
new file mode 100644
index 0000000000..97348fd0cc
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/UserPreferencesSerializer.kt
@@ -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)
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
index 4007b09ecd..76ec06d6cf 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
@@ -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
@@ -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>()
@@ -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()
}
}
@@ -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
@@ -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()
+ }
}
}
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 d2500bc94d..11800791d2 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,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() {
@@ -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)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
index a196d4ae90..0ed85c94cd 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
@@ -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,
@@ -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
}
diff --git a/android/app/src/main/proto/user_prefs.proto b/android/app/src/main/proto/user_prefs.proto
new file mode 100644
index 0000000000..3a7e79285f
--- /dev/null
+++ b/android/app/src/main/proto/user_prefs.proto
@@ -0,0 +1,6 @@
+syntax = "proto3";
+
+option java_package = "net.mullvad.mullvadvpn.repository";
+option java_multiple_files = true;
+
+message UserPreferences { bool is_privacy_disclosure_accepted = 1; }