Add support for Direct Boot by loading default preferences for first unlock

This commit is contained in:
Aleksandras Kostarevas 2024-07-13 07:24:48 +03:00
parent 7d492897cc
commit b6206e3059
17 changed files with 279 additions and 83 deletions

View File

@ -32,8 +32,6 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:allowBackup="true" android:allowBackup="true"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:largeHeap="true" android:largeHeap="true"
android:name=".CrashLoggingApplication"> android:name=".CrashLoggingApplication">
@ -41,6 +39,7 @@
<service android:name=".LatinIME" <service android:name=".LatinIME"
android:label="@string/english_ime_name" android:label="@string/english_ime_name"
android:permission="android.permission.BIND_INPUT_METHOD" android:permission="android.permission.BIND_INPUT_METHOD"
android:directBootAware="true"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.view.InputMethod"/> <action android:name="android.view.InputMethod"/>

View File

@ -159,9 +159,7 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
} }
public static KeyboardTheme getKeyboardTheme(final Context context) { public static KeyboardTheme getKeyboardTheme(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); return KEYBOARD_THEMES[0];
final KeyboardTheme[] availableThemeArray = getAvailableThemeArray(context);
return getKeyboardTheme(prefs, BuildCompatUtils.EFFECTIVE_SDK_INT, availableThemeArray);
} }
/* package private for testing */ /* package private for testing */

View File

@ -200,9 +200,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
PointerTracker.init(mainKeyboardViewAttr, mTimerHandler, this /* DrawingProxy */); PointerTracker.init(mainKeyboardViewAttr, mTimerHandler, this /* DrawingProxy */);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final boolean forceNonDistinctMultitouch = false;
final boolean forceNonDistinctMultitouch = prefs.getBoolean(
DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, false);
final boolean hasDistinctMultitouch = context.getPackageManager() final boolean hasDistinctMultitouch = context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT) .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)
&& !forceNonDistinctMultitouch; && !forceNonDistinctMultitouch;

View File

@ -1,5 +1,9 @@
package org.futo.inputmethod.latin package org.futo.inputmethod.latin
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
@ -19,7 +23,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -50,6 +53,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.settings.Settings
import org.futo.inputmethod.latin.uix.BasicThemeProvider import org.futo.inputmethod.latin.uix.BasicThemeProvider
import org.futo.inputmethod.latin.uix.DynamicThemeProvider import org.futo.inputmethod.latin.uix.DynamicThemeProvider
import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner
@ -67,6 +71,7 @@ import org.futo.inputmethod.latin.uix.differsFrom
import org.futo.inputmethod.latin.uix.getSetting import org.futo.inputmethod.latin.uix.getSetting
import org.futo.inputmethod.latin.uix.getSettingBlocking import org.futo.inputmethod.latin.uix.getSettingBlocking
import org.futo.inputmethod.latin.uix.getSettingFlow import org.futo.inputmethod.latin.uix.getSettingFlow
import org.futo.inputmethod.latin.uix.isDirectBootUnlocked
import org.futo.inputmethod.latin.uix.setSetting import org.futo.inputmethod.latin.uix.setSetting
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.ThemeOption import org.futo.inputmethod.latin.uix.theme.ThemeOption
@ -76,10 +81,18 @@ import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator
import org.futo.inputmethod.updates.scheduleUpdateCheckingJob import org.futo.inputmethod.updates.scheduleUpdateCheckingJob
private class UnlockedBroadcastReceiver(val onDeviceUnlocked: () -> Unit) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
println("Unlocked Broadcast Receiver: ${intent?.action}")
if (intent?.action == Intent.ACTION_USER_UNLOCKED) {
onDeviceUnlocked()
}
}
}
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner,
LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner { LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner {
private lateinit var mLifecycleRegistry: LifecycleRegistry private lateinit var mLifecycleRegistry: LifecycleRegistry
private lateinit var mViewModelStore: ViewModelStore private lateinit var mViewModelStore: ViewModelStore
private lateinit var mSavedStateRegistryController: SavedStateRegistryController private lateinit var mSavedStateRegistryController: SavedStateRegistryController
@ -222,9 +235,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
jobs.clear() jobs.clear()
} }
private var unlockReceiver = UnlockedBroadcastReceiver { onDeviceUnlocked() }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED)
registerReceiver(unlockReceiver, filter)
mLifecycleRegistry = LifecycleRegistry(this) mLifecycleRegistry = LifecycleRegistry(this)
mLifecycleRegistry.currentState = Lifecycle.State.INITIALIZED mLifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
@ -264,7 +282,10 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
latinIMELegacy.onCreate() latinIMELegacy.onCreate()
languageModelFacilitator.launchProcessor() languageModelFacilitator.launchProcessor()
languageModelFacilitator.loadHistoryLog()
if(isDirectBootUnlocked) {
languageModelFacilitator.loadHistoryLog()
}
scheduleUpdateCheckingJob(this) scheduleUpdateCheckingJob(this)
launchJob { uixManager.showUpdateNoticeIfNeeded() } launchJob { uixManager.showUpdateNoticeIfNeeded() }
@ -322,6 +343,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
override fun onDestroy() { override fun onDestroy() {
unregisterReceiver(unlockReceiver)
stopJobs() stopJobs()
mLifecycleRegistry.currentState = Lifecycle.State.DESTROYED mLifecycleRegistry.currentState = Lifecycle.State.DESTROYED
viewModelStore.clear() viewModelStore.clear()
@ -666,4 +689,22 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
get() = mSavedStateRegistryController.savedStateRegistry get() = mSavedStateRegistryController.savedStateRegistry
override val viewModelStore: ViewModelStore override val viewModelStore: ViewModelStore
get() = mViewModelStore get() = mViewModelStore
private fun onDeviceUnlocked() {
Log.i("LatinIME", "DEVICE has UNLOCKED!!! Reloading settings...")
// Every place that called getDefaultSharedPreferences now needs to be refreshed or call it again
// Mainly Settings singleton needs to be refreshed
Settings.init(applicationContext)
Settings.getInstance().onSharedPreferenceChanged(null /* unused */, "")
latinIMELegacy.loadSettings()
recreateKeyboard()
Log.i("LatinIME", "DEVICE has UNLOCKED!!! Finished reloading: ${Settings.getInstance().current.dump()}")
languageModelFacilitator.loadHistoryLog()
// TODO: Spell checker service
}
} }

View File

@ -38,7 +38,6 @@ import android.os.Build;
import android.os.Debug; import android.os.Debug;
import android.os.IBinder; import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.preference.PreferenceManager;
import android.text.InputType; import android.text.InputType;
import android.util.Log; import android.util.Log;
import android.util.PrintWriterPrinter; import android.util.PrintWriterPrinter;
@ -573,7 +572,6 @@ public class LatinIMELegacy implements KeyboardActionListener,
public void onCreate() { public void onCreate() {
Settings.init(mInputMethodService); Settings.init(mInputMethodService);
DebugFlags.init(PreferenceManager.getDefaultSharedPreferences(mInputMethodService));
RichInputMethodManager.init(mInputMethodService); RichInputMethodManager.init(mInputMethodService);
mRichImm = RichInputMethodManager.getInstance(); mRichImm = RichInputMethodManager.getInstance();
AudioAndHapticFeedbackManager.init(mInputMethodService); AudioAndHapticFeedbackManager.init(mInputMethodService);
@ -934,10 +932,6 @@ public class LatinIMELegacy implements KeyboardActionListener,
final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
final boolean isDifferentTextField = !restarting || inputTypeChanged; final boolean isDifferentTextField = !restarting || inputTypeChanged;
StatsUtils.onStartInputView(editorInfo.inputType,
Settings.getInstance().getCurrent().mDisplayOrientation,
!isDifferentTextField);
// The EditorInfo might have a flag that affects fullscreen mode. // The EditorInfo might have a flag that affects fullscreen mode.
// Note: This call should be done by InputMethodService? // Note: This call should be done by InputMethodService?
updateFullscreenMode(); updateFullscreenMode();

View File

@ -34,6 +34,7 @@ import org.futo.inputmethod.annotations.UsedForTesting;
import org.futo.inputmethod.compat.InputMethodManagerCompatWrapper; import org.futo.inputmethod.compat.InputMethodManagerCompatWrapper;
import org.futo.inputmethod.compat.InputMethodSubtypeCompatUtils; import org.futo.inputmethod.compat.InputMethodSubtypeCompatUtils;
import org.futo.inputmethod.latin.settings.Settings; import org.futo.inputmethod.latin.settings.Settings;
import org.futo.inputmethod.latin.uix.PreferenceUtils;
import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils; import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils;
import org.futo.inputmethod.latin.utils.LanguageOnSpacebarUtils; import org.futo.inputmethod.latin.utils.LanguageOnSpacebarUtils;
import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils; import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils;
@ -108,7 +109,7 @@ public class RichInputMethodManager {
} }
public InputMethodSubtype[] getAdditionalSubtypes() { public InputMethodSubtype[] getAdditionalSubtypes() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); final SharedPreferences prefs = PreferenceUtils.INSTANCE.getDefaultSharedPreferences(mContext);
final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes( final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
prefs, mContext.getResources()); prefs, mContext.getResources());
return AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes); return AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes);

View File

@ -37,6 +37,7 @@ import okhttp3.internal.toImmutableList
import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.uix.SettingsKey import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.getSettingBlocking import org.futo.inputmethod.latin.uix.getSettingBlocking
import org.futo.inputmethod.latin.uix.isDirectBootUnlocked
import org.futo.inputmethod.latin.uix.setSettingBlocking import org.futo.inputmethod.latin.uix.setSettingBlocking
import org.futo.inputmethod.latin.uix.settings.NavigationItem import org.futo.inputmethod.latin.uix.settings.NavigationItem
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
@ -93,6 +94,8 @@ object Subtypes {
} }
fun addDefaultSubtypesIfNecessary(context: Context) { fun addDefaultSubtypesIfNecessary(context: Context) {
if(!context.isDirectBootUnlocked) return
val currentSubtypes = context.getSettingBlocking(SubtypesSetting) val currentSubtypes = context.getSettingBlocking(SubtypesSetting)
if(currentSubtypes.isNotEmpty()) { if(currentSubtypes.isNotEmpty()) {
removeExtensionsIfNecessary(context) removeExtensionsIfNecessary(context)
@ -116,7 +119,7 @@ object Subtypes {
addLanguage(context, Locale.forLanguageTag("zz"), "qwerty") addLanguage(context, Locale.forLanguageTag("zz"), "qwerty")
} }
context.setSettingBlocking(ActiveSubtype.key, context.getSettingBlocking(SubtypesSetting).first()) context.setSettingBlocking(ActiveSubtype.key, context.getSettingBlocking(SubtypesSetting).firstOrNull() ?: return)
} }
fun findClosestLocaleLayouts(locale: Locale): List<String> { fun findClosestLocaleLayouts(locale: Locale): List<String> {
@ -224,6 +227,30 @@ object Subtypes {
} }
}.mapValues { it.value.toImmutableList() } }.mapValues { it.value.toImmutableList() }
} }
fun getDirectBootInitialLayouts(context: Context): Set<String> {
val layouts = mutableSetOf("en_US:")
val locales = context.resources.configuration.locales
if(locales.size() == 0) return layouts
for(i in 0 until locales.size()) {
val locale = locales.get(i).stripExtensionsIfNeeded()
val layout = findClosestLocaleLayouts(locale).firstOrNull() ?: continue
val value = subtypeToString(
InputMethodSubtypeBuilder()
.setSubtypeLocale(locale.stripExtensionsIfNeeded().toString())
.setSubtypeExtraValue("KeyboardLayoutSet=$layout")
.build()
)
layouts.add(value)
}
return layouts
}
} }

View File

@ -22,7 +22,6 @@ import android.content.pm.ApplicationInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import org.futo.inputmethod.compat.BuildCompatUtils; import org.futo.inputmethod.compat.BuildCompatUtils;
@ -30,6 +29,7 @@ import org.futo.inputmethod.latin.AudioAndHapticFeedbackManager;
import org.futo.inputmethod.latin.InputAttributes; import org.futo.inputmethod.latin.InputAttributes;
import org.futo.inputmethod.latin.R; import org.futo.inputmethod.latin.R;
import org.futo.inputmethod.latin.common.StringUtils; import org.futo.inputmethod.latin.common.StringUtils;
import org.futo.inputmethod.latin.uix.PreferenceUtils;
import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils; import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils;
import org.futo.inputmethod.latin.utils.ResourceUtils; import org.futo.inputmethod.latin.utils.ResourceUtils;
import org.futo.inputmethod.latin.utils.RunInLocale; import org.futo.inputmethod.latin.utils.RunInLocale;
@ -145,7 +145,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
private void onCreate(final Context context) { private void onCreate(final Context context) {
mContext = context; mContext = context;
mRes = context.getResources(); mRes = context.getResources();
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
if(mPrefs != null) {
mPrefs.unregisterOnSharedPreferenceChangeListener(this);
}
mPrefs = PreferenceUtils.INSTANCE.getDefaultSharedPreferences(context);
mPrefs.registerOnSharedPreferenceChangeListener(this); mPrefs.registerOnSharedPreferenceChangeListener(this);
upgradeAutocorrectionSettings(mPrefs, mRes); upgradeAutocorrectionSettings(mPrefs, mRes);
} }

View File

@ -36,6 +36,7 @@ import org.futo.inputmethod.latin.RichInputMethodSubtype;
import org.futo.inputmethod.latin.SuggestedWords; import org.futo.inputmethod.latin.SuggestedWords;
import org.futo.inputmethod.latin.common.ComposedData; import org.futo.inputmethod.latin.common.ComposedData;
import org.futo.inputmethod.latin.settings.SettingsValuesForSuggestion; import org.futo.inputmethod.latin.settings.SettingsValuesForSuggestion;
import org.futo.inputmethod.latin.uix.PreferenceUtils;
import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils; import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils;
import org.futo.inputmethod.latin.utils.ScriptUtils; import org.futo.inputmethod.latin.utils.ScriptUtils;
import org.futo.inputmethod.latin.utils.SuggestionResults; import org.futo.inputmethod.latin.utils.SuggestionResults;
@ -95,7 +96,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
super.onCreate(); super.onCreate();
mRecommendedThreshold = Float.parseFloat( mRecommendedThreshold = Float.parseFloat(
getString(R.string.spellchecker_recommended_threshold_value)); getString(R.string.spellchecker_recommended_threshold_value));
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final SharedPreferences prefs = PreferenceUtils.INSTANCE.getDefaultSharedPreferences(this);
prefs.registerOnSharedPreferenceChangeListener(this); prefs.registerOnSharedPreferenceChangeListener(this);
onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
} }

View File

@ -1,27 +1,147 @@
package org.futo.inputmethod.latin.uix package org.futo.inputmethod.latin.uix
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.UserManager
import android.preference.PreferenceManager
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.preferencesOf
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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 import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") // 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
// Initializes unlockedDataStore, or uses DefaultDataStore if device is still locked (direct boot)
@OptIn(DelicateCoroutinesApi::class)
val Context.dataStore: DataStore<Preferences>
get() {
val userManager = getSystemService(Context.USER_SERVICE) as UserManager
if (userManager.isUserUnlocked) {
// The device has been unlocked
return unlockedDataStore ?: run {
val newDataStore = PreferenceDataStoreFactory.create(
corruptionHandler = null,
migrations = listOf(),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
) {
applicationContext.preferencesDataStoreFile("settings")
}
unlockedDataStore = newDataStore
// Send new values to the DefaultDataStore for any listeners
GlobalScope.launch {
newDataStore.data.collect { value ->
DefaultDataStore.sharedData.emit(value)
}
}
newDataStore
}
} else {
// The device is still locked, return default data store
if(!DefaultDataStore.subtypesInitialized) {
DefaultDataStore.subtypesInitialized = true
GlobalScope.launch {
DefaultDataStore.updateSubtypes(Subtypes.getDirectBootInitialLayouts(this@dataStore))
}
}
return DefaultDataStore
}
}
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
}
suspend fun <T> Context.getSetting(key: Preferences.Key<T>, default: T): T { suspend fun <T> Context.getSetting(key: Preferences.Key<T>, default: T): T {
val valueFlow: Flow<T> = val valueFlow: Flow<T> =

View File

@ -60,6 +60,7 @@ import org.futo.inputmethod.latin.uix.PersistentActionState
import org.futo.inputmethod.latin.uix.PersistentStateInitialization import org.futo.inputmethod.latin.uix.PersistentStateInitialization
import org.futo.inputmethod.latin.uix.SettingsKey import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.getSettingBlocking import org.futo.inputmethod.latin.uix.getSettingBlocking
import org.futo.inputmethod.latin.uix.isDirectBootUnlocked
import org.futo.inputmethod.latin.uix.settings.ScrollableList import org.futo.inputmethod.latin.uix.settings.ScrollableList
import org.futo.inputmethod.latin.uix.settings.pages.ParagraphText import org.futo.inputmethod.latin.uix.settings.pages.ParagraphText
import org.futo.inputmethod.latin.uix.settings.pages.PaymentSurface import org.futo.inputmethod.latin.uix.settings.pages.PaymentSurface
@ -268,6 +269,8 @@ class ClipboardHistoryManager(val context: Context, val coroutineScope: Lifecycl
} }
private fun saveClipboard() { private fun saveClipboard() {
if(!context.isDirectBootUnlocked) return
coroutineScope.launch { coroutineScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
pruneOldItems() pruneOldItems()
@ -281,6 +284,8 @@ class ClipboardHistoryManager(val context: Context, val coroutineScope: Lifecycl
} }
private suspend fun loadClipboard() { private suspend fun loadClipboard() {
if(!context.isDirectBootUnlocked) return
try { try {
val file = File(context.filesDir, "clipboard.json") val file = File(context.filesDir, "clipboard.json")
@ -374,6 +379,7 @@ val ClipboardHistoryAction = Action(
}, },
persistentStateInitialization = PersistentStateInitialization.OnKeyboardLoad, persistentStateInitialization = PersistentStateInitialization.OnKeyboardLoad,
windowImpl = { manager, persistent -> windowImpl = { manager, persistent ->
val unlocked = manager.getContext().isDirectBootUnlocked
val clipboardHistoryManager = persistent as ClipboardHistoryManager val clipboardHistoryManager = persistent as ClipboardHistoryManager
manager.getLifecycleScope().launch { clipboardHistoryManager.pruneOldItems() } manager.getLifecycleScope().launch { clipboardHistoryManager.pruneOldItems() }
@ -443,7 +449,13 @@ val ClipboardHistoryAction = Action(
override fun WindowContents(keyboardShown: Boolean) { override fun WindowContents(keyboardShown: Boolean) {
val view = LocalView.current val view = LocalView.current
val clipboardHistory = useDataStore(ClipboardHistoryEnabled, blocking = true) val clipboardHistory = useDataStore(ClipboardHistoryEnabled, blocking = true)
if(!clipboardHistory.value) { if(!unlocked) {
ScrollableList {
PaymentSurface(isPrimary = true, title = "Device Locked") {
ParagraphText("Please unlock your device to access clipboard history")
}
}
} else if(!clipboardHistory.value) {
ScrollableList { ScrollableList {
PaymentSurface(isPrimary = true, title = "Clipboard History Inactive") { PaymentSurface(isPrimary = true, title = "Clipboard History Inactive") {
ParagraphText("Clipboard history is not enabled. To save clipboard items, you can enable clipboard history. This will keep up to 25 items for 3 days unless pinned. Passwords and other items marked sensitive are excluded from history.") ParagraphText("Clipboard history is not enabled. To save clipboard items, you can enable clipboard history. This will keep up to 25 items for 3 days unless pinned. Passwords and other items marked sensitive are excluded from history.")

View File

@ -1,7 +1,6 @@
package org.futo.inputmethod.latin.uix.settings package org.futo.inputmethod.latin.uix.settings
import android.content.SharedPreferences import android.content.SharedPreferences
import android.preference.PreferenceManager
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -20,6 +19,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.futo.inputmethod.latin.uix.PreferenceUtils
import org.futo.inputmethod.latin.uix.SettingsKey import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.dataStore import org.futo.inputmethod.latin.uix.dataStore
import org.futo.inputmethod.latin.uix.getSetting import org.futo.inputmethod.latin.uix.getSetting
@ -94,7 +94,7 @@ fun <T> useDataStore(key: SettingsKey<T>, blocking: Boolean = false): DataStoreI
fun<T> useSharedPrefsGeneric(key: String, default: T, get: (SharedPreferences, String, T) -> T, put: (SharedPreferences, String, T) -> Unit): DataStoreItem<T> { fun<T> useSharedPrefsGeneric(key: String, default: T, get: (SharedPreferences, String, T) -> T, put: (SharedPreferences, String, T) -> Unit): DataStoreItem<T> {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val sharedPrefs = remember { PreferenceManager.getDefaultSharedPreferences(context) } val sharedPrefs = remember { PreferenceUtils.getDefaultSharedPreferences(context) }
val value = remember { mutableStateOf(get(sharedPrefs, key, default)) } val value = remember { mutableStateOf(get(sharedPrefs, key, default)) }
@ -158,7 +158,7 @@ fun useSharedPrefsInt(key: String, default: Int): DataStoreItem<Int> {
@Composable @Composable
private fun<T> SyncDataStoreToPreferences(key: SettingsKey<T>, update: (newValue: T, editor: SharedPreferences.Editor) -> Unit) { private fun<T> SyncDataStoreToPreferences(key: SettingsKey<T>, update: (newValue: T, editor: SharedPreferences.Editor) -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val sharedPrefs = remember { PreferenceManager.getDefaultSharedPreferences(context) } val sharedPrefs = remember { PreferenceUtils.getDefaultSharedPreferences(context) }
val value = useDataStoreValueBlocking(key) val value = useDataStoreValueBlocking(key)
LaunchedEffect(value) { LaunchedEffect(value) {

View File

@ -1,7 +1,6 @@
package org.futo.inputmethod.latin.uix.settings.pages package org.futo.inputmethod.latin.uix.settings.pages
import android.content.Context import android.content.Context
import android.preference.PreferenceManager
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -59,6 +58,7 @@ import org.futo.inputmethod.latin.uix.AndroidTextInput
import org.futo.inputmethod.latin.uix.KeyHintsSetting import org.futo.inputmethod.latin.uix.KeyHintsSetting
import org.futo.inputmethod.latin.uix.KeyboardBottomOffsetSetting import org.futo.inputmethod.latin.uix.KeyboardBottomOffsetSetting
import org.futo.inputmethod.latin.uix.KeyboardHeightMultiplierSetting import org.futo.inputmethod.latin.uix.KeyboardHeightMultiplierSetting
import org.futo.inputmethod.latin.uix.PreferenceUtils
import org.futo.inputmethod.latin.uix.SHOW_EMOJI_SUGGESTIONS import org.futo.inputmethod.latin.uix.SHOW_EMOJI_SUGGESTIONS
import org.futo.inputmethod.latin.uix.SettingsKey import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.actions.AllActions import org.futo.inputmethod.latin.uix.actions.AllActions
@ -437,7 +437,7 @@ fun TypingScreen(navController: NavHostController = rememberNavController()) {
val (vibration, _) = useDataStore(key = vibrationDurationSetting.key, default = vibrationDurationSetting.default) val (vibration, _) = useDataStore(key = vibrationDurationSetting.key, default = vibrationDurationSetting.default)
LaunchedEffect(vibration) { LaunchedEffect(vibration) {
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) val sharedPrefs = PreferenceUtils.getDefaultSharedPreferences(context)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
sharedPrefs.edit { sharedPrefs.edit {
putInt(PREF_VIBRATION_DURATION_SETTINGS, vibration) putInt(PREF_VIBRATION_DURATION_SETTINGS, vibration)

View File

@ -69,20 +69,10 @@ public final class ImportantNoticeUtils {
} }
} }
@UsedForTesting
static SharedPreferences getImportantNoticePreferences(final Context context) {
return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
}
@UsedForTesting
static boolean hasContactsNoticeShown(final Context context) {
return getImportantNoticePreferences(context).getBoolean(
KEY_SUGGEST_CONTACTS_NOTICE, false);
}
public static boolean shouldShowImportantNotice(final Context context, public static boolean shouldShowImportantNotice(final Context context,
final SettingsValues settingsValues) { final SettingsValues settingsValues) {
// Check to see whether "Use Contacts" is enabled by the user. // Check to see whether "Use Contacts" is enabled by the user.
/*
if (!settingsValues.mUseContactsDict) { if (!settingsValues.mUseContactsDict) {
return false; return false;
} }
@ -107,8 +97,8 @@ public final class ImportantNoticeUtils {
if (hasContactsNoticeTimeoutPassed(context, System.currentTimeMillis())) { if (hasContactsNoticeTimeoutPassed(context, System.currentTimeMillis())) {
updateContactsNoticeShown(context); updateContactsNoticeShown(context);
return false; return false;
} }*/
return true; return false;
} }
public static String getSuggestContactsNoticeTitle(final Context context) { public static String getSuggestContactsNoticeTitle(final Context context) {
@ -118,23 +108,11 @@ public final class ImportantNoticeUtils {
@UsedForTesting @UsedForTesting
static boolean hasContactsNoticeTimeoutPassed( static boolean hasContactsNoticeTimeoutPassed(
final Context context, final long currentTimeInMillis) { final Context context, final long currentTimeInMillis) {
final SharedPreferences prefs = getImportantNoticePreferences(context);
if (!prefs.contains(KEY_TIMESTAMP_OF_CONTACTS_NOTICE)) { return false;
prefs.edit()
.putLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE, currentTimeInMillis)
.apply();
}
final long firstDisplayTimeInMillis = prefs.getLong(
KEY_TIMESTAMP_OF_CONTACTS_NOTICE, currentTimeInMillis);
final long elapsedTime = currentTimeInMillis - firstDisplayTimeInMillis;
return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE;
} }
public static void updateContactsNoticeShown(final Context context) { public static void updateContactsNoticeShown(final Context context) {
getImportantNoticePreferences(context)
.edit()
.putBoolean(KEY_SUGGEST_CONTACTS_NOTICE, true)
.remove(KEY_TIMESTAMP_OF_CONTACTS_NOTICE)
.apply();
} }
} }

View File

@ -4,6 +4,7 @@ import android.content.Context
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.futo.inputmethod.latin.uix.isDirectBootUnlocked
import java.io.File import java.io.File
@Serializable @Serializable
@ -23,6 +24,8 @@ data class HistoryLogForTraining(
) )
fun saveHistoryLogBackup(context: Context, log: List<HistoryLogForTraining>) { fun saveHistoryLogBackup(context: Context, log: List<HistoryLogForTraining>) {
if(!context.isDirectBootUnlocked) return
val json = Json.encodeToString(log) val json = Json.encodeToString(log)
val file = File(context.cacheDir, "historyLog.json") val file = File(context.cacheDir, "historyLog.json")
@ -30,6 +33,8 @@ fun saveHistoryLogBackup(context: Context, log: List<HistoryLogForTraining>) {
} }
fun loadHistoryLogBackup(context: Context, to: MutableList<HistoryLogForTraining>) { fun loadHistoryLogBackup(context: Context, to: MutableList<HistoryLogForTraining>) {
if(!context.isDirectBootUnlocked) return
try { try {
val file = File(context.cacheDir, "historyLog.json") val file = File(context.cacheDir, "historyLog.json")
if(file.exists()) { if(file.exists()) {

View File

@ -25,6 +25,7 @@ import kotlinx.coroutines.withContext
import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.USE_TRANSFORMER_FINETUNING import org.futo.inputmethod.latin.uix.USE_TRANSFORMER_FINETUNING
import org.futo.inputmethod.latin.uix.getSetting import org.futo.inputmethod.latin.uix.getSetting
import org.futo.inputmethod.latin.uix.isDirectBootUnlocked
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -289,6 +290,7 @@ class TrainingWorker(val context: Context, val parameters: WorkerParameters) : C
private val WORKER_TAG: String = "TRAINING_WORKER" private val WORKER_TAG: String = "TRAINING_WORKER"
public fun scheduleTrainingWorkerBackground(context: Context) { public fun scheduleTrainingWorkerBackground(context: Context) {
if(!context.isDirectBootUnlocked) return
val workManager = WorkManager.getInstance(context) val workManager = WorkManager.getInstance(context)
workManager.cancelAllWorkByTag(WORKER_TAG) workManager.cancelAllWorkByTag(WORKER_TAG)

View File

@ -2,6 +2,7 @@ package org.futo.inputmethod.latin
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.UserManager
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import org.acra.ACRA import org.acra.ACRA
import org.acra.config.dialog import org.acra.config.dialog
@ -12,35 +13,40 @@ import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
class CrashLoggingApplication : Application() { class CrashLoggingApplication : Application() {
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
initAcra { val userManager = getSystemService(Context.USER_SERVICE) as UserManager
reportFormat = StringFormat.JSON if(userManager.isUserUnlocked) {
println("Initializing ACRA, as user is unlocked")
initAcra {
reportFormat = StringFormat.JSON
dialog { dialog {
text = getString( text = getString(
//if(BuildConfig.ENABLE_ACRA) { //if(BuildConfig.ENABLE_ACRA) {
// R.string.crashed_text // R.string.crashed_text
//} else { //} else {
R.string.crashed_text_email R.string.crashed_text_email
//} //}
) )
title = getString(R.string.crashed_title) title = getString(R.string.crashed_title)
positiveButtonText = getString(R.string.crash_report_accept) positiveButtonText = getString(R.string.crash_report_accept)
negativeButtonText = getString(R.string.crash_report_reject) negativeButtonText = getString(R.string.crash_report_reject)
resTheme = android.R.style.Theme_DeviceDefault_Dialog resTheme = android.R.style.Theme_DeviceDefault_Dialog
} }
//if(BuildConfig.ENABLE_ACRA) { //if(BuildConfig.ENABLE_ACRA) {
// httpSender { // httpSender {
// uri = BuildConfig.ACRA_URL // uri = BuildConfig.ACRA_URL
// basicAuthLogin = BuildConfig.ACRA_USER // basicAuthLogin = BuildConfig.ACRA_USER
// basicAuthPassword = BuildConfig.ACRA_PASSWORD // basicAuthPassword = BuildConfig.ACRA_PASSWORD
// httpMethod = HttpSender.Method.POST // httpMethod = HttpSender.Method.POST
// } // }
//} else { //} else {
mailSender { mailSender {
mailTo = "keyboard@futo.org" mailTo = "keyboard@futo.org"
reportAsFile = true reportAsFile = true
@ -49,14 +55,23 @@ class CrashLoggingApplication : Application() {
body = body =
"I experienced this crash. My version: ${BuildConfig.VERSION_NAME}.\n\n(Enter details here if necessary)" "I experienced this crash. My version: ${BuildConfig.VERSION_NAME}.\n\n(Enter details here if necessary)"
} }
//} //}
}
acraInitialized = true
} else {
println("Skipping ACRA, as user is locked")
} }
} }
companion object { companion object {
var acraInitialized = false
fun logPreferences(preferences: Preferences) { fun logPreferences(preferences: Preferences) {
preferences.asMap().forEach { if(acraInitialized) {
ACRA.errorReporter.putCustomData(it.key.name, it.value.toString()) preferences.asMap().forEach {
ACRA.errorReporter.putCustomData(it.key.name, it.value.toString())
}
} }
} }
} }