package org.futo.inputmethod.latin import android.content.res.Configuration import android.inputmethodservice.InputMethodService import android.os.Build import android.os.Bundle import android.view.KeyEvent import android.view.View import android.view.inputmethod.CompletionInfo import android.view.inputmethod.EditorInfo import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsResponse import android.view.inputmethod.InputMethodSubtype import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.size import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.futo.inputmethod.latin.uix.BasicThemeProvider import org.futo.inputmethod.latin.uix.DynamicThemeProvider import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner import org.futo.inputmethod.latin.uix.THEME_KEY import org.futo.inputmethod.latin.uix.UixManager import org.futo.inputmethod.latin.uix.createInlineSuggestionsRequest import org.futo.inputmethod.latin.uix.deferGetSetting import org.futo.inputmethod.latin.uix.deferSetSetting import org.futo.inputmethod.latin.uix.differsFrom import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.ThemeOption import org.futo.inputmethod.latin.uix.theme.ThemeOptions import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator import org.futo.inputmethod.updates.scheduleUpdateCheckingJob class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner { private val mSavedStateRegistryController = SavedStateRegistryController.create(this) override val savedStateRegistry: SavedStateRegistry get() = mSavedStateRegistryController.savedStateRegistry private val mLifecycleRegistry = LifecycleRegistry(this) private fun handleLifecycleEvent(event: Lifecycle.Event) = mLifecycleRegistry.handleLifecycleEvent(event) override val lifecycle get() = mLifecycleRegistry private val store = ViewModelStore() override val viewModelStore get() = store fun setOwners() { val decorView = window.window?.decorView if (decorView?.findViewTreeLifecycleOwner() == null) { decorView?.setViewTreeLifecycleOwner(this) } if (decorView?.findViewTreeViewModelStoreOwner() == null) { decorView?.setViewTreeViewModelStoreOwner(this) } if (decorView?.findViewTreeSavedStateRegistryOwner() == null) { decorView?.setViewTreeSavedStateRegistryOwner(this) } } val latinIMELegacy = LatinIMELegacy( this as InputMethodService, this as LatinIMELegacy.SuggestionStripController ) val inputLogic get() = latinIMELegacy.mInputLogic val languageModelFacilitator = LanguageModelFacilitator( this, latinIMELegacy.mInputLogic, latinIMELegacy.mDictionaryFacilitator, latinIMELegacy.mSettings, latinIMELegacy.mKeyboardSwitcher, lifecycleScope ) val uixManager = UixManager(this) private var activeThemeOption: ThemeOption? = null private var activeColorScheme = DarkColorScheme private var colorSchemeLoaderJob: Job? = null private var pendingRecreateKeyboard: Boolean = false val themeOption get() = activeThemeOption val colorScheme get() = activeColorScheme private var drawableProvider: DynamicThemeProvider? = null private var lastEditorInfo: EditorInfo? = null // TODO: Calling this repeatedly as the theme changes tends to slow everything to a crawl private fun recreateKeyboard() { latinIMELegacy.updateTheme() latinIMELegacy.mKeyboardSwitcher.mState.onLoadKeyboard(latinIMELegacy.currentAutoCapsState, latinIMELegacy.currentRecapitalizeState); } private fun updateDrawableProvider(colorScheme: ColorScheme) { activeColorScheme = colorScheme drawableProvider = BasicThemeProvider(this, overrideColorScheme = colorScheme) window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor uixManager.onColorSchemeChanged() } override fun getDrawableProvider(): DynamicThemeProvider { if (drawableProvider == null) { if (colorSchemeLoaderJob != null && !colorSchemeLoaderJob!!.isCompleted) { // Must have completed by now! runBlocking { colorSchemeLoaderJob!!.join() } } drawableProvider = BasicThemeProvider(this, activeColorScheme) } return drawableProvider!! } private fun updateColorsIfDynamicChanged() { if(activeThemeOption?.dynamic == true) { val currColors = activeColorScheme val nextColors = activeThemeOption!!.obtainColors(this) if(currColors.differsFrom(nextColors)) { updateDrawableProvider(nextColors) recreateKeyboard() } } deferGetSetting(THEME_KEY) { key -> if(key != activeThemeOption?.key) { ThemeOptions[key]?.let { if(it.available(this)) updateTheme(it) } } } } fun updateTheme(newTheme: ThemeOption) { assert(newTheme.available(this)) if (activeThemeOption != newTheme) { activeThemeOption = newTheme updateDrawableProvider(newTheme.obtainColors(this)) deferSetSetting(THEME_KEY, newTheme.key) if(!uixManager.isMainKeyboardHidden) { recreateKeyboard() } else { pendingRecreateKeyboard = true } } } // Called by UixManager when the intention is to subsequently call LegacyKeyboardView with hidden=false // Maybe this can be changed to LaunchedEffect fun onKeyboardShown() { //if(pendingRecreateKeyboard) { // pendingRecreateKeyboard = false // recreateKeyboard() //} } override fun onCreate() { super.onCreate() colorSchemeLoaderJob = deferGetSetting(THEME_KEY) { val themeOptionFromSettings = ThemeOptions[it] val themeOption = when { themeOptionFromSettings == null -> VoiceInputTheme !themeOptionFromSettings.available(this@LatinIME) -> VoiceInputTheme else -> themeOptionFromSettings } activeThemeOption = themeOption activeColorScheme = themeOption.obtainColors(this@LatinIME) } mSavedStateRegistryController.performRestore(null) handleLifecycleEvent(Lifecycle.Event.ON_RESUME) latinIMELegacy.onCreate() languageModelFacilitator.launchProcessor() languageModelFacilitator.loadHistoryLog() scheduleUpdateCheckingJob(this) lifecycleScope.launch { uixManager.showUpdateNoticeIfNeeded() } } override fun onDestroy() { languageModelFacilitator.saveHistoryLog() runBlocking { languageModelFacilitator.destroyModel() } latinIMELegacy.onDestroy() super.onDestroy() } override fun onConfigurationChanged(newConfig: Configuration) { latinIMELegacy.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig) } override fun onInitializeInterface() { latinIMELegacy.onInitializeInterface() } private var legacyInputView: View? = null private var touchableHeight: Int = 0 override fun onCreateInputView(): View { legacyInputView = latinIMELegacy.onCreateInputView() val composeView = uixManager.createComposeView() latinIMELegacy.setComposeInputView(composeView) return composeView } private var inputViewHeight: Int = -1 // Both called by UixManager fun updateTouchableHeight(to: Int) { touchableHeight = to } fun getInputViewHeight(): Int = inputViewHeight // The keyboard view really doesn't like being detached, so it's always // shown, but resized to 0 if an action window is open @Composable internal fun LegacyKeyboardView(hidden: Boolean) { LaunchedEffect(hidden) { if(hidden) { latinIMELegacy.mKeyboardSwitcher.saveKeyboardState() } else { if(pendingRecreateKeyboard) { pendingRecreateKeyboard = false recreateKeyboard() } } } val modifier = if(hidden) { Modifier .clipToBounds() .size(0.dp) } else { Modifier.onSizeChanged { inputViewHeight = it.height } } key(legacyInputView) { AndroidView(factory = { legacyInputView!! }, update = { }, modifier = modifier) } } // necessary for when KeyboardSwitcher updates the theme fun updateLegacyView(newView: View) { legacyInputView = newView uixManager.setContent() uixManager.getComposeView()?.let { latinIMELegacy.setComposeInputView(it) } latinIMELegacy.setInputView(legacyInputView) } override fun setInputView(view: View?) { super.setInputView(view) uixManager.getComposeView()?.let { latinIMELegacy.setComposeInputView(it) } latinIMELegacy.setInputView(legacyInputView) } override fun setCandidatesView(view: View?) { return latinIMELegacy.setCandidatesView(view) } override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) { super.onStartInput(attribute, restarting) latinIMELegacy.onStartInput(attribute, restarting) } override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { lastEditorInfo = info super.onStartInputView(info, restarting) latinIMELegacy.onStartInputView(info, restarting) } override fun onFinishInputView(finishingInput: Boolean) { super.onFinishInputView(finishingInput) latinIMELegacy.onFinishInputView(finishingInput) uixManager.onInputFinishing() } override fun onFinishInput() { super.onFinishInput() latinIMELegacy.onFinishInput() uixManager.onInputFinishing() languageModelFacilitator.saveHistoryLog() } override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) { super.onCurrentInputMethodSubtypeChanged(newSubtype) latinIMELegacy.onCurrentInputMethodSubtypeChanged(newSubtype) } override fun onWindowShown() { super.onWindowShown() latinIMELegacy.onWindowShown() updateColorsIfDynamicChanged() } override fun onWindowHidden() { super.onWindowHidden() latinIMELegacy.onWindowHidden() uixManager.onInputFinishing() } override fun onUpdateSelection( oldSelStart: Int, oldSelEnd: Int, newSelStart: Int, newSelEnd: Int, candidatesStart: Int, candidatesEnd: Int ) { super.onUpdateSelection( oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd ) latinIMELegacy.onUpdateSelection( oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd ) } override fun onExtractedTextClicked() { latinIMELegacy.onExtractedTextClicked() super.onExtractedTextClicked() } override fun onExtractedCursorMovement(dx: Int, dy: Int) { latinIMELegacy.onExtractedCursorMovement(dx, dy) super.onExtractedCursorMovement(dx, dy) } override fun hideWindow() { latinIMELegacy.hideWindow() super.hideWindow() } override fun onDisplayCompletions(completions: Array?) { latinIMELegacy.onDisplayCompletions(completions) } override fun onComputeInsets(outInsets: Insets?) { val composeView = uixManager.getComposeView() // This method may be called before {@link #setInputView(View)}. if (legacyInputView == null || composeView == null) { return } val inputHeight: Int = composeView.height if (latinIMELegacy.isImeSuppressedByHardwareKeyboard && !legacyInputView!!.isShown) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. latinIMELegacy.setInsets(outInsets!!.apply { contentTopInsets = inputHeight visibleTopInsets = inputHeight }) return } val visibleTopY = inputHeight - touchableHeight val touchLeft = 0 val touchTop = visibleTopY val touchRight = composeView.width val touchBottom = inputHeight latinIMELegacy.setInsets(outInsets!!.apply { touchableInsets = Insets.TOUCHABLE_INSETS_REGION; touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); contentTopInsets = visibleTopY visibleTopInsets = visibleTopY }) } override fun onShowInputRequested(flags: Int, configChange: Boolean): Boolean { return latinIMELegacy.onShowInputRequested( flags, configChange ) || super.onShowInputRequested(flags, configChange) } override fun onEvaluateInputViewShown(): Boolean { return latinIMELegacy.onEvaluateInputViewShown() || super.onEvaluateInputViewShown() } override fun onEvaluateFullscreenMode(): Boolean { return latinIMELegacy.onEvaluateFullscreenMode(super.onEvaluateFullscreenMode()) } override fun updateFullscreenMode() { super.updateFullscreenMode() latinIMELegacy.updateFullscreenMode() } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { return latinIMELegacy.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event) } override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { return latinIMELegacy.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event) } override fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) { uixManager.updateVisibility(shouldShowSuggestionsStrip, fullscreenMode) } override fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) { uixManager.setSuggestions(suggestedWords, rtlSubtype) } override fun maybeShowImportantNoticeTitle(): Boolean { return false } override fun onLowMemory() { super.onLowMemory() uixManager.cleanUpPersistentStates() } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) uixManager.cleanUpPersistentStates() } @RequiresApi(Build.VERSION_CODES.R) override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest { return createInlineSuggestionsRequest(this, this.activeColorScheme) } @RequiresApi(Build.VERSION_CODES.R) override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { return uixManager.onInlineSuggestionsResponse(response) } fun postUpdateSuggestionStrip(inputStyle: Int): Boolean { if(languageModelFacilitator.shouldPassThroughToLegacy()) return false languageModelFacilitator.updateSuggestionStripAsync(inputStyle); return true } }