Commit graph

127 commits

Author SHA1 Message Date
jima
2a3d18cd9c Vault export/import: encrypted backup with password or QR code
New lib-vault-crypto module with Argon2id + AES-256-GCM in C via JNI.
Export screen (full Scaffold) with item selection, password mode (min 12
chars, upper/lower/digit/special), and QR mode (background generation,
save/share gating, confirmation dialog). Import via bottom sheet with
SAF file picker, password/QR decryption, and conflict resolution.
Kebab menu replaces settings icon in ConnectionListScreen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:22:31 +02:00
jima
2958d422c6 Docs update, dead code cleanup, new tests, consolidate markdown files
- Remove dead code: MainViewModel.switchToPreviousSession(),
  getConnectionIdForSession(), unused DisposableEffect import
- Fix RefactorPhase1Test: remove tests referencing deleted Routes.SFTP
- Add 32 unit tests for TabType, SftpTabInfo, duplicate label logic
- Update CLAUDE.md: SDK 27/36, Room v11, single-Activity UI section,
  SFTP+port forwarding now implemented
- Update TECHNICAL.md: Room v11, MIGRATION_10_11, SessionEntry fields,
  MainViewModel SFTP tabs, TerminalPane softInputEnabled, SftpScreen
  in Layer 1
- Move FULL_AUDIT.md and AUDIT_LOG.md to docs/, add architecture notes
- Delete TERMINAL_KEYBOARD_LIBRARY.md and MEGA_TASK.md (redundant)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:04:02 +02:00
jima
cdeb3a5045 SFTP tabs in Layer 1, fix dual-keyboard bug, add keyboard diagnostics
SFTP is now rendered as an independent tab (amber accent) in the terminal
layer instead of a NavHost route. Tabs support duplicate, rename, close.

Fix dual-keyboard bug where system keyboard appeared on top of custom
keyboard: set softInputEnabled=false on TerminalSurfaceView in CKB mode
so tapping the terminal no longer triggers the IME.

Add diagnostic logging for keyboard show/hide transitions, softInputEnabled
changes, and DataStore keyboard type persistence to debug field reports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:51:39 +02:00
jima
790ad57e4e Session tab polish: ordering, numbering, keyboard, menu guards
Tab ordering: separate _tabOrder List<Long> in ViewModel instead of
relying on Map iteration order (StateFlow conflates equal maps,
silently dropping reorders). Duplicates insert right after source tab.

Numbering: scan existing labels to find max (N), no counter state.
Closes (2), duplicates → (4). All gone → resets to (2).

Keyboard: factory starts kbContainer GONE, default keyboardType=""
to prevent flash of custom keyboard on system-keyboard users.

Menu: Duplicate and SFTP only shown when session is Connected.

Session picker: LazyColumn for scrollable list with many sessions.

Code cleanup: removed dead _sessionFontSizes StateFlow, removed
reorderSessionAfter from TerminalService, deduplicated IME handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:36:37 +02:00
jima
a5e3111214 Auto-scroll tab bar to show newly created session
When sessions.size changes, animateScrollTo(maxValue) scrolls the
horizontal tab bar to the end so the new duplicate tab is visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:40:16 +02:00
jima
8b8989b19f Don't reflow ScreenBuffer in onSurfaceReady
surfaceChanged fires before layout settles, giving tiny dimensions
(e.g. 80x8 instead of 80x26). reflowIfNeeded would truncate the
buffer to these wrong dimensions. onDimensionsChanged fires ~200ms
later with the correct post-layout size and handles the reflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:38:54 +02:00
jima
59835a29f0 Remove requestLayout post that broke pinch-to-zoom
The post { requestLayout() } ran on every recomposition, causing
continuous layout cycles that interfered with ScaleGestureDetector.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:31:35 +02:00
jima
ef26037e14 Force keyboard re-layout when session becomes visible
KeyboardView (ViewPager2) may not measure correctly if its container
was INVISIBLE during initial layout. Post requestLayout() on the
keyboard container whenever the session becomes visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:16:20 +02:00
jima
f567be75ce Pass source fontSizeSp directly to connectSSH on duplicate
Was setting fontSizeSp on the new SessionEntry AFTER connectSSH
returned — race with TerminalPane factory which reads it at creation.
Now passed as parameter so it's set at SessionEntry construction time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:07:59 +02:00
jima
4c9d09dcf9 Fix: pinch zoom was not saving to Room — reconnect lost font size
onZoomResize wrote to SessionEntry but never called setSessionFontSize
which writes to Room. Re-added onZoomChanged callback from TerminalPane
to MainActivity so pinch zoom saves to both SessionEntry AND Room
(when single session for host).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:02:53 +02:00
jima
b5a957df16 Add ADB fontsize extra + font/grid info in cursor log
ADB receiver now accepts --es fontsize "8.0" to set per-session zoom.
Cursor log now includes font size and grid dimensions for debugging.

Verified font persistence across:
- Back-exit + reopen: 8sp persisted via Room (72x25 grid) 
- Force-stop + reopen: 8sp persisted via Room 

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:34:33 +02:00
jima
f0c66ff28f Pass fontSizeSp through connectSSH to eliminate all zoom loss paths
Five gaps where fontSizeSp was lost:
1. connectSSHInternal destroys old SessionEntry (disconnectSession)
2. scheduleAutoReconnect removes old entry (sessions.remove)
3. MainViewModel.connect() never passed Room's saved value
4. MainViewModel.reconnectSession() didn't preserve from old entry
5. Race: observer seeds from Room in IO, factory reads on Main first

Fix: added fontSizeSp parameter to connectSSH/connectSSHInternal.
Set on SessionEntry at creation time — no race, no loss.

Every caller now passes the right value:
- connect(): reads from Room (saved?.fontSizeSp)
- reconnectSession(): reads from old SessionEntry
- duplicateSession(): reads from source SessionEntry
- scheduleAutoReconnect(): reads from old entry before remove

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:28:03 +02:00
jima
29789904a3 Write connecting banners to terminal buffer during SSH connect
Added writeConnectBanner() that writes bright cyan text to the terminal
buffer so the user sees connection progress:
- Direct: "── Connecting to user@host:port ──"
- Jump hosts: "── Connecting to jump 1/2: user@jumphost:port ──"
- After jump chain: "── Connecting to user@destination:port ──"

Uses the same ANSI style as the reconnecting banner (bold bright cyan).
Visible underneath the spinner overlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:16:22 +02:00
jima
dc1cfa6a0f Persist font size to Room for single-session hosts, copy on duplicate
Three-tier zoom persistence:
- View: live fontSizeSp on TerminalSurfaceView (view lifetime)
- Service: SessionEntry.fontSizeSp (survives Activity death)
- Room: SavedConnection.fontSizeSp (survives app restart)

Rules:
- Zoom always saves to SessionEntry
- If only 1 session for the host: also saves to Room (permanent)
- If 2+ sessions for same host: SessionEntry only (per-session)
- New connection: reads default from Room, seeds SessionEntry
- Duplicate: copies fontSizeSp from source SessionEntry
- View recreation: factory reads from SessionEntry before surface fires

Added Room migration 10→11 (fontSizeSp REAL column).
Documented three-tier model in TECHNICAL.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:26:09 +02:00
jima
57ef899917 Persist zoom, rename, theme in service SessionEntry
All three per-session overrides were stored only in ViewModel StateFlows
which die on Activity destruction (back-exit). Moved to
TerminalService.SessionEntry which lives in the foreground service:

- fontSizeSp: restored in TerminalSurfaceView factory before surface
  callbacks fire, preventing wrong-dimension reflow
- customLabel: restored in session observer label resolution
- customThemeName: restored in session observer theme resolution

Saved on every user action (zoom pinch, rename dialog, theme picker).
Removed unused fontSizeOverride/onZoomChanged params from TerminalPane.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:18:18 +02:00
jima
e48f557808 Stable Compose identity + INVISIBLE to preserve SurfaceView state
Two causes of view recreation:
1. No key(sid) in the for loop — Compose used positional identity,
   so adding/removing sessions reshuffled all views.
2. View.GONE destroys SurfaceView surfaces — switched to INVISIBLE
   so surfaces (and their font size, scroll state) persist when
   switching sessions or navigating to the connection list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:50:30 +02:00
jima
34aa8e4529 Keep TerminalPanes always in Compose tree, toggle visibility
TerminalPanes were removed from the tree when switching to NAV_HOST,
destroying all AndroidViews (surfaces, keyboards). Every return to
terminal recreated everything from scratch, losing zoom, scroll
position, and causing unnecessary work.

Now the terminal layer stays in the tree with alpha(0) when hidden.
Views persist across navigation — zoom, scroll, keyboard state all
survive naturally without save/restore plumbing. Removed the
fontSizeOverride/onZoomChanged params since views are never recreated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:27:29 +02:00
jima
012a02a18d Fix back-exit reopening terminal; persist zoom in service
1. Removed aggressive auto-switch in checkSessionRecovery that forced
   terminal pane whenever sessions existed, ignoring back navigation.
   Now only recovers from saved DataStore state (process death case).

2. Moved per-session font size storage from ViewModel (dies on back-exit)
   to TerminalService.SessionEntry.fontSizeSp (survives Activity
   destruction). Font sizes are restored from service entries when the
   ViewModel loads sessions on rebind.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:21:43 +02:00
jima
fc2f665413 Don't dismiss keyboard when opening tab 3-dot menu
The DropdownMenu popup was focusable by default, stealing focus from
the terminal and dismissing the keyboard. Set focusable=false on the
PopupProperties so the menu opens without affecting keyboard state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:14:11 +02:00
jima
2bf147929c Preserve per-session zoom level across navigation
Zoom (font size) was lost when going back to the connection list because
TerminalPanes leave the Compose tree, destroying the views. On re-entry
new TerminalSurfaceViews started with the default 14sp.

Store zoom per session in MainViewModel._sessionFontSizes. The
onZoomResize callback saves the new font size, and fontSizeOverride
restores it when the TerminalPane factory recreates the view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:11:35 +02:00
jima
4c25a9772b Send pending resize when SSH session transitions to Connected
During Connecting, resize() skips the changeWindowDimensions call.
After connect, the PTY has the initial size (80x24) but the
ScreenBuffer was already reflowed to match the surface (42x29).
Added LaunchedEffect that sends resizePty on Connected transition
so the server matches the actual terminal dimensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:01:37 +02:00
jima
3787c85ce4 Back button always goes to connection list from terminal
Previously, back cycled between sessions when multiple tabs were open.
Now it always switches to the connection list (NavHost) regardless of
session count. Session switching stays in the tab bar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:56:02 +02:00
jima
30d0f20783 Fix auto-switch on reopen, session stability, and line wrap
1. Don't auto-switch to terminal when reopening app from launcher.
   Added initialBindComplete flag — first collect emission after service
   bind treats all sessions as pre-existing (no auto-switch). Also stop
   clearing _activeSessions in onServiceUnbound() to prevent brief
   "0 sessions" flash in connection list.

2. Reflow ScreenBuffer when terminal dimensions change. The buffer was
   created with 80 cols but the surface reported 42 — SSH server
   formatted for 42 but the buffer wrapped at 80, putting content
   off-screen. Now reflowResize() + replaceEngine() on every dimension
   change ensures buffer matches the actual terminal size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:51:27 +02:00
jima
584803cf0c Tab menu: rename/theme; spinner overlay; reconnect stays; bright colors
Session tab 3-dot menu: add Rename (dialog with text field) and Theme
(picker with 7 themes) menu items. All 5 actions now restored from the
pre-Compose implementation.

FIX 1: Centered spinner overlay on TerminalPane during Connecting state
with a Cancel button that appears after 3 seconds.

FIX 2: After 3 failed auto-reconnect attempts, session stays in
activeSessions as Disconnected instead of being silently removed.
DisconnectedBar now has a Close button so user decides when to dismiss.

FIX 3: Brighten ANSI banner colors — error 31→91, disconnect 33→93,
reconnect 36→96, SSH debug log 2;90→37, port forward errors 31→91.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:36:36 +02:00
jima
c3c62b9386 Restore 3-dot overflow menu on session tab chips
The menu was lost when SessionTabBar was rewritten in Compose (Phase 2).
Adds DropdownMenu with Duplicate, Connect via SFTP (SSH/Telnet only),
and Close actions. Chips widened from 120dp to 150dp to fit the icon.

Added MainViewModel.duplicateSession() which connects to the same host
with a new session ID, preserving auth, jump host, and lock settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:27:16 +02:00
jima
e8361e7712 ADB functional test pass: 22 passed, 2 bugs fixed, test script added
Full ADB test pass on Zebra TC21 covering app launch, SSH connection,
terminal I/O, key sequences, session lifecycle, alternate screen (vim,
htop), scrollback, keepalive, rapid input, keyboard sequences, security
log redaction, app lifecycle, and foreground notification.

Bugs fixed in prior commits:
- FileLogger not writing to file on startup (8faa3c0)
- Double PTY resize sent to SSH server (7c8cdd2)

Added scripts/adb_functional_test.sh — reusable test suite with
pass/fail/skip tracking and manual step prompts. Un-ignored scripts/*.sh
in .gitignore so deployment and test scripts are tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:32:06 +01:00
jima
7c8cdd2b64 Fix double PTY resize by deduplicating before coroutine launch
SSHSession.resize() checked currentPtyCols/Rows before scope.launch
but updated them inside the launched coroutine, so rapid back-to-back
calls from onSurfaceReady and onDimensionsChanged both passed the check
and sent duplicate changeWindowDimensions to the SSH server. Move the
update before the launch so the second call sees the already-set values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:27:17 +01:00
jima
8faa3c0caa Fix FileLogger not writing to file on app startup
FileLogger.init() was only called when the user manually copied or
cleared the log, leaving the file writer null for the entire session.
Initialize in Application.onCreate() with Downloads dir for debug
builds so file logging works from first launch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:08:28 +01:00
jima
d2a2aa919f AUDIT_LOG: instrumented test verification — 39/39 green on both devices
All 39 instrumented tests pass on TC21 (Android 14) and CT45P (Android 11)
with zero failures. No test fixes needed after single-Activity refactor —
BiometricAuth reflection field was already updated in previous commit.
Combined full suite (lint + unit + instrumented) BUILD SUCCESSFUL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:42:09 +01:00
jima
7fa4d7c380 Final audit pass: AUDIT_LOG updated, TECHNICAL.md modernized
- AUDIT_LOG: added Final Audit Pass section with all 20 checklist items,
  6/9 Phase 3 gaps resolved, fixes summary table, remaining gaps noted
- TECHNICAL.md: replaced TerminalActivity/SftpActivity sections with
  single-Activity architecture (MainActivity, MainViewModel, TerminalPane,
  SessionTabBar, three-layer Box). Updated SDK versions. Removed references
  to deleted files (TerminalConnectionHelper, TerminalSessionNav, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:35:00 +01:00
jima
165408218b Per-connection theme, WiFi permissions, manifest audit
- Per-connection theme: MainViewModel resolves SavedConnection.themeName
  per session, passes to TerminalPane as themeOverride. When set, overrides
  global theme preference. Empty = use global default.
- AndroidManifest: added ACCESS_WIFI_STATE + CHANGE_WIFI_STATE (needed for
  per-session WiFi lock in TerminalService)
- MainViewModel: sessionThemes StateFlow tracks per-connection themes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:31:34 +01:00
jima
ff92d9877b Audit fixes: monotonic clock, ViewModel error state reset
- BiometricAuthManager: System.currentTimeMillis() → System.nanoTime()
  Wall clock can jump backward/forward; monotonic clock is immune.
  Field renamed lastAuthTimeMs → lastAuthTimeNs. Timeout comparison
  adjusted: nanos > AUTH_TIMEOUT_MS * 1_000_000L
- BiometricAuthInstrumentedTest: updated reflection field name + nanos
- KeyManagerViewModel: reset _error before generateKey() and importKey()
- SftpViewModel: reset _error before createDirectory(), rename(), delete()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:29:12 +01:00
jima
381287ec24 Phase 3 gaps 1-5,7,9: Keyboard, quick bar, disconnected bar, ADB receiver, session tracking
- TerminalPane: full custom keyboard + quick bar via TerminalKeyboard library
  - Keyboard attached below terminal, quick bar above keyboard
  - Key events routed through onInput to TerminalService.writeInput()
  - IME hidden in custom keyboard mode, quick bar always visible
  - Dynamic keyboard height, language, haptic, hints from TerminalPreferences
  - Per-connection theme applied to TerminalSurfaceView
- TerminalPane: disconnected bar overlay (Compose)
  - Shows yellow (disconnect) or red (error) banner at bottom
  - Reconnect and Save buffer action buttons
  - Clean exit shows neutral banner
- MainActivity: ADB broadcast receiver (debug only)
  - Action: com.roundingmobile.sshworkbench.INPUT
  - Extras: text, enter, esc, bytes (hex), log (cursor position)
  - RECEIVER_EXPORTED only when DEV_DEFAULTS == true
- MainViewModel: session tracking (Pro)
  - Tracks connect/disconnect transitions in session observer
  - Updates SavedConnectionDao.updateSessionStart/End with timestamps
  - Gated by proFeatures.sessionTracking
- MainViewModel: reconnect, writeToSession, expose terminalPrefs/proFeatures
- Fix: collectAsState → collectAsStateWithLifecycle in MainActivity
- Strings: reconnect, save_buffer added to EN/ES/SV

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:24:30 +01:00
jima
7ea7ee79a6 Phase 2 step 7: Tests, AUDIT_LOG, deploy
8 unit tests for Pane enum and AppState (all green). Deleted obsolete
SessionNavTest (referenced deleted NavMode). Updated AUDIT_LOG.md with
complete Phase 2 documentation: architecture diagram, new/deleted files,
eliminated hacks, WiFi lock verification, known limitations.

Lint clean. All unit tests green. Deployed to duero + Zebra TC21.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:01:03 +01:00
jima
47d7d73595 Phase 2 step 6: Delete old Activities, clean manifest
Deleted 8 files (3,800+ lines removed):
- TerminalActivity.kt (1,221 lines)
- TerminalConnectionHelper.kt
- TerminalSnippetHelper.kt
- TerminalKeyboardSettingsHelper.kt
- TerminalDisconnectedMode.kt
- TerminalSessionNav.kt
- SftpActivity.kt
- ConnectionManagerActivity.kt

Manifest: removed all three old Activity declarations. Only MainActivity
remains as the launcher Activity.

TerminalService: updated notification intent and import to target
MainActivity instead of deleted ConnectionManagerActivity.

Instrumented test updated: ConnectionManagerLaunchTest now references
MainActivity.

Zero startActivity() calls between screens. Zero volatile static fields.
Zero FLAG_ACTIVITY_REORDER_TO_FRONT hacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:58:55 +01:00
jima
f33c8ec79e Phase 2 step 5: SessionTabBar Compose component + session labels
SessionTabBar.kt: pure Compose tab bar matching the terminal aesthetic
(41dp height, 120dp uniform chips, teal accent, rounded corners).
Shows one chip per active session + pinned + button.

MainViewModel: session label resolution — fetches SavedConnection name
from Room for each session, falls back to user@host. Labels cleaned up
when sessions are removed.

MainActivity: Layer 3 wired — SessionTabBar sits above the terminal
surface in a Column when TERMINAL pane is active. + button navigates
to pickHost route.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:57:10 +01:00
jima
783fcaf0a0 Phase 2 step 4: Auto-switch pane on session connect/disconnect
MainViewModel now auto-switches to TERMINAL pane when a new session
appears in the activeSessions map (Connecting or Connected state).
Also auto-switches away from terminal when the active session is
removed (to another session or back to NAV_HOST).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:55:15 +01:00
jima
9fe5ebd906 Phase 2 step 3: MainActivity with 3-layer Box architecture
Single Activity replacing CMA/TerminalActivity/SftpActivity. Three-layer
Box: terminal surfaces (Layer 1, always in tree, visibility toggled),
NavHost (Layer 2, AnimatedVisibility when NAV_HOST pane), tab bar (Layer 3,
future step).

MainActivity owns service binding, MainViewModel, biometric lock.
BackHandler: multiple sessions → previous session; single session → NAV_HOST.
Notification intent updated to target MainActivity.

Manifest: MainActivity is now the launcher. Old activities kept temporarily
for backwards compatibility (deleted in step 6).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:54:33 +01:00
jima
4c6172ee82 Phase 2 step 2: TerminalPane composable wrapping TerminalSurfaceView
AndroidView wrapper for the terminal surface. Each session gets its own
TerminalPane; visibility toggled via visible flag (invisible panes stay
in tree at size 0 — no View recreation). Handles screen buffer binding,
PTY resize callbacks, input routing, URL tap, and surface ready events.

Keyboard and quick bar will be added in subsequent steps once the
main Activity architecture is in place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:52:58 +01:00
jima
8fdad550d4 Phase 2 step 1: AppState + Pane enum + MainViewModel
AppState data class with Pane enum (NAV_HOST, TERMINAL) as the single
source of truth for what the user sees. MainViewModel (@HiltViewModel)
owns AppState, manages pane switching, service binding, connect flow,
session recovery, and session observation. Replaces the scattered state
management across ConnectionManagerActivity and TerminalActivity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:51:24 +01:00
jima
dd5ba43caa Refactor Phase 1: SftpScreen Compose, pickHost route, ScreenMode enum
Single-Activity Compose migration Phase 1. No existing Activities deleted.

ScreenMode enum (HOME/PICKER) controls ConnectionListScreen rendering.
PICKER mode: minimal top bar with close button, no settings/keys/FAB,
tap-to-connect only, no edit/delete/duplicate context menus.

pickHost route added to NavGraph — renders ConnectionListScreen in PICKER
mode. "New session" from TerminalActivity now launches CMA with
START_ROUTE=pickHost via FLAG_ACTIVITY_REORDER_TO_FRONT instead of
calling finish().

SftpScreen: pure Compose rewrite of SftpActivity UI (~530 lines).
Path breadcrumbs, LazyColumn file list, SAF upload/download via
rememberLauncherForActivityResult, transfer progress, dialogs for
new folder/rename/delete/large download confirmation. SFTP session
lifecycle managed via LaunchedEffect (open) + DisposableEffect (close).
sftp/{connectionId} route in NavGraph.

SFTP launch points in TerminalSessionNav now navigate via CMA with
START_ROUTE=sftp/$id. SftpActivity marked @Deprecated (kept in
manifest for Phase 2 removal).

CMA reads START_ROUTE intent extra and navigates via LaunchedEffect
on first composition. TerminalService passed through NavGraph to
SftpScreen for session management.

8 unit tests (ScreenMode enum, Routes constants, sftp path generator).
Lint clean, all existing tests green. AUDIT_LOG.md updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:43:12 +01:00
jima
09ab5b3de4 Session nav overhaul: uniform chips, per-session color/theme/rename, SFTP tab bar
Top bar: remove SFTP icon (+ menu still has it), show connection alias
instead of user@ip, increase height to 41dp, uniform 120dp chips with
3-dot overflow menu (#BBBBBB matching snippet hint color).

Chip menu: Duplicate, Connect via SFTP (SSH only), Color, Theme, Close.
Color dialog merges rename + color picker (name field + 32-color grid).
Theme picker reuses ThemePickerDialog (same bottom sheet as settings).
Contrasting FG color auto-computed for colored chip backgrounds.

Per-session ephemeral data (custom name, chip color, theme) stored in
SessionChipMeta map, cleared on session close.

SFTP activity gets its own session tab bar showing terminal sessions as
teal chips and active SFTP as amber chip. Tapping a terminal chip uses
FLAG_ACTIVITY_REORDER_TO_FRONT to bring TerminalActivity forward without
destroying SFTP. TerminalActivity.pendingSwitchSessionId + onResume()
handles the session switch. SFTP launch also uses REORDER_TO_FRONT.

New strings (EN/ES/SV): session_duplicate, session_close, session_rename,
session_color, session_theme, session_connect_sftp, session_connect_terminal,
session_rename_title, session_rename_hint, session_color_title,
session_theme_title, session_no_color.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:15:24 +01:00
jima
a8ab99ffae SFTP file browser: full implementation replacing stub
lib-ssh — SftpSession:
- Wraps SSHJ SFTPClient with coroutines API on single-threaded dispatcher
- listDirectory (folders first, sort by name/size/date), downloadFile,
  uploadFile (64KB streaming with progress), createDirectory, rename,
  delete, canonicalize
- SftpEntry data class with Unix permissions string formatting
- Reuses existing SSHClient via newSFTPClient() — no second TCP connection

app — SftpViewModel:
- Directory browsing with currentPath/entries/isLoading/error StateFlows
- Transfer tracking: TransferState with ACTIVE/COMPLETE/FAILED/CANCELLED
- Large file confirmation (≥5MB) via confirmLargeDownload SharedFlow
- Upload, download, mkdir, rename, delete, sort order, download URI prefs

app — SftpActivity (replaces stub):
- Dark terminal aesthetic, programmatic layout
- Path breadcrumb bar with tappable segments
- File list: folder/file icons, name, permissions, size, date
- Tap folder to navigate, tap file to download, long-press context menu
- Bottom action bar: Upload (SAF multi-file) + Download folder (SAF tree)
- Transfer progress cards with direction badges, cancel, retry, auto-dismiss
- SAF integration: persistable URI permissions, stale URI recovery

app — TerminalService SFTP session management:
- openSftpSession/getSftpSession/closeSftpSession methods
- Auto-cleanup on parent SSH session disconnect

22 new unit tests (SftpSession, SftpViewModel, SftpEntrySort)
25 new SFTP string resources in EN/ES/SV

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:21:03 +01:00
jima
902ff21417 Session navigation: top bar tabs + side drawer, SFTP stub, SOCKS5 test docs
Dual-mode session navigation for TerminalActivity:
- Top bar mode: 36dp tab strip with scrollable session tabs (teal accent),
  SFTP tab (amber), + popup menu (New session / Open SFTP / Switch to drawer)
- Drawer mode: 260dp left DrawerLayout with session list (teal dot indicators),
  SFTP section, hamburger button on quick bar, footer to switch back
- In-session switching via popup/drawer footer; global default in Settings
- Session switching rebinds TerminalSurfaceView.screenBuffer to target session

New files:
- TerminalSessionNav.kt — tab bar, drawer, session switching, hamburger button
- SftpActivity.kt — stub ("SFTP — coming soon"), registered in manifest
- SessionNavTest.kt — 8 unit tests for NavMode enum and label truncation

Settings:
- Session navigation preference (Top bar / Drawer) in Appearance section
- Show session tab bar toggle (when top bar mode selected)
- sessionNavStyle + showSessionTabBar in TerminalPreferences + SettingsViewModel

Also: rewrote docs/SOCKS5_TEST.md to use adb forward from dev machine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:27:50 +01:00
jima
ece519d768 Security audit, dead code sweep, SOCKS5 dynamic forwarding, 120+ tests
Security fixes:
- Password zeroing in connectSSHInternal catch block (jump chain failures)
- API level guard for TelephonyManager.signalStrength (min SDK 27 vs API 28)
- Port forward delete confirmation dialog added to EditConnectionScreen

Dead code removal (95 entries):
- 88 unused string resources from EN/ES/SV
- 5 unused color resources (Android template defaults)
- 2 unused XML resources (backup_rules, data_extraction_rules)

DRY refactoring:
- EditConnectionViewModel: buildSavedConnection(), saveOrDeletePassword()
- TerminalDisconnectedMode: createBarActionButton()

SOCKS5 dynamic port forwarding (new feature):
- Socks5Proxy.kt: RFC 1928 + RFC 1929 SOCKS5 proxy server
- CONNECT command with IPv4/IPv6/domain, NO_AUTH + username/password auth
- 64KB relay buffers, thread-per-connection, proper half-close
- Specific error codes: ConnectException→0x05, UnknownHostException→0x04
- TerminalService DYNAMIC branch now opens real SOCKS5 tunnel via
  SSHClient.newDirectConnection() per SOCKS5 CONNECT request
- UI: default port 1080 pre-fill, SOCKS5 hint text

Port forwarding verification:
- Schema, DAO, UI, ViewModel, tunnel activation all verified
- Confirmed NOT gated by ProFeatures (free + pro both have access)

Test coverage (120+ new tests):
- Unit: CredentialStore, KeyGenerator, ScreenBuffer, VtParser,
  ModifierStateManager, TelnetSession, JumpChain, AutoReconnect,
  ProFeatures, PortForwardDao, TunnelLifecycle, Socks5Proxy,
  FieldVisibility, FreeGate
- Instrumented: CredentialStore on-device, Room migrations 1-10,
  BiometricAuthManager timeout, ConnectionManager launch,
  CASCADE delete, port forward CRUD, SOCKS5 flow
- docs/SOCKS5_TEST.md: manual smoke test commands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:32:17 +01:00
jima
169cb0c5e3 Update TECHNICAL.md: comprehensive docs for all recent features
Added/updated: password auth flow, dialog system, Firebase, ADB testing,
network-aware reconnect, selection snap, quick bar enhancements, port
forwarding schema, launcher icons, permissions, string resources.
Expanded security section with credential storage, TOFU, biometric,
auth cascade, and redaction details. DB version 7→10, removed ExtraKeysBar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:30:15 +01:00
jima
40f653d0ba Launcher icons: free (teal) and pro (amber) flavor icons
Adaptive icons (API 26+) with solid color backgrounds and PNG foregrounds.
Legacy mipmap PNGs for mdpi through xxxhdpi. Design: large W with >_ overlay,
monospace bold, rendered from SVG spec via cairosvg.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:22:01 +01:00
jima
73e7629a5a Password auth dialog, credential fixes, dialog icons, Firebase, ADB testing
- Password auth: server-driven flow (try connect first, prompt on auth failure),
  IME Done key triggers Connect, masked by default, keyboard focus/show
- Credential fixes: deletePassword when cleared in edit screen, no auto-save
  of password in autoSaveConnection (only explicit "Remember" saves)
- Dialog icons: moved from centered body to title bar (setIcon) across all dialogs
- Keyboard: defer terminalView focus until Connected state, show AKB explicitly
  after auth retry succeeds
- Password dialog: ADJUST_PAN soft input mode, requestFocus + showSoftInput
- Firebase Analytics + Crashlytics integration (BoM, plugins, .gitignore)
- ADB testing: broadcast receiver enhancements, test script, docs/TESTING.md
- Quick bar: double-tap word jump, CTRL modifier for system keyboard,
  key repeat on quick bar, green highlight for active modifiers
- Snippet hint colors lightened (#BBBBBB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:06:39 +01:00
jima
34fd76e48d System keyboard quick bar, selection snap, ExtraKeysBar removal, UX fixes
- Remove ExtraKeysBar entirely — quick bar from custom keyboard now serves
  both keyboard modes, always visible
- System keyboard mode: quick bar always shown, custom keyboard container
  hidden, no longer forced visible on Connected state re-observation
- Selection handle snap-to-content on drop: end handle snaps backward to
  last non-whitespace, start handle snaps forward to next content line
- Suppress IME suggestions with VISIBLE_PASSWORD input type
- Context menu: show "New Session" (green) instead of "Connect" when host
  has active sessions
- Fix APK date: resolve date at task execution time, not configuration
  time, so it's never stale from Gradle config cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 08:12:01 +01:00
jima
7b1a795008 Reconnect buffer preservation, network-aware auto-reconnect, launcher fix
- Preserve terminal screen buffer across reconnect for both SSH and Telnet
  sessions (was only working for SSH triggerReconnect path)
- Add network awareness to auto-reconnect: wait for connectivity before
  counting attempts, reset counter when network returns
- Add ACCESS_NETWORK_STATE permission for ConnectivityManager callbacks
- Fix launcher icon reopening: detect MAIN/LAUNCHER intent in
  ConnectionManagerActivity and finish() to reveal existing TerminalActivity
- Fix spurious blank line on resume: only write separator on
  Connecting→Connected transition, not on re-observation
- Guard performEditorAction to only send Enter for explicit IME actions
- Add reconnecting_in and waiting_for_network strings (EN/ES/SV)
- Strengthen CLAUDE.md strings.xml rule

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:55:06 +01:00