Newer
Older
package org.futo.inputmethod.latin.uix
import android.content.Context
Aleksandras Kostarevas
committed
import android.content.SharedPreferences
import android.os.UserManager
import android.preference.PreferenceManager
import androidx.datastore.core.DataStore
Aleksandras Kostarevas
committed
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
Aleksandras Kostarevas
committed
import androidx.datastore.preferences.core.preferencesOf
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
Aleksandras Kostarevas
committed
import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
Aleksandras Kostarevas
committed
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
Aleksandras Kostarevas
committed
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
Aleksandras Kostarevas
committed
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
Aleksandras Kostarevas
committed
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
Aleksandras Kostarevas
committed
import org.futo.inputmethod.latin.ActiveSubtype
import org.futo.inputmethod.latin.Subtypes
import org.futo.inputmethod.latin.SubtypesSetting
import org.futo.inputmethod.latin.uix.theme.presets.ClassicMaterialDark
import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme
Aleksandras Kostarevas
committed
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// Used before first unlock (direct boot)
private object DefaultDataStore : DataStore<Preferences> {
private var activePreferences = preferencesOf(
ActiveSubtype.key to "en_US:",
SubtypesSetting.key to setOf("en_US:"),
THEME_KEY.key to ClassicMaterialDark.key,
KeyHintsSetting.key to true
)
var subtypesInitialized = false
suspend fun updateSubtypes(subtypes: Set<String>) {
val newPreferences = activePreferences.toMutablePreferences()
newPreferences[SubtypesSetting.key] = subtypes
activePreferences = newPreferences
sharedData.emit(activePreferences)
}
val sharedData = MutableSharedFlow<Preferences>(1)
override val data: Flow<Preferences>
get() {
return unlockedDataStore?.data ?: sharedData
}
init {
sharedData.tryEmit(activePreferences)
}
override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences {
return unlockedDataStore?.updateData(transform) ?: run {
val newActiveSubtype = transform(activePreferences)[ActiveSubtype.key]
if(newActiveSubtype != null && newActiveSubtype != activePreferences[ActiveSubtype.key]) {
val newPreferences = activePreferences.toMutablePreferences()
newPreferences[ActiveSubtype.key] = newActiveSubtype
activePreferences = newPreferences
sharedData.emit(newPreferences)
}
return activePreferences
}
}
}
// Set and used after first unlock (direct boot)
private var unlockedDataStore: DataStore<Preferences>? = null
// To prevent two threads trying to create a datastore at once
private val dataStoreCreationMutex = Mutex()
Aleksandras Kostarevas
committed
// Initializes unlockedDataStore, or uses DefaultDataStore if device is still locked (direct boot)
@OptIn(DelicateCoroutinesApi::class)
val Context.dataStore: DataStore<Preferences>
get() {
return unlockedDataStore ?: run {
val userManager = getSystemService(Context.USER_SERVICE) as UserManager
return if (userManager.isUserUnlocked && dataStoreCreationMutex.tryLock()) {
try {
// The device has been unlocked
val newDataStore = PreferenceDataStoreFactory.create(
corruptionHandler = null,
migrations = listOf(),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) {
applicationContext.preferencesDataStoreFile("settings")
}
Aleksandras Kostarevas
committed
unlockedDataStore = newDataStore
Aleksandras Kostarevas
committed
// Send new values to the DefaultDataStore for any listeners
GlobalScope.launch {
newDataStore.data.collect { value ->
DefaultDataStore.sharedData.emit(value)
}
Aleksandras Kostarevas
committed
}
newDataStore
} finally {
dataStoreCreationMutex.unlock()
}
// The device is still locked (or an unlocked datastore is in the process of being
// created), return default data store
if (!DefaultDataStore.subtypesInitialized) {
DefaultDataStore.subtypesInitialized = true
GlobalScope.launch {
DefaultDataStore.updateSubtypes(Subtypes.getDirectBootInitialLayouts(this@dataStore))
}
Aleksandras Kostarevas
committed
}
Aleksandras Kostarevas
committed
}
}
object PreferenceUtils {
fun getDefaultSharedPreferences(context: Context): SharedPreferences {
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
return if (userManager.isUserUnlocked) {
PreferenceManager.getDefaultSharedPreferences(context)
} else {
PreferenceManager.getDefaultSharedPreferences(context.createDeviceProtectedStorageContext())
}
}
}
val Context.isDirectBootUnlocked: Boolean
get() {
val userManager = getSystemService(Context.USER_SERVICE) as UserManager
return userManager.isUserUnlocked
}
class DataStoreHelper {
@OptIn(DelicateCoroutinesApi::class)
companion object {
private var initialized: Boolean = false
private var currentPreferences: Preferences = preferencesOf()
@JvmStatic
fun init(context: Context) {
if(initialized) return
initialized = true
GlobalScope.launch {
context.dataStore.data.collect {
currentPreferences = it
}
}
}
@JvmStatic
fun<T> getSettingOrNull(key: Preferences.Key<T>): T? = currentPreferences[key]
@JvmStatic
fun<T> getSetting(key: Preferences.Key<T>, default: T): T = getSettingOrNull(key) ?: default
@JvmStatic
fun<T> getSettingOrNull(setting: SettingsKey<T>): T? = getSettingOrNull(setting.key)
@JvmStatic
fun<T> getSetting(setting: SettingsKey<T>): T = getSettingOrNull(setting.key) ?: setting.default
}
}
suspend fun <T> Context.getSetting(key: Preferences.Key<T>, default: T): T {
/*val valueFlow: Flow<T> =
this.dataStore.data.map { preferences -> preferences[key] ?: default }.take(1)
return valueFlow.first()*/
return DataStoreHelper.getSetting(key, default)
fun <T> Context.getSettingFlow(key: Preferences.Key<T>, default: T): Flow<T> {
return dataStore.data.map { preferences -> preferences[key] ?: default }
suspend fun <T> Context.setSetting(key: Preferences.Key<T>, value: T) {
this.dataStore.edit { preferences ->
preferences[key] = value
}
}
fun <T> Context.getSettingBlocking(key: Preferences.Key<T>, default: T): T {
val context = this
return runBlocking {
context.getSetting(key, default)
}*/
return DataStoreHelper.getSetting(key, default)
fun <T> Context.getSettingBlocking(key: SettingsKey<T>): T {
return getSettingBlocking(key.key, key.default)
}
fun <T> Context.setSettingBlocking(key: Preferences.Key<T>, value: T) {
val context = this
runBlocking {
context.setSetting(key, value)
}
}
fun <T> LifecycleOwner.deferGetSetting(key: Preferences.Key<T>, default: T, onObtained: (T) -> Unit): Job {
val context = (this as Context)
return lifecycleScope.launch {
withContext(Dispatchers.Default) {
val value = context.getSetting(key, default)
withContext(Dispatchers.Main) {
onObtained(value)
}
}
}
}
fun <T> LifecycleOwner.deferSetSetting(key: Preferences.Key<T>, value: T): Job {
val context = (this as Context)
return lifecycleScope.launch {
withContext(Dispatchers.Default) {
context.setSetting(key, value)
}
}
}
data class SettingsKey<T>(
val key: Preferences.Key<T>,
val default: T
)
suspend fun <T> Context.getSetting(key: SettingsKey<T>): T {
return getSetting(key.key, key.default)
}
fun <T> Context.getSettingFlow(key: SettingsKey<T>): Flow<T> {
return getSettingFlow(key.key, key.default)
}
suspend fun <T> Context.setSetting(key: SettingsKey<T>, value: T) {
return setSetting(key.key, value)
}
fun <T> LifecycleOwner.deferGetSetting(key: SettingsKey<T>, onObtained: (T) -> Unit): Job {
return deferGetSetting(key.key, key.default, onObtained)
fun <T> LifecycleOwner.deferSetSetting(key: SettingsKey<T>, value: T): Job {
return deferSetSetting(key.key, value)
}
val THEME_KEY = SettingsKey(
key = stringPreferencesKey("activeThemeOption"),
default = DynamicSystemTheme.key
)
val USE_SYSTEM_VOICE_INPUT = SettingsKey(
key = booleanPreferencesKey("useSystemVoiceInput"),
default = false
)
val USE_TRANSFORMER_FINETUNING = SettingsKey(
key = booleanPreferencesKey("useTransformerFinetuning"),
default = false
)
val SUGGESTION_BLACKLIST = SettingsKey(
key = stringSetPreferencesKey("suggestionBlacklist"),
default = setOf()
)
val SHOW_EMOJI_SUGGESTIONS = SettingsKey(
key = booleanPreferencesKey("suggestEmojis"),