diff --git a/app/src/main/java/helium314/keyboard/settings/Preference.kt b/app/src/main/java/helium314/keyboard/settings/Preference.kt deleted file mode 100644 index 1ff179bb..00000000 --- a/app/src/main/java/helium314/keyboard/settings/Preference.kt +++ /dev/null @@ -1,392 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -package helium314.keyboard.settings - -import android.content.SharedPreferences -import androidx.annotation.DrawableRes -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.Hyphens -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.core.content.edit -import helium314.keyboard.keyboard.internal.KeyboardIconsSet -import helium314.keyboard.latin.R -import helium314.keyboard.latin.utils.Log -import helium314.keyboard.latin.utils.getActivity -import helium314.keyboard.latin.utils.getStringResourceOrName -import helium314.keyboard.latin.utils.prefs -import helium314.keyboard.settings.dialogs.ListPickerDialog -import helium314.keyboard.settings.dialogs.ReorderDialog -import helium314.keyboard.settings.dialogs.SliderDialog -import helium314.keyboard.settings.screens.GetIcon -import kotlin.math.roundToInt - -// partially taken from StreetComplete / SCEE - -@Composable -fun PreferenceCategory( - title: String, - modifier: Modifier = Modifier, -) { - Column { - HorizontalDivider() - Text( - text = title, - modifier = modifier.padding(top = 12.dp, start = 16.dp, end = 8.dp, bottom = 8.dp), - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.titleSmall - ) - } -} - -@Composable -fun Preference( - name: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - description: String? = null, - @DrawableRes icon: Int? = null, - value: @Composable (RowScope.() -> Unit)? = null, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable { onClick() } - .heightIn(min = 44.dp) - .padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - if (icon != null) - Icon(painterResource(icon), name, modifier = Modifier.size(36.dp)) - Column(modifier = Modifier.weight(1f)) { - Text(text = name, style = MaterialTheme.typography.bodyLarge) - if (description != null) { - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodyMedium, - LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant - ) { - Text( - text = description, - modifier = Modifier.padding(top = 2.dp) - ) - } - } - } - if (value != null) { - CompositionLocalProvider( - LocalTextStyle provides LocalTextStyle.current.copy( - textAlign = TextAlign.End, - hyphens = Hyphens.Auto - ), - LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant - ) { - Row( - horizontalArrangement = Arrangement.spacedBy( - space = 8.dp, - alignment = Alignment.End - ), - verticalAlignment = Alignment.CenterVertically, - ) { value() } - } - } - } -} - -@Composable -fun SwitchPreference( - name: String, - modifier: Modifier = Modifier, - pref: String, - default: Boolean, - description: String? = null, - allowCheckedChange: (Boolean) -> Boolean = { true }, // true means ok, usually for showing some dialog - onCheckedChange: (Boolean) -> Unit = { }, -) { - val ctx = LocalContext.current - val prefs = ctx.prefs() - val b = (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.collectAsState() - if ((b?.value ?: 0) < 0) - Log.v("irrelevant", "stupid way to trigger recomposition on preference change") - var value = prefs.getBoolean(pref, default) - fun switched(newValue: Boolean) { - if (!allowCheckedChange(newValue)) { - value = !newValue - return - } - value = newValue - prefs.edit().putBoolean(pref, newValue).apply() - onCheckedChange(newValue) - } - Preference( - name = name, - onClick = { switched(!value) }, - modifier = modifier, - description = description - ) { - Switch( - checked = value, - onCheckedChange = { switched(it) }, - // switch is really ugly... how -// colors = SwitchDefaults.colors(uncheckedBorderColor = Color.Transparent) - ) - } -} - -@Composable -fun SwitchPreference( - def: PrefDef, - default: Boolean, - allowCheckedChange: (Boolean) -> Boolean = { true }, - onCheckedChange: (Boolean) -> Unit = { } -) { - SwitchPreference( - name = def.title, - description = def.description, - pref = def.key, - default = default, - allowCheckedChange = allowCheckedChange, - onCheckedChange = onCheckedChange - ) -} - -@Composable -/** Slider preference for Int or Float (weird casting stuff, but should be fine) */ -fun SliderPreference( - name: String, - modifier: Modifier = Modifier, - pref: String, - description: @Composable (T) -> String, - default: T, - range: ClosedFloatingPointRange, - stepSize: Int? = null, - onValueChanged: (Float) -> Unit = { }, -) { - val ctx = LocalContext.current - val prefs = ctx.prefs() - val b = (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.collectAsState() - if ((b?.value ?: 0) < 0) - Log.v("irrelevant", "stupid way to trigger recomposition on preference change") - val initialValue = if (default is Int || default is Float) - getPrefOfType(prefs, pref, default) - else throw IllegalArgumentException("only float and int are supported") - - var showDialog by remember { mutableStateOf(false) } - Preference( - name = name, - onClick = { showDialog = true }, - modifier = modifier, - description = description(initialValue) - ) - if (showDialog) - SliderDialog( - onDismissRequest = { showDialog = false }, - onDone = { - if (default is Int) prefs.edit().putInt(pref, it.toInt()).apply() - else prefs.edit().putFloat(pref, it).apply() - }, - initialValue = initialValue.toFloat(), - range = range, - positionString = { - @Suppress("UNCHECKED_CAST") - description((if (default is Int) it.roundToInt() else it) as T) - }, - onValueChanged = onValueChanged, - showDefault = true, - onDefault = { prefs.edit().remove(pref).apply() }, - intermediateSteps = stepSize?.let { - // this is not nice, but slider wants it like this... - ((range.endInclusive - range.start) / it - 1).toInt() - } - ) -} - -@Composable -fun ListPreference( - def: PrefDef, - items: List>, - default: T, - onChanged: (T) -> Unit = { } -) { - var showDialog by remember { mutableStateOf(false) } - val prefs = LocalContext.current.prefs() - val selected = items.firstOrNull { it.second == getPrefOfType(prefs, def.key, default) } - Preference( - name = def.title, - description = selected?.first, - onClick = { showDialog = true } - ) - if (showDialog) { - ListPickerDialog( - onDismissRequest = { showDialog = false }, - items = items, - onItemSelected = { - if (it == selected) return@ListPickerDialog - putPrefOfType(prefs, def.key, it.second) - onChanged(it.second) - }, - selectedItem = selected, - title = { Text(def.title) }, - getItemName = { it.first } - ) - } -} - -@Composable -fun ReorderSwitchPreference(def: PrefDef, default: String) { - var showDialog by remember { mutableStateOf(false) } - Preference( - name = def.title, - description = def.description, - onClick = { showDialog = true }, - ) - if (showDialog) { - val ctx = LocalContext.current - val prefs = ctx.prefs() - val items = prefs.getString(def.key, default)!!.split(";").mapTo(ArrayList()) { - val both = it.split(",") - KeyAndState(both.first(), both.last().toBoolean()) - } - ReorderDialog( - onConfirmed = { reorderedItems -> - val value = reorderedItems.joinToString(";") { it.name + "," + it.state } - prefs.edit().putString(def.key, value).apply() - keyboardNeedsReload = true - }, - onDismissRequest = { showDialog = false }, - onNeutral = { prefs.edit().remove(def.key).apply() }, - neutralButtonText = if (prefs.contains(def.key)) stringResource(R.string.button_default) else null, - items = items, - title = { Text(def.title) }, - displayItem = { item -> - var checked by remember { mutableStateOf(item.state) } - Row(verticalAlignment = Alignment.CenterVertically) { - KeyboardIconsSet.instance.GetIcon(item.name) - val text = item.name.lowercase().getStringResourceOrName("", ctx) - Text(text, Modifier.weight(1f)) - Switch( - checked = checked, - onCheckedChange = { item.state = it; checked = it } - ) - } - }, - getKey = { it.name } - ) - } -} - -private class KeyAndState(var name: String, var state: Boolean) - -@Suppress("UNCHECKED_CAST") -private fun getPrefOfType(prefs: SharedPreferences, key: String, default: T): T = - when (default) { - is String -> prefs.getString(key, default) - is Int -> prefs.getInt(key, default) - is Long -> prefs.getLong(key, default) - is Float -> prefs.getFloat(key, default) - is Boolean -> prefs.getBoolean(key, default) - else -> throw IllegalArgumentException("unknown type ${default.javaClass}") - } as T - -private fun putPrefOfType(prefs: SharedPreferences, key: String, value: T) = - prefs.edit { - when (value) { - is String -> putString(key, value) - is Int -> putInt(key, value) - is Long -> putLong(key, value) - is Float -> putFloat(key, value) - is Boolean -> putBoolean(key, value) - else -> throw IllegalArgumentException("unknown type ${value.javaClass}") - } - } - -@Preview -@Composable -private fun PreferencePreview() { - Surface { - Column { - PreferenceCategory("Preference Category") - Preference( - name = "Preference", - onClick = {}, - ) - Preference( - name = "Preference with icon", - onClick = {}, - icon = R.drawable.ic_settings_about_foreground - ) - SliderPreference( - name = "SliderPreference", - pref = "", - default = 1, - description = { it.toString() }, - range = -5f..5f - ) - Preference( - name = "Preference with icon and description", - description = "some text", - onClick = {}, - icon = R.drawable.ic_settings_about_foreground - ) - Preference( - name = "Preference with switch", - onClick = {} - ) { - Switch(checked = true, onCheckedChange = {}) - } - SwitchPreference( - name = "SwitchPreference", - pref = "none", - default = true - ) - Preference( - name = "Preference", - onClick = {}, - description = "A long description which may actually be several lines long, so it should wrap." - ) { - Icon(painterResource(R.drawable.ic_arrow_left), null) - } - Preference( - name = "Long preference name that wraps", - onClick = {}, - ) { - Text("Long preference value") - } - Preference( - name = "Long preference name 2", - onClick = {}, - description = "hello I am description" - ) { - Text("Long preference value") - } - } - } -} diff --git a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt index 78a384da..6b096ae5 100644 --- a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import helium314.keyboard.latin.R +import helium314.keyboard.settings.preferences.PreferenceCategory @Composable fun SearchPrefScreen( diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt index eabe8c0e..4d3b8e87 100644 --- a/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt +++ b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt @@ -14,7 +14,6 @@ import helium314.keyboard.latin.utils.prefs import kotlinx.coroutines.flow.MutableStateFlow // todo (roughly in order) -// maybe move some large prefs out of their screens into separate files (backup/restore!) // use better / more structured and clear names and arrangement of files // the prefDef and AllPrefs, also be clear about pref <-> key <-> prefKey (all used, probably should be latter) // there is a lot more ambiguous naming... diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt new file mode 100644 index 00000000..e26091c5 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt @@ -0,0 +1,126 @@ +package helium314.keyboard.settings.preferences + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import helium314.keyboard.latin.R +import helium314.keyboard.latin.common.FileUtils +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.Log +import helium314.keyboard.latin.utils.getActivity +import helium314.keyboard.latin.utils.prefs +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.SettingsActivity2 +import helium314.keyboard.settings.dialogs.ConfirmationDialog +import helium314.keyboard.settings.dialogs.InfoDialog +import helium314.keyboard.settings.keyboardNeedsReload +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun BackgroundImagePref(def: PrefDef, isLandscape: Boolean) { + var showDayNightDialog by remember { mutableStateOf(false) } + var showSelectionDialog by remember { mutableStateOf(false) } + var showErrorDialog by remember { mutableStateOf(false) } + var isNight by remember { mutableStateOf(false) } + val ctx = LocalContext.current + fun getFile() = Settings.getCustomBackgroundFile(ctx, isNight, isLandscape) + val b = (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.collectAsState() + if ((b?.value ?: 0) < 0) // necessary to reload dayNightPref + Log.v("irrelevant", "stupid way to trigger recomposition on preference change") + val dayNightPref = Settings.readDayNightPref(ctx.prefs(), ctx.resources) + if (!dayNightPref) + isNight = false + val scope = rememberCoroutineScope() + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult + val uri = result.data?.data ?: return@rememberLauncherForActivityResult + showSelectionDialog = false + showDayNightDialog = false + scope.launch(Dispatchers.IO) { + if (!setBackgroundImage(ctx, uri, isNight, isLandscape)) + showErrorDialog = true + } + } + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("image/*") + Preference( + name = def.title, + onClick = { + if (dayNightPref) { + showDayNightDialog = true + } else if (!getFile().exists()) { + launcher.launch(intent) + } else { + showSelectionDialog = true + } + } + ) + if (showDayNightDialog) { + ConfirmationDialog( + onDismissRequest = { showDayNightDialog = false }, + onConfirmed = { + isNight = false + if (getFile().exists()) + showSelectionDialog = true + else launcher.launch(intent) + }, + confirmButtonText = stringResource(R.string.day_or_night_day), + cancelButtonText = "", + onNeutral = { + isNight = true + if (getFile().exists()) + showSelectionDialog = true + else launcher.launch(intent) + }, + neutralButtonText = stringResource(R.string.day_or_night_night), + title = { Text(stringResource(R.string.day_or_night_image)) }, + ) + } + if (showSelectionDialog) { + ConfirmationDialog( + onDismissRequest = { showSelectionDialog = false }, + title = { Text(stringResource(R.string.customize_background_image)) }, + confirmButtonText = stringResource(R.string.button_load_custom), + onConfirmed = { launcher.launch(intent) }, + neutralButtonText = stringResource(R.string.delete), + onNeutral = { + getFile().delete() + Settings.clearCachedBackgroundImages() + keyboardNeedsReload = true + } + ) + } + if (showErrorDialog) { + InfoDialog(stringResource(R.string.file_read_error)) { showErrorDialog = false } + } +} + +private fun setBackgroundImage(ctx: Context, uri: Uri, isNight: Boolean, isLandscape: Boolean): Boolean { + val imageFile = Settings.getCustomBackgroundFile(ctx, isNight, isLandscape) + FileUtils.copyContentUriToNewFile(uri, ctx, imageFile) + keyboardNeedsReload = true + try { + BitmapFactory.decodeFile(imageFile.absolutePath) + } catch (_: Exception) { + imageFile.delete() + return false + } + Settings.clearCachedBackgroundImages() + return true +} diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt new file mode 100644 index 00000000..52b84715 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt @@ -0,0 +1,303 @@ +package helium314.keyboard.settings.preferences + +import android.app.Activity +import android.content.Intent +import android.content.SharedPreferences +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.preference.PreferenceManager +import helium314.keyboard.dictionarypack.DictionaryPackConstants +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMBER +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMPAD +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMPAD_LANDSCAPE +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_PHONE +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_PHONE_SYMBOLS +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_ARABIC +import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_SHIFTED +import helium314.keyboard.latin.R +import helium314.keyboard.latin.checkVersionUpgrade +import helium314.keyboard.latin.common.FileUtils +import helium314.keyboard.latin.common.LocaleUtils.constructLocale +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.settings.USER_DICTIONARY_SUFFIX +import helium314.keyboard.latin.utils.AdditionalSubtypeUtils +import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX +import helium314.keyboard.latin.utils.DeviceProtectedUtils +import helium314.keyboard.latin.utils.ExecutorUtils +import helium314.keyboard.latin.utils.Log +import helium314.keyboard.latin.utils.getActivity +import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged +import helium314.keyboard.latin.utils.prefs +import helium314.keyboard.latin.utils.reloadEnabledSubtypes +import helium314.keyboard.latin.utils.updateAdditionalSubtypes +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.SettingsActivity2 +import helium314.keyboard.settings.dialogs.ConfirmationDialog +import helium314.keyboard.settings.dialogs.InfoDialog +import helium314.keyboard.settings.keyboardNeedsReload +import kotlinx.serialization.json.Json +import java.io.File +import java.io.FileInputStream +import java.io.OutputStream +import java.util.concurrent.CountDownLatch +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +@Composable +fun BackupRestorePreference(def: PrefDef) { + var showDialog by remember { mutableStateOf(false) } + val ctx = LocalContext.current + val prefs = ctx.prefs() + var error: String? by remember { mutableStateOf(null) } + val backupFilePatterns by lazy { listOf( + "blacklists/.*\\.txt".toRegex(), + "layouts/$CUSTOM_LAYOUT_PREFIX+\\..{0,4}".toRegex(), // can't expect a period at the end, as this would break restoring older backups + "dicts/.*/.*user\\.dict".toRegex(), + "UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(), + "custom_background_image.*".toRegex(), + "custom_font".toRegex(), + ) } + val backupLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult + val uri = result.data?.data ?: return@rememberLauncherForActivityResult + // zip all files matching the backup patterns + // essentially this is the typed words information, and user-added dictionaries + val filesDir = ctx.filesDir ?: return@rememberLauncherForActivityResult + val filesPath = filesDir.path + File.separator + val files = mutableListOf() + filesDir.walk().forEach { file -> + val path = file.path.replace(filesPath, "") + if (backupFilePatterns.any { path.matches(it) }) + files.add(file) + } + val protectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx) + val protectedFilesPath = protectedFilesDir.path + File.separator + val protectedFiles = mutableListOf() + protectedFilesDir.walk().forEach { file -> + val path = file.path.replace(protectedFilesPath, "") + if (backupFilePatterns.any { path.matches(it) }) + protectedFiles.add(file) + } + val wait = CountDownLatch(1) + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute { + try { + ctx.getActivity()?.contentResolver?.openOutputStream(uri)?.use { os -> + // write files to zip + val zipStream = ZipOutputStream(os) + files.forEach { + val fileStream = FileInputStream(it).buffered() + zipStream.putNextEntry(ZipEntry(it.path.replace(filesPath, ""))) + fileStream.copyTo(zipStream, 1024) + fileStream.close() + zipStream.closeEntry() + } + protectedFiles.forEach { + val fileStream = FileInputStream(it).buffered() + zipStream.putNextEntry(ZipEntry(it.path.replace(protectedFilesDir.path, "unprotected"))) + fileStream.copyTo(zipStream, 1024) + fileStream.close() + zipStream.closeEntry() + } + zipStream.putNextEntry(ZipEntry(PREFS_FILE_NAME)) + settingsToJsonStream(prefs.all, zipStream) + zipStream.closeEntry() + zipStream.putNextEntry(ZipEntry(PROTECTED_PREFS_FILE_NAME)) + settingsToJsonStream(PreferenceManager.getDefaultSharedPreferences(ctx).all, zipStream) + zipStream.closeEntry() + zipStream.close() + } + } catch (t: Throwable) { + error = "b" + t.message + Log.w("AdvancedScreen", "error during backup", t) + } finally { + wait.countDown() + } + } + wait.await() + } + val restoreLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult + val uri = result.data?.data ?: return@rememberLauncherForActivityResult + val wait = CountDownLatch(1) + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute { + try { + ctx.getActivity()?.contentResolver?.openInputStream(uri)?.use { inputStream -> + ZipInputStream(inputStream).use { zip -> + var entry: ZipEntry? = zip.nextEntry + val filesDir = ctx.filesDir?.path ?: return@execute + val deviceProtectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx).path + Settings.getInstance().stopListener() + while (entry != null) { + if (entry.name.startsWith("unprotected${File.separator}")) { + val adjustedName = entry.name.substringAfter("unprotected${File.separator}") + if (backupFilePatterns.any { adjustedName.matches(it) }) { + val targetFileName = upgradeFileNames(adjustedName) + val file = File(deviceProtectedFilesDir, targetFileName) + FileUtils.copyStreamToNewFile(zip, file) + } + } else if (backupFilePatterns.any { entry!!.name.matches(it) }) { + val targetFileName = upgradeFileNames(entry.name) + val file = File(filesDir, targetFileName) + FileUtils.copyStreamToNewFile(zip, file) + } else if (entry.name == PREFS_FILE_NAME) { + val prefLines = String(zip.readBytes()).split("\n") + prefs.edit().clear().apply() + readJsonLinesToSettings(prefLines, prefs) + } else if (entry.name == PROTECTED_PREFS_FILE_NAME) { + val prefLines = String(zip.readBytes()).split("\n") + val protectedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx) + protectedPrefs.edit().clear().apply() + readJsonLinesToSettings(prefLines, protectedPrefs) + } + zip.closeEntry() + entry = zip.nextEntry + } + } + } + } catch (t: Throwable) { + error = "r" + t.message + Log.w("AdvancedScreen", "error during restore", t) + } finally { + wait.countDown() + } + } + wait.await() + checkVersionUpgrade(ctx) + Settings.getInstance().startListener() + val additionalSubtypes = Settings.readPrefAdditionalSubtypes(prefs, ctx.resources) + updateAdditionalSubtypes(AdditionalSubtypeUtils.createAdditionalSubtypesArray(additionalSubtypes)) + reloadEnabledSubtypes(ctx) + val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION) + ctx.getActivity()?.sendBroadcast(newDictBroadcast) + onCustomLayoutFileListChanged() + (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.value = 210 // for settings reload + keyboardNeedsReload = true + } + Preference( + name = def.title, + onClick = { showDialog = true } + ) + if (showDialog) { + ConfirmationDialog( + onDismissRequest = { showDialog = false }, + title = { Text(stringResource(R.string.backup_restore_title)) }, + text = { Text(stringResource(R.string.backup_restore_message)) }, + confirmButtonText = stringResource(R.string.button_backup), + neutralButtonText = stringResource(R.string.button_restore), + onNeutral = { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("application/zip") + restoreLauncher.launch(intent) + }, + onConfirmed = { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .putExtra( + Intent.EXTRA_TITLE, + ctx.getString(R.string.english_ime_name) + .replace(" ", "_") + "_backup.zip" + ) + .setType("application/zip") + backupLauncher.launch(intent) + } + ) + } + if (error != null) { + InfoDialog( + if (error!!.startsWith("b")) + stringResource(R.string.backup_error, error!!.drop(1)) + else stringResource(R.string.restore_error, error!!.drop(1)) + ) { error = null } + } +} + +@Suppress("UNCHECKED_CAST") // it is checked... but whatever (except string set, because can't check for that)) +private fun settingsToJsonStream(settings: Map, out: OutputStream) { + val booleans = settings.filter { it.key is String && it.value is Boolean } as Map + val ints = settings.filter { it.key is String && it.value is Int } as Map + val longs = settings.filter { it.key is String && it.value is Long } as Map + val floats = settings.filter { it.key is String && it.value is Float } as Map + val strings = settings.filter { it.key is String && it.value is String } as Map + val stringSets = settings.filter { it.key is String && it.value is Set<*> } as Map> + // now write + out.write("boolean settings\n".toByteArray()) + out.write(Json.encodeToString(booleans).toByteArray()) + out.write("\nint settings\n".toByteArray()) + out.write(Json.encodeToString(ints).toByteArray()) + out.write("\nlong settings\n".toByteArray()) + out.write(Json.encodeToString(longs).toByteArray()) + out.write("\nfloat settings\n".toByteArray()) + out.write(Json.encodeToString(floats).toByteArray()) + out.write("\nstring settings\n".toByteArray()) + out.write(Json.encodeToString(strings).toByteArray()) + out.write("\nstring set settings\n".toByteArray()) + out.write(Json.encodeToString(stringSets).toByteArray()) +} + +private fun readJsonLinesToSettings(list: List, prefs: SharedPreferences): Boolean { + val i = list.iterator() + val e = prefs.edit() + try { + while (i.hasNext()) { + when (i.next()) { + "boolean settings" -> Json.decodeFromString>(i.next()).forEach { e.putBoolean(it.key, it.value) } + "int settings" -> Json.decodeFromString>(i.next()).forEach { e.putInt(it.key, it.value) } + "long settings" -> Json.decodeFromString>(i.next()).forEach { e.putLong(it.key, it.value) } + "float settings" -> Json.decodeFromString>(i.next()).forEach { e.putFloat(it.key, it.value) } + "string settings" -> Json.decodeFromString>(i.next()).forEach { e.putString(it.key, it.value) } + "string set settings" -> Json.decodeFromString>>(i.next()).forEach { e.putStringSet(it.key, it.value) } + } + } + e.apply() + return true + } catch (e: Exception) { + return false + } +} + +// todo (later): remove this when new package name has been in use for long enough, this is only for migrating from old openboard name +private fun upgradeFileNames(originalName: String): String { + return when { + originalName.endsWith(USER_DICTIONARY_SUFFIX) -> { + // replace directory after switch to language tag + val dirName = originalName.substringAfter(File.separator).substringBefore(File.separator) + originalName.replace(dirName, dirName.constructLocale().toLanguageTag()) + } + originalName.startsWith("blacklists") -> { + // replace file name after switch to language tag + val fileName = originalName.substringAfter("blacklists${File.separator}").substringBefore(".txt") + originalName.replace(fileName, fileName.constructLocale().toLanguageTag()) + } + originalName.startsWith("layouts") -> { + // replace file name after switch to language tag, but only if it's not a layout + val localeString = originalName.substringAfter(".").substringBefore(".") + if (localeString in listOf(LAYOUT_SYMBOLS, LAYOUT_SYMBOLS_SHIFTED, LAYOUT_SYMBOLS_ARABIC, LAYOUT_NUMBER, LAYOUT_NUMPAD, LAYOUT_NUMPAD_LANDSCAPE, LAYOUT_PHONE, LAYOUT_PHONE_SYMBOLS)) + return originalName // it's a layout! + val locale = localeString.constructLocale() + if (locale.toLanguageTag() != "und") + originalName.replace(localeString, locale.toLanguageTag()) + else + originalName // no valid locale -> must be symbols layout, don't change + } + originalName.startsWith("UserHistoryDictionary") -> { + val localeString = originalName.substringAfter(".").substringBefore(".") + val locale = localeString.constructLocale() + originalName.replace(localeString, locale.toLanguageTag()) + } + else -> originalName + } +} + +private const val PREFS_FILE_NAME = "preferences.json" +private const val PROTECTED_PREFS_FILE_NAME = "protected_preferences.json" diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/CustomFontPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/CustomFontPreference.kt new file mode 100644 index 00000000..440052f9 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/CustomFontPreference.kt @@ -0,0 +1,74 @@ +package helium314.keyboard.settings.preferences + +import android.app.Activity +import android.content.Intent +import android.graphics.Typeface +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import helium314.keyboard.latin.R +import helium314.keyboard.latin.common.FileUtils +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.DeviceProtectedUtils +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.dialogs.ConfirmationDialog +import helium314.keyboard.settings.dialogs.InfoDialog +import helium314.keyboard.settings.keyboardNeedsReload +import java.io.File + +@Composable +fun CustomFontPreference(def: PrefDef) { + val ctx = LocalContext.current + var showDialog by remember { mutableStateOf(false) } + var showErrorDialog by remember { mutableStateOf(false) } + val fontFile = Settings.getCustomFontFile(ctx) + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult + val uri = it.data?.data ?: return@rememberLauncherForActivityResult + val tempFile = File(DeviceProtectedUtils.getFilesDir(ctx), "temp_file") + FileUtils.copyContentUriToNewFile(uri, ctx, tempFile) + try { + Typeface.createFromFile(tempFile) + fontFile.delete() + tempFile.renameTo(fontFile) + Settings.clearCachedTypeface() + keyboardNeedsReload = true + } catch (_: Exception) { + showErrorDialog = true + tempFile.delete() + } + } + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*") + Preference( + name = def.title, + onClick = { + if (fontFile.exists()) + showDialog = true + else launcher.launch(intent) + }, + ) + if (showDialog) + ConfirmationDialog( + onDismissRequest = { showDialog = false }, + onConfirmed = { launcher.launch(intent) }, + onNeutral = { + fontFile.delete() + Settings.clearCachedTypeface() + keyboardNeedsReload = true + }, + neutralButtonText = stringResource(R.string.delete), + confirmButtonText = stringResource(R.string.load), + title = { Text(stringResource(R.string.custom_font)) } + ) + if (showErrorDialog) + InfoDialog(stringResource(R.string.file_read_error)) { showErrorDialog = false } +} diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/LayoutEditPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/LayoutEditPreference.kt new file mode 100644 index 00000000..27dd1d3f --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/LayoutEditPreference.kt @@ -0,0 +1,57 @@ +package helium314.keyboard.settings.preferences + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX +import helium314.keyboard.latin.utils.getCustomLayoutFiles +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.dialogs.LayoutEditDialog +import helium314.keyboard.settings.dialogs.ListPickerDialog +import java.io.File + +@Composable +fun LayoutEditPreference( + def: PrefDef, + items: List, + getItemName: @Composable (String) -> String, + getDefaultLayout: @Composable (String?) -> String?, +) { + var showDialog by remember { mutableStateOf(false) } + val ctx = LocalContext.current + var layout: String? by remember { mutableStateOf(null) } + Preference( + name = def.title, + onClick = { showDialog = true } + ) + if (showDialog) { + ListPickerDialog( + onDismissRequest = { showDialog = false }, + showRadioButtons = false, + confirmImmediately = true, + items = items, + getItemName = getItemName, + onItemSelected = { layout = it }, + title = { Text(def.title) } + ) + } + if (layout != null) { + val customLayoutName = getCustomLayoutFiles(ctx).firstOrNull { + if (layout!!.startsWith(CUSTOM_LAYOUT_PREFIX)) + it.name.startsWith("$layout.") + else it.name.startsWith("$CUSTOM_LAYOUT_PREFIX$layout.") + }?.name + val originalLayout = if (customLayoutName != null) null + else getDefaultLayout(layout)?.let { ctx.assets.open("layouts" + File.separator + it).reader().readText() } + LayoutEditDialog( + layoutName = customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.", + startContent = originalLayout, + displayName = getItemName(layout!!), + onDismissRequest = { layout = null } + ) + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt new file mode 100644 index 00000000..1ba91de9 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt @@ -0,0 +1,111 @@ +package helium314.keyboard.settings.preferences + +import android.app.Activity +import android.content.Intent +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import helium314.keyboard.latin.R +import helium314.keyboard.latin.common.FileUtils +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.ChecksumCalculator +import helium314.keyboard.latin.utils.JniUtils +import helium314.keyboard.latin.utils.prefs +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.dialogs.ConfirmationDialog +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException + +@Composable +fun LoadGestureLibPreference(def: PrefDef) { + var showDialog by remember { mutableStateOf(false) } + val ctx = LocalContext.current + val prefs = ctx.prefs() + val abi = Build.SUPPORTED_ABIS[0] + val libFile = File(ctx.filesDir.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) + fun renameToLibFileAndRestart(file: File, checksum: String) { + libFile.delete() + // store checksum in default preferences (soo JniUtils) + prefs.edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit() + file.renameTo(libFile) + Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded + } + var tempFilePath: String? by remember { mutableStateOf(null) } + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult + val uri = result.data?.data ?: return@rememberLauncherForActivityResult + val tmpfile = File(ctx.filesDir.absolutePath + File.separator + "tmplib") + try { + val otherTemporaryFile = File(ctx.filesDir.absolutePath + File.separator + "tmpfile") + FileUtils.copyContentUriToNewFile(uri, ctx, otherTemporaryFile) + val inputStream = FileInputStream(otherTemporaryFile) + val outputStream = FileOutputStream(tmpfile) + outputStream.use { + tmpfile.setReadOnly() // as per recommendations in https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading + FileUtils.copyStreamToOtherStream(inputStream, it) + } + otherTemporaryFile.delete() + + val checksum = ChecksumCalculator.checksum(tmpfile.inputStream()) ?: "" + if (checksum == JniUtils.expectedDefaultChecksum()) { + renameToLibFileAndRestart(tmpfile, checksum) + } else { + tempFilePath = tmpfile.absolutePath + AlertDialog.Builder(ctx) + .setMessage(ctx.getString(R.string.checksum_mismatch_message, abi)) + .setPositiveButton(android.R.string.ok) { _, _ -> renameToLibFileAndRestart(tmpfile, checksum) } + .setNegativeButton(android.R.string.cancel) { _, _ -> tmpfile.delete() } + .show() + } + } catch (e: IOException) { + tmpfile.delete() + // should inform user, but probably the issues will only come when reading the library + } + } + Preference( + name = def.title, + onClick = { showDialog = true } + ) + if (showDialog) { + ConfirmationDialog( + onDismissRequest = { showDialog = false }, + onConfirmed = { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("application/octet-stream") + launcher.launch(intent) + }, + title = { Text(stringResource(R.string.load_gesture_library)) }, + text = { Text(stringResource(R.string.load_gesture_library_message, abi)) }, + neutralButtonText = if (libFile.exists()) stringResource(R.string.load_gesture_library_button_delete) else null, + onNeutral = { + libFile.delete() + prefs.edit().remove(Settings.PREF_LIBRARY_CHECKSUM).commit() + Runtime.getRuntime().exit(0) + } + ) + } + if (tempFilePath != null) + ConfirmationDialog( + onDismissRequest = { + File(tempFilePath!!).delete() + tempFilePath = null + }, + text = { Text(stringResource(R.string.checksum_mismatch_message, abi)) }, + onConfirmed = { + val tempFile = File(tempFilePath!!) + renameToLibFileAndRestart(tempFile, ChecksumCalculator.checksum(tempFile.inputStream()) ?: "") + } + ) +} diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/Preference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/Preference.kt new file mode 100644 index 00000000..66d70bc4 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/Preference.kt @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-only +package helium314.keyboard.settings.preferences + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.Hyphens +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import helium314.keyboard.latin.R + +// partially taken from StreetComplete / SCEE + +@Composable +fun PreferenceCategory( + title: String, + modifier: Modifier = Modifier, +) { + Column { + HorizontalDivider() + Text( + text = title, + modifier = modifier.padding(top = 12.dp, start = 16.dp, end = 8.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.titleSmall + ) + } +} + +@Composable +fun Preference( + name: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + description: String? = null, + @DrawableRes icon: Int? = null, + value: @Composable (RowScope.() -> Unit)? = null, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .heightIn(min = 44.dp) + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) + Icon(painterResource(icon), name, modifier = Modifier.size(36.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(text = name, style = MaterialTheme.typography.bodyLarge) + if (description != null) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodyMedium, + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant + ) { + Text( + text = description, + modifier = Modifier.padding(top = 2.dp) + ) + } + } + } + if (value != null) { + CompositionLocalProvider( + LocalTextStyle provides LocalTextStyle.current.copy( + textAlign = TextAlign.End, + hyphens = Hyphens.Auto + ), + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant + ) { + Row( + horizontalArrangement = Arrangement.spacedBy( + space = 8.dp, + alignment = Alignment.End + ), + verticalAlignment = Alignment.CenterVertically, + ) { value() } + } + } + } +} + +@Preview +@Composable +private fun PreferencePreview() { + Surface { + Column { + PreferenceCategory("Preference Category") + Preference( + name = "Preference", + onClick = {}, + ) + Preference( + name = "Preference with icon", + onClick = {}, + icon = R.drawable.ic_settings_about_foreground + ) + SliderPreference( + name = "SliderPreference", + pref = "", + default = 1, + description = { it.toString() }, + range = -5f..5f + ) + Preference( + name = "Preference with icon and description", + description = "some text", + onClick = {}, + icon = R.drawable.ic_settings_about_foreground + ) + Preference( + name = "Preference with switch", + onClick = {} + ) { + Switch(checked = true, onCheckedChange = {}) + } + SwitchPreference( + name = "SwitchPreference", + pref = "none", + default = true + ) + Preference( + name = "Preference", + onClick = {}, + description = "A long description which may actually be several lines long, so it should wrap." + ) { + Icon(painterResource(R.drawable.ic_arrow_left), null) + } + Preference( + name = "Long preference name that wraps", + onClick = {}, + ) { + Text("Long preference value") + } + Preference( + name = "Long preference name 2", + onClick = {}, + description = "hello I am description" + ) { + Text("Long preference value") + } + } + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/ReorderSwitchPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/ReorderSwitchPreference.kt new file mode 100644 index 00000000..66c42d0d --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/ReorderSwitchPreference.kt @@ -0,0 +1,67 @@ +package helium314.keyboard.settings.preferences + +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import helium314.keyboard.keyboard.internal.KeyboardIconsSet +import helium314.keyboard.latin.R +import helium314.keyboard.latin.utils.getStringResourceOrName +import helium314.keyboard.latin.utils.prefs +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.dialogs.ReorderDialog +import helium314.keyboard.settings.keyboardNeedsReload +import helium314.keyboard.settings.screens.GetIcon + +@Composable +fun ReorderSwitchPreference(def: PrefDef, default: String) { + var showDialog by remember { mutableStateOf(false) } + Preference( + name = def.title, + description = def.description, + onClick = { showDialog = true }, + ) + if (showDialog) { + val ctx = LocalContext.current + val prefs = ctx.prefs() + val items = prefs.getString(def.key, default)!!.split(";").mapTo(ArrayList()) { + val both = it.split(",") + KeyAndState(both.first(), both.last().toBoolean()) + } + ReorderDialog( + onConfirmed = { reorderedItems -> + val value = reorderedItems.joinToString(";") { it.name + "," + it.state } + prefs.edit().putString(def.key, value).apply() + keyboardNeedsReload = true + }, + onDismissRequest = { showDialog = false }, + onNeutral = { prefs.edit().remove(def.key).apply() }, + neutralButtonText = if (prefs.contains(def.key)) stringResource(R.string.button_default) else null, + items = items, + title = { Text(def.title) }, + displayItem = { item -> + var checked by remember { mutableStateOf(item.state) } + Row(verticalAlignment = Alignment.CenterVertically) { + KeyboardIconsSet.instance.GetIcon(item.name) + val text = item.name.lowercase().getStringResourceOrName("", ctx) + Text(text, Modifier.weight(1f)) + Switch( + checked = checked, + onCheckedChange = { item.state = it; checked = it } + ) + } + }, + getKey = { it.name } + ) + } +} + +private class KeyAndState(var name: String, var state: Boolean) diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt new file mode 100644 index 00000000..fc2234be --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt @@ -0,0 +1,127 @@ +package helium314.keyboard.settings.preferences + +import android.content.SharedPreferences +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.edit +import helium314.keyboard.latin.utils.Log +import helium314.keyboard.latin.utils.getActivity +import helium314.keyboard.latin.utils.prefs +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.SettingsActivity2 +import helium314.keyboard.settings.dialogs.ListPickerDialog +import helium314.keyboard.settings.dialogs.SliderDialog +import kotlin.math.roundToInt + +@Composable +/** Slider preference for Int or Float (weird casting stuff, but should be fine) */ +fun SliderPreference( + name: String, + modifier: Modifier = Modifier, + pref: String, + description: @Composable (T) -> String, + default: T, + range: ClosedFloatingPointRange, + stepSize: Int? = null, + onValueChanged: (Float) -> Unit = { }, +) { + val ctx = LocalContext.current + val prefs = ctx.prefs() + val b = (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.collectAsState() + if ((b?.value ?: 0) < 0) + Log.v("irrelevant", "stupid way to trigger recomposition on preference change") + val initialValue = if (default is Int || default is Float) + getPrefOfType(prefs, pref, default) + else throw IllegalArgumentException("only float and int are supported") + + var showDialog by remember { mutableStateOf(false) } + Preference( + name = name, + onClick = { showDialog = true }, + modifier = modifier, + description = description(initialValue) + ) + if (showDialog) + SliderDialog( + onDismissRequest = { showDialog = false }, + onDone = { + if (default is Int) prefs.edit().putInt(pref, it.toInt()).apply() + else prefs.edit().putFloat(pref, it).apply() + }, + initialValue = initialValue.toFloat(), + range = range, + positionString = { + @Suppress("UNCHECKED_CAST") + description((if (default is Int) it.roundToInt() else it) as T) + }, + onValueChanged = onValueChanged, + showDefault = true, + onDefault = { prefs.edit().remove(pref).apply() }, + intermediateSteps = stepSize?.let { + // this is not nice, but slider wants it like this... + ((range.endInclusive - range.start) / it - 1).toInt() + } + ) +} + +@Composable +// just in here so we can keep getPrefOfType private... rename file? +fun ListPreference( + def: PrefDef, + items: List>, + default: T, + onChanged: (T) -> Unit = { } +) { + var showDialog by remember { mutableStateOf(false) } + val prefs = LocalContext.current.prefs() + val selected = items.firstOrNull { it.second == getPrefOfType(prefs, def.key, default) } + Preference( + name = def.title, + description = selected?.first, + onClick = { showDialog = true } + ) + if (showDialog) { + ListPickerDialog( + onDismissRequest = { showDialog = false }, + items = items, + onItemSelected = { + if (it == selected) return@ListPickerDialog + putPrefOfType(prefs, def.key, it.second) + onChanged(it.second) + }, + selectedItem = selected, + title = { Text(def.title) }, + getItemName = { it.first } + ) + } +} + +@Suppress("UNCHECKED_CAST") +private fun getPrefOfType(prefs: SharedPreferences, key: String, default: T): T = + when (default) { + is String -> prefs.getString(key, default) + is Int -> prefs.getInt(key, default) + is Long -> prefs.getLong(key, default) + is Float -> prefs.getFloat(key, default) + is Boolean -> prefs.getBoolean(key, default) + else -> throw IllegalArgumentException("unknown type ${default.javaClass}") + } as T + +private fun putPrefOfType(prefs: SharedPreferences, key: String, value: T) = + prefs.edit { + when (value) { + is String -> putString(key, value) + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is Float -> putFloat(key, value) + is Boolean -> putBoolean(key, value) + else -> throw IllegalArgumentException("unknown type ${value.javaClass}") + } + } diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/SwitchPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/SwitchPreference.kt new file mode 100644 index 00000000..dd1fd53a --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/SwitchPreference.kt @@ -0,0 +1,68 @@ +package helium314.keyboard.settings.preferences + +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import helium314.keyboard.latin.utils.Log +import helium314.keyboard.latin.utils.getActivity +import helium314.keyboard.latin.utils.prefs +import helium314.keyboard.settings.PrefDef +import helium314.keyboard.settings.SettingsActivity2 + + +@Composable +fun SwitchPreference( + name: String, + modifier: Modifier = Modifier, + pref: String, + default: Boolean, + description: String? = null, + allowCheckedChange: (Boolean) -> Boolean = { true }, // true means ok, usually for showing some dialog + onCheckedChange: (Boolean) -> Unit = { }, +) { + val ctx = LocalContext.current + val prefs = ctx.prefs() + val b = (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.collectAsState() + if ((b?.value ?: 0) < 0) + Log.v("irrelevant", "stupid way to trigger recomposition on preference change") + var value = prefs.getBoolean(pref, default) + fun switched(newValue: Boolean) { + if (!allowCheckedChange(newValue)) { + value = !newValue + return + } + value = newValue + prefs.edit().putBoolean(pref, newValue).apply() + onCheckedChange(newValue) + } + Preference( + name = name, + onClick = { switched(!value) }, + modifier = modifier, + description = description + ) { + Switch( + checked = value, + onCheckedChange = { switched(it) }, + ) + } +} + +@Composable +fun SwitchPreference( + def: PrefDef, + default: Boolean, + allowCheckedChange: (Boolean) -> Boolean = { true }, + onCheckedChange: (Boolean) -> Unit = { } +) { + SwitchPreference( + name = def.title, + description = def.description, + pref = def.key, + default = default, + allowCheckedChange = allowCheckedChange, + onCheckedChange = onCheckedChange + ) +} diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt index 7cac388f..6be24052 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt @@ -32,7 +32,7 @@ import helium314.keyboard.latin.utils.prefs import helium314.keyboard.settings.AllPrefs import helium314.keyboard.settings.NonSettingsPrefs import helium314.keyboard.settings.PrefDef -import helium314.keyboard.settings.Preference +import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 import helium314.keyboard.settings.Theme diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt index 6fd9c890..787fed70 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt @@ -2,14 +2,8 @@ package helium314.keyboard.settings.screens import android.annotation.SuppressLint -import android.app.Activity import android.content.Context -import android.content.Intent -import android.content.SharedPreferences import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -24,73 +18,37 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.preference.PreferenceManager -import helium314.keyboard.dictionarypack.DictionaryPackConstants import helium314.keyboard.keyboard.KeyboardActionListener import helium314.keyboard.keyboard.KeyboardLayoutSet -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMBER -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMPAD -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMPAD_LANDSCAPE -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_PHONE -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_PHONE_SYMBOLS -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_ARABIC -import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_SHIFTED import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser import helium314.keyboard.latin.BuildConfig import helium314.keyboard.latin.R import helium314.keyboard.latin.SystemBroadcastReceiver -import helium314.keyboard.latin.checkVersionUpgrade -import helium314.keyboard.latin.common.FileUtils -import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.common.splitOnWhitespace import helium314.keyboard.latin.settings.DebugSettings import helium314.keyboard.latin.settings.Settings -import helium314.keyboard.latin.settings.USER_DICTIONARY_SUFFIX -import helium314.keyboard.latin.utils.AdditionalSubtypeUtils import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_NORMAL import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX -import helium314.keyboard.latin.utils.ChecksumCalculator -import helium314.keyboard.latin.utils.DeviceProtectedUtils -import helium314.keyboard.latin.utils.ExecutorUtils -import helium314.keyboard.latin.utils.JniUtils -import helium314.keyboard.latin.utils.Log -import helium314.keyboard.latin.utils.getActivity -import helium314.keyboard.latin.utils.getCustomLayoutFiles import helium314.keyboard.latin.utils.getStringResourceOrName -import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged import helium314.keyboard.latin.utils.prefs -import helium314.keyboard.latin.utils.reloadEnabledSubtypes -import helium314.keyboard.latin.utils.updateAdditionalSubtypes import helium314.keyboard.settings.AllPrefs -import helium314.keyboard.settings.ListPreference +import helium314.keyboard.settings.preferences.ListPreference import helium314.keyboard.settings.NonSettingsPrefs import helium314.keyboard.settings.PrefDef -import helium314.keyboard.settings.Preference +import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 import helium314.keyboard.settings.SettingsDestination -import helium314.keyboard.settings.SliderPreference -import helium314.keyboard.settings.SwitchPreference +import helium314.keyboard.settings.preferences.SliderPreference +import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme -import helium314.keyboard.settings.dialogs.ConfirmationDialog -import helium314.keyboard.settings.dialogs.InfoDialog -import helium314.keyboard.settings.dialogs.LayoutEditDialog -import helium314.keyboard.settings.dialogs.ListPickerDialog import helium314.keyboard.settings.dialogs.TextInputDialog import helium314.keyboard.settings.keyboardNeedsReload -import kotlinx.serialization.json.Json -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException -import java.io.OutputStream -import java.util.concurrent.CountDownLatch -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream +import helium314.keyboard.settings.preferences.BackupRestorePreference +import helium314.keyboard.settings.preferences.LayoutEditPreference +import helium314.keyboard.settings.preferences.LoadGestureLibPreference @Composable fun AdvancedSettingsScreen( @@ -236,241 +194,24 @@ fun createAdvancedPrefs(context: Context) = listOf( ListPreference(def, items, "main") { KeyboardLayoutSet.onSystemLocaleChanged() } }, PrefDef(context, NonSettingsPrefs.CUSTOM_SYMBOLS_NUMBER_LAYOUTS, R.string.customize_symbols_number_layouts) { def -> - var showDialog by remember { mutableStateOf(false) } - val ctx = LocalContext.current - var layout: String? by remember { mutableStateOf(null) } - Preference( - name = def.title, - onClick = { showDialog = true } + LayoutEditPreference( + def = def, + items = RawKeyboardParser.symbolAndNumberLayouts, + getItemName = { it.getStringResourceOrName("layout_", LocalContext.current) }, + getDefaultLayout = { LocalContext.current.assets.list("layouts")?.firstOrNull { it.startsWith("$it.") } } ) - if (showDialog) { - ListPickerDialog( - onDismissRequest = { showDialog = false }, - showRadioButtons = false, - confirmImmediately = true, - items = RawKeyboardParser.symbolAndNumberLayouts, - getItemName = { it.getStringResourceOrName("layout_", ctx) }, - onItemSelected = { layout = it }, - title = { Text(def.title) } - ) - } - if (layout != null) { - val customLayoutName = getCustomLayoutFiles(ctx).firstOrNull { it.name.startsWith("$CUSTOM_LAYOUT_PREFIX$layout.")}?.name - val originalLayout = if (customLayoutName != null) null - else { - ctx.assets.list("layouts")?.firstOrNull { it.startsWith("$layout.") } - ?.let { ctx.assets.open("layouts" + File.separator + it).reader().readText() } - } - LayoutEditDialog( - layoutName = customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.", - startContent = originalLayout, - displayName = layout?.getStringResourceOrName("layout_", ctx), - onDismissRequest = { layout = null} - ) - } }, PrefDef(context, NonSettingsPrefs.CUSTOM_FUNCTIONAL_LAYOUTS, R.string.customize_functional_key_layouts) { def -> - var showDialog by remember { mutableStateOf(false) } - val ctx = LocalContext.current - var layout: String? by remember { mutableStateOf(null) } - Preference( - name = def.title, - onClick = { showDialog = true } + LayoutEditPreference( + def = def, + items = listOf(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED) + .map { it.substringBeforeLast(".") }, + getItemName = { it.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", LocalContext.current) }, + getDefaultLayout = { if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json" } ) - if (showDialog) { - ListPickerDialog( - onDismissRequest = { showDialog = false }, - showRadioButtons = false, - confirmImmediately = true, - items = listOf(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED) - .map { it.substringBeforeLast(".") }, - getItemName = { it.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", ctx) }, - onItemSelected = { layout = it }, - title = { Text(def.title) } - ) - } - if (layout != null) { - val customLayoutName = getCustomLayoutFiles(ctx).map { it.name } - .firstOrNull { it.startsWith("$layout.") } - val originalLayout = if (customLayoutName != null) null - else { - val defaultLayoutName = if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json" - ctx.assets.open("layouts" + File.separator + defaultLayoutName).reader().readText() - } - LayoutEditDialog( - layoutName = customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.", - startContent = originalLayout, - displayName = layout?.substringAfter(CUSTOM_LAYOUT_PREFIX)?.getStringResourceOrName("layout_", ctx), - onDismissRequest = { layout = null} - ) - } }, - PrefDef(context, NonSettingsPrefs.BACKUP_RESTORE, R.string.backup_restore_title) { def -> - var showDialog by remember { mutableStateOf(false) } - val ctx = LocalContext.current - val prefs = ctx.prefs() - var error: String? by remember { mutableStateOf(null) } - val backupFilePatterns by lazy { listOf( - "blacklists/.*\\.txt".toRegex(), - "layouts/$CUSTOM_LAYOUT_PREFIX+\\..{0,4}".toRegex(), // can't expect a period at the end, as this would break restoring older backups - "dicts/.*/.*user\\.dict".toRegex(), - "UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(), - "custom_background_image.*".toRegex(), - "custom_font".toRegex(), - ) } - val backupLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult - val uri = result.data?.data ?: return@rememberLauncherForActivityResult - // zip all files matching the backup patterns - // essentially this is the typed words information, and user-added dictionaries - val filesDir = ctx.filesDir ?: return@rememberLauncherForActivityResult - val filesPath = filesDir.path + File.separator - val files = mutableListOf() - filesDir.walk().forEach { file -> - val path = file.path.replace(filesPath, "") - if (backupFilePatterns.any { path.matches(it) }) - files.add(file) - } - val protectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx) - val protectedFilesPath = protectedFilesDir.path + File.separator - val protectedFiles = mutableListOf() - protectedFilesDir.walk().forEach { file -> - val path = file.path.replace(protectedFilesPath, "") - if (backupFilePatterns.any { path.matches(it) }) - protectedFiles.add(file) - } - val wait = CountDownLatch(1) - ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute { - try { - ctx.getActivity()?.contentResolver?.openOutputStream(uri)?.use { os -> - // write files to zip - val zipStream = ZipOutputStream(os) - files.forEach { - val fileStream = FileInputStream(it).buffered() - zipStream.putNextEntry(ZipEntry(it.path.replace(filesPath, ""))) - fileStream.copyTo(zipStream, 1024) - fileStream.close() - zipStream.closeEntry() - } - protectedFiles.forEach { - val fileStream = FileInputStream(it).buffered() - zipStream.putNextEntry(ZipEntry(it.path.replace(protectedFilesDir.path, "unprotected"))) - fileStream.copyTo(zipStream, 1024) - fileStream.close() - zipStream.closeEntry() - } - zipStream.putNextEntry(ZipEntry(PREFS_FILE_NAME)) - settingsToJsonStream(prefs.all, zipStream) - zipStream.closeEntry() - zipStream.putNextEntry(ZipEntry(PROTECTED_PREFS_FILE_NAME)) - settingsToJsonStream(PreferenceManager.getDefaultSharedPreferences(ctx).all, zipStream) - zipStream.closeEntry() - zipStream.close() - } - } catch (t: Throwable) { - error = "b" + t.message - Log.w("AdvancedScreen", "error during backup", t) - } finally { - wait.countDown() - } - } - wait.await() - } - val restoreLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult - val uri = result.data?.data ?: return@rememberLauncherForActivityResult - val wait = CountDownLatch(1) - ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute { - try { - ctx.getActivity()?.contentResolver?.openInputStream(uri)?.use { inputStream -> - ZipInputStream(inputStream).use { zip -> - var entry: ZipEntry? = zip.nextEntry - val filesDir = ctx.filesDir?.path ?: return@execute - val deviceProtectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx).path - Settings.getInstance().stopListener() - while (entry != null) { - if (entry.name.startsWith("unprotected${File.separator}")) { - val adjustedName = entry.name.substringAfter("unprotected${File.separator}") - if (backupFilePatterns.any { adjustedName.matches(it) }) { - val targetFileName = upgradeFileNames(adjustedName) - val file = File(deviceProtectedFilesDir, targetFileName) - FileUtils.copyStreamToNewFile(zip, file) - } - } else if (backupFilePatterns.any { entry!!.name.matches(it) }) { - val targetFileName = upgradeFileNames(entry.name) - val file = File(filesDir, targetFileName) - FileUtils.copyStreamToNewFile(zip, file) - } else if (entry.name == PREFS_FILE_NAME) { - val prefLines = String(zip.readBytes()).split("\n") - prefs.edit().clear().apply() - readJsonLinesToSettings(prefLines, prefs) - } else if (entry.name == PROTECTED_PREFS_FILE_NAME) { - val prefLines = String(zip.readBytes()).split("\n") - val protectedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx) - protectedPrefs.edit().clear().apply() - readJsonLinesToSettings(prefLines, protectedPrefs) - } - zip.closeEntry() - entry = zip.nextEntry - } - } - } - } catch (t: Throwable) { - error = "r" + t.message - Log.w("AdvancedScreen", "error during restore", t) - } finally { - wait.countDown() - } - } - wait.await() - checkVersionUpgrade(ctx) - Settings.getInstance().startListener() - val additionalSubtypes = Settings.readPrefAdditionalSubtypes(prefs, ctx.resources) - updateAdditionalSubtypes(AdditionalSubtypeUtils.createAdditionalSubtypesArray(additionalSubtypes)) - reloadEnabledSubtypes(ctx) - val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION) - ctx.getActivity()?.sendBroadcast(newDictBroadcast) - onCustomLayoutFileListChanged() - (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.value = 210 // for settings reload - keyboardNeedsReload = true - } - Preference( - name = def.title, - onClick = { showDialog = true } - ) - if (showDialog) { - ConfirmationDialog( - onDismissRequest = { showDialog = false }, - title = { Text(stringResource(R.string.backup_restore_title)) }, - text = { Text(stringResource(R.string.backup_restore_message)) }, - confirmButtonText = stringResource(R.string.button_backup), - neutralButtonText = stringResource(R.string.button_restore), - onNeutral = { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType("application/zip") - restoreLauncher.launch(intent) - }, - onConfirmed = { - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .putExtra( - Intent.EXTRA_TITLE, - ctx.getString(R.string.english_ime_name) - .replace(" ", "_") + "_backup.zip" - ) - .setType("application/zip") - backupLauncher.launch(intent) - } - ) - } - if (error != null) { - InfoDialog( - if (error!!.startsWith("b")) - stringResource(R.string.backup_error, error!!.drop(1)) - else stringResource(R.string.restore_error, error!!.drop(1)) - ) { error = null } - } + PrefDef(context, NonSettingsPrefs.BACKUP_RESTORE, R.string.backup_restore_title) { + BackupRestorePreference(it) }, PrefDef(context, NonSettingsPrefs.DEBUG_SETTINGS, R.string.debug_settings_title) { Preference( @@ -516,86 +257,8 @@ fun createAdvancedPrefs(context: Context) = listOf( PrefDef(context, Settings.PREF_URL_DETECTION, R.string.url_detection_title, R.string.url_detection_summary) { SwitchPreference(it, false) }, - PrefDef(context, NonSettingsPrefs.LOAD_GESTURE_LIB, R.string.load_gesture_library, R.string.load_gesture_library_summary) { def -> - var showDialog by remember { mutableStateOf(false) } - val ctx = LocalContext.current - val prefs = ctx.prefs() - val abi = Build.SUPPORTED_ABIS[0] - val libFile = File(ctx.filesDir.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) - fun renameToLibFileAndRestart(file: File, checksum: String) { - libFile.delete() - // store checksum in default preferences (soo JniUtils) - prefs.edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit() - file.renameTo(libFile) - Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded - } - var tempFilePath: String? by remember { mutableStateOf(null) } - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult - val uri = result.data?.data ?: return@rememberLauncherForActivityResult - val tmpfile = File(ctx.filesDir.absolutePath + File.separator + "tmplib") - try { - val otherTemporaryFile = File(ctx.filesDir.absolutePath + File.separator + "tmpfile") - FileUtils.copyContentUriToNewFile(uri, ctx, otherTemporaryFile) - val inputStream = FileInputStream(otherTemporaryFile) - val outputStream = FileOutputStream(tmpfile) - outputStream.use { - tmpfile.setReadOnly() // as per recommendations in https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading - FileUtils.copyStreamToOtherStream(inputStream, it) - } - otherTemporaryFile.delete() - - val checksum = ChecksumCalculator.checksum(tmpfile.inputStream()) ?: "" - if (checksum == JniUtils.expectedDefaultChecksum()) { - renameToLibFileAndRestart(tmpfile, checksum) - } else { - tempFilePath = tmpfile.absolutePath - AlertDialog.Builder(ctx) - .setMessage(ctx.getString(R.string.checksum_mismatch_message, abi)) - .setPositiveButton(android.R.string.ok) { _, _ -> renameToLibFileAndRestart(tmpfile, checksum) } - .setNegativeButton(android.R.string.cancel) { _, _ -> tmpfile.delete() } - .show() - } - } catch (e: IOException) { - tmpfile.delete() - // should inform user, but probably the issues will only come when reading the library - } - } - Preference( - name = def.title, - onClick = { showDialog = true } - ) - if (showDialog) { - ConfirmationDialog( - onDismissRequest = { showDialog = false }, - onConfirmed = { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType("application/octet-stream") - launcher.launch(intent) - }, - title = { Text(stringResource(R.string.load_gesture_library)) }, - text = { Text(stringResource(R.string.load_gesture_library_message, abi)) }, - neutralButtonText = if (libFile.exists()) stringResource(R.string.load_gesture_library_button_delete) else null, - onNeutral = { - libFile.delete() - prefs.edit().remove(Settings.PREF_LIBRARY_CHECKSUM).commit() - Runtime.getRuntime().exit(0) - } - ) - } - if (tempFilePath != null) - ConfirmationDialog( - onDismissRequest = { - File(tempFilePath!!).delete() - tempFilePath = null - }, - text = { Text(stringResource(R.string.checksum_mismatch_message, abi))}, - onConfirmed = { - val tempFile = File(tempFilePath!!) - renameToLibFileAndRestart(tempFile, ChecksumCalculator.checksum(tempFile.inputStream()) ?: "") - } - ) + PrefDef(context, NonSettingsPrefs.LOAD_GESTURE_LIB, R.string.load_gesture_library, R.string.load_gesture_library_summary) { + LoadGestureLibPreference(it) }, ) @@ -609,84 +272,3 @@ private fun Preview() { } } } - -// stuff for backup / restore -private const val PREFS_FILE_NAME = "preferences.json" -private const val PROTECTED_PREFS_FILE_NAME = "protected_preferences.json" - -@Suppress("UNCHECKED_CAST") // it is checked... but whatever (except string set, because can't check for that)) -private fun settingsToJsonStream(settings: Map, out: OutputStream) { - val booleans = settings.filter { it.key is String && it.value is Boolean } as Map - val ints = settings.filter { it.key is String && it.value is Int } as Map - val longs = settings.filter { it.key is String && it.value is Long } as Map - val floats = settings.filter { it.key is String && it.value is Float } as Map - val strings = settings.filter { it.key is String && it.value is String } as Map - val stringSets = settings.filter { it.key is String && it.value is Set<*> } as Map> - // now write - out.write("boolean settings\n".toByteArray()) - out.write(Json.encodeToString(booleans).toByteArray()) - out.write("\nint settings\n".toByteArray()) - out.write(Json.encodeToString(ints).toByteArray()) - out.write("\nlong settings\n".toByteArray()) - out.write(Json.encodeToString(longs).toByteArray()) - out.write("\nfloat settings\n".toByteArray()) - out.write(Json.encodeToString(floats).toByteArray()) - out.write("\nstring settings\n".toByteArray()) - out.write(Json.encodeToString(strings).toByteArray()) - out.write("\nstring set settings\n".toByteArray()) - out.write(Json.encodeToString(stringSets).toByteArray()) -} - -private fun readJsonLinesToSettings(list: List, prefs: SharedPreferences): Boolean { - val i = list.iterator() - val e = prefs.edit() - try { - while (i.hasNext()) { - when (i.next()) { - "boolean settings" -> Json.decodeFromString>(i.next()).forEach { e.putBoolean(it.key, it.value) } - "int settings" -> Json.decodeFromString>(i.next()).forEach { e.putInt(it.key, it.value) } - "long settings" -> Json.decodeFromString>(i.next()).forEach { e.putLong(it.key, it.value) } - "float settings" -> Json.decodeFromString>(i.next()).forEach { e.putFloat(it.key, it.value) } - "string settings" -> Json.decodeFromString>(i.next()).forEach { e.putString(it.key, it.value) } - "string set settings" -> Json.decodeFromString>>(i.next()).forEach { e.putStringSet(it.key, it.value) } - } - } - e.apply() - return true - } catch (e: Exception) { - return false - } -} - -// todo (later): remove this when new package name has been in use for long enough, this is only for migrating from old openboard name -private fun upgradeFileNames(originalName: String): String { - return when { - originalName.endsWith(USER_DICTIONARY_SUFFIX) -> { - // replace directory after switch to language tag - val dirName = originalName.substringAfter(File.separator).substringBefore(File.separator) - originalName.replace(dirName, dirName.constructLocale().toLanguageTag()) - } - originalName.startsWith("blacklists") -> { - // replace file name after switch to language tag - val fileName = originalName.substringAfter("blacklists${File.separator}").substringBefore(".txt") - originalName.replace(fileName, fileName.constructLocale().toLanguageTag()) - } - originalName.startsWith("layouts") -> { - // replace file name after switch to language tag, but only if it's not a layout - val localeString = originalName.substringAfter(".").substringBefore(".") - if (localeString in listOf(LAYOUT_SYMBOLS, LAYOUT_SYMBOLS_SHIFTED, LAYOUT_SYMBOLS_ARABIC, LAYOUT_NUMBER, LAYOUT_NUMPAD, LAYOUT_NUMPAD_LANDSCAPE, LAYOUT_PHONE, LAYOUT_PHONE_SYMBOLS)) - return originalName // it's a layout! - val locale = localeString.constructLocale() - if (locale.toLanguageTag() != "und") - originalName.replace(localeString, locale.toLanguageTag()) - else - originalName // no valid locale -> must be symbols layout, don't change - } - originalName.startsWith("UserHistoryDictionary") -> { - val localeString = originalName.substringAfter(".").substringBefore(".") - val locale = localeString.constructLocale() - originalName.replace(localeString, locale.toLanguageTag()) - } - else -> originalName - } -} diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt index 4bcd342b..6a519d6f 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt @@ -1,15 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only package helium314.keyboard.settings.screens -import android.app.Activity import android.content.Context -import android.content.Intent -import android.graphics.BitmapFactory -import android.graphics.Typeface -import android.net.Uri import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,7 +10,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -25,35 +17,30 @@ import androidx.compose.ui.tooling.preview.Preview import helium314.keyboard.keyboard.KeyboardSwitcher import helium314.keyboard.keyboard.KeyboardTheme import helium314.keyboard.latin.R -import helium314.keyboard.latin.common.FileUtils import helium314.keyboard.latin.settings.ColorsNightSettingsFragment import helium314.keyboard.latin.settings.ColorsSettingsFragment import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.SettingsValues -import helium314.keyboard.latin.utils.DeviceProtectedUtils import helium314.keyboard.latin.utils.Log import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.getStringResourceOrName import helium314.keyboard.latin.utils.prefs import helium314.keyboard.latin.utils.switchTo import helium314.keyboard.settings.AllPrefs -import helium314.keyboard.settings.ListPreference +import helium314.keyboard.settings.preferences.ListPreference import helium314.keyboard.settings.NonSettingsPrefs import helium314.keyboard.settings.PrefDef -import helium314.keyboard.settings.Preference +import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 -import helium314.keyboard.settings.SliderPreference -import helium314.keyboard.settings.SwitchPreference +import helium314.keyboard.settings.preferences.SliderPreference +import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme -import helium314.keyboard.settings.dialogs.ConfirmationDialog import helium314.keyboard.settings.dialogs.CustomizeIconsDialog -import helium314.keyboard.settings.dialogs.InfoDialog import helium314.keyboard.settings.dialogs.TextInputDialog import helium314.keyboard.settings.keyboardNeedsReload -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.io.File +import helium314.keyboard.settings.preferences.BackgroundImagePref +import helium314.keyboard.settings.preferences.CustomFontPreference @Composable fun AppearanceScreen( @@ -301,53 +288,8 @@ fun createAppearancePrefs(context: Context) = listOf( ) } }, - PrefDef(context, NonSettingsPrefs.CUSTOM_FONT, R.string.custom_font) { def -> - val ctx = LocalContext.current - var showDialog by remember { mutableStateOf(false) } - var showErrorDialog by remember { mutableStateOf(false) } - val fontFile = Settings.getCustomFontFile(ctx) - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult - val uri = it.data?.data ?: return@rememberLauncherForActivityResult - val tempFile = File(DeviceProtectedUtils.getFilesDir(context), "temp_file") - FileUtils.copyContentUriToNewFile(uri, ctx, tempFile) - try { - Typeface.createFromFile(tempFile) - fontFile.delete() - tempFile.renameTo(fontFile) - Settings.clearCachedTypeface() - keyboardNeedsReload = true - } catch (_: Exception) { - showErrorDialog = true - tempFile.delete() - } - } - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType("*/*") - Preference( - name = def.title, - onClick = { - if (fontFile.exists()) - showDialog = true - else launcher.launch(intent) - }, - ) - if (showDialog) - ConfirmationDialog( - onDismissRequest = { showDialog = false }, - onConfirmed = { launcher.launch(intent) }, - onNeutral = { - fontFile.delete() - Settings.clearCachedTypeface() - keyboardNeedsReload = true - }, - neutralButtonText = stringResource(R.string.delete), - confirmButtonText = stringResource(R.string.load), - title = { Text(stringResource(R.string.custom_font)) } - ) - if (showErrorDialog) - InfoDialog(stringResource(R.string.file_read_error)) { showErrorDialog = false } + PrefDef(context, NonSettingsPrefs.CUSTOM_FONT, R.string.custom_font) { + CustomFontPreference(it) }, PrefDef(context, Settings.PREF_FONT_SCALE, R.string.prefs_font_scale) { def -> SliderPreference( @@ -369,100 +311,6 @@ fun createAppearancePrefs(context: Context) = listOf( }, ) -@Composable -private fun BackgroundImagePref(def: PrefDef, isLandscape: Boolean) { - var showDayNightDialog by remember { mutableStateOf(false) } - var showSelectionDialog by remember { mutableStateOf(false) } - var showErrorDialog by remember { mutableStateOf(false) } - var isNight by remember { mutableStateOf(false) } - val ctx = LocalContext.current - fun getFile() = Settings.getCustomBackgroundFile(ctx, isNight, isLandscape) - val b = (ctx.getActivity() as? SettingsActivity2)?.prefChanged?.collectAsState() - if ((b?.value ?: 0) < 0) // necessary to reload dayNightPref - Log.v("irrelevant", "stupid way to trigger recomposition on preference change") - val dayNightPref = Settings.readDayNightPref(ctx.prefs(), ctx.resources) - if (!dayNightPref) - isNight = false - val scope = rememberCoroutineScope() - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult - val uri = result.data?.data ?: return@rememberLauncherForActivityResult - showSelectionDialog = false - showDayNightDialog = false - scope.launch(Dispatchers.IO) { - if (!setBackgroundImage(ctx, uri, isNight, isLandscape)) - showErrorDialog = true - } - } - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType("image/*") - Preference( - name = def.title, - onClick = { - if (dayNightPref) { - showDayNightDialog = true - } else if (!getFile().exists()) { - launcher.launch(intent) - } else { - showSelectionDialog = true - } - } - ) - if (showDayNightDialog) { - ConfirmationDialog( - onDismissRequest = { showDayNightDialog = false }, - onConfirmed = { - isNight = false - if (getFile().exists()) - showSelectionDialog = true - else launcher.launch(intent) - }, - confirmButtonText = stringResource(R.string.day_or_night_day), - cancelButtonText = "", - onNeutral = { - isNight = true - if (getFile().exists()) - showSelectionDialog = true - else launcher.launch(intent) - }, - neutralButtonText = stringResource(R.string.day_or_night_night), - title = { Text(stringResource(R.string.day_or_night_image)) }, - ) - } - if (showSelectionDialog) { - ConfirmationDialog( - onDismissRequest = { showSelectionDialog = false }, - title = { Text(stringResource(R.string.customize_background_image)) }, - confirmButtonText = stringResource(R.string.button_load_custom), - onConfirmed = { launcher.launch(intent) }, - neutralButtonText = stringResource(R.string.delete), - onNeutral = { - getFile().delete() - Settings.clearCachedBackgroundImages() - keyboardNeedsReload = true - } - ) - } - if (showErrorDialog) { - InfoDialog(stringResource(R.string.file_read_error)) { showErrorDialog = false } - } -} - -private fun setBackgroundImage(ctx: Context, uri: Uri, isNight: Boolean, isLandscape: Boolean): Boolean { - val imageFile = Settings.getCustomBackgroundFile(ctx, isNight, isLandscape) - FileUtils.copyContentUriToNewFile(uri, ctx, imageFile) - keyboardNeedsReload = true - try { - BitmapFactory.decodeFile(imageFile.absolutePath) - } catch (_: Exception) { - imageFile.delete() - return false - } - Settings.clearCachedBackgroundImages() - return true -} - @Preview @Composable private fun Preview() { diff --git a/app/src/main/java/helium314/keyboard/settings/screens/DebugScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/DebugScreen.kt index 87639d80..231d6137 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/DebugScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/DebugScreen.kt @@ -22,10 +22,10 @@ import helium314.keyboard.latin.settings.DebugSettingsFragment import helium314.keyboard.latin.utils.prefs import helium314.keyboard.settings.AllPrefs import helium314.keyboard.settings.PrefDef -import helium314.keyboard.settings.Preference +import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 -import helium314.keyboard.settings.SwitchPreference +import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme import helium314.keyboard.settings.dialogs.ConfirmationDialog import helium314.keyboard.settings.keyboardNeedsReload diff --git a/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt index 26ab6d8d..3412d4ef 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt @@ -17,8 +17,8 @@ import helium314.keyboard.settings.AllPrefs import helium314.keyboard.settings.PrefDef import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 -import helium314.keyboard.settings.SliderPreference -import helium314.keyboard.settings.SwitchPreference +import helium314.keyboard.settings.preferences.SliderPreference +import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme import helium314.keyboard.settings.keyboardNeedsReload diff --git a/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt index fea94e00..afe4b901 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt @@ -25,8 +25,8 @@ import helium314.keyboard.latin.settings.ToolbarSettingsFragment import helium314.keyboard.latin.utils.JniUtils import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.switchTo -import helium314.keyboard.settings.Preference -import helium314.keyboard.settings.PreferenceCategory +import helium314.keyboard.settings.preferences.Preference +import helium314.keyboard.settings.preferences.PreferenceCategory import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.Theme diff --git a/app/src/main/java/helium314/keyboard/settings/screens/PreferencesScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/PreferencesScreen.kt index 45bca012..5d7cad20 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/PreferencesScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/PreferencesScreen.kt @@ -23,13 +23,13 @@ import helium314.keyboard.latin.utils.getEnabledSubtypes import helium314.keyboard.latin.utils.locale import helium314.keyboard.latin.utils.prefs import helium314.keyboard.settings.AllPrefs -import helium314.keyboard.settings.ListPreference +import helium314.keyboard.settings.preferences.ListPreference import helium314.keyboard.settings.PrefDef -import helium314.keyboard.settings.ReorderSwitchPreference +import helium314.keyboard.settings.preferences.ReorderSwitchPreference import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 -import helium314.keyboard.settings.SliderPreference -import helium314.keyboard.settings.SwitchPreference +import helium314.keyboard.settings.preferences.SliderPreference +import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme import helium314.keyboard.settings.keyboardNeedsReload diff --git a/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt index 8248bb41..20c517d4 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt @@ -29,13 +29,13 @@ import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.prefs import helium314.keyboard.latin.utils.switchTo import helium314.keyboard.settings.AllPrefs -import helium314.keyboard.settings.ListPreference +import helium314.keyboard.settings.preferences.ListPreference import helium314.keyboard.settings.NonSettingsPrefs import helium314.keyboard.settings.PrefDef -import helium314.keyboard.settings.Preference +import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 -import helium314.keyboard.settings.SwitchPreference +import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme import helium314.keyboard.settings.dialogs.ConfirmationDialog import helium314.keyboard.settings.keyboardNeedsReload diff --git a/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt index 0adef00b..fcf88417 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt @@ -32,11 +32,11 @@ import helium314.keyboard.latin.utils.defaultToolbarPref import helium314.keyboard.settings.AllPrefs import helium314.keyboard.settings.NonSettingsPrefs import helium314.keyboard.settings.PrefDef -import helium314.keyboard.settings.Preference -import helium314.keyboard.settings.ReorderSwitchPreference +import helium314.keyboard.settings.preferences.Preference +import helium314.keyboard.settings.preferences.ReorderSwitchPreference import helium314.keyboard.settings.SearchPrefScreen import helium314.keyboard.settings.SettingsActivity2 -import helium314.keyboard.settings.SwitchPreference +import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme import helium314.keyboard.settings.dialogs.ToolbarKeysCustomizer import helium314.keyboard.settings.keyboardNeedsReload