mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-1328: Add support for changing language with picker (#577)
This commit is contained in:
parent
f95e5cc3cb
commit
0d171e91b9
74 changed files with 520 additions and 50 deletions
|
@ -62,6 +62,11 @@ The following is a list of all third-party dependencies included as part of the
|
|||
- Purpose: Supplementary Android Compose features.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Appcompat**
|
||||
- https://developer.android.com/jetpack/androidx/releases/appcompat
|
||||
- Purpose: Allows access to new APIs on older API versions.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Browser**
|
||||
- https://developer.android.com/jetpack/androidx/releases/browser
|
||||
- Purpose: Displays webpages with the user's default browser.
|
||||
|
|
|
@ -29,6 +29,52 @@ android {
|
|||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
|
||||
// This is so the build system only includes language resources in the APK for these
|
||||
// languages, preventing translated strings from being included from other libraries that
|
||||
// might support languages this app does not.
|
||||
resourceConfigurations += arrayOf(
|
||||
"af",
|
||||
"be",
|
||||
"bg",
|
||||
"ca",
|
||||
"cs",
|
||||
"da",
|
||||
"de",
|
||||
"el",
|
||||
"en",
|
||||
"en-rGB",
|
||||
"es",
|
||||
"et",
|
||||
"fa",
|
||||
"fi",
|
||||
"fr",
|
||||
"hi",
|
||||
"hr",
|
||||
"hu",
|
||||
"in",
|
||||
"it",
|
||||
"iw",
|
||||
"ja",
|
||||
"ko",
|
||||
"lv",
|
||||
"ml",
|
||||
"nb",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt-rBR",
|
||||
"pt-rPT",
|
||||
"ro",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"th",
|
||||
"tr",
|
||||
"uk",
|
||||
"vi",
|
||||
"zh-rCN",
|
||||
"zh-rTW"
|
||||
)
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
@ -102,6 +148,7 @@ dependencies {
|
|||
}
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/LaunchTheme"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
@ -52,6 +53,15 @@
|
|||
<action android:name="android.service.autofill.AutofillService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -2,26 +2,40 @@ package com.x8bit.bitwarden
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Primary entry point for the application.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
// Within the app the language will change dynamically and will be managed
|
||||
// by the OS, but we need to ensure we properly set the language when
|
||||
// upgrading from older versions that handle this differently.
|
||||
settingsRepository.appLanguage.localeName?.let { localeName ->
|
||||
val localeList = LocaleListCompat.forLanguageTags(localeName)
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
setContent {
|
||||
BitwardenTheme {
|
||||
RootNavScreen(
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Primary access point for general settings-related disk information.
|
||||
*/
|
||||
interface SettingsDiskSource {
|
||||
/**
|
||||
* The currently persisted app language (or `null` if not set).
|
||||
*/
|
||||
var appLanguage: AppLanguage?
|
||||
|
||||
/**
|
||||
* Gets the current vault timeout (in minutes) for the given [userId] (or `null` if the vault
|
||||
* should never time out).
|
||||
|
|
|
@ -4,16 +4,19 @@ import android.content.SharedPreferences
|
|||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
|
||||
private const val APP_LANGUAGE_KEY = "$BASE_KEY:appLocale"
|
||||
private const val VAULT_TIMEOUT_ACTION_KEY = "$BASE_KEY:vaultTimeoutAction"
|
||||
private const val VAULT_TIME_IN_MINUTES_KEY = "$BASE_KEY:vaultTimeout"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class SettingsDiskSourceImpl(
|
||||
val sharedPreferences: SharedPreferences,
|
||||
) : BaseDiskSource(sharedPreferences = sharedPreferences),
|
||||
|
@ -24,6 +27,18 @@ class SettingsDiskSourceImpl(
|
|||
private val mutableVaultTimeoutInMinutesFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Int?>>()
|
||||
|
||||
override var appLanguage: AppLanguage?
|
||||
get() = getString(key = APP_LANGUAGE_KEY)
|
||||
?.let { storedValue ->
|
||||
AppLanguage.entries.firstOrNull { storedValue == it.localeName }
|
||||
}
|
||||
set(value) {
|
||||
putString(
|
||||
key = APP_LANGUAGE_KEY,
|
||||
value = value?.localeName,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getVaultTimeoutInMinutes(userId: String): Int? =
|
||||
getInt(key = "${VAULT_TIME_IN_MINUTES_KEY}_$userId")
|
||||
|
||||
|
|
|
@ -2,12 +2,18 @@ package com.x8bit.bitwarden.data.platform.repository
|
|||
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Provides an API for observing and modifying settings state.
|
||||
*/
|
||||
interface SettingsRepository {
|
||||
/**
|
||||
* The [AppLanguage] for the current user.
|
||||
*/
|
||||
var appLanguage: AppLanguage
|
||||
|
||||
/**
|
||||
* The [VaultTimeout] for the current user.
|
||||
*/
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
|||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -23,6 +24,12 @@ class SettingsRepositoryImpl(
|
|||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
override var appLanguage: AppLanguage
|
||||
get() = settingsDiskSource.appLanguage ?: AppLanguage.DEFAULT
|
||||
set(value) {
|
||||
settingsDiskSource.appLanguage = value
|
||||
}
|
||||
|
||||
override var vaultTimeout: VaultTimeout
|
||||
get() = activeUserId
|
||||
?.let {
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
@ -25,12 +26,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
|
||||
/**
|
||||
* Displays the appearance screen.
|
||||
|
@ -74,7 +79,7 @@ fun AppearanceScreen(
|
|||
) {
|
||||
LanguageSelectionRow(
|
||||
currentSelection = state.language,
|
||||
onThemeSelection = remember(viewModel) {
|
||||
onLanguageSelection = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AppearanceAction.LanguageChange(it)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
@ -105,15 +110,22 @@ fun AppearanceScreen(
|
|||
|
||||
@Composable
|
||||
private fun LanguageSelectionRow(
|
||||
currentSelection: AppearanceState.Language,
|
||||
onThemeSelection: (AppearanceState.Language) -> Unit,
|
||||
currentSelection: AppLanguage,
|
||||
onLanguageSelection: (AppLanguage) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var shouldShowLanguageSelectionDialog by remember { mutableStateOf(false) }
|
||||
var languageChangedDialogState: BasicDialogState by rememberSaveable {
|
||||
mutableStateOf(BasicDialogState.Hidden)
|
||||
}
|
||||
var shouldShowLanguageSelectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = languageChangedDialogState,
|
||||
onDismissRequest = { languageChangedDialogState = BasicDialogState.Hidden },
|
||||
)
|
||||
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = R.string.language),
|
||||
description = stringResource(id = R.string.language_change_requires_app_restart),
|
||||
onClick = { shouldShowLanguageSelectionDialog = true },
|
||||
modifier = modifier,
|
||||
) {
|
||||
|
@ -129,14 +141,16 @@ private fun LanguageSelectionRow(
|
|||
title = stringResource(id = R.string.language),
|
||||
onDismissRequest = { shouldShowLanguageSelectionDialog = false },
|
||||
) {
|
||||
AppearanceState.Language.entries.forEach { option ->
|
||||
AppLanguage.entries.forEach { option ->
|
||||
BitwardenSelectionRow(
|
||||
text = option.text,
|
||||
isSelected = option == currentSelection,
|
||||
onClick = {
|
||||
shouldShowLanguageSelectionDialog = false
|
||||
onThemeSelection(
|
||||
AppearanceState.Language.entries.first { it == option },
|
||||
onLanguageSelection(option)
|
||||
languageChangedDialogState = BasicDialogState.Shown(
|
||||
title = R.string.language.asText(),
|
||||
message = R.string.language_change_x_description.asText(option.text),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
@ -18,11 +22,12 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class AppearanceViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<AppearanceState, AppearanceEvent, AppearanceAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: AppearanceState(
|
||||
language = AppearanceState.Language.DEFAULT,
|
||||
language = settingsRepository.appLanguage,
|
||||
showWebsiteIcons = false,
|
||||
theme = AppearanceState.Theme.DEFAULT,
|
||||
),
|
||||
|
@ -39,8 +44,13 @@ class AppearanceViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLanguageChanged(action: AppearanceAction.LanguageChange) {
|
||||
// TODO: BIT-1328 implement language selection support
|
||||
mutableStateFlow.update { it.copy(language = action.language) }
|
||||
settingsRepository.appLanguage = action.language
|
||||
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(
|
||||
action.language.localeName,
|
||||
)
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
|
||||
private fun handleShowWebsiteIconsToggled(action: AppearanceAction.ShowWebsiteIconsToggle) {
|
||||
|
@ -59,20 +69,10 @@ class AppearanceViewModel @Inject constructor(
|
|||
*/
|
||||
@Parcelize
|
||||
data class AppearanceState(
|
||||
val language: Language,
|
||||
val language: AppLanguage,
|
||||
val showWebsiteIcons: Boolean,
|
||||
val theme: Theme,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the languages supported by the app.
|
||||
*
|
||||
* TODO BIT-1328 populate values
|
||||
*/
|
||||
enum class Language(val text: Text) {
|
||||
DEFAULT(text = R.string.default_system.asText()),
|
||||
ENGLISH(text = "English".asText()),
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the theme options the user can set.
|
||||
*/
|
||||
|
@ -106,7 +106,7 @@ sealed class AppearanceAction {
|
|||
* Indicates that the user changed the Language.
|
||||
*/
|
||||
data class LanguageChange(
|
||||
val language: AppearanceState.Language,
|
||||
val language: AppLanguage,
|
||||
) : AppearanceAction()
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model
|
||||
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
||||
/**
|
||||
* Represents the languages supported by the app.
|
||||
*/
|
||||
enum class AppLanguage(
|
||||
val localeName: String?,
|
||||
val text: Text,
|
||||
) {
|
||||
DEFAULT(
|
||||
localeName = null,
|
||||
text = R.string.default_system.asText(),
|
||||
),
|
||||
AFRIKAANS(
|
||||
localeName = "af",
|
||||
text = "Afrikaans".asText(),
|
||||
),
|
||||
BELARUSIAN(
|
||||
localeName = "be",
|
||||
text = "Беларуская".asText(),
|
||||
),
|
||||
BULGARIAN(
|
||||
localeName = "bg",
|
||||
text = "български".asText(),
|
||||
),
|
||||
CATALAN(
|
||||
localeName = "ca",
|
||||
text = "català".asText(),
|
||||
),
|
||||
CZECH(
|
||||
localeName = "cs",
|
||||
text = "čeština".asText(),
|
||||
),
|
||||
DANISH(
|
||||
localeName = "da",
|
||||
text = "Dansk".asText(),
|
||||
),
|
||||
GERMAN(
|
||||
localeName = "de",
|
||||
text = "Deutsch".asText(),
|
||||
),
|
||||
GREEK(
|
||||
localeName = "el",
|
||||
text = "Ελληνικά".asText(),
|
||||
),
|
||||
ENGLISH(
|
||||
localeName = "en",
|
||||
text = "English".asText(),
|
||||
),
|
||||
ENGLISH_BRITISH(
|
||||
localeName = "en-GB",
|
||||
text = "English (British)".asText(),
|
||||
),
|
||||
SPANISH(
|
||||
localeName = "es",
|
||||
text = "Español".asText(),
|
||||
),
|
||||
ESTONIAN(
|
||||
localeName = "et",
|
||||
text = "eesti".asText(),
|
||||
),
|
||||
PERSIAN(
|
||||
localeName = "fa",
|
||||
text = "فارسی".asText(),
|
||||
),
|
||||
FINNISH(
|
||||
localeName = "fi",
|
||||
text = "suomi".asText(),
|
||||
),
|
||||
FRENCH(
|
||||
localeName = "fr",
|
||||
text = "Français".asText(),
|
||||
),
|
||||
HINDI(
|
||||
localeName = "hi",
|
||||
text = "हिन्दी".asText(),
|
||||
),
|
||||
CROATIAN(
|
||||
localeName = "hr",
|
||||
text = "hrvatski".asText(),
|
||||
),
|
||||
HUNGARIAN(
|
||||
localeName = "hu",
|
||||
text = "magyar".asText(),
|
||||
),
|
||||
INDONESIAN(
|
||||
localeName = "in",
|
||||
text = "Bahasa Indonesia".asText(),
|
||||
),
|
||||
ITALIAN(
|
||||
localeName = "it",
|
||||
text = "Italiano".asText(),
|
||||
),
|
||||
HEBREW(
|
||||
localeName = "iw",
|
||||
text = "עברית".asText(),
|
||||
),
|
||||
JAPANESE(
|
||||
localeName = "ja",
|
||||
text = "日本語".asText(),
|
||||
),
|
||||
KOREAN(
|
||||
localeName = "ko",
|
||||
text = "한국어".asText(),
|
||||
),
|
||||
LATVIAN(
|
||||
localeName = "lv",
|
||||
text = "Latvietis".asText(),
|
||||
),
|
||||
MALAYALAM(
|
||||
localeName = "ml",
|
||||
text = "മലയാളം".asText(),
|
||||
),
|
||||
NORWEGIAN(
|
||||
localeName = "nb",
|
||||
text = "norsk (bokmål)".asText(),
|
||||
),
|
||||
DUTCH(
|
||||
localeName = "nl",
|
||||
text = "Nederlands".asText(),
|
||||
),
|
||||
POLISH(
|
||||
localeName = "pl",
|
||||
text = "Polski".asText(),
|
||||
),
|
||||
PORTUGUESE_BRAZILIAN(
|
||||
localeName = "pt-BR",
|
||||
text = "Português do Brasil".asText(),
|
||||
),
|
||||
PORTUGUESE(
|
||||
localeName = "pt-PT",
|
||||
text = "Português".asText(),
|
||||
),
|
||||
ROMANIAN(
|
||||
localeName = "ro",
|
||||
text = "română".asText(),
|
||||
),
|
||||
RUSSIAN(
|
||||
localeName = "ru",
|
||||
text = "русский".asText(),
|
||||
),
|
||||
SLOVAK(
|
||||
localeName = "sk",
|
||||
text = "slovenčina".asText(),
|
||||
),
|
||||
SWEDISH(
|
||||
localeName = "sv",
|
||||
text = "svenska".asText(),
|
||||
),
|
||||
THAI(
|
||||
localeName = "th",
|
||||
text = "ไทย".asText(),
|
||||
),
|
||||
TURKISH(
|
||||
localeName = "tr",
|
||||
text = "Türkçe".asText(),
|
||||
),
|
||||
UKRAINIAN(
|
||||
localeName = "uk",
|
||||
text = "українська".asText(),
|
||||
),
|
||||
VIETNAMESE(
|
||||
localeName = "vi",
|
||||
text = "Tiếng Việt".asText(),
|
||||
),
|
||||
CHINESE_SIMPLIFIED(
|
||||
localeName = "zh-CN",
|
||||
text = "中文(中国大陆)".asText(),
|
||||
),
|
||||
CHINESE_TRADITIONAL(
|
||||
localeName = "zh-TW",
|
||||
text = "中文(台灣)".asText(),
|
||||
),
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="BaseTheme" parent="android:Theme.Material.Light.DarkActionBar">
|
||||
<style name="BaseTheme" parent="Theme.Design.Light.NoActionBar">
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:textCursorDrawable">@null</item>
|
||||
|
|
43
app/src/main/res/xml/locales_config.xml
Normal file
43
app/src/main/res/xml/locales_config.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="af"/> <!-- Afrikaans -->
|
||||
<locale android:name="be"/> <!-- Belarusian -->
|
||||
<locale android:name="bg"/> <!-- Bulgarian -->
|
||||
<locale android:name="ca"/> <!-- Catalan -->
|
||||
<locale android:name="cs"/> <!-- Czech -->
|
||||
<locale android:name="da"/> <!-- Danish -->
|
||||
<locale android:name="de"/> <!-- German -->
|
||||
<locale android:name="el"/> <!-- Greek -->
|
||||
<locale android:name="en"/> <!-- English (US) -->
|
||||
<locale android:name="en-GB"/> <!-- English (Great Britain) -->
|
||||
<locale android:name="es"/> <!-- Spanish -->
|
||||
<locale android:name="et"/> <!-- Estonian -->
|
||||
<locale android:name="fa"/> <!-- Persian -->
|
||||
<locale android:name="fi"/> <!-- Finnish -->
|
||||
<locale android:name="fr"/> <!-- French -->
|
||||
<locale android:name="hi"/> <!-- Hindi -->
|
||||
<locale android:name="hr"/> <!-- Croatian -->
|
||||
<locale android:name="hu"/> <!-- Hungarian -->
|
||||
<locale android:name="in"/> <!-- Indonesian -->
|
||||
<locale android:name="it"/> <!-- Italian -->
|
||||
<locale android:name="iw"/> <!-- Hebrew -->
|
||||
<locale android:name="ja"/> <!-- Japanese -->
|
||||
<locale android:name="ko"/> <!-- Korean -->
|
||||
<locale android:name="lv"/> <!-- Latvian -->
|
||||
<locale android:name="ml"/> <!-- Malayalam -->
|
||||
<locale android:name="nb"/> <!-- Norwegian -->
|
||||
<locale android:name="nl"/> <!-- Dutch -->
|
||||
<locale android:name="pl"/> <!-- Polish -->
|
||||
<locale android:name="pt-BR"/> <!-- Portuguese (Brazilian) -->
|
||||
<locale android:name="pt-PT"/> <!-- Portuguese (Portugal) -->
|
||||
<locale android:name="ro"/> <!-- Romanian -->
|
||||
<locale android:name="ru"/> <!-- Russian -->
|
||||
<locale android:name="sk"/> <!-- Slovak -->
|
||||
<locale android:name="sv"/> <!-- Swedish -->
|
||||
<locale android:name="th"/> <!-- Thai -->
|
||||
<locale android:name="tr"/> <!-- Turkish -->
|
||||
<locale android:name="uk"/> <!-- Ukrainian -->
|
||||
<locale android:name="vi"/> <!-- Vietnamese -->
|
||||
<locale android:name="zh-CN"/> <!-- Chinese (Simplified) -->
|
||||
<locale android:name="zh-TW"/> <!-- Chinese (Traditional) -->
|
||||
</locale-config>
|
|
@ -4,6 +4,7 @@ import androidx.core.content.edit
|
|||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
|
@ -18,6 +19,45 @@ class SettingsDiskSourceTest {
|
|||
sharedPreferences = fakeSharedPreferences,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `appLanguage should pull from SharedPreferences`() {
|
||||
val appLanguageKey = "bwPreferencesStorage:appLocale"
|
||||
val expected = AppLanguage.AFRIKAANS
|
||||
|
||||
// Verify initial value is null and disk source matches shared preferences.
|
||||
assertNull(fakeSharedPreferences.getString(appLanguageKey, null))
|
||||
assertNull(settingsDiskSource.appLanguage)
|
||||
|
||||
// Updating the shared preferences should update disk source.
|
||||
fakeSharedPreferences
|
||||
.edit()
|
||||
.putString(
|
||||
appLanguageKey,
|
||||
expected.localeName,
|
||||
)
|
||||
.apply()
|
||||
val actual = settingsDiskSource.appLanguage
|
||||
assertEquals(
|
||||
expected,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setting appLanguage should update SharedPreferences`() {
|
||||
val appLanguageKey = "bwPreferencesStorage:appLocale"
|
||||
val appLanguage = AppLanguage.ENGLISH
|
||||
settingsDiskSource.appLanguage = appLanguage
|
||||
val actual = fakeSharedPreferences.getString(
|
||||
appLanguageKey,
|
||||
AppLanguage.DEFAULT.localeName,
|
||||
)
|
||||
assertEquals(
|
||||
appLanguage.localeName,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVaultTimeoutInMinutes when values are present should pull from SharedPreferences`() {
|
||||
val vaultTimeoutBaseKey = "bwPreferencesStorage:vaultTimeout"
|
||||
|
@ -44,7 +84,8 @@ class SettingsDiskSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getVaultTimeoutInMinutesFlow should react to changes in getOrganizations`() = runTest {
|
||||
fun `getVaultTimeoutInMinutesFlow should react to changes in getVaultTimeoutInMinutes`() =
|
||||
runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
val vaultTimeoutInMinutes = 360
|
||||
settingsDiskSource.getVaultTimeoutInMinutesFlow(userId = mockUserId).test {
|
||||
|
@ -123,7 +164,7 @@ class SettingsDiskSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getVaultTimeoutActionFlow should react to changes in getOrganizations`() = runTest {
|
||||
fun `getVaultTimeoutActionFlow should react to changes in getVaultTimeoutAction`() = runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
val vaultTimeoutAction = VaultTimeoutAction.LOCK
|
||||
settingsDiskSource.getVaultTimeoutActionFlow(userId = mockUserId).test {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.disk.util
|
|||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
|
@ -21,6 +22,8 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
private val storedVaultTimeoutActions = mutableMapOf<String, VaultTimeoutAction?>()
|
||||
private val storedVaultTimeoutInMinutes = mutableMapOf<String, Int?>()
|
||||
|
||||
override var appLanguage: AppLanguage? = null
|
||||
|
||||
override fun getVaultTimeoutInMinutes(userId: String): Int? =
|
||||
storedVaultTimeoutInMinutes[userId]
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
|||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
@ -87,6 +88,28 @@ class SettingsRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `appLanguage should pull from and update SettingsDiskSource`() {
|
||||
assertEquals(
|
||||
AppLanguage.DEFAULT,
|
||||
settingsRepository.appLanguage,
|
||||
)
|
||||
|
||||
// Updates to the disk source change the repository value.
|
||||
fakeSettingsDiskSource.appLanguage = AppLanguage.ENGLISH
|
||||
assertEquals(
|
||||
AppLanguage.ENGLISH,
|
||||
settingsRepository.appLanguage,
|
||||
)
|
||||
|
||||
// Updates to the repository value change the disk source.
|
||||
settingsRepository.appLanguage = AppLanguage.DUTCH
|
||||
assertEquals(
|
||||
AppLanguage.DUTCH,
|
||||
fakeSettingsDiskSource.appLanguage,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultTimeout should pull from and update SettingsDiskSource for the current user`() {
|
||||
every { authDiskSource.userState?.activeUserId } returns null
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
|
@ -10,6 +11,7 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -55,10 +57,26 @@ class AppearanceScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on language selection dialog item click should send LanguageChange`() {
|
||||
fun `on language selection dialog item click should send LanguageChange and show dialog`() {
|
||||
// Clicking the Language row shows the language selection dialog
|
||||
composeTestRule.onNodeWithText("Language").performClick()
|
||||
// Selecting a language dismisses this dialog and displays the confirmation
|
||||
composeTestRule
|
||||
.onAllNodesWithText("English")
|
||||
.onAllNodesWithText("Afrikaans")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Afrikaans")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
// Should show confirmation dialog
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
// Clicking "Ok" should dismiss confirmation dialog
|
||||
composeTestRule.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
@ -66,7 +84,7 @@ class AppearanceScreenTest : BaseComposeTest() {
|
|||
verify {
|
||||
viewModel.trySendAction(
|
||||
AppearanceAction.LanguageChange(
|
||||
language = AppearanceState.Language.ENGLISH,
|
||||
language = AppLanguage.AFRIKAANS,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -133,7 +151,7 @@ class AppearanceScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
private val DEFAULT_STATE = AppearanceState(
|
||||
language = AppearanceState.Language.DEFAULT,
|
||||
language = AppLanguage.DEFAULT,
|
||||
showWebsiteIcons = false,
|
||||
theme = AppearanceState.Theme.DEFAULT,
|
||||
)
|
||||
|
|
|
@ -1,13 +1,40 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AppearanceViewModelTest : BaseViewModelTest() {
|
||||
private val mockSettingsRepository = mockk<SettingsRepository> {
|
||||
every { appLanguage } returns AppLanguage.DEFAULT
|
||||
every { appLanguage = AppLanguage.ENGLISH } just runs
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(AppCompatDelegate::setApplicationLocales)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(AppCompatDelegate::setApplicationLocales)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when not set`() {
|
||||
val viewModel = createViewModel(state = null)
|
||||
|
@ -31,21 +58,30 @@ class AppearanceViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on LanguageChange should update state`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
fun `on LanguageChange should update state and store language`() = runTest {
|
||||
val viewModel = createViewModel(
|
||||
settingsRepository = mockSettingsRepository,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(
|
||||
AppearanceAction.LanguageChange(AppearanceState.Language.ENGLISH),
|
||||
AppearanceAction.LanguageChange(AppLanguage.ENGLISH),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(language = AppearanceState.Language.ENGLISH),
|
||||
DEFAULT_STATE.copy(
|
||||
language = AppLanguage.ENGLISH,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify {
|
||||
AppCompatDelegate.setApplicationLocales(any())
|
||||
mockSettingsRepository.appLanguage
|
||||
mockSettingsRepository.appLanguage = AppLanguage.ENGLISH
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -82,15 +118,17 @@ class AppearanceViewModelTest : BaseViewModelTest() {
|
|||
|
||||
private fun createViewModel(
|
||||
state: AppearanceState? = null,
|
||||
settingsRepository: SettingsRepository = mockSettingsRepository,
|
||||
) = AppearanceViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state)
|
||||
},
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = AppearanceState(
|
||||
language = AppearanceState.Language.DEFAULT,
|
||||
language = AppLanguage.DEFAULT,
|
||||
showWebsiteIcons = false,
|
||||
theme = AppearanceState.Theme.DEFAULT,
|
||||
)
|
||||
|
|
|
@ -23,6 +23,7 @@ androidxNavigation = "2.7.6"
|
|||
androidxRoom = "2.6.1"
|
||||
androidXSecurityCrypto = "1.1.0-alpha06"
|
||||
androidxSplash = "1.1.0-alpha02"
|
||||
androidXAppCompat = "1.6.1"
|
||||
# Once the app and SDK reach a critical point of completeness we should begin fixing the version
|
||||
# here (BIT-311).
|
||||
bitwardenSdk = "0.4.0-20240111.141006-33"
|
||||
|
@ -53,6 +54,7 @@ zxing = "3.5.2"
|
|||
[libraries]
|
||||
# Format: <maintainer>-<artifact-name>
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidXAppCompat" }
|
||||
androidx-browser = { module = "androidx.browser:browser", version.ref = "androidxBrowser" }
|
||||
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidxCamera" }
|
||||
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "androidxCamera" }
|
||||
|
|
Loading…
Reference in a new issue