Files
dwarf-go/analysis/UI_REFERENCE.md
Jacquin Antoine 814a836c5a Reverse-engineer DWARF II telescope API and build open-source Go client
Complete reverse-engineering of the DWARFLAB Android app (v3.4.0) protocol
and implementation of a working CLI tool to control DWARF II telescopes.

Analysis (from APK decompilation with jadx):
- Extracted 17 protobuf definitions (382 messages) from embedded descriptors
- Mapped all 323 WebSocket command IDs across 16 modules
- Documented the full protocol: BLE discovery, WebSocket control (port 9900),
  RTSP preview, WsPacket envelope (proto v2.3)
- Documented the Android UI structure (screens, navigation, shooting modes)
- Key discovery: telescope responds with type=3 (reply), not type=1 (response),
  and several commands are fire-and-forget (RGB, camera open/close)

dwarfctl Go client:
- Protobuf bindings generated from extracted .proto files (397 messages)
- WebSocket transport layer with request-response matching and notification fan-out
- Typed API covering cameras, motors, astrophotography, focus, tracking, system, power
- Cobra CLI with 30+ subcommands and --debug traffic logging
- 57 unit tests (transport round-trip, command routing, proto encoding)
- Validated on real hardware: state, photo, motor slew (all directions/speeds),
  focus, RGB, time/location sync all confirmed working

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-12 15:18:56 +02:00

25 KiB
Raw Permalink Blame History

DWARFLAB App — UI / UX Reference (reverse-engineered)

Reverse-engineered from DWARFLAB.apk v3.4.0 (com.convergence.dwarflab). This document describes the user interface, screen hierarchy, navigation, and interactions so the app can be reimplemented in another language/framework.

Focus: Splash → Home → Connection → Capture (main shooter) → Settings. (Atlas/Sky-Atlas and Album/Gallery screens are documented separately.)


1. App structure & navigation model

The app uses Jetpack Navigation (single-activity MainActivity + a NavHostFragment) with a BottomNavigationView of 4 tabs, plus several full-screen Activities launched outside the nav graph.

1.1 Entry / launch flow

SplashActivity  (launcher, branding + init)
   └─▶ MainActivity  (NavHost + BottomNavigationView, 4 tabs)
                         ├─ Tab 1: Home     → HomeFragment
                         ├─ Tab 2: Atlas    → AtlasActivity (separate)
                         ├─ Tab 3: Album    → AlbumFragment / AlbumActivity
                         └─ Tab 4: Settings → SettingsFragment
  • mobile_navigation.xml is the main nav graph; startDestination = navigation_device (Home).
  • HeartbeatEntryActivity is a transparent launcher alias used to keep a foreground service alive (the BLE/Wi-Fi heartbeat to the scope).
  • Tab labels (from bottom_nav_menu.xml + strings home_3/4/5, camera_astro_69):
    tab id label (en) icon
    1 navigation_device Home icon_function_tabbar_home
    2 navigation_atlas Atlas icon_function_tabbar_atlas
    3 navigation_album Album icon_function_tabbar_album
    4 navigation_settings Settings icon_function_tabbar_settings

The bottom bar can be hidden (MainActivity.isShowBottomNav) when entering full-screen sub-flows (device connect, OTA, capture).

1.2 Activities (beyond the nav graph)

Activity Purpose
SplashActivity Cold-start splash, version checks, routing to connect vs. main
MainActivity Hosts the 4 tabs + NavHost
CaptureActivity The shooting screen (9199 lines — the heart of the app)
AtlasActivity Sky atlas / star map / GoTo target selection
AlbumActivity Gallery of captured photos/videos/FITS
LocationActivity Map to set observing location
CaliFrameActivity Calibration-frame capture/management
HelperActivity In-app help / guide center
WebActivity / NavActivity Embedded web views (privacy, OSS notice, help docs)
NfcWriteActivity NFC tag update (device name change)
MosaicGuideActivity, SkyLightTipActivity, StarSearchActivity Atlas sub-screens

2. Splash & first-run

SplashActivity → decides between:

  • Not connected: routes to HomeFragment which shows a device-discovery / connect CTA when no scope is paired.
  • App upgrade available: shows AppUpdateDialog (force-update path uses FirmwareForceUpdateDialog).
  • System notice: AppNotice banner.

MainActivity also handles: app-upgrade dialogs, product-activation dialog (ProductActivateDialog), not-enough-storage dialog, and an activation-error dialog. Activation is gated behind internet access (strings settings_2545).


3. Home tab (HomeFragment) — fragment_home.xml

The dashboard / device hub. Two visual states:

3.1 Disconnected state

  • Title bar: device-name placeholder + a "Guide" button (home_16, top-right, primary pill).
  • Centered illustration + "Connect" call-to-action → navigates to DeviceSearchFragment (BLE scan).
  • view_bg_home_disconnected background shown.

3.2 Connected state

Top bar becomes a device selector dropdown (tv_title with drawableEnd icon_action_arrow_down): tapping opens device switcher. Below: device status card, then action entries.

Navigation actions declared on navigation_device (HomeFragment):

Action Destination Meaning
action_deviceFragment_to_deviceSearchFragment Device Search Add/switch device
action_homeFragment_to_locationFragment Location Set GPS/observing site
action_homeFragment_to_ScheduleFragment Schedule Shooting plans list
action_homeFragment_to_upgradeFragment OTA Upgrade Firmware update
action_homeFragment_to_scheduleDetailFragment Schedule Detail Edit a plan
action_homeFragment_to_helperFragment Helper In-app guide
action_homeFragment_to_webFragment Web Embedded web content
action_home_to_taskListFragment Task List Task/creation jobs (createType arg)
action_home_to_astroFitsFragment Astro FITS View a FITS file (filePath)
action_home_to_caliFrameListFragment Cali-Frame List Manage calibration frames (caliFrameType, cameraType)

So Home is a launcher into: connect, location, schedule, OTA, tasks, cali-frames, help. The actual shooting is entered by tapping the connected device card → launches CaptureActivity.


4. Device connection flow

A multi-step wizard (mostly bottom-sheet dialogs) living under navigation_device_connect and friends.

DeviceSearchFragment (BLE scan, lists nearby scopes)
   ├─▶ DeviceConnectFragment (dialog) — enter device password
   │     ├─ success → DeviceConnectSuccessFragment
   │     ├─ wrong pwd → DeviceConnectPwdErrorAgainFragment
   │     └─ upgrade needed → UpgradeReminderFragment → OTAUpgradeFragment
   ├─▶ WiFiListFragment (STA mode: scan Wi-Fi for the scope)
   │     └─▶ StaInfoFragment (enter Wi-Fi password) → DeviceConnectFragment
   ├─▶ DeviceConnectManualFragment (manual IP entry)
   ├─▶ DeviceStaPwdFragment / DeviceStaConnectFailedFragment
   └─▶ DeviceResetTutorialFragment / DeviceConnectFailedFragment

Key strings reveal the model:

  • Connection modes (settings_connection_1421):
    • Auto (Recommended) — auto-choose AP vs STA.
    • STA mode — phone & scope join the same home Wi-Fi (scope gets internet).
    • AP mode — connect directly to the DWARF hotspot (outdoor/no-Wi-Fi).
  • Wi-Fi Compatible Mode + Enable 2.4GHz toggles for region/legacy fixes.
  • Region setting (settings_connection_10) — country code for Wi-Fi regs.
  • Activate Wi-Fi at Startup — auto-enable hotspot on boot.
  • Device password screen distinguishes default password vs custom (isDefaultPwd arg → DevicePasswordFragment).

DeviceSearchFragment args: type (0 = first connect, 1 = add another), deviceName (nullable, for reconnect). Result carries a DeviceInfo (com.convergence.dwarflab.net.discovery.DeviceInfo) containing BLE-resolved IP/SSID/PSK (see API_REFERENCE §2.1 for the BLE DwarfEcho payload).


5. Capture screen (CaptureActivity) — the shooter

This is the most complex screen (~9200 lines). Landscape-oriented camera viewfinder with overlaid controls. Layout: activity_capture.xml.

5.1 Layout regions

┌──────────────────────────────────────────────────────────────┐
│ [ TechniqueBar (top-right) ]   [ Sky-finder actions (top-right) ] │
│                                                                │
│  ┌────────────────────────────┐   ┌──────────┐                 │
│  │                            │   │ Wide PIP │ ← wide-angle    │
│  │   MAIN PREVIEW (RTSP)      │   │ preview  │   inset (top-L) │
│  │   Tele camera              │   └──────────┘                 │
│  │   (RtspPlayerView)         │                                 │
│  │   + TrackBoxView overlay   │   [ RGB container (right) ]     │
│  │   + PreviewBorderView      │                                 │
│  │   + ShootingProgressView   │                                 │
│  └────────────────────────────┘                                 │
│                                                                │
│  ┌─Left─┐            ┌─Joystick─┐         ┌──Right panel──┐    │
│  │panel │            │ (polar   │         │ Focus         │    │
│  │      │            │  D-pad)  │         │ ┌───────────┐ │    │
│  │ ✕back│            │          │         │ │ CAPTURE   │ │    │
│  │ PIP  │            └──────────┘         │ │  BUTTON   │ │    │
│  │ more │                                 │ └───────────┘ │    │
│  │ ?    │   [ ParameterIndicatorBar ]     │ Parameter     │    │
│  │ album│   [ DeviceIndicatorBar ]        └───────────────┘    │
│  │ clear│                                                      │
│  └──────┘   [ GoTo composition pill ]  [ ZoomLensPanel ]        │
│                                                                │
│   (overlays: FocusProgress, StopProgress, CaptureStatus,        │
│    FullscreenProgress, CaliFrameProgress, PanoComposition)       │
└──────────────────────────────────────────────────────────────┘

5.2 Left control panel (view_capture_left_control_panel.xml, 48dp wide)

Vertical icon stack, top→bottom:

id icon action
iv_photography_cancel arrow-left Back (exit capture)
iv_photography_pip pip icon Toggle PIP (picture-in-picture wide inset)
iv_photography_more_feature_set (dynamic) More settings (opens CamMoreFeatureSetDialog)
iv_photography_guide question-circle Guide/help (hidden by default)
iv_open_album album icon Open Album (jump to gallery)
ivClearAll clear-all Clear (reset selection/overlay)

5.3 Right control panel (view_capture_right_panel.xml, 84dp wide)

Vertical stack:

id label icon action
btn_focus_view_capture_right_panel Focus (camera_general_2) icon_function_main_focus Open focus controls (AF, manual, astro-AF)
item_main_capture_button_view — (76dp circular) dynamic Main shutter (see §5.5)
btn_parameter_view_capture_right_panel Parameter (camera_astro_222) icon_function_general_parameter Open CamParameterSetDialog (exposure/gain/WB/ISP)

ZoomLensPanel sits just left of the right panel (zoom/crop-ratio control).

5.4 TechniqueBar (top-right) — mode selector

A pill that shows the current ShootingModeType + ShootingTech and a right-side action (RightActionType). Tapping opens ShootingModeSelectDialog to pick a mode.

ShootingModeType (the "scene"/target class) — ShootingModeType.java:

value name notes
1 NORMAL generic
2 DSO deep-sky object (needs calibration + EQ)
3 SUN_MOON solar/lunar
4 MILKY_WAY Milky Way wide-field
5 STAR_TRAIL star-trail time accumulation
6 AUTO_TRACKING generic object tracking
7 PANORAMA panorama/grid capture
8 SUN sun (ND filter warning!)
9 MOON moon
10 PLANET planetary
0 UNKNOWN default

Each mode carries flags: isWidePreviewOnly, isWidePreviewSuggested, isPipWindowBtnEnable, isHidePip, isViewRectangleVisible, isAddQuickSet, cameraType (Tele/Wide), shootingTech, isAstroFocus, isShowAutoFocusSwitch, isSunOrMoon, isLongTouchStop, isSolarSystem, isShowZoomLens, isEnterAtlasVisible, isEnableCurves, isAreaFocusEnable. These drive which UI controls are visible.

ShootingTech (the capture technique) — ShootingTech.java:

value name UI ring style
0 UNKNOWN
1 SINGLE_SHOT single photo
2 STACKING live-stacking (astro)
3 BURST burst
4 VIDEO video record
5 TIMELAPSE timelapse
6 PANORAMA panorama/grid

RightActionType (what the TechniqueBar's right button does): NORMAL, FOCUS, PARAMETER, TECHNOLOGY, TECHNOLOGY_SETTING, MODE, LongPressStop.

5.5 Main capture button (MainCaptureButtonView)

A 76dp circular shutter whose appearance is driven by CaptureState (CaptureState.java) and the current ShootingModeType/ShootingTech. States include: None, PreparingAstronomyDarkFrame, RawDarkTaking, RawDarkStopping, RawDarkStopped, RawTaking, RawStopping, GoLive, AIEnhance, AIEnhanceStopping, PanoramaTaking, PanoramaStopping, RecordTaking, RecordStopping, …

Behavior:

  • Tap → start capture (photo/burst/stack/record depending on tech).
  • Long-press → stop (isLongPressTriggered; shows item_long_press_stop pill "Long Press to Stop" / camera_general_153, then "Stopping…").
  • Ring/teeth animations reflect exposure countdown (ShootingProgressView) and long-exposure progress (NotifyProto.LongExpPhotoProgress).

5.6 Joystick (PolarDpadJoystickView, 258dp)

Bottom-left polar D-pad for manual slewing. Sends CMD_STEP_MOTOR_SERVICE_JOYSTICK (14006) with vector_angle/vector_length. Has low/high speed indicators and four directional triangle overlays.

5.7 Overlays (z-ordered by elevation)

Widget elevation purpose
TrackBoxView (tele + wide) 4dp tracking bounding-box overlay
PreviewBorderView PIP border / crop frame
ShootingProgressView (main + minor) 1dp exposure countdown ring/text
FocusProgressLayout 3dp autofocus sweep overlay
StopProgressLayout 3dp stop-in-progress overlay
CaptureStatusLayout 3dp status HUD (bottom-center)
FullscreenProgressLayout 4dp full-screen progress (calibration, GoTo)
CaliFrameProgressLayout 4dp calibration-frame progress
PanoCompositionLayout 30dp (top) panorama composition canvas
ParameterIndicatorBar / DeviceIndicatorBar 1dp bottom status chips

5.8 Sky-finder / Milky-Way action rows (top-right & bottom-center)

  • item_action_sky_finder: calibration icon, AR-toggle, exit — for the Sky-Target-Finder / AR overlay mode (CMD_ASTRO_START_SKY_TARGET_FINDER).
  • item_action_milky_way: "Locate Milky Way" (camera_astro_419) + "Confirm" (camera_astro_412) pills — Milky Way composition helper.
  • flGoComposition: "GoTo" composition pill (camera_pano_52) for panorama framing.

5.9 Key dialogs launched from Capture

Dialog Trigger Purpose
ShootingModeSelectDialog TechniqueBar tap Pick ShootingModeType
CamModeSelectDialog mode button Pick ShootingTech (photo/video/stack/…)
CamParameterSetDialog / CamParamSetVM Parameter button Exposure/Gain/WB/ISP per camera
CamMoreFeatureSetDialog left "more" icon Extra feature toggles
PenalSetDialog panel button Side-panel config
StatusHUDDialog indicator bar Detailed status overlay
DarkSceneShootingDialog dark-frame flow Dark-frame capture wizard
CaliFramePrepareDialog cali-frame flow Calibration-frame prep
AtlasGotoDialog GoTo action Confirm GoTo target
AiEnhanceProgressDialog AI enhance AI post-process progress
ReservationCurveDialog curves Tone/exposure curves editor
SkyFinderNodeTargetsDialog sky finder Pick target from AR nodes
TrackSunHintDialog sun tracking Sun-tracking safety hint
BottomTipDialog / TipDialog / MessageDialog various Tips & confirmations

6. Settings tab (SettingsFragment) — fragment_settings.xml

A single RecyclerView whose rows are built programmatically in SettingsFragment.java (grouped sections). Title: "Settings" (settings_1).

6.1 Top-level rows (from nav actions on navigation_settings)

Row (string) Destination fragment
Profile / Login-Register ProfileFragment / LoginRegisterFragment
My Device (settings_2) MyDeviceFragment
Connection Settings (settings_3) ConnectionSettingsFragment
Advanced Settings (settings_4) AdvancedFragment
Location (settings_5) LocationFragment
General (settings_6) GeneralFragment
Voice Assistant VoiceAssistantFragment
Support (settings_7) SupportFragment
About (settings_9) AboutFragment
Clear Cache (settings_8) (action, no nav)

6.2 My Device (MyDeviceFragment) sub-tree

  • Device name → DeviceNameFragment (edit name; also triggers NFC update).
  • Device password → DevicePasswordFragment (change pwd; isDefaultPwd flag) → DeviceResetFragment (factory reset).
  • NFC Update (settings_64) → NfcWriteActivity — hold phone near ring light.
  • Activation infoActivateInfoFragment (status, time, warranty).
  • StorageStorageFragment (total/available size args).
  • Firmware/OTA — reuses OTAUpgradeFragment.

6.3 Connection Settings (ConnectionSettingsFragment)

Toggles + radio (strings settings_connection_129):

  • Connection mode: Auto / STA / AP (radio).
  • Activate Wi-Fi at Startup (switch).
  • Enable 2.4GHz, Wi-Fi Compatible Mode (switches).
  • RegionRegionSettingsFragment (country code).
  • Configure STA network → WiFiListFragmentStaInfoFragment/StaInfoSetFragment.

6.4 General (GeneralFragment) sub-tree

  • LanguageLanguageSettingsFragment.
  • Temperature unitTemperatureUnitFragment.
  • Appearance / Dark modeAppearanceFragment.
  • Image correctionImageCorrectionFragment → list (ImageCorrectionListFragment) → item (ImageCorrectionListItemFragment) + helper.
  • Floating windowFloatingWindowFragment.
  • Calendar (dialog) → CalendarFragment.
  • Auto-stop durationAutoStopDurationFragment.
  • Emotion / voiceEmotionFragment, EmotionScaleFragment, SpeechRateFragment, VoiceTypeFragment, VoiceNavigationModeFragment.
  • Upload log / Log typeUploadLogFragment, LogTypeFragment.

6.5 Advanced (AdvancedFragment)

Device-level toggles: master lock, MTP mode, CPU mode, low-temp protection, lens defog, auto-cooling, auto-shutdown (the CMD_SYSTEM_* / CMD_DEVICE_* commands). Shows layout_advanced_settings_unavailable.xml when the connected device/firmware doesn't support an option.

6.6 Support / About

  • Support (settings_7): FAQ/web links, contact support@dwarflab.com, log upload.
  • About (settings_9): app version, OSS notice (site.dwarflabapp.com/app-about/open-source-software-android), privacy & terms (en/zh).

7. OTA / firmware upgrade flow

UpgradeReminderFragment (dialog: "new firmware available")
   └─▶ OTAUpgradeFragment  (download + apply)
         └─▶ WiFiConnectFragment (dialog, if scope needs Wi-Fi for download)
  • Args: ssid, psd, isStaMode — the scope may need to join Wi-Fi to fetch the firmware; the app drives this via BLE (ReqSta) then polls download progress.
  • Firmware download URL: https://dwarflab.com/pages/dwarflab-app-firmware-download.
  • FirmwareForceUpdateDialog blocks the app if firmware is too old for a required feature (e.g. astrophotography — see camera_astro_116).

8. Schedule / shooting-plan flow

HomeFragment
  └─▶ ScheduleFragment (list of plans)
        ├─▶ ScheduleDetailFragment (edit one plan; add targets, set params)
        │     └─▶ ScheduleErrorReasonFragment (if sync fails) [args: reason]
        └─▶ (sync/lock/unlock/delete via ScheduleProto cmds 1610016108)
  • A plan = a timed shooting session with one or more targets, per-camera params, and a date/time window (sunset→sunrise auto-calculated from Atlas).
  • Plans can be locked (16107) so they reserve their time slot; unlocking (16106) frees it.
  • "Add to Schedule" (camera_general_113), "Date of Plan" (camera_general_114), and the 12-hour-ahead sync limit (camera_general_118) are key UX constraints.

9. Shared UI conventions & components

9.1 Custom widgets (ui/widget/)

Widget Path Role
RtspPlayerView media/ RTSP preview (ijkplayer, TCP transport)
PolarDpadJoystickView widget/joystick/ Polar slew D-pad
TechniqueBar widget/capture/ Mode/tech selector pill
MainCaptureButtonView widget/capture/ Circular shutter
CaptureLeftControlPanel / CaptureRightControlPanel widget/capture/ Side control columns
ZoomLensPanel widget/capture/ Zoom / crop-ratio
ParameterIndicatorBar / DeviceIndicatorBar widget/capture/ Bottom status chips
ShootingProgressView widget/capture/ Exposure countdown ring
TrackBoxView widget/camera/module/ Tracking bbox overlay
PreviewBorderView widget/camera/module/ PIP/crop border
PanoPreviewGridLineView widget/camera/module/ Panorama grid
TargetRectView widget/camera/module/ Target rectangle
FocusProgressLayout / StopProgressLayout widget/camera/module/ Full-screen progress
RGBContainerLayout widget/camera/function/ RGB ring-light control
FullscreenProgressLayout / CaliFrameProgressLayout / PanoCompositionLayout widget/capture/ Full-screen overlays
DWRangeSliderView widget/atlas/ Range slider (params)
FitSystemView widget/ Status-bar inset helper

9.2 Design tokens

  • Material 3 color scheme: dl_color_on_surface, dl_color_surface_container_low_dark, dl_color_primary_dark, dl_color_mask_strong, dl_color_border_variant_dark, dl_color_white_80, dl_color_transparent
  • Spacing scale: dl_gap_level_4/8/12/16/24/32 (4-pt grid), dl_margin_level_3, dl_padding_level_2, dl_radius_rounded, dl_radius_medium.
  • Typography styles: title_large_emphasized, title_small_emphasized, body_medium, body_medium_emphasized, body_small_emphasized, label_medium, Photography_label_2, CamControlBtnStyle.
  • Transitions: right_slide_enter/exit, left_slide_enter/exit (push), dialog_bottom_enter/exit (bottom-sheets).

9.3 Guide / onboarding overlays

ui/guide/overlay/: BaseGuideDialogFragment, GuideDialogFragment, ActivityGuideDialogFragment, FullScreenOverlayDialogFragment — coach-mark overlays with anchor points (ui/guide/anchor/) used for first-run feature discovery.


10. Internationalization

  • Default strings in res/values/strings.xml; localized variants exist (Chinese values-zh, etc.). All user-visible text is keyed (camera_astro_N, camera_general_N, settings_N, home_N, settings_connection_N, album_N, guide_N…).
  • Help docs link to language-specific URLs: https://help.dwarflab.com/en vs /zh.
  • Voice assistant has its own strings and supports emotion/speech-rate tuning.

11. Map: UI action → API command

UI action WS command(s) Proto
Tap shutter (photo) 10002 / 12022 ReqPhotograph
Burst start/stop 10003 / 10004 …Burst
Video record start/stop 10005 / 10006 …Record
Set exposure 10009 / 12004 ReqSetExp
Set gain 10013 / 12006 ReqSetGain
WB / brightness / contrast / saturation / hue / sharpness 1001510024 ReqSetWB…
Open/close camera 10000/10001 (tele), 12000/12001 (wide)
Joystick slew 14006 ReqMotorServiceJoystick
GoTo target 11002/11003/11013/11014 ReqStartGotoDSO/…SolarSystem
Calibrate 11000 ReqStartCalibration
Live stacking 11005/11006 Req…LiveStacking
Dark frames 11007/11021 Req…RawDark…
Auto-focus 15000 ReqAutoFocus
Start track 14800 ReqStartTrack
Panorama grid 15500/15501 Req…Grid
AI enhance 11029/11030 Req…AiEnhance
EQ solving 11018/11019 Req…EqSolving
Power / reboot / RGB 1350013505 RGB.proto
Set time/location 13000/13010 ReqSetTime/ReqSetLocation
Sync schedule 16100 Schedule.proto

Full command table: CMD_TABLE.md. Proto payloads: analysis/protos/.


12. Suggested reimplementation plan (cross-platform)

  1. Screens to build first (MVP): Splash → Home (connect CTA) → DeviceSearch (BLE) → DeviceConnect (password) → Capture (preview + shutter
    • params + joystick) → Settings (My Device + Connection).
  2. State model: a single DeviceStore (connected? device_id, ip, cameras on/off, current ShootingModeType/Tech, CaptureState, live params) fed by the WebSocket NOTIFY stream (§API_REFERENCE §3.2). All UI reads from this.
  3. Capture screen is the hard part — replicate the overlay z-order (§5.7) and drive the capture button from CaptureState.
  4. Two preview surfaces (Tele main + Wide PIP) each need an RTSP consumer; toggle PIP via CMD_SWITCH_MAIN_PREVIEW (14809).
  5. Keep the mode-driven visibility model: each ShootingModeType flag (isWidePreviewOnly, isAstroFocus, isShowZoomLens…) controls which controls render — don't hardcode per-screen logic.