mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-19 21:59:11 +00:00
move large preferences to separate files
This commit is contained in:
parent
0f4ac58582
commit
b62eaba3e4
21 changed files with 1147 additions and 1010 deletions
|
@ -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 <T: Number> SliderPreference(
|
|
||||||
name: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
pref: String,
|
|
||||||
description: @Composable (T) -> String,
|
|
||||||
default: T,
|
|
||||||
range: ClosedFloatingPointRange<Float>,
|
|
||||||
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 <T: Any> ListPreference(
|
|
||||||
def: PrefDef,
|
|
||||||
items: List<Pair<String, T>>,
|
|
||||||
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 <T: Any> 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 <T: Any> 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -43,6 +43,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import helium314.keyboard.latin.R
|
import helium314.keyboard.latin.R
|
||||||
|
import helium314.keyboard.settings.preferences.PreferenceCategory
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchPrefScreen(
|
fun SearchPrefScreen(
|
||||||
|
|
|
@ -14,7 +14,6 @@ import helium314.keyboard.latin.utils.prefs
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
// todo (roughly in order)
|
// 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
|
// 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)
|
// the prefDef and AllPrefs, also be clear about pref <-> key <-> prefKey (all used, probably should be latter)
|
||||||
// there is a lot more ambiguous naming...
|
// there is a lot more ambiguous naming...
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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<File>()
|
||||||
|
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<File>()
|
||||||
|
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<String?, Any?>, out: OutputStream) {
|
||||||
|
val booleans = settings.filter { it.key is String && it.value is Boolean } as Map<String, Boolean>
|
||||||
|
val ints = settings.filter { it.key is String && it.value is Int } as Map<String, Int>
|
||||||
|
val longs = settings.filter { it.key is String && it.value is Long } as Map<String, Long>
|
||||||
|
val floats = settings.filter { it.key is String && it.value is Float } as Map<String, Float>
|
||||||
|
val strings = settings.filter { it.key is String && it.value is String } as Map<String, String>
|
||||||
|
val stringSets = settings.filter { it.key is String && it.value is Set<*> } as Map<String, Set<String>>
|
||||||
|
// 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<String>, prefs: SharedPreferences): Boolean {
|
||||||
|
val i = list.iterator()
|
||||||
|
val e = prefs.edit()
|
||||||
|
try {
|
||||||
|
while (i.hasNext()) {
|
||||||
|
when (i.next()) {
|
||||||
|
"boolean settings" -> Json.decodeFromString<Map<String, Boolean>>(i.next()).forEach { e.putBoolean(it.key, it.value) }
|
||||||
|
"int settings" -> Json.decodeFromString<Map<String, Int>>(i.next()).forEach { e.putInt(it.key, it.value) }
|
||||||
|
"long settings" -> Json.decodeFromString<Map<String, Long>>(i.next()).forEach { e.putLong(it.key, it.value) }
|
||||||
|
"float settings" -> Json.decodeFromString<Map<String, Float>>(i.next()).forEach { e.putFloat(it.key, it.value) }
|
||||||
|
"string settings" -> Json.decodeFromString<Map<String, String>>(i.next()).forEach { e.putString(it.key, it.value) }
|
||||||
|
"string set settings" -> Json.decodeFromString<Map<String, Set<String>>>(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"
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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<String>,
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()) ?: "")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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 <T: Number> SliderPreference(
|
||||||
|
name: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
pref: String,
|
||||||
|
description: @Composable (T) -> String,
|
||||||
|
default: T,
|
||||||
|
range: ClosedFloatingPointRange<Float>,
|
||||||
|
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 <T: Any> ListPreference(
|
||||||
|
def: PrefDef,
|
||||||
|
items: List<Pair<String, T>>,
|
||||||
|
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 <T: Any> 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 <T: Any> 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}")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ import helium314.keyboard.latin.utils.prefs
|
||||||
import helium314.keyboard.settings.AllPrefs
|
import helium314.keyboard.settings.AllPrefs
|
||||||
import helium314.keyboard.settings.NonSettingsPrefs
|
import helium314.keyboard.settings.NonSettingsPrefs
|
||||||
import helium314.keyboard.settings.PrefDef
|
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.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
import helium314.keyboard.settings.SettingsActivity2
|
||||||
import helium314.keyboard.settings.Theme
|
import helium314.keyboard.settings.Theme
|
||||||
|
|
|
@ -2,14 +2,8 @@
|
||||||
package helium314.keyboard.settings.screens
|
package helium314.keyboard.settings.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
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.Icon
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
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.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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.KeyboardActionListener
|
||||||
import helium314.keyboard.keyboard.KeyboardLayoutSet
|
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.keyboard.internal.keyboard_parser.RawKeyboardParser
|
||||||
import helium314.keyboard.latin.BuildConfig
|
import helium314.keyboard.latin.BuildConfig
|
||||||
import helium314.keyboard.latin.R
|
import helium314.keyboard.latin.R
|
||||||
import helium314.keyboard.latin.SystemBroadcastReceiver
|
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.common.splitOnWhitespace
|
||||||
import helium314.keyboard.latin.settings.DebugSettings
|
import helium314.keyboard.latin.settings.DebugSettings
|
||||||
import helium314.keyboard.latin.settings.Settings
|
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_NORMAL
|
||||||
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS
|
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_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED
|
||||||
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
|
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.getStringResourceOrName
|
||||||
import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged
|
|
||||||
import helium314.keyboard.latin.utils.prefs
|
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.AllPrefs
|
||||||
import helium314.keyboard.settings.ListPreference
|
import helium314.keyboard.settings.preferences.ListPreference
|
||||||
import helium314.keyboard.settings.NonSettingsPrefs
|
import helium314.keyboard.settings.NonSettingsPrefs
|
||||||
import helium314.keyboard.settings.PrefDef
|
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.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
import helium314.keyboard.settings.SettingsActivity2
|
||||||
import helium314.keyboard.settings.SettingsDestination
|
import helium314.keyboard.settings.SettingsDestination
|
||||||
import helium314.keyboard.settings.SliderPreference
|
import helium314.keyboard.settings.preferences.SliderPreference
|
||||||
import helium314.keyboard.settings.SwitchPreference
|
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||||
import helium314.keyboard.settings.Theme
|
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.dialogs.TextInputDialog
|
||||||
import helium314.keyboard.settings.keyboardNeedsReload
|
import helium314.keyboard.settings.keyboardNeedsReload
|
||||||
import kotlinx.serialization.json.Json
|
import helium314.keyboard.settings.preferences.BackupRestorePreference
|
||||||
import java.io.File
|
import helium314.keyboard.settings.preferences.LayoutEditPreference
|
||||||
import java.io.FileInputStream
|
import helium314.keyboard.settings.preferences.LoadGestureLibPreference
|
||||||
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
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdvancedSettingsScreen(
|
fun AdvancedSettingsScreen(
|
||||||
|
@ -236,241 +194,24 @@ fun createAdvancedPrefs(context: Context) = listOf(
|
||||||
ListPreference(def, items, "main") { KeyboardLayoutSet.onSystemLocaleChanged() }
|
ListPreference(def, items, "main") { KeyboardLayoutSet.onSystemLocaleChanged() }
|
||||||
},
|
},
|
||||||
PrefDef(context, NonSettingsPrefs.CUSTOM_SYMBOLS_NUMBER_LAYOUTS, R.string.customize_symbols_number_layouts) { def ->
|
PrefDef(context, NonSettingsPrefs.CUSTOM_SYMBOLS_NUMBER_LAYOUTS, R.string.customize_symbols_number_layouts) { def ->
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
LayoutEditPreference(
|
||||||
val ctx = LocalContext.current
|
def = def,
|
||||||
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 = RawKeyboardParser.symbolAndNumberLayouts,
|
items = RawKeyboardParser.symbolAndNumberLayouts,
|
||||||
getItemName = { it.getStringResourceOrName("layout_", ctx) },
|
getItemName = { it.getStringResourceOrName("layout_", LocalContext.current) },
|
||||||
onItemSelected = { layout = it },
|
getDefaultLayout = { LocalContext.current.assets.list("layouts")?.firstOrNull { it.startsWith("$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 ->
|
PrefDef(context, NonSettingsPrefs.CUSTOM_FUNCTIONAL_LAYOUTS, R.string.customize_functional_key_layouts) { def ->
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
LayoutEditPreference(
|
||||||
val ctx = LocalContext.current
|
def = def,
|
||||||
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 = listOf(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED)
|
items = listOf(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED)
|
||||||
.map { it.substringBeforeLast(".") },
|
.map { it.substringBeforeLast(".") },
|
||||||
getItemName = { it.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", ctx) },
|
getItemName = { it.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", LocalContext.current) },
|
||||||
onItemSelected = { layout = it },
|
getDefaultLayout = { if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json" }
|
||||||
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 ->
|
PrefDef(context, NonSettingsPrefs.BACKUP_RESTORE, R.string.backup_restore_title) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
BackupRestorePreference(it)
|
||||||
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<File>()
|
|
||||||
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<File>()
|
|
||||||
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.DEBUG_SETTINGS, R.string.debug_settings_title) {
|
PrefDef(context, NonSettingsPrefs.DEBUG_SETTINGS, R.string.debug_settings_title) {
|
||||||
Preference(
|
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) {
|
PrefDef(context, Settings.PREF_URL_DETECTION, R.string.url_detection_title, R.string.url_detection_summary) {
|
||||||
SwitchPreference(it, false)
|
SwitchPreference(it, false)
|
||||||
},
|
},
|
||||||
PrefDef(context, NonSettingsPrefs.LOAD_GESTURE_LIB, R.string.load_gesture_library, R.string.load_gesture_library_summary) { def ->
|
PrefDef(context, NonSettingsPrefs.LOAD_GESTURE_LIB, R.string.load_gesture_library, R.string.load_gesture_library_summary) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
LoadGestureLibPreference(it)
|
||||||
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()) ?: "")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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<String?, Any?>, out: OutputStream) {
|
|
||||||
val booleans = settings.filter { it.key is String && it.value is Boolean } as Map<String, Boolean>
|
|
||||||
val ints = settings.filter { it.key is String && it.value is Int } as Map<String, Int>
|
|
||||||
val longs = settings.filter { it.key is String && it.value is Long } as Map<String, Long>
|
|
||||||
val floats = settings.filter { it.key is String && it.value is Float } as Map<String, Float>
|
|
||||||
val strings = settings.filter { it.key is String && it.value is String } as Map<String, String>
|
|
||||||
val stringSets = settings.filter { it.key is String && it.value is Set<*> } as Map<String, Set<String>>
|
|
||||||
// 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<String>, prefs: SharedPreferences): Boolean {
|
|
||||||
val i = list.iterator()
|
|
||||||
val e = prefs.edit()
|
|
||||||
try {
|
|
||||||
while (i.hasNext()) {
|
|
||||||
when (i.next()) {
|
|
||||||
"boolean settings" -> Json.decodeFromString<Map<String, Boolean>>(i.next()).forEach { e.putBoolean(it.key, it.value) }
|
|
||||||
"int settings" -> Json.decodeFromString<Map<String, Int>>(i.next()).forEach { e.putInt(it.key, it.value) }
|
|
||||||
"long settings" -> Json.decodeFromString<Map<String, Long>>(i.next()).forEach { e.putLong(it.key, it.value) }
|
|
||||||
"float settings" -> Json.decodeFromString<Map<String, Float>>(i.next()).forEach { e.putFloat(it.key, it.value) }
|
|
||||||
"string settings" -> Json.decodeFromString<Map<String, String>>(i.next()).forEach { e.putString(it.key, it.value) }
|
|
||||||
"string set settings" -> Json.decodeFromString<Map<String, Set<String>>>(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
package helium314.keyboard.settings.screens
|
package helium314.keyboard.settings.screens
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
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 android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -17,7 +10,6 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
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.KeyboardSwitcher
|
||||||
import helium314.keyboard.keyboard.KeyboardTheme
|
import helium314.keyboard.keyboard.KeyboardTheme
|
||||||
import helium314.keyboard.latin.R
|
import helium314.keyboard.latin.R
|
||||||
import helium314.keyboard.latin.common.FileUtils
|
|
||||||
import helium314.keyboard.latin.settings.ColorsNightSettingsFragment
|
import helium314.keyboard.latin.settings.ColorsNightSettingsFragment
|
||||||
import helium314.keyboard.latin.settings.ColorsSettingsFragment
|
import helium314.keyboard.latin.settings.ColorsSettingsFragment
|
||||||
import helium314.keyboard.latin.settings.Settings
|
import helium314.keyboard.latin.settings.Settings
|
||||||
import helium314.keyboard.latin.settings.SettingsValues
|
import helium314.keyboard.latin.settings.SettingsValues
|
||||||
import helium314.keyboard.latin.utils.DeviceProtectedUtils
|
|
||||||
import helium314.keyboard.latin.utils.Log
|
import helium314.keyboard.latin.utils.Log
|
||||||
import helium314.keyboard.latin.utils.getActivity
|
import helium314.keyboard.latin.utils.getActivity
|
||||||
import helium314.keyboard.latin.utils.getStringResourceOrName
|
import helium314.keyboard.latin.utils.getStringResourceOrName
|
||||||
import helium314.keyboard.latin.utils.prefs
|
import helium314.keyboard.latin.utils.prefs
|
||||||
import helium314.keyboard.latin.utils.switchTo
|
import helium314.keyboard.latin.utils.switchTo
|
||||||
import helium314.keyboard.settings.AllPrefs
|
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.NonSettingsPrefs
|
||||||
import helium314.keyboard.settings.PrefDef
|
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.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
import helium314.keyboard.settings.SettingsActivity2
|
||||||
import helium314.keyboard.settings.SliderPreference
|
import helium314.keyboard.settings.preferences.SliderPreference
|
||||||
import helium314.keyboard.settings.SwitchPreference
|
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||||
import helium314.keyboard.settings.Theme
|
import helium314.keyboard.settings.Theme
|
||||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
|
||||||
import helium314.keyboard.settings.dialogs.CustomizeIconsDialog
|
import helium314.keyboard.settings.dialogs.CustomizeIconsDialog
|
||||||
import helium314.keyboard.settings.dialogs.InfoDialog
|
|
||||||
import helium314.keyboard.settings.dialogs.TextInputDialog
|
import helium314.keyboard.settings.dialogs.TextInputDialog
|
||||||
import helium314.keyboard.settings.keyboardNeedsReload
|
import helium314.keyboard.settings.keyboardNeedsReload
|
||||||
import kotlinx.coroutines.Dispatchers
|
import helium314.keyboard.settings.preferences.BackgroundImagePref
|
||||||
import kotlinx.coroutines.launch
|
import helium314.keyboard.settings.preferences.CustomFontPreference
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppearanceScreen(
|
fun AppearanceScreen(
|
||||||
|
@ -301,53 +288,8 @@ fun createAppearancePrefs(context: Context) = listOf(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PrefDef(context, NonSettingsPrefs.CUSTOM_FONT, R.string.custom_font) { def ->
|
PrefDef(context, NonSettingsPrefs.CUSTOM_FONT, R.string.custom_font) {
|
||||||
val ctx = LocalContext.current
|
CustomFontPreference(it)
|
||||||
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, Settings.PREF_FONT_SCALE, R.string.prefs_font_scale) { def ->
|
PrefDef(context, Settings.PREF_FONT_SCALE, R.string.prefs_font_scale) { def ->
|
||||||
SliderPreference(
|
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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preview() {
|
private fun Preview() {
|
||||||
|
|
|
@ -22,10 +22,10 @@ import helium314.keyboard.latin.settings.DebugSettingsFragment
|
||||||
import helium314.keyboard.latin.utils.prefs
|
import helium314.keyboard.latin.utils.prefs
|
||||||
import helium314.keyboard.settings.AllPrefs
|
import helium314.keyboard.settings.AllPrefs
|
||||||
import helium314.keyboard.settings.PrefDef
|
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.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
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.Theme
|
||||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
||||||
import helium314.keyboard.settings.keyboardNeedsReload
|
import helium314.keyboard.settings.keyboardNeedsReload
|
||||||
|
|
|
@ -17,8 +17,8 @@ import helium314.keyboard.settings.AllPrefs
|
||||||
import helium314.keyboard.settings.PrefDef
|
import helium314.keyboard.settings.PrefDef
|
||||||
import helium314.keyboard.settings.SearchPrefScreen
|
import helium314.keyboard.settings.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
import helium314.keyboard.settings.SettingsActivity2
|
||||||
import helium314.keyboard.settings.SliderPreference
|
import helium314.keyboard.settings.preferences.SliderPreference
|
||||||
import helium314.keyboard.settings.SwitchPreference
|
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||||
import helium314.keyboard.settings.Theme
|
import helium314.keyboard.settings.Theme
|
||||||
import helium314.keyboard.settings.keyboardNeedsReload
|
import helium314.keyboard.settings.keyboardNeedsReload
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,8 @@ import helium314.keyboard.latin.settings.ToolbarSettingsFragment
|
||||||
import helium314.keyboard.latin.utils.JniUtils
|
import helium314.keyboard.latin.utils.JniUtils
|
||||||
import helium314.keyboard.latin.utils.getActivity
|
import helium314.keyboard.latin.utils.getActivity
|
||||||
import helium314.keyboard.latin.utils.switchTo
|
import helium314.keyboard.latin.utils.switchTo
|
||||||
import helium314.keyboard.settings.Preference
|
import helium314.keyboard.settings.preferences.Preference
|
||||||
import helium314.keyboard.settings.PreferenceCategory
|
import helium314.keyboard.settings.preferences.PreferenceCategory
|
||||||
import helium314.keyboard.settings.SearchPrefScreen
|
import helium314.keyboard.settings.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.Theme
|
import helium314.keyboard.settings.Theme
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,13 @@ import helium314.keyboard.latin.utils.getEnabledSubtypes
|
||||||
import helium314.keyboard.latin.utils.locale
|
import helium314.keyboard.latin.utils.locale
|
||||||
import helium314.keyboard.latin.utils.prefs
|
import helium314.keyboard.latin.utils.prefs
|
||||||
import helium314.keyboard.settings.AllPrefs
|
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.PrefDef
|
||||||
import helium314.keyboard.settings.ReorderSwitchPreference
|
import helium314.keyboard.settings.preferences.ReorderSwitchPreference
|
||||||
import helium314.keyboard.settings.SearchPrefScreen
|
import helium314.keyboard.settings.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
import helium314.keyboard.settings.SettingsActivity2
|
||||||
import helium314.keyboard.settings.SliderPreference
|
import helium314.keyboard.settings.preferences.SliderPreference
|
||||||
import helium314.keyboard.settings.SwitchPreference
|
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||||
import helium314.keyboard.settings.Theme
|
import helium314.keyboard.settings.Theme
|
||||||
import helium314.keyboard.settings.keyboardNeedsReload
|
import helium314.keyboard.settings.keyboardNeedsReload
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,13 @@ import helium314.keyboard.latin.utils.getActivity
|
||||||
import helium314.keyboard.latin.utils.prefs
|
import helium314.keyboard.latin.utils.prefs
|
||||||
import helium314.keyboard.latin.utils.switchTo
|
import helium314.keyboard.latin.utils.switchTo
|
||||||
import helium314.keyboard.settings.AllPrefs
|
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.NonSettingsPrefs
|
||||||
import helium314.keyboard.settings.PrefDef
|
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.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
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.Theme
|
||||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
||||||
import helium314.keyboard.settings.keyboardNeedsReload
|
import helium314.keyboard.settings.keyboardNeedsReload
|
||||||
|
|
|
@ -32,11 +32,11 @@ import helium314.keyboard.latin.utils.defaultToolbarPref
|
||||||
import helium314.keyboard.settings.AllPrefs
|
import helium314.keyboard.settings.AllPrefs
|
||||||
import helium314.keyboard.settings.NonSettingsPrefs
|
import helium314.keyboard.settings.NonSettingsPrefs
|
||||||
import helium314.keyboard.settings.PrefDef
|
import helium314.keyboard.settings.PrefDef
|
||||||
import helium314.keyboard.settings.Preference
|
import helium314.keyboard.settings.preferences.Preference
|
||||||
import helium314.keyboard.settings.ReorderSwitchPreference
|
import helium314.keyboard.settings.preferences.ReorderSwitchPreference
|
||||||
import helium314.keyboard.settings.SearchPrefScreen
|
import helium314.keyboard.settings.SearchPrefScreen
|
||||||
import helium314.keyboard.settings.SettingsActivity2
|
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.Theme
|
||||||
import helium314.keyboard.settings.dialogs.ToolbarKeysCustomizer
|
import helium314.keyboard.settings.dialogs.ToolbarKeysCustomizer
|
||||||
import helium314.keyboard.settings.keyboardNeedsReload
|
import helium314.keyboard.settings.keyboardNeedsReload
|
||||||
|
|
Loading…
Add table
Reference in a new issue