diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bd621ee..5d10d49 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,8 @@ dependencies { ksp(libs.androidx.hilt.compiler) implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.work.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e55916..5f545e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Wallenc" + android:localeConfig="@xml/locales_config" android:enableOnBackInvokedCallback="true" tools:targetApi="37"> = AppLocaleStorage.languageFlow(context) + + override suspend fun setLanguage(language: AppLanguage) { + AppLocaleStorage.persistLanguage(context, language) + } + + override suspend fun applyStoredLocale() { + AppLocaleStorage.applyStored(context) + } +} diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/locale/AppLocaleStorage.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/locale/AppLocaleStorage.kt new file mode 100644 index 0000000..c3070bc --- /dev/null +++ b/app/src/main/java/com/github/nullptroma/wallenc/app/locale/AppLocaleStorage.kt @@ -0,0 +1,108 @@ +package com.github.nullptroma.wallenc.app.locale + +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.github.nullptroma.wallenc.ui.locale.AppLanguage +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking + +/** + * Хранение выбранного языка. SharedPreferences — для синхронного чтения в + * [android.app.Application.attachBaseContext]; DataStore — только для миграции со старой версии. + */ +internal object AppLocaleStorage { + + private const val PREFS_NAME = "app_locale" + private const val PREFS_KEY_LANGUAGE = "app_language" + + private val legacyLanguageKey = stringPreferencesKey(PREFS_KEY_LANGUAGE) + + private val Context.legacyLocaleDataStore: DataStore by preferencesDataStore( + name = "app_locale", + ) + + /** В [android.app.Application.attachBaseContext] [Context.getApplicationContext] ещё null. */ + private fun storageContext(context: Context): Context = + context.applicationContext ?: context + + private fun prefs(context: Context) = + storageContext(context).getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + fun languageFlow(context: Context): Flow = callbackFlow { + val storage = storageContext(context) + val listener = + android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == PREFS_KEY_LANGUAGE) { + trySend(readLanguageSync(storage)) + } + } + trySend(readLanguageSync(storage)) + prefs(storage).registerOnSharedPreferenceChangeListener(listener) + awaitClose { prefs(storage).unregisterOnSharedPreferenceChangeListener(listener) } + } + + fun persistLanguage(context: Context, language: AppLanguage) { + prefs(context).edit().putString(PREFS_KEY_LANGUAGE, language.storageValue).apply() + applyLanguage(language) + } + + fun readLanguageSync(context: Context): AppLanguage { + val raw = prefs(context).getString(PREFS_KEY_LANGUAGE, null) + return raw?.toAppLanguage() ?: AppLanguage.System + } + + /** Без блокировок; безопасно из [android.app.Application.attachBaseContext]. */ + fun applyStored(context: Context) { + applyLanguage(readLanguageSync(context)) + } + + fun applyLanguage(language: AppLanguage) { + val localeList = when (language) { + AppLanguage.System -> LocaleListCompat.getEmptyLocaleList() + AppLanguage.English -> LocaleListCompat.forLanguageTags("en") + AppLanguage.Russian -> LocaleListCompat.forLanguageTags("ru") + } + AppCompatDelegate.setApplicationLocales(localeList) + } + + /** Однократно переносит значение из DataStore (если SP ещё пуст). */ + fun migrateLegacyDataStoreIfNeeded(context: Context) { + val storage = storageContext(context) + if (prefs(storage).contains(PREFS_KEY_LANGUAGE)) { + return + } + runBlocking { + runCatching { + val legacy = storage.legacyLocaleDataStore.data.first()[legacyLanguageKey] + if (legacy != null) { + prefs(storage).edit().putString(PREFS_KEY_LANGUAGE, legacy).apply() + } + } + } + } + + private fun String.toAppLanguage(): AppLanguage = when (this) { + STORAGE_EN -> AppLanguage.English + STORAGE_RU -> AppLanguage.Russian + else -> AppLanguage.System + } + + private val AppLanguage.storageValue: String + get() = when (this) { + AppLanguage.System -> STORAGE_SYSTEM + AppLanguage.English -> STORAGE_EN + AppLanguage.Russian -> STORAGE_RU + } + + private const val STORAGE_SYSTEM = "system" + private const val STORAGE_EN = "en" + private const val STORAGE_RU = "ru" +} diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/tasks/TaskPipelineForegroundService.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/tasks/TaskPipelineForegroundService.kt index 98737f6..4861ed4 100644 --- a/app/src/main/java/com/github/nullptroma/wallenc/app/tasks/TaskPipelineForegroundService.kt +++ b/app/src/main/java/com/github/nullptroma/wallenc/app/tasks/TaskPipelineForegroundService.kt @@ -17,6 +17,8 @@ import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundItem import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState import com.github.nullptroma.wallenc.domain.tasks.TaskId +import com.github.nullptroma.wallenc.ui.resources.UiStringResolver +import com.github.nullptroma.wallenc.ui.resources.resolve import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -43,6 +45,9 @@ class TaskPipelineForegroundService : Service() { @Inject lateinit var orchestrator: ITaskOrchestrator + @Inject + lateinit var uiStrings: UiStringResolver + private var repeat = false private var canPush = true private var lastUiState: TaskForegroundUiState? = null @@ -291,7 +296,7 @@ class TaskPipelineForegroundService : Service() { remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.VISIBLE) remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.VISIBLE) remoteViews.setTextViewText(TASK_TITLE_IDS[index], task.title) - val label = task.progress?.label?.trim().orEmpty() + val label = task.progress?.label?.resolve(uiStrings).orEmpty() val fraction = task.progress?.fraction if (fraction != null) { if (label.isNotEmpty()) { diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..ebb78a4 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,9 @@ + + + Wallenc + Фоновые задачи + Задачи Wallenc + Подготовка… + Выполняется… + Отмена + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86e9e74..0e544d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,9 @@ + Wallenc - Фоновые задачи - Задачи Wallenc - Подготовка… - Выполняется… - Отмена + Background tasks + Wallenc tasks + Preparing… + Running… + Cancel diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 25b0609..1c40d1f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,7 +1,7 @@ -