Foldable Device Support
Overview
This document describes the foldable device support implementation for Thunderbird Android. The feature automatically switches between single-pane and split-view layouts based on the device’s fold state.
Motivation
Foldable devices like Samsung Galaxy Fold and Google Pixel Fold offer different screen sizes depending on their posture:
- Folded: Smaller outer display (typically 6-7 inches)
- Unfolded: Large inner display (typically 7-8 inches)
Thunderbird already supports split-screen views, but these are static (Always/Never) or orientation-based (When in Landscape). Users of foldable devices must manually change the setting when switching between folded and unfolded states.
Implementation
Components
1. SplitViewMode Enum Extension
File: core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/GeneralSettings.kt
Added new option:
enum class SplitViewMode {
ALWAYS,
NEVER,
WHEN_IN_LANDSCAPE,
WHEN_UNFOLDED, // New
}
2. FoldableStateObserver
File: legacy/ui/legacy/src/main/java/com/fsck/k9/ui/foldable/FoldableStateObserver.kt
Responsibilities:
- Observes
WindowInfoTrackerfrom Jetpack WindowManager - Converts
WindowLayoutInfointo simplifiedFoldableState - Provides
StateFlow<FoldableState>for lifecycle-aware collection - Implements 300ms debouncing to prevent layout thrashing
FoldableState Enum:
enum class FoldableState {
FOLDED, // Device is folded (small screen)
UNFOLDED, // Device is unfolded (large screen)
UNKNOWN, // Not a foldable or state unknown
}
State Detection:
FoldingFeature.State.FLAT→UNFOLDEDFoldingFeature.State.HALF_OPENED→UNFOLDED(laptop mode)- No
FoldingFeature→UNKNOWN
3. MainActivity Integration
File: legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt
Changes:
- Injection of
FoldableStateObservervia Koin - Lifecycle registration of observer
- Extended
useSplitView()withWHEN_UNFOLDEDlogic - Flow collection for state changes
- Automatic
recreate()on fold/unfold events
Behavior
User Flow
- User selects Settings → Display → Show split-screen → “When device is unfolded”
- On folded device: Single-pane view (message list only)
- Device unfolds:
FoldableStateObserverdetectsUNFOLDED- After 300ms debounce,
handleFoldableStateChange()is called - Activity recreates with split-view layout
- Message list on left, detail pane on right
- Device folds:
- Observer detects
FOLDED - Switches to single-pane
- Currently displayed message is preserved
- Observer detects
Technical Flow
WindowInfoTracker (Android System)
↓
WindowLayoutInfo with FoldingFeature
↓
FoldableStateObserver.processWindowLayoutInfo()
↓ (Debounce 300ms)
FoldableState (FOLDED/UNFOLDED/UNKNOWN)
↓
StateFlow emission
↓
MainActivity.handleFoldableStateChange()
↓
recreate() if layout switch needed
↓
onCreate() → useSplitView() checks currentState
↓
Correct layout loaded
Dependencies
gradle/libs.versions.toml:
[versions]
androidxWindow = "1.3.0"
[libraries]
androidx-window = { module = "androidx.window:window", version.ref = "androidxWindow" }
legacy/ui/legacy/build.gradle.kts:
implementation(libs.androidx.window)
Edge Cases & Limitations
Edge Cases
- Rapid fold/unfold:
- Solution: 300ms debounce prevents multiple recreate calls
- Orientation change during fold:
- Both events can occur
recreate()is only called once (Android standard behavior)
- Multi-window mode:
- WindowManager provides correct information per window
- Layout based on active window
- Half-open state (laptop mode):
- Treated as
UNFOLDED - User gets split-view
- Treated as
Known Limitations
- Layout switch via recreate():
- Brief flash during transition
- Alternative: Dynamic layout swapping (more complex, future improvement)
- Tablet detection:
- Large tablets without FoldingFeature →
UNKNOWN - User should choose ALWAYS or WHEN_IN_LANDSCAPE
- Large tablets without FoldingFeature →
- No hinge-position utilization:
FoldingFeature.boundsnot used- Future: Adapt content to hinge position
Testing
Unit Tests
File: core/ui/compose/common/src/test/kotlin/app/k9mail/core/ui/compose/common/window/FoldableStateObserverTest.kt
Tests:
- FoldableState mapping from WindowLayoutInfo
- Debouncing works correctly
- Lifecycle observation starts/stops correctly
- StateFlow emits correct values
Manual Testing
Emulator: Foldable device (e.g., “7.6" Fold-in with outer display”)
Test scenarios:
- Setting on WHEN_UNFOLDED
- Layout switches on fold/unfold
- Selected message persists
- Scroll position preserved
- Multi-window works
- Orientation change + fold simultaneously
Future Improvements
- Dynamic layout swapping without
recreate():- Smooth transitions without restart
- Runtime fragment container swapping
- Hinge-aware layouts:
- Content positioning around hinge
- Avoid important UI elements at fold
- Tablet detection:
- Auto-detect large non-foldables
- Auto-enable split-view on tablets
- Compose migration:
- Foldable-aware Composables
WindowSizeClassintegration