feat: Add a launcher orientation button to the System menu

JO
3d ago
+427-5

Description

Adds a rotate button to the System drawer header, next to the "System" title. Each press turns the launcher 90° and stores the result, so the next launch opens in the orientation the user left it in. New feature, so it falls under "Other" in the template below.

Five states. The four orientations plus auto (SCREEN_ORIENTATION_UNSPECIFIED), which releases the lock back to the system. Auto is the default, so existing users see no change until they press the button.

Ring anchored to the device's natural orientation, so the first press is always a real quarter turn instead of landing on what auto already shows:

  • portrait-natural (phones): landscape -> reverse portrait -> reverse landscape -> portrait -> auto
  • landscape-natural (handhelds, e.g. Retroid Pocket 6): portrait -> reverse landscape -> reverse portrait -> landscape -> auto

Table-driven, never derived from display.rotation. On landscape-natural panels display.rotation does not map onto the portrait-natural ActivityInfo constants; the first version derived its position that way, which skipped states and repeated the current one (landscape -> reverse portrait on the first press).

One writer for requestedOrientation. MainActivity.setOrientationTo stays the only place that sets it. While no container is running the launcher applies its own stored choice; a running game keeps its existing allowed-orientation + sensor logic untouched. That also removed the interaction where the game's allowed-orientation set clamps the launcher to landscape-only.

Storage. PrefManager.launcherOrientation (DataStore, int). Auto is stored as UNSPECIFIED, so it is remembered like any other state.

UI. The button is filled with the theme's primaryContainer while the launcher is in auto and transparent when locked, so the state is visible without a label. It reuses the existing toggle_orientation string as its content description, so no new translations. It has an explicit focus requester with up wired from the first menu item, because the header previously sat outside the drawer's focus traversal and was unreachable with a gamepad.

Verification. LauncherOrientationTest (9 tests) covers both natural orientations: ring order, first press always changes the screen, all five states reachable, the natural orientation reachable as a locked state, unknown stored value restarting cleanly. :app:assembleModernDebug builds. lintKotlinMain reports no findings in the touched files. Manually checked on a landscape-natural handheld (Retroid Pocket 6): all five states reachable, choice survives a restart. Not yet checked on a portrait-natural phone.

Known limits. On a landscape-natural panel the two portrait states rotate the display, so the launcher renders sideways/cramped - Android honouring the request rather than letterboxing it. The cycle assumes auto shows the natural orientation; with system rotation locked to a non-natural orientation the first press can land on what is already on screen. VR builds keep their fixed landscape projection (the handler is a no-op via MainActivity.isHeadset).

Recording

https://github.com/user-attachments/assets/6b61439f-9204-4cf0-84cd-bc7c0823c3c7

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Adds a rotate button to the System menu header that cycles the launcher through four 90° orientations plus auto, persisting the choice so the next launch opens the way the user left it.

  • The five-state ring is anchored to the device's natural orientation, so the first press is always a real quarter turn. From auto, it enters at the first orientation whose shape differs from what's on screen, so the press never looks dead when system rotation is locked to a non-natural orientation.
  • The launcher applies its stored choice only while it owns the orientation (auto set); games keep their allowed-orientation logic, including the request sent before the container starts. The stored choice is restored only when no container is running.
  • The ring is a fixed table per natural orientation, never derived from display.rotation.
  • Auto (SCREEN_ORIENTATION_UNSPECIFIED) stays the default, so existing users see no change until they press the button.
  • The button is highlighted while in auto, gamepad-reachable from the first menu item, and a no-op on VR headsets, which keep their fixed landscape projection.

Summary by CodeRabbit

  • New Features
    • Added a system menu control to cycle the launcher through portrait and landscape orientations, including automatic rotation.
    • The launcher remembers the selected orientation and restores it when reopened.
    • The orientation control does not change the launcher orientation on headset devices.