diff options
| author | David Göransson <david.goransson@mullvad.net> | 2024-12-19 13:49:20 +0100 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2024-12-19 13:49:20 +0100 |
| commit | ccd7525a21676967574e0469ece35ec236bed200 (patch) | |
| tree | 632677c4c72b389aa543c1e44909a2e961c29c93 /android/app/src | |
| parent | 83b08bf485a753f63d8dbe6101740f42efda6bc4 (diff) | |
| parent | 3c259a8001ce5725e4953c7901628a21839afd93 (diff) | |
| download | mullvadvpn-ccd7525a21676967574e0469ece35ec236bed200.tar.xz mullvadvpn-ccd7525a21676967574e0469ece35ec236bed200.zip | |
Merge branch 'migrate-from-sharedpreferences-to-datastore-droid-1688'
Diffstat (limited to 'android/app/src')
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; } |
