Skip to content
CleverKeys Wiki
implemented v1.5.0

Settings System Architecture

Overview

The settings system manages user preferences through SharedPreferences, provides a Material 3 Compose UI for configuration, and applies settings at runtime via the Config singleton. All compile-time default values are centralized in the Defaults object inside Config.kt to prevent mismatches between UI display and actual behavior.

This document covers the cross-cutting architecture (defaults, Config singleton, ConfigurationManager, UI shell). Per-area details — labels, ranges, validation, and screenshots — live in the dedicated area specs (see Per-Area Settings Specs).

Key Components

FileClass/FunctionPurpose
src/main/kotlin/tribixbite/cleverkeys/Config.ktConfig, DefaultsGlobal configuration class, centralized defaults
src/main/kotlin/tribixbite/cleverkeys/SettingsActivity.ktSettingsActivitySettings shell — hosts the Compose screen, launchers, and search index
src/main/kotlin/tribixbite/cleverkeys/ui/settings/SettingsScreen, SettingsSearch, controls/dialogs/persistenceDecomposed settings UI layer (v1.5.0 refactor)
src/main/kotlin/tribixbite/cleverkeys/ui/settings/sections/17 per-section composables (AppearanceSection, ClipboardSection, …)One file per collapsible section
src/main/kotlin/tribixbite/cleverkeys/ConfigurationManager.ktConfigurationManagerOwns Config + FoldStateTracker; observes prefs and notifies listeners
src/main/kotlin/tribixbite/cleverkeys/DirectBootAwarePreferences.ktDirectBootAwarePreferencesDevice-protected preferences for direct-boot scenarios
src/main/kotlin/tribixbite/cleverkeys/theme/KeyboardTheme.ktKeyboardThemeTheme data and application
src/main/kotlin/tribixbite/cleverkeys/backup/SettingsDefaults.ktSETTINGS_DEFAULTSBackup/restore default snapshot (derived from Defaults via PrefValue.toExportableValue())

Architecture

SettingsActivity (Material 3 Compose)
    ├── PreferenceScreen (collapsible sections)
    │   ├── Appearance Section
    │   ├── Input Behavior Section
    │   ├── Neural Prediction Section
    │   ├── Gestures Section
    │   ├── Layout Section
    │   ├── Clipboard Section
    │   └── Advanced Section
    ├── Config (reads SharedPreferences)
    └── ConfigurationManager (observes prefs, notifies ConfigChangeListeners)

Defaults Architecture:
    └── Defaults object (Config.kt:18)
        ├── Single source of truth for all compile-time defaults
        ├── Referenced by Config refresh
        ├── Referenced by SettingsActivity loadCurrentSettings()
        └── Referenced by SETTINGS_DEFAULTS via PrefValue.toExportableValue()

Storage Strategy:
    ├── SharedPreferences (settings data)
    ├── DirectBootAwarePreferences (device-protected storage)
    ├── App-specific storage (getExternalFilesDir)
    └── Scoped storage (Android 11+)

Configuration

Defaults Object

The Defaults object (Config.kt:18) centralizes all app default values:

// Config.kt (abridged; line numbers per current source)
object Defaults {
    // Appearance
    const val THEME = "cleverkeysdark"          // :20
    const val KEYBOARD_HEIGHT_PORTRAIT = 27    // :23
    const val KEYBOARD_HEIGHT_LANDSCAPE = 40   // :24
    const val KEY_OPACITY = 100                // :27

    // Input Behavior / Haptics
    const val LONGPRESS_TIMEOUT = 600          // :85
    const val HAPTIC_ENABLED = true            // :75
    const val VIBRATE_CUSTOM = false           // :72
    const val VIBRATE_DURATION = 20            // :73

    // Neural Prediction
    const val NEURAL_BEAM_WIDTH = 6            // :134
    const val NEURAL_MAX_LENGTH = 20           // :135
    const val NEURAL_CONFIDENCE_THRESHOLD = 0.01f  // :136
    const val SWIPE_TYPING_ENABLED = true      // :164

    // Gestures (units: % of key diagonal)
    const val SHORT_GESTURE_MIN_DISTANCE = 28  // :119
    const val SHORT_GESTURE_MAX_DISTANCE = 141 // :120
    const val TAP_DURATION_THRESHOLD = 150     // :103

    // Clipboard
    const val CLIPBOARD_HISTORY_ENABLED = true              // :206
    const val CLIPBOARD_HISTORY_LIMIT = "0"                 // :215 (unlimited)
    const val CLIPBOARD_EXCLUDE_PASSWORD_MANAGERS = true    // :227

    // ... ~100 more constants organized by category
}

For exact current values per area, see the per-area specs (Appearance, Input Behavior, Haptics, Neural Settings).

Settings Categories

CategorySettings CountKey Settings
Appearance~15theme, keyboard_height, opacity, borders
Input Behavior~10longpress_timeout, vibration, key_repeat
Neural~8beam_width, confidence, swipe_enabled
Gestures~12short_swipe distances, slider sensitivity
Layout~8margins, number_row, extra_keys
Clipboard~5history_enabled, history_size, exclusions
Accessibility~6sticky_keys, voice_guidance
Debug~4debug_mode, logging

The settings screen has a search box that finds any control by title or keyword and scrolls to it:

  • SearchableSetting (ui/settings/SettingsSearch.kt:60) carries title, keywords, sectionName, an expandSection lambda, and an optional gatedBy key (e.g. a setting hidden behind the swipe-typing toggle).
  • The index is generated from the actual control titlesscripts/generate_settings_search_index.py scans the section composables so every rendered control is findable; a drift test keeps the index complete.
  • Selecting a result expands the owning collapsible section and animates the scroll to the control’s measured position (SettingsSearch.kt:27: scrollState.animateScrollTo(maxOf(0, position - 16))).
  • Gated results (gatedBy != null) are handled at SettingsSearch.kt:136-139: when the gate is disabled, search navigates to the gate toggle instead of a hidden control.

Public API

Config

Config is constructed by ConfigurationManager and exposed as a global singleton via the companion object (Config.kt:1101-1121):

class Config private constructor(
    private val _prefs: SharedPreferences,
    res: Resources,
    @JvmField val handler: IKeyEventHandler?,
    foldableUnfolded: Boolean?
) {
    // From preferences
    @JvmField var layouts: List<KeyboardData?> = emptyList()
    @JvmField var show_numpad = false
    @JvmField var haptic_enabled = Defaults.HAPTIC_ENABLED
    // ... ~100 @JvmField vars sourced from Defaults

    companion object {
        private var _globalConfig: Config? = null

        fun globalConfig(): Config = _globalConfig!!
        fun globalPrefs(): SharedPreferences = _globalConfig!!._prefs
    }
}

ConfigurationManager

ConfigurationManager owns the Config instance, observes SharedPreferences, and uses the observer pattern to decouple config refresh from config propagation:

class ConfigurationManager(
    private val context: Context,
    private val config: Config,
    private val foldStateTracker: FoldStateTracker
) : SharedPreferences.OnSharedPreferenceChangeListener {

    private val listeners = mutableListOf<ConfigChangeListener>()

    init {
        foldStateTracker.setChangedCallback {
            refresh(context.resources)
        }
    }
    // refresh, addListener, onSharedPreferenceChanged, etc.
}

Components that need to respond to config changes implement ConfigChangeListener and register with ConfigurationManager.

SettingsActivity

SettingsActivity is a ComponentActivity that uses Material 3 Compose:

class SettingsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CleverKeysSettingsTheme {
                SettingsScreen(
                    onNavigateBack = { finish() }
                )
            }
        }
    }
}

@Composable
fun SettingsScreen(onNavigateBack: () -> Unit) {
    // Collapsible sections for each category
    var appearanceExpanded by remember { mutableStateOf(false) }
    var inputExpanded by remember { mutableStateOf(false) }
    // ...

    LazyColumn {
        item { SettingsSection("Appearance", appearanceExpanded, { appearanceExpanded = it }) {
            ThemePicker()
            HeightSlider()
            OpacitySlider()
        }}
        item { SettingsSection("Input Behavior", inputExpanded, { inputExpanded = it }) {
            VibrationToggle()
            LongpressSlider()
        }}
        // ... other sections
    }
}

Implementation Details

Settings UI Components

@Composable
fun SettingsSwitch(
    title: String,
    description: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit
) {
    Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Column(modifier = Modifier.weight(1f)) {
            Text(title, style = MaterialTheme.typography.bodyLarge)
            Text(description, style = MaterialTheme.typography.bodySmall)
        }
        Switch(checked = checked, onCheckedChange = onCheckedChange)
    }
}

@Composable
fun SettingsSlider(
    title: String,
    value: Float,
    range: ClosedFloatingPointRange<Float>,
    onValueChange: (Float) -> Unit
) {
    Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Text(title, style = MaterialTheme.typography.bodyLarge)
        Slider(value = value, valueRange = range, onValueChange = onValueChange)
    }
}

Storage Permissions (Android 11+)

<!-- AndroidManifest.xml -->
<!-- Legacy permissions for Android 10 and below -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

<!-- For Android 11+, use scoped storage via MediaStore or SAF -->

App-specific storage doesn’t require permissions:

val appDir = context.getExternalFilesDir(null)  // No permission needed

Theme Application

// ConfigurationManager.kt
fun applyTheme(themeName: String) {
    val theme = KeyboardTheme.loadTheme(context, themeName)
    KeyboardTheme.current = theme

    // Notify keyboard view to redraw
    CleverKeysService.getInstance()?.requestKeyboardRedraw()
}

Settings Change Listener

Config reacts to preference changes through ConfigurationManager, which implements SharedPreferences.OnSharedPreferenceChangeListener and notifies registered ConfigChangeListener instances. Components that depend on a specific subset of preferences (theme, keyboard height, neural parameters, etc.) implement the listener interface rather than each reading SharedPreferences directly.

Collapsible Sections Pattern

Settings UI uses collapsible sections (not hierarchical navigation):

@Composable
fun CollapsibleSection(
    title: String,
    expanded: Boolean,
    onExpandedChange: (Boolean) -> Unit,
    content: @Composable ColumnScope.() -> Unit
) {
    Column {
        Row(
            modifier = Modifier.clickable { onExpandedChange(!expanded) }
                .fillMaxWidth().padding(16.dp)
        ) {
            Text(title, style = MaterialTheme.typography.titleMedium)
            Spacer(Modifier.weight(1f))
            Icon(
                if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                contentDescription = null
            )
        }
        AnimatedVisibility(visible = expanded) {
            Column(content = content)
        }
    }
}

This pattern means settings paths are “Settings > [expand section] > [setting]” rather than hierarchical navigation like “Settings > Appearance > Theme”.

Per-Area Settings Specs

This architectural spec covers the shared scaffolding. For exact values, ranges, validation rules, and per-key behavior, refer to:

For backup/restore and the import-preview pipeline (which exercises SETTINGS_DEFAULTS, PrefValue, and the diff engine), see the backup/restore specs.