Skip to content

Conversation

@someonesocial
Copy link

Summary

Adds automatic split-screen layout switching for foldable devices. Users can select "When device is unfolded" in the split-screen settings, and the app will automatically adapt the layout based on the device's fold state.

Motivation

Foldable devices like Galaxy Z Fold series offer both compact (folded) and tablet-like (unfolded) form factors. Users need a way to automatically switch between single-pane and split-view layouts based on the device's current state, providing the optimal experience for each form factor.

Changes

Core Components Added

  • FoldableStateObserver: Lifecycle-aware observer using Jetpack WindowManager
    • Tracks device fold state via WindowInfoTracker
    • Exposes StateFlow<FoldableState> for reactive updates
    • Implements 300ms debouncing to prevent layout thrashing during fold/unfold animations
    • Location: core/ui/compose/common/src/main/kotlin/.../window/
  • FoldableState Enum: Represents device fold states
    • FOLDED: Device is folded
    • UNFOLDED: Device is unfolded or half-opened
    • UNKNOWN: Not a foldable device or state cannot be determined

Settings Integration

  • 🔧 Added WHEN_UNFOLDED to SplitViewMode enum in core/preference/api
  • 🔧 Integrated foldable state observation in MainActivity
  • 🔧 Settings UI updated with new "When device is unfolded" option

Tests & Documentation

  • ✅ Unit tests following project conventions (AAA pattern, TestLogger fakes, AssertK)
  • ✅ Test coverage: state mapping, lifecycle behavior, debouncing
  • 📚 Comprehensive documentation added

Dependencies

  • 📦 Added androidx.window:window:1.3.0 to gradle/libs.versions.toml

Technical Details

Architecture

  • Module placement: New core functionality in core:ui:compose:common (not in legacy modules) ✓
  • Dependency injection: Uses Koin with constructor injection ✓
  • Reactive updates: StateFlow for lifecycle-aware state observation ✓
  • Debouncing: 300ms delay prevents rapid recreate() calls during fold animations

State Flow

Device Fold State
    ↓
WindowInfoTracker (Android System)
    ↓  
FoldableStateObserver
    ↓ (StateFlow)
MainActivity
    ↓
Layout Recreation (if WHEN_UNFOLDED mode active)

How It Works

  1. User selects "When device is unfolded" in Settings → Display → Show split-screen
  2. FoldableStateObserver monitors device state using WindowManager
  3. On state change (fold/unfold):
    • Observer emits new FoldableState via StateFlow
    • After 300ms debounce period
    • MainActivity checks if layout should change
    • If needed, calls recreate() to switch layouts
  4. State is preserved across recreation (selected message, navigation stack, etc.)

Testing

Unit Tests

  • All 6 unit tests pass
  • Tests cover: state mapping, lifecycle events, debouncing behavior
  • Follow project conventions: AAA pattern, testSubject naming, AssertK assertions
  • Use TestLogger (fake) instead of mocks ✓

Manual Testing Checklist

  • Tested on foldable emulator (7.6" Fold-in with outer display)
  • Fold/unfold transitions work smoothly
  • Message selection preserved during transitions
  • No crashes or memory leaks
  • Works with orientation changes
  • Respects all split-view mode settings

Build & Quality Checks

  • Code compiles without errors
  • No Spotless formatting violations
  • CI checks green

Backward Compatibility

Fully backward compatible

  • No database migrations required
  • No breaking API changes
  • Existing split-view settings (Always/Never/When in Landscape) unchanged
  • Optional opt-in feature - users must manually select new option
  • Non-foldable devices: New option appears but behaves like "Never"

Performance Considerations

  • Debouncing: 300ms delay prevents layout thrashing during fold animations
  • Lifecycle-aware: Observer automatically stops when activity is not visible
  • State preservation: Uses Android's built-in state restoration mechanism
  • Memory: No memory leaks (lifecycle observer properly cleanup)

Known Limitations

  1. Activity recreation: Brief flash during layout transition (uses recreate() for simplicity)
  2. No hinge-aware layout: Content not positioned around physical hinge (future enhancement?)

Security & Privacy

  • ✅ No PII in logs (only logs state transitions)
  • ✅ No new permissions required
  • ✅ Uses official Google Jetpack library (androidx.window)

Accessibility & i18n

  • ✅ All UI strings externalized to resources
  • ✅ Following project translations policy (English only in PR)
  • ✅ Will be translated via Weblate by community
  • ✅ No accessibility regressions

Checklist

Code Quality

  • Follows project architecture guidelines
  • New code in core:* modules, not legacy:*
  • Uses Koin for dependency injection ✓
  • Follows code style (Spotless/Detekt)
  • Unit tests added with project conventions

Documentation

  • Code has KDoc comments
  • Package README included
  • Technical documentation added

Testing

  • Unit tests pass
  • Integration with MainActivity tested
  • Manual testing on foldable emulator
  • CI checks pass

Process

  • Branch created from main
  • Conventional commit format used
  • PR description complete
  • Ready to respond to review feedback

Related Issues

No existing issue - community contribution

Documentation

  • Technical documentation: docs/developer/foldable-device-support.md
  • Package documentation: Included in FoldableStateObserver.kt

Notes for Reviewers

This is my first open source contribution to Thunderbird Android.

What I focused on:

  • Following project architecture (core modules, not legacy)
  • Using project testing conventions (AAA, fakes, AssertK, testSubject)
  • Backward compatibility (opt-in feature, no breaking changes)
  • Comprehensive documentation

Questions/Areas for feedback:

  • Any improvements to the state detection logic?

I'm ready to address any feedback and make changes as needed. Thank you for reviewing! 🙏


Implementation Details:

  • Lines of code: ~400 (code) + ~600 (documentation)
  • Modules changed: core:ui:compose:common, core:preference:api, legacy:ui:legacy
  • New dependency: androidx.window:window:1.3.0
  • Tests: 6 unit tests, all passing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants