Android IME APIs & Service Integration
Overview
Comprehensive guide to Android's InputMethodService API and how FlorisBoard integrates with the Android system.
Introduction
FlorisBoard extends Android's InputMethodService
to provide a fully-featured keyboard implementation. This document covers the core Android IME APIs, lifecycle management, and how FlorisBoard integrates with the Android system through custom lifecycle-aware components.
Key Concepts
InputMethodService
The InputMethodService
is the base class for implementing an Input Method Editor (IME) in Android. It provides:
- Lifecycle Management: Handles creation, visibility, and destruction of the keyboard
- Input View Management: Creates and manages the keyboard UI
- Input Connection: Communicates with the target application's text field
- Configuration Handling: Responds to device configuration changes
- Window Management: Controls keyboard window properties and behavior
LifecycleInputMethodService
FlorisBoard extends InputMethodService
with a custom LifecycleInputMethodService
that adds:
- Lifecycle Awareness: Implements
LifecycleOwner
for AndroidX Lifecycle support - ViewModel Support: Implements
ViewModelStoreOwner
for ViewModel integration - SavedState Support: Implements
SavedStateRegistryOwner
for state preservation - Coroutine Scope: Provides lifecycle-aware coroutine scope
InputConnection
The InputConnection
interface is the communication channel between the IME and the target application:
- Text Manipulation: Insert, delete, and replace text
- Cursor Control: Move cursor and manage selection
- Composing Text: Handle text composition for predictive input
- Editor Actions: Perform actions like "Go", "Search", "Send"
Implementation Details
FlorisImeService Architecture
class FlorisImeService : LifecycleInputMethodService() {
// Core managers injected via dependency injection
private val keyboardManager by keyboardManager()
private val editorInstance by editorInstance()
private val themeManager by themeManager()
...
Lifecycle Events
The service follows this lifecycle:
onCreate() → onStartInput() → onStartInputView() → onWindowShown()
↓ ↓
onDestroy() ← onFinishInput() ← onFinishInputView() ← onWindowHidden()
onCreate()
override fun onCreate() {
super.onCreate()
FlorisImeServiceReference = WeakReference(this)
WindowCompat.setDecorFitsSystemWindows(window.window!!, false)
subtypeManager.activeSubtypeFlow.collectIn(lifecycleScope) { subtype ->
val config = Configuration(resources.configuration)
if (prefs.localization.displayKeyboardLabelsInSubtypeLanguage.get()) {
config.setLocale(subtype.primaryLocale.base)
}
resourcesContext = createConfigurationContext(config)
}
...
onCreateInputView()
Creates the Compose-based keyboard UI:
override fun onCreateInputView(): View {
super.installViewTreeOwners()
// Instantiate and install bottom sheet host UI view
val bottomSheetView = FlorisBottomSheetHostUiView()
window.window!!.findViewById<ViewGroup>(android.R.id.content).addView(bottomSheetView)
// Instantiate and return input view
val composeView = ComposeInputView()
inputWindowView = composeView
return composeView
}
onStartInput() & onStartInputView()
Called when a new text field receives focus:
override fun onStartInput(info: EditorInfo?, restarting: Boolean) {
flogInfo { "restarting=$restarting info=${info?.debugSummarize()}" }
super.onStartInput(info, restarting)
if (info == null) return
val editorInfo = FlorisEditorInfo.wrap(info)
editorInstance.handleStartInput(editorInfo)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
flogInfo { "restarting=$restarting info=${info?.debugSummarize()}" }
super.onStartInputView(info, restarting)
if (info == null) return
val editorInfo = FlorisEditorInfo.wrap(info)
activeState.batchEdit {
if (activeState.imeUiMode != ImeUiMode.CLIPBOARD || prefs.clipboard.historyHideOnNextTextField.get()) {
activeState.imeUiMode = ImeUiMode.TEXT
}
activeState.isSelectionMode = editorInfo.initialSelection.isSelectionMode
editorInstance.handleStartInputView(editorInfo, isRestart = restarting)
}
}
EditorInfo Processing
FlorisBoard wraps Android's EditorInfo
to extract input attributes:
- Input Type: Text, number, phone, email, password, etc.
- IME Options: Action buttons (Go, Search, Send, Done)
- Input Flags: Auto-correct, auto-capitalize, multi-line
- Package Name: Identify the calling application
Code Examples
Accessing InputConnection
fun commitText(text: String): Boolean {
val ic = currentInputConnection() ?: return false
// Handle phantom space
if (phantomSpace.determine(text)) {
ic.commitText(" ", 1)
}
phantomSpace.setInactive()
// Commit text
ic.commitText(text, 1)
// Update auto-space state
autoSpace.updateState(text)
return true
}
Handling Hardware Keys
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return keyboardManager.onHardwareKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return keyboardManager.onHardwareKeyUp(keyCode, event) || super.onKeyUp(keyCode, event)
}
Best Practices
1. Always Check InputConnection Validity
val ic = currentInputConnection() ?: return false
// Use ic safely
2. Handle Configuration Changes
Override onConfigurationChanged()
to respond to orientation, locale, or theme changes:
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
themeManager.configurationChangeCounter.update { it + 1 }
}
3. Use Lifecycle-Aware Components
Leverage lifecycleScope
for coroutines that should be cancelled when the service is destroyed:
prefs.somePreference.asFlow().collectIn(lifecycleScope) { value ->
// Handle preference change
}
4. Manage Window Parameters
Control keyboard window behavior for different scenarios:
override fun updateFullscreenMode() {
super.updateFullscreenMode()
isFullscreenUiMode = isFullscreenMode
updateSoftInputWindowLayoutParameters()
}
Common Patterns
Switching Input Methods
fun switchToVoiceInputMethod(): Boolean {
val ims = FlorisImeServiceReference.get() ?: return false
val imm = ims.systemServiceOrNull(InputMethodManager::class) ?: return false
val list: List<InputMethodInfo> = imm.enabledInputMethodList
for (el in list) {
for (i in 0 until el.subtypeCount) {
if (el.getSubtypeAt(i).mode != "voice") continue
if (AndroidVersion.ATLEAST_API28_P) {
ims.switchInputMethod(el.id, el.getSubtypeAt(i))
return true
}
}
}
return false
}
Launching Settings Activity
fun launchSettings() {
val ims = FlorisImeServiceReference.get() ?: return
ims.requestHideSelf(0)
ims.launchActivity(FlorisAppActivity::class) {
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or
Intent.FLAG_ACTIVITY_CLEAR_TOP
}
}
Troubleshooting
InputConnection Returns Null
Problem: currentInputConnection()
returns null unexpectedly.
Solutions:
- Check if the keyboard is actually visible
- Verify the target app properly implements
InputConnection
- Add null checks before using
InputConnection
Keyboard Not Showing
Problem: Keyboard doesn't appear when text field is focused.
Solutions:
- Check if IME is enabled in system settings
- Verify
onStartInputView()
is being called - Check window visibility flags
Configuration Changes Break UI
Problem: UI breaks after rotation or theme change.
Solutions:
- Properly handle
onConfigurationChanged()
- Use
rememberSaveable
for Compose state - Implement
SavedStateRegistryOwner
Related Topics
- Architecture Overview - System architecture
- Input Processing Pipeline - How input is processed
- Custom UI Components - Compose UI implementation
- State Management - Managing keyboard state
Next Steps
- Explore Input Processing Pipeline to understand how touch events become text
- Learn about Custom UI Components for building keyboard UI
- Review State Management for reactive state handling
Note: This documentation is continuously being improved. Contributions are welcome!