diff --git a/build/bloom-typography.json b/build/bloom-typography.json index e1a0dab90..5711fd63c 100644 --- a/build/bloom-typography.json +++ b/build/bloom-typography.json @@ -96,7 +96,7 @@ "unclassified_data": "unchanged", "fragment_kinds": ["copy", "data"], "maximum_fragments": 8, - "classified_copy_call_sites": 77, - "classified_mixed_call_sites": 12 + "classified_copy_call_sites": 81, + "classified_mixed_call_sites": 13 } } diff --git a/build/dependencies.lock b/build/dependencies.lock index aaa126dc7..d32151a3c 100644 --- a/build/dependencies.lock +++ b/build/dependencies.lock @@ -209,7 +209,7 @@ manifest = "build/legacy-manifest.json" schema = 1 component_count = 332 file_count = 2131 -byte_count = 314683902 +byte_count = 314683965 source_build_components = 228 unresolved_components = 104 inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b" diff --git a/build/legacy-manifest.json b/build/legacy-manifest.json index 2535f6dcd..dbf682152 100644 --- a/build/legacy-manifest.json +++ b/build/legacy-manifest.json @@ -85,8 +85,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/runtime.sh", "file_count": 1, - "byte_count": 45051, - "tree_sha256": "c6a9227bdbe235e730a9610bff62444889e975b7de8ba135f9c3c639948d657a", + "byte_count": 45114, + "tree_sha256": "1262e7dea822dd05d1c941707c52b989a9f5f0c550d775c4e45d6026c88ccda0", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index 8862d4ead..8e211013a 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -46,11 +46,11 @@ shoulder button. | Themes/localization | Appearance-only schema 2; schema-1 compatibility; Classic/Dense/Large Text/High Contrast authority; accessibility wins over custom themes; fallback on malformed input; exact developer-only pseudolocale opt-in; classified Bloom-copy expansion; typed mixed copy/data fragments with byte-exact runtime data; malformed, unbounded, copy-free, and capacity rejection; pseudolocale disabled in immutable Safe Mode/recovery UI | | Critical lifecycle | Exact 640x480 and 752x560 built-in projections; source/target/path and legacy-file rejection; boot/shutdown/save/low-battery/recovery/Safe Mode ignore theme/config/media; charging reflects bounded actual level; installer uses one deterministic surface; missing optional text/font fallback; signed Full-update boot/charging/shutdown/recovery checkpoint | | Settings | One sectioned surface; inline values; capability filtering; one-layer details; hidden Developer section; optional overlapping L1/R1 list paging | -| Quick Settings | START from core surfaces; immediate brightness/volume/mute; capability-gated Wi-Fi; qualified Plus remains toggleable while a powered-off radio has no `wlan0`; Off-to-On reaches the fixed backend; readable battery; B/START close | +| Quick Settings | START from core surfaces; immediate brightness/volume/mute; capability-gated Wi-Fi opens the dedicated detail surface; Wi-Fi left/right never mutates the radio; readable battery; B/START close | | Network services | Desired/runtime separation; carrier without IPv4 remains acquiring/not-ready; canonical DHCP restart and bounded post-address listener reconciliation; SSH shell and SFTP independent key-only states; canonical transfer roots and modes; Saves read-only; private roots denied; traversal/symlink/path bounds; same-filesystem atomic publication; authorized-key import; quiesce/health; stale FTP/Telnet/FileBrowser/inherited-Samba/WPS/hotspot flags cannot activate listeners; process-aware listener audit; Samba hidden until a materially different source-build candidate passes feasibility | | Wi-Fi profile store | JFFS2 private root; stdin-only bounded replacement and `bloomctl network profile upsert ID`; requested/document ID binding; explicit `network forget ID`; inactive-ID deletion with active-profile refusal; active forget only after a separately confirmed fallback connection; schema/unknown-field/count/size enforcement; WPA2-PSK, explicit open, and manual hidden WPA2; WEP and hidden open denial; unique IDs/SSIDs; redacted status; explicit local credential-free UI list with one active marker; mode-0600 atomic single-profile supplicant projection; symlink, occupied-slot, live-lock, invalid-profile, and prior-byte preservation | | Wi-Fi scan API | Asynchronous start; redacted status; explicit ID-scoped local results; cancel and terminal discard; one active worker; five-second rate limit; 15-second runtime; 64-KiB raw ceiling; 32 unique printable SSIDs; eight-operation history; open/WPA2-PSK/unsupported classification; duplicate signal reconciliation; backend failure, timeout, oversize, unsafe-path, stale/live-lock, and unsupported-hardware handling | -| Wi-Fi UI | Original Mini hidden; exact profile/scan schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence | +| Wi-Fi UI | Original Mini hidden; Settings, Shell Quick Settings, and GameSwitcher Quick Settings converge on `--settings-wifi`; direct radio toggle removed; bounded fixed-argv profile-list load outside render; connection state and saved-network detail; exact profile/scan schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence | | GameSwitcher | MENU open/toggle; Left/Right select; L1/R1 page; A resume; X actions; START Quick Settings; B Home; no inherited brightness/view/delete behavior | | Apps/Tweaks | Every app disposition recorded; normal configuration requires no Tweaks; supervised launch remains bounded | | Persistent state | Versioned atomic publication; exact restoration across Details/game/GameSwitcher/Quick Settings; malformed/oversized/symlink input defaults without Safe Mode | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e1786cac9..cb58ed790 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -543,7 +543,14 @@ model. It accepts only exact schema-1 service identities, 16 unique stored profiles, 32 unique scan results, printable 32-byte SSIDs, fixed security and operation states, one active profile, and one 18–32 byte scan ID. Unknown fields—including any credential-like field—make the snapshot unusable. This -first model slice adds no direct radio toggle or arbitrary command boundary. +model adds no direct radio toggle or arbitrary command boundary. The Settings +Wi-Fi row and both Shell and GameSwitcher Quick Settings now open one dedicated +detail route instead of invoking `bloom-network request enable|disable`. +Opening the route performs one bounded fixed-argv `bloomctl network profile +list` read outside the render path; the surface shows connection state and +saved networks only. Left/right on the Wi-Fi quick row is deliberately inert. +Scan, association, credential entry, forget, and a recovery-qualified power +control remain later NET-05 slices. Profile association reuses the one guarded network-transaction authority rather than adding a second recovery mechanism. `begin` copies the exact current diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d6f9f6ec0..36e121f74 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -366,6 +366,13 @@ unknown fields. Keep its output out of shell history, screenshots, logs, support bundles, and validation evidence. Exercise it only with synthetic profiles in host or `/tmp` fixtures until the NET-05 UI gate is reached. +The NET-05 saved-network surface is entered through `bloom-shell +--settings-wifi`. Settings A and both Quick Settings A paths navigate there; +left/right on the Wi-Fi quick row is a no-op. Do not restore a direct +`bloom-network request enable|disable` call to any of those handlers. Profile +loading must remain a single bounded fixed-argv read outside `draw()`, and UI +tests must use synthetic SSIDs only. + The NET-04 scan slice is asynchronous and keeps result data only under `/tmp`: ```sh diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 6a95ac022..8d395ca7b 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -119,6 +119,14 @@ fields so a credential-bearing response cannot cross into UI state. Do not capture this local SSID list in diagnostics, screenshots, logs, or durable evidence. +Bloom Shell consumes that list only when the user opens the Wi-Fi detail +surface. The child process receives the fixed arguments `network profile list`, +the JSON is capped at 4 KiB, and parsing fails closed before any SSID reaches +render state. Settings and both Quick Settings implementations have no direct +radio enable/disable adapter: A navigates to the detail surface and left/right +does not mutate Wi-Fi. This prevents an ordinary UI edge from severing the +only automation route or bypassing the guarded network transaction authority. + Wi-Fi scan status and logs never contain SSIDs. An explicit local results call may return at most 32 printable SSIDs for the future on-device UI, but that ephemeral mode-0600 result is excluded from diagnostics, support exports, diff --git a/docs/program/BloomOS_Work_Program_v3.json b/docs/program/BloomOS_Work_Program_v3.json index df9da60d9..79d616a7f 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -880,15 +880,19 @@ "NET-04", "UI-05" ], - "github_prs": [], + "github_prs": [ + 545 + ], "risk_tier": 1, "deployment_tier": "hot", "device_gate_required": true, - "last_audited_main": "dbe30da0a2fe9ee2a5211ce471988f132026e759", + "last_audited_main": "3b0a13a30d34733f03831b0d041785c4fc6dc3ae", "evidence": [ "issue #544 tracks NET-05 on current main after merged NET-04A-D and UI-05; the audit found that Bloom Shell still treats Wi-Fi as an inline toggle whose A and left/right handlers synchronously call bloom-network enable or disable instead of opening the bounded profile and scan UI", "the first host-safe slice adds an explicit local credential-free projection of at most 16 validated profile IDs, printable SSIDs, security classes, hidden flags, and one active marker; the private service never returns a PSK and the Shell model rejects unknown or credential-bearing fields", - "the bounded Shell model accepts at most 32 unique scan results, one 18-32 byte operation ID, fixed scan phases and errors, integral timestamps and signal values, and exact schema-1 service identities; this slice adds no rendered surface, radio toggle, scan, association, profile transition, listener mutation, reboot, update, or device mutation" + "the bounded Shell model accepts at most 32 unique scan results, one 18-32 byte operation ID, fixed scan phases and errors, integral timestamps and signal values, and exact schema-1 service identities; this slice adds no rendered surface, radio toggle, scan, association, profile transition, listener mutation, reboot, update, or device mutation", + "GitHub PR #545 passed format in 16 seconds, native in 1 minute 24 seconds, the affected ARM build in 1 minute 26 seconds, AddressSanitizer in 1 minute 55 seconds, the 736-test shell gate in 2 minutes 3 seconds, and static analysis in 2 minutes 4 seconds; it merged by squash as 3b0a13a30d34733f03831b0d041785c4fc6dc3ae", + "the dependent rendered-surface slice changes Wi-Fi from a toggle to a detail row, converges Settings and both Quick Settings implementations on the exact --settings-wifi route, removes the direct radio mutation helper, makes Wi-Fi left/right inert, and loads only the 4-KiB-bounded credential-free saved-profile projection outside the render path; scan, association, credential entry, forget, and recovery-qualified radio power remain pending" ] }, { diff --git a/src/bloomShell/bloom_shell_settings.c b/src/bloomShell/bloom_shell_settings.c index ca1b6d93a..507868b1a 100644 --- a/src/bloomShell/bloom_shell_settings.c +++ b/src/bloomShell/bloom_shell_settings.c @@ -53,7 +53,7 @@ static const BloomShellSettingsDefinition settings_rows[] = { 0, 0}, {{BLOOM_SHELL_SETTINGS_NETWORK_SECTION, BLOOM_SHELL_SETTINGS_ROW_SECTION, "NETWORK"}, 1, 0}, - {{BLOOM_SHELL_SETTINGS_WIFI, BLOOM_SHELL_SETTINGS_ROW_TOGGLE, "Wi-Fi"}, 1, 0}, + {{BLOOM_SHELL_SETTINGS_WIFI, BLOOM_SHELL_SETTINGS_ROW_DETAIL, "Wi-Fi"}, 1, 0}, {{BLOOM_SHELL_SETTINGS_SSH, BLOOM_SHELL_SETTINGS_ROW_TOGGLE, "SSH"}, 1, 0}, {{BLOOM_SHELL_SETTINGS_SFTP, BLOOM_SHELL_SETTINGS_ROW_TOGGLE, "SFTP"}, 1, 0}, {{BLOOM_SHELL_SETTINGS_SAMBA, BLOOM_SHELL_SETTINGS_ROW_TOGGLE, "Samba"}, 1, 0, 1}, @@ -168,7 +168,7 @@ int bloom_shell_settings_row_format(const BloomShellCapabilities *capabilities, : snprintf(label, label_size, "Mute unavailable"); break; case BLOOM_SHELL_SETTINGS_WIFI: - length = values->ready ? snprintf(label, label_size, "Wi-Fi %s", + length = values->ready ? snprintf(label, label_size, "Wi-Fi %s >", !values->wifi_enabled ? "Off" : !values->wifi_status_ready @@ -729,58 +729,6 @@ static int run_request(const char *path, const char *first, const char *second, return wait_child(child); } -static int run_network_request(const char *path, const char *operation) -{ - if (path == NULL || path[0] != '/' || operation == NULL) - return -1; - pid_t child = fork(); - if (child < 0) - return -1; - if (child == 0) { - if (setpgid(0, 0) != 0) - _exit(127); - FILE *null = fopen("/dev/null", "w"); - if (null != NULL) { - dup2(fileno(null), STDOUT_FILENO); - dup2(fileno(null), STDERR_FILENO); - } - execl(path, path, "request", operation, (char *)NULL); - _exit(127); - } - if (setpgid(child, child) != 0 && errno != EACCES && errno != ESRCH) { - kill(child, SIGKILL); - wait_child(child); - return -1; - } - int status = 0; - for (int attempt = 0; attempt < 100; ++attempt) { - pid_t result = waitpid(child, &status, WNOHANG); - if (result == child) - return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : -1; - if (result < 0 && errno != EINTR) - return -1; - poll(NULL, 0, 50); - } - kill(-child, SIGTERM); - kill(child, SIGTERM); - for (int attempt = 0; attempt < 10; ++attempt) { - pid_t result = waitpid(child, &status, WNOHANG); - if (result == child) { - kill(-child, SIGKILL); - return -1; - } - if (result < 0 && errno != EINTR) - return -1; - poll(NULL, 0, 50); - } - kill(-child, SIGKILL); - kill(child, SIGKILL); - while (waitpid(child, &status, 0) < 0) - if (errno != EINTR) - break; - return -1; -} - int bloom_shell_network_status_parse(const char *json, BloomShellQuickValues *values) { if (json == NULL || values == NULL) @@ -994,7 +942,7 @@ int bloom_shell_theme_variant_change(BloomShellQuickValues *values, int directio int bloom_shell_quick_settings_adjust(const BloomShellCapabilities *capabilities, BloomShellQuickValues *values, size_t row, int direction, - const char *controls_path, const char *network_path) + const char *controls_path) { if (capabilities == NULL || values == NULL || !values->ready || (direction != -1 && direction != 1)) @@ -1029,20 +977,6 @@ int bloom_shell_quick_settings_adjust(const BloomShellCapabilities *capabilities values->mute = 0; return 0; } - if (capabilities->wifi && row == 2) { - int next = direction > 0; - if (next == values->wifi_enabled) - return 0; - if (run_network_request(network_path, next ? "enable" : "disable") != 0) { - bloom_shell_network_status_load(network_path, values); - return -1; - } - values->wifi_enabled = next; - values->wifi_status_ready = 0; - values->wifi_associated = 0; - bloom_shell_network_status_load(network_path, values); - return 0; - } return -1; } @@ -1066,21 +1000,21 @@ int bloom_shell_mute_toggle(BloomShellQuickValues *values, const char *controls_ int bloom_shell_quick_settings_activate(const BloomShellCapabilities *capabilities, BloomShellQuickValues *values, size_t row, - const char *controls_path, const char *network_path, - int *open_settings) + const char *controls_path, + BloomShellQuickOpen *open) { - if (capabilities == NULL || values == NULL || open_settings == NULL) + if (capabilities == NULL || values == NULL || open == NULL) return -1; - *open_settings = 0; + *open = BLOOM_SHELL_QUICK_OPEN_NONE; if (row == 1) return bloom_shell_mute_toggle(values, controls_path); - if (capabilities->wifi && row == 2) - return bloom_shell_quick_settings_adjust(capabilities, values, row, - values->wifi_enabled ? -1 : 1, controls_path, - network_path); + if (capabilities->wifi && row == 2) { + *open = BLOOM_SHELL_QUICK_OPEN_WIFI; + return 0; + } size_t battery_row = capabilities->wifi ? 3 : 2; if (row == battery_row + 1) { - *open_settings = 1; + *open = BLOOM_SHELL_QUICK_OPEN_SETTINGS; return 0; } return row == 0 || row == battery_row ? 0 : -1; diff --git a/src/bloomShell/bloom_shell_settings.h b/src/bloomShell/bloom_shell_settings.h index dba3978aa..677fb43bd 100644 --- a/src/bloomShell/bloom_shell_settings.h +++ b/src/bloomShell/bloom_shell_settings.h @@ -97,6 +97,12 @@ typedef enum { BLOOM_SHELL_SETTINGS_DIAGNOSTICS, } BloomShellSettingsRowId; +typedef enum { + BLOOM_SHELL_QUICK_OPEN_NONE = 0, + BLOOM_SHELL_QUICK_OPEN_SETTINGS, + BLOOM_SHELL_QUICK_OPEN_WIFI, +} BloomShellQuickOpen; + typedef struct { BloomShellSettingsRowId id; BloomShellSettingsRowKind kind; @@ -139,12 +145,12 @@ int bloom_shell_quick_settings_format(const BloomShellCapabilities *capabilities char *label, size_t label_size); int bloom_shell_quick_settings_adjust(const BloomShellCapabilities *capabilities, BloomShellQuickValues *values, size_t row, int direction, - const char *controls_path, const char *network_path); + const char *controls_path); int bloom_shell_mute_toggle(BloomShellQuickValues *values, const char *controls_path); int bloom_shell_quick_settings_activate(const BloomShellCapabilities *capabilities, BloomShellQuickValues *values, size_t row, - const char *controls_path, const char *network_path, - int *open_settings); + const char *controls_path, + BloomShellQuickOpen *open); int bloom_shell_first_run_parse(const char *json, BloomShellFirstRun *first_run); int bloom_shell_first_run_load(const char *settings_path, BloomShellFirstRun *first_run); int bloom_shell_first_run_finish(const char *settings_path, BloomShellFirstRun *first_run, diff --git a/src/bloomShell/bloom_shell_wifi_ui.c b/src/bloomShell/bloom_shell_wifi_ui.c index 98510983b..d8ffe8ee1 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.c +++ b/src/bloomShell/bloom_shell_wifi_ui.c @@ -2,9 +2,16 @@ #include +#include #include +#include +#include #include #include +#include +#include + +#define BLOOM_SHELL_WIFI_PROFILE_OUTPUT_LIMIT 4096 static int exact_object(const cJSON *value, int fields) { @@ -136,6 +143,76 @@ int bloom_shell_wifi_profiles_parse(const char *json, BloomShellWifiUi *wifi) return 0; } +static int wait_child(pid_t child) +{ + int status = 0; + while (waitpid(child, &status, 0) < 0) + if (errno != EINTR) + return -1; + return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : -1; +} + +int bloom_shell_wifi_profiles_load(const char *bloomctl_path, BloomShellWifiUi *wifi) +{ + if (bloomctl_path == NULL || bloomctl_path[0] != '/' || wifi == NULL) + return -1; + int output[2]; + if (pipe(output) != 0) + return -1; + pid_t child = fork(); + if (child < 0) { + close(output[0]); + close(output[1]); + return -1; + } + if (child == 0) { + close(output[0]); + if (dup2(output[1], STDOUT_FILENO) < 0) + _exit(127); + close(output[1]); + execl(bloomctl_path, bloomctl_path, "network", "profile", "list", (char *)NULL); + _exit(127); + } + close(output[1]); + char json[BLOOM_SHELL_WIFI_PROFILE_OUTPUT_LIMIT + 1]; + size_t length = 0; + int failed = 0; + while (!failed && length < BLOOM_SHELL_WIFI_PROFILE_OUTPUT_LIMIT) { + struct pollfd descriptor = {.fd = output[0], .events = POLLIN | POLLHUP}; + int ready; + do { + ready = poll(&descriptor, 1, 5000); + } while (ready < 0 && errno == EINTR); + if (ready <= 0) { + failed = 1; + break; + } + ssize_t count = read(output[0], json + length, + BLOOM_SHELL_WIFI_PROFILE_OUTPUT_LIMIT - length); + if (count < 0 && errno == EINTR) + continue; + if (count < 0) { + failed = 1; + break; + } + if (count == 0) + break; + length += (size_t)count; + } + if (length == BLOOM_SHELL_WIFI_PROFILE_OUTPUT_LIMIT) + failed = 1; + close(output[0]); + if (failed) + kill(child, SIGKILL); + int child_result = wait_child(child); + json[length] = '\0'; + if (failed || child_result != 0 || bloom_shell_wifi_profiles_parse(json, wifi) != 0) { + bloom_shell_wifi_ui_init(wifi); + return -1; + } + return 0; +} + int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi) { if (json == NULL || wifi == NULL) diff --git a/src/bloomShell/bloom_shell_wifi_ui.h b/src/bloomShell/bloom_shell_wifi_ui.h index 6382c516d..913f37541 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.h +++ b/src/bloomShell/bloom_shell_wifi_ui.h @@ -52,6 +52,7 @@ typedef struct { void bloom_shell_wifi_ui_init(BloomShellWifiUi *wifi); int bloom_shell_wifi_profiles_parse(const char *json, BloomShellWifiUi *wifi); +int bloom_shell_wifi_profiles_load(const char *bloomctl_path, BloomShellWifiUi *wifi); int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi); int bloom_shell_wifi_scan_status_parse(const char *json, BloomShellWifiUi *wifi); int bloom_shell_wifi_scan_results_parse(const char *json, BloomShellWifiUi *wifi); diff --git a/src/bloomShell/main.c b/src/bloomShell/main.c index 2e0820ac8..d62ddd0ac 100644 --- a/src/bloomShell/main.c +++ b/src/bloomShell/main.c @@ -20,6 +20,7 @@ #include "bloom_shell_state.h" #include "bloom_shell_status.h" #include "bloom_shell_theme_media.h" +#include "bloom_shell_wifi_ui.h" #include "../bloomLibrary/bloom_library_mutation.h" #include "../bloomUi/bloom_ui_components.h" @@ -1983,6 +1984,71 @@ static void draw_remote_keys(SDL_Surface *screen, const BloomUiLayout *layout, T layout->content.width - 40, result > 0 ? cream : sand); } +static void draw_wifi_details(SDL_Surface *screen, const BloomUiLayout *layout, TTF_Font *font, + TTF_Font *compact_font, + const BloomUiTheme *theme, + const BloomShellQuickValues *quick_values, + const BloomShellWifiUi *wifi, const BloomUiFocus *focus) +{ + SDL_Color cream = {243, 226, 189, 0}; + SDL_Color sand = {214, 187, 137, 0}; + SDL_Color canvas = {33, 23, 17, 0}; + BloomUiRect panel = layout->content; + if (bloom_ui_render_panel(screen, theme, &panel, 0) != 0) + return; + render_copy_label(screen, font, "Wi-Fi", layout->content.x + 20, + layout->content.y + 8, layout->content.width - 40, cream); + const char *connection = !quick_values->wifi_enabled + ? "Off" + : !quick_values->wifi_status_ready + ? "On" + : quick_values->wifi_associated ? "Connected" + : "On / No connection"; + render_copy_label(screen, compact_font, connection, layout->content.x + 20, + layout->content.y + layout->row_height, + layout->content.width - 40, sand); + render_copy_label(screen, compact_font, "Saved networks", + layout->content.x + 20, + layout->content.y + layout->row_height * 2, + layout->content.width - 40, sand); + int top = layout->content.y + layout->row_height * 3; + size_t visible_rows = layout->visible_rows > 3 ? layout->visible_rows - 3 : 1; + for (size_t row = 0; + row < visible_rows && focus->window_start + row < focus->item_count; ++row) { + size_t item = focus->window_start + row; + int y = top + (int)row * layout->row_height; + int selected = item == focus->selected; + BloomUiRect row_bounds = {layout->content.x + 8, y, + layout->content.width - 16, + layout->row_height - 2}; + BloomUiComponentSlots slots; + if (bloom_ui_render_list_row( + screen, theme, &row_bounds, + selected ? BLOOM_UI_COMPONENT_FOCUSED : BLOOM_UI_COMPONENT_DEFAULT, + &slots) != 0) + continue; + SDL_Color row_color = + selected && (theme == NULL || theme->focus == BLOOM_UI_THEME_FOCUS_INVERT) + ? canvas + : cream; + if (wifi->ready && wifi->profile_count > 0 && item < wifi->profile_count) { + const BloomShellWifiProfile *profile = &wifi->profiles[item]; + const BloomUiTextFragment fragments[] = { + {BLOOM_UI_TEXT_FRAGMENT_DATA, profile->ssid}, + {BLOOM_UI_TEXT_FRAGMENT_COPY, profile->active ? " Connected" : " Saved"}, + }; + render_fragment_label(screen, compact_font, fragments, 2, slots.content.x, + y + layout->row_height / 3, slots.content.width, + row_color); + } + else + render_copy_label(screen, compact_font, + wifi->ready ? "No saved networks" : "Profile list unavailable", + slots.content.x, y + layout->row_height / 3, + slots.content.width, row_color); + } +} + static void draw_remote_key_remove_confirm(SDL_Surface *screen, const BloomUiLayout *layout, TTF_Font *font, const BloomUiDialogFocus *dialog) @@ -2579,6 +2645,8 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l const BloomShellCapabilities *capabilities, const BloomShellQuickValues *quick_values, const BloomShellNetworkCredential *network_credential, + const BloomShellWifiUi *wifi, int wifi_open, + const BloomUiFocus *wifi_focus, const BloomShellRemoteKeys *remote_keys, int remote_keys_open, const BloomUiFocus *remote_keys_focus, int remote_keys_result, int remote_key_remove_confirm_open, @@ -2957,6 +3025,8 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l theme); const char *footer = quick_settings ? "Left/Right Change A Toggle/Open B/START Close" + : wifi_open + ? "Up/Down Browse L/R Page B Settings" : remote_keys_open ? remote_keys_focus->selected < remote_keys->key_count ? "X Remove L/R Page B Settings" @@ -2970,7 +3040,7 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l : destination == BLOOM_UI_DESTINATION_ROOT ? "Left/Right Choose A Open MENU Switcher START Quick" : destination == BLOOM_UI_DESTINATION_SETTINGS - ? "D-pad Change L/R Page A Toggle B Home START Quick" + ? "D-pad Change L/R Page A Toggle/Open B Home START Quick" : destination == BLOOM_UI_DESTINATION_APPS ? "A Open B Home START Quick" : destination == BLOOM_UI_DESTINATION_COLLECTIONS && @@ -2989,6 +3059,9 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l if (remote_keys_open && !quick_settings) draw_remote_keys(screen, layout, font, compact_font, remote_keys, remote_keys_focus, remote_keys_result); + if (wifi_open && !quick_settings) + draw_wifi_details(screen, layout, font, compact_font, theme, quick_values, wifi, + wifi_focus); if (ra_form_open) draw_ra_form(screen, layout, font, ra_form); if (search->open) @@ -3108,9 +3181,11 @@ int main(int argc, char **argv) } int root_requested = argc == 2 && strcmp(argv[1], "--root") == 0; int settings_requested = argc == 2 && strcmp(argv[1], "--settings") == 0; + int wifi_requested = argc == 2 && strcmp(argv[1], "--settings-wifi") == 0; int remote_keys_requested = argc == 2 && strcmp(argv[1], "--settings-remote-keys") == 0; - if (argc != 1 && !root_requested && !settings_requested && !remote_keys_requested) { + if (argc != 1 && !root_requested && !settings_requested && !wifi_requested && + !remote_keys_requested) { bloom_shell_achievements_destroy(&achievement_index); free(games); return 2; @@ -3195,13 +3270,14 @@ int main(int argc, char **argv) !safe_mode && bloom_shell_state_load(FRONTEND_STATE_PATH, &frontend_state) == 0; BloomShellGameListView game_list_view = bloom_shell_gamelist_view_parse( frontend_state_loaded ? frontend_state.view : NULL); - BloomUiDestination destination = (settings_requested || remote_keys_requested) && !safe_mode - ? BLOOM_UI_DESTINATION_SETTINGS - : root_requested - ? BLOOM_UI_DESTINATION_ROOT - : frontend_state_loaded - ? frontend_destination_restore(frontend_state.destination) - : BLOOM_UI_DESTINATION_ROOT; + BloomUiDestination destination = + (settings_requested || wifi_requested || remote_keys_requested) && !safe_mode + ? BLOOM_UI_DESTINATION_SETTINGS + : root_requested + ? BLOOM_UI_DESTINATION_ROOT + : frontend_state_loaded + ? frontend_destination_restore(frontend_state.destination) + : BLOOM_UI_DESTINATION_ROOT; BloomShellRootState root; if (bloom_shell_root_populate(&root, has_recent && !safe_mode, &games_browser, recent_count, favorite_count, game_count, collections.collection_count, @@ -3259,11 +3335,12 @@ int main(int argc, char **argv) } bloom_ui_focus_init(&settings_focus, bloom_shell_settings_count(&capabilities)); settings_focus.selected = bloom_shell_settings_first_selectable(&capabilities); - if (remote_keys_requested && !safe_mode && capabilities.wifi) { + if ((wifi_requested || remote_keys_requested) && !safe_mode && capabilities.wifi) { for (size_t row = 0; row < settings_focus.item_count; ++row) { BloomShellSettingsRow settings_row = {0}; if (bloom_shell_settings_row(&capabilities, row, &settings_row) == 0 && - settings_row.id == BLOOM_SHELL_SETTINGS_REMOTE_KEYS) { + settings_row.id == (wifi_requested ? BLOOM_SHELL_SETTINGS_WIFI + : BLOOM_SHELL_SETTINGS_REMOTE_KEYS)) { focus_select(&settings_focus, row, layout.visible_rows); break; } @@ -3320,6 +3397,16 @@ int main(int argc, char **argv) frontend_state.scroll_anchor, layout.visible_rows); int quick_settings = 0; BloomShellNetworkCredential network_credential = {0}; + BloomShellWifiUi wifi_ui; + bloom_shell_wifi_ui_init(&wifi_ui); + int wifi_open = wifi_requested && !safe_mode && capabilities.wifi; + if (wifi_open) + bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui); + BloomUiFocus wifi_focus; + bloom_ui_focus_init(&wifi_focus, + wifi_ui.ready && wifi_ui.profile_count > 0 + ? wifi_ui.profile_count + : 1); int remote_keys_open = remote_keys_requested && !safe_mode && capabilities.wifi; int remote_keys_result = 0; BloomUiFocus remote_keys_focus; @@ -3394,7 +3481,8 @@ int main(int argc, char **argv) draw(screen, video, &layout, &immutable_layout, &theme, font, compact_font, immutable_font, immutable_compact_font, destination, &root, &games_browser, &favorites_focus, &recent_focus, &all_games_focus, &collections, games, &achievement_index, favorites, recents, has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, apps, &status, - &capabilities, &quick_values, &network_credential, &remote_keys, remote_keys_open, + &capabilities, &quick_values, &network_credential, &wifi_ui, wifi_open, &wifi_focus, + &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, update_confirm_result, @@ -3565,20 +3653,61 @@ int main(int argc, char **argv) action == BLOOM_UI_ACTION_FOCUS_RIGHT)) bloom_shell_quick_settings_adjust( &capabilities, &quick_values, quick_settings_focus.selected, - action == BLOOM_UI_ACTION_FOCUS_RIGHT ? 1 : -1, BLOOM_CONTROLS_BINARY, - BLOOM_NETWORK_BINARY); + action == BLOOM_UI_ACTION_FOCUS_RIGHT ? 1 : -1, BLOOM_CONTROLS_BINARY); else if (quick_settings && action == BLOOM_UI_ACTION_CONFIRM) { - int open_settings = 0; + BloomShellQuickOpen quick_open = BLOOM_SHELL_QUICK_OPEN_NONE; if (bloom_shell_quick_settings_activate( &capabilities, &quick_values, quick_settings_focus.selected, - BLOOM_CONTROLS_BINARY, BLOOM_NETWORK_BINARY, &open_settings) == 0 && - open_settings) { + BLOOM_CONTROLS_BINARY, &quick_open) == 0 && + quick_open != BLOOM_SHELL_QUICK_OPEN_NONE) { quick_settings = 0; destination = BLOOM_UI_DESTINATION_SETTINGS; + if (quick_open == BLOOM_SHELL_QUICK_OPEN_WIFI) { + for (size_t row = 0; row < settings_focus.item_count; ++row) { + BloomShellSettingsRow settings_row = {0}; + if (bloom_shell_settings_row(&capabilities, row, + &settings_row) == 0 && + settings_row.id == BLOOM_SHELL_SETTINGS_WIFI) { + focus_select(&settings_focus, row, layout.visible_rows); + break; + } + } + bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui); + bloom_ui_focus_init(&wifi_focus, + wifi_ui.ready && wifi_ui.profile_count > 0 + ? wifi_ui.profile_count + : 1); + wifi_open = 1; + } } } else if (quick_settings) continue; + else if (wifi_open && action == BLOOM_UI_ACTION_GAME_SWITCHER) { + wifi_open = 0; + if (run_game_switcher_child(destination == BLOOM_UI_DESTINATION_ROOT) > 0) { + exit_code = LAUNCH_READY_EXIT; + running = 0; + } + } + else if (wifi_open && action == BLOOM_UI_ACTION_QUICK_SETTINGS) + quick_settings = 1; + else if (wifi_open && action == BLOOM_UI_ACTION_FOCUS_UP) + bloom_ui_focus_step(&wifi_focus, -1, + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + else if (wifi_open && action == BLOOM_UI_ACTION_FOCUS_DOWN) + bloom_ui_focus_step(&wifi_focus, 1, + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + else if (wifi_open && + (action == BLOOM_UI_ACTION_PAGE_BACK || + action == BLOOM_UI_ACTION_PAGE_FORWARD)) + bloom_ui_focus_page(&wifi_focus, + action == BLOOM_UI_ACTION_PAGE_BACK ? -1 : 1, + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + else if (wifi_open && action == BLOOM_UI_ACTION_BACK) + wifi_open = 0; + else if (wifi_open) + continue; else if (remote_key_remove_confirm_open && action == BLOOM_UI_ACTION_GAME_SWITCHER) { remote_key_remove_confirm_open = 0; @@ -4657,13 +4786,11 @@ int main(int argc, char **argv) quick_row = 0; else if (selected_settings.id == BLOOM_SHELL_SETTINGS_VOLUME) quick_row = 1; - else if (selected_settings.id == BLOOM_SHELL_SETTINGS_WIFI) - quick_row = 2; if (quick_row != (size_t)-1) bloom_shell_quick_settings_adjust( &capabilities, &quick_values, quick_row, - action == BLOOM_UI_ACTION_FOCUS_RIGHT ? 1 : -1, BLOOM_CONTROLS_BINARY, - BLOOM_NETWORK_BINARY); + action == BLOOM_UI_ACTION_FOCUS_RIGHT ? 1 : -1, + BLOOM_CONTROLS_BINARY); else if (selected_settings.id == BLOOM_SHELL_SETTINGS_THEME && bloom_shell_theme_variant_change( &quick_values, @@ -4724,9 +4851,12 @@ int main(int argc, char **argv) else if (action == BLOOM_UI_ACTION_CONFIRM && destination == BLOOM_UI_DESTINATION_SETTINGS && selected_settings_valid && selected_settings.id == BLOOM_SHELL_SETTINGS_WIFI) { - bloom_shell_quick_settings_adjust(&capabilities, &quick_values, 2, - quick_values.wifi_enabled ? -1 : 1, - BLOOM_CONTROLS_BINARY, BLOOM_NETWORK_BINARY); + bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui); + bloom_ui_focus_init(&wifi_focus, + wifi_ui.ready && wifi_ui.profile_count > 0 + ? wifi_ui.profile_count + : 1); + wifi_open = 1; } else if (action == BLOOM_UI_ACTION_CONFIRM && destination == BLOOM_UI_DESTINATION_SETTINGS && selected_settings_valid && @@ -4870,7 +5000,8 @@ int main(int argc, char **argv) draw(screen, video, &layout, &immutable_layout, &theme, font, compact_font, immutable_font, immutable_compact_font, destination, &root, &games_browser, &favorites_focus, &recent_focus, &all_games_focus, &collections, games, &achievement_index, favorites, recents, has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, - apps, &status, &capabilities, &quick_values, &network_credential, &remote_keys, + apps, &status, &capabilities, &quick_values, &network_credential, + &wifi_ui, wifi_open, &wifi_focus, &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, @@ -4896,7 +5027,8 @@ int main(int argc, char **argv) immutable_font, immutable_compact_font, destination, &root, &games_browser, &favorites_focus, &recent_focus, &all_games_focus, &collections, games, &achievement_index, favorites, recents, has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, apps, &status, - &capabilities, &quick_values, &network_credential, &remote_keys, + &capabilities, &quick_values, &network_credential, + &wifi_ui, wifi_open, &wifi_focus, &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, @@ -4920,7 +5052,8 @@ int main(int argc, char **argv) immutable_font, immutable_compact_font, destination, &root, &games_browser, &favorites_focus, &recent_focus, &all_games_focus, &collections, games, &achievement_index, favorites, recents, has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, apps, &status, - &capabilities, &quick_values, &network_credential, &remote_keys, + &capabilities, &quick_values, &network_credential, + &wifi_ui, wifi_open, &wifi_focus, &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, diff --git a/src/gameSwitcher/gameSwitcher.c b/src/gameSwitcher/gameSwitcher.c index eaad16204..7ffe3fed2 100644 --- a/src/gameSwitcher/gameSwitcher.c +++ b/src/gameSwitcher/gameSwitcher.c @@ -193,10 +193,15 @@ int main(int argc, char *argv[]) char error[256] = {0}; print_debug("Returning to Bloom Shell"); remove("/mnt/SDCARD/.tmp_update/.runGameSwitcher"); - if (!shell_child || appState.exit_to_settings || return_to_remote_keys || + if (!shell_child || appState.exit_to_settings || appState.exit_to_wifi || + return_to_remote_keys || (!appState.return_to_prior_context && !shell_child_root)) { int staged; - if (appState.exit_to_settings) + if (appState.exit_to_wifi) + staged = bloom_shell_stage_executable_argument( + BLOOM_SHELL_BINARY, "--settings-wifi", CMD_TO_RUN_PATH, error, + sizeof(error)); + else if (appState.exit_to_settings) staged = bloom_shell_stage_executable_argument( BLOOM_SHELL_BINARY, "--settings", CMD_TO_RUN_PATH, error, sizeof(error)); else if (!appState.return_to_prior_context) diff --git a/src/gameSwitcher/gs_appState.h b/src/gameSwitcher/gs_appState.h index a472a9669..31fa83485 100644 --- a/src/gameSwitcher/gs_appState.h +++ b/src/gameSwitcher/gs_appState.h @@ -14,6 +14,7 @@ typedef struct { bool quit; bool exit_to_menu; bool exit_to_settings; + bool exit_to_wifi; bool return_to_prior_context; bool changed; bool current_game_changed; @@ -38,6 +39,7 @@ static AppState appState = { .quit = false, .exit_to_menu = false, .exit_to_settings = false, + .exit_to_wifi = false, .return_to_prior_context = false, .changed = true, .current_game_changed = true, diff --git a/src/gameSwitcher/gs_bloomRender.h b/src/gameSwitcher/gs_bloomRender.h index 6054cf3c8..943595de0 100644 --- a/src/gameSwitcher/gs_bloomRender.h +++ b/src/gameSwitcher/gs_bloomRender.h @@ -133,7 +133,7 @@ static void bloomGsRenderOverlayFooter(int quick_settings) bloomGsText(bloomGsHintFont(), "Up/Down Choose Left/Right Change", (int)(55.0 * g_scale), y + (int)(7.0 * g_scale), g_display.width - (int)(70.0 * g_scale), GS_BLOOM_SAND); - bloomGsText(bloomGsHintFont(), "A Toggle B/START Close", + bloomGsText(bloomGsHintFont(), "A Toggle/Open B/START Close", (int)(55.0 * g_scale), y + (int)(31.0 * g_scale), g_display.width - (int)(70.0 * g_scale), GS_BLOOM_SAND); } diff --git a/src/gameSwitcher/gs_quickSettings.h b/src/gameSwitcher/gs_quickSettings.h index 32941b698..f18fd033f 100644 --- a/src/gameSwitcher/gs_quickSettings.h +++ b/src/gameSwitcher/gs_quickSettings.h @@ -9,7 +9,6 @@ #include "gs_bloomRender.h" #define BLOOM_CONTROLS_BINARY "/mnt/SDCARD/.tmp_update/bin/bloom-controls" -#define BLOOM_NETWORK_BINARY "/mnt/SDCARD/.tmp_update/bin/bloom-network" #define BLOOM_PLATFORM_BINARY "/mnt/SDCARD/.tmp_update/bin/bloom-platform" #define BLOOM_SETTINGS_BINARY "/mnt/SDCARD/.tmp_update/bin/bloom-settings" #define DEVICE_MODEL_PATH "/tmp/deviceModel" @@ -73,9 +72,10 @@ static void quickSettings_close(void) static void quickSettings_adjust(int direction) { int row = appState.quick_settings_list.active_pos; + if (quick_capabilities.wifi && row == 2) + return; if (bloom_shell_quick_settings_adjust(&quick_capabilities, &quick_values, (size_t)row, - direction, BLOOM_CONTROLS_BINARY, - BLOOM_NETWORK_BINARY) == 0) { + direction, BLOOM_CONTROLS_BINARY) == 0) { quickSettings_refresh(); appState.changed = true; } @@ -96,8 +96,11 @@ static void quickSettings_activate(void) appState.changed = true; } } - else if (quick_capabilities.wifi && row == 2) - quickSettings_adjust(quick_values.wifi_enabled ? -1 : 1); + else if (quick_capabilities.wifi && row == 2) { + appState.exit_to_menu = true; + appState.exit_to_wifi = true; + appState.quit = true; + } } static void quickSettings_handle(KeyState *keystate) diff --git a/static/build/.tmp_update/runtime.sh b/static/build/.tmp_update/runtime.sh index 0c8105fe6..9f76dcd7b 100644 --- a/static/build/.tmp_update/runtime.sh +++ b/static/build/.tmp_update/runtime.sh @@ -940,6 +940,7 @@ bloom_shell_return_command_pending() { "exec '$sysdir/bin/bloom-shell'" | \ "exec '$sysdir/bin/bloom-shell' '--root'" | \ "exec '$sysdir/bin/bloom-shell' '--settings'" | \ + "exec '$sysdir/bin/bloom-shell' '--settings-wifi'" | \ "exec '$sysdir/bin/bloom-shell' '--settings-remote-keys'") return 0 ;; esac return 1 diff --git a/test/shell/bloom_shell_runtime.bats b/test/shell/bloom_shell_runtime.bats index f9bebfcee..775bee9af 100644 --- a/test/shell/bloom_shell_runtime.bats +++ b/test/shell/bloom_shell_runtime.bats @@ -58,7 +58,7 @@ eval "$(sed -n '/^bloom_shell_return_command_pending() {/,/^}/p' "$runtime")" eval "$(sed -n '/^check_switcher() {/,/^}/p' "$runtime")" - for argument in "" " '--root'" " '--settings'" " '--settings-remote-keys'"; do + for argument in "" " '--root'" " '--settings'" " '--settings-wifi'" " '--settings-remote-keys'"; do printf '#!/bin/sh\nexec '\''%s/bin/bloom-shell'\''%s\n' "$sysdir" "$argument" \ >"$sysdir/cmd_to_run.sh" check_switcher diff --git a/test/shell/bloom_shell_settings.bats b/test/shell/bloom_shell_settings.bats index a38eb93df..36fa9016b 100644 --- a/test/shell/bloom_shell_settings.bats +++ b/test/shell/bloom_shell_settings.bats @@ -24,7 +24,6 @@ grep -F 'settings_focus->window_start + row' "$shell" grep -F 'bloom_shell_quick_settings_adjust(' "$shell" grep -F 'BLOOM_CONTROLS_BINARY,' "$shell" - grep -F 'BLOOM_NETWORK_BINARY);' "$shell" grep -F 'bloom_shell_support_export(BLOOMCTL_BINARY)' "$shell" grep -F 'bloom_shell_mute_toggle(&quick_values, BLOOM_CONTROLS_BINARY);' "$shell" grep -F '"Health: Support export %s"' "$shell" @@ -65,7 +64,7 @@ grep -F 'bloom_shell_quick_settings_activate(' "$shell" grep -F 'destination = BLOOM_UI_DESTINATION_SETTINGS;' "$shell" grep -F 'strcmp(argv[1], "--settings") == 0' "$shell" - grep -F '(settings_requested || remote_keys_requested) && !safe_mode' "$shell" + grep -F '(settings_requested || wifi_requested || remote_keys_requested) && !safe_mode' "$shell" grep -F 'bloom_shell_quick_values_load(BLOOM_SETTINGS_BINARY, &quick_values);' "$shell" grep -F 'BLOOM_CONTROLS_BINARY' "$shell" grep -F 'BLOOM_NETWORK_BINARY' "$shell" @@ -81,6 +80,24 @@ [[ "$render" != *'exec'* ]] } +@test "Wi-Fi opens one bounded detail surface without a direct radio mutation" { + shell=/workspace/src/bloomShell/main.c + model=/workspace/src/bloomShell/bloom_shell_settings.c + wifi=/workspace/src/bloomShell/bloom_shell_wifi_ui.c + switcher=/workspace/src/gameSwitcher/gs_quickSettings.h + + grep -F '{BLOOM_SHELL_SETTINGS_WIFI, BLOOM_SHELL_SETTINGS_ROW_DETAIL, "Wi-Fi"}' "$model" + grep -F 'strcmp(argv[1], "--settings-wifi") == 0' "$shell" + grep -F 'bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui);' "$shell" + grep -F 'wifi_open && action == BLOOM_UI_ACTION_BACK' "$shell" + grep -F 'draw_wifi_details(' "$shell" + grep -F 'execl(bloomctl_path, bloomctl_path, "network", "profile", "list"' "$wifi" + grep -F 'if (quick_capabilities.wifi && row == 2)' "$switcher" + grep -F 'appState.exit_to_wifi = true;' "$switcher" + ! grep -F 'run_network_request' "$model" + ! grep -F 'BLOOM_NETWORK_BINARY' "$switcher" +} + @test "Network settings expose bounded authorized key management" { shell=/workspace/src/bloomShell/main.c model=/workspace/src/bloomShell/bloom_shell_settings.c diff --git a/test/shell/gameswitcher_controls.bats b/test/shell/gameswitcher_controls.bats index 2f275c723..67ae1151f 100644 --- a/test/shell/gameswitcher_controls.bats +++ b/test/shell/gameswitcher_controls.bats @@ -34,8 +34,9 @@ grep -F 'bloom_shell_quick_settings_adjust(' "$quick" grep -F 'bloom_shell_mute_toggle(' "$quick" grep -F 'appState.exit_to_settings = true;' "$quick" + grep -F 'appState.exit_to_wifi = true;' "$quick" grep -F 'BLOOM_CONTROLS_BINARY' "$quick" - grep -F 'BLOOM_NETWORK_BINARY' "$quick" + ! grep -F 'BLOOM_NETWORK_BINARY' "$quick" } @test "GameSwitcher has one presentation and no inherited navigation quirks" { @@ -64,9 +65,10 @@ grep -F 'bloomGsRenderFooter(game_list_len > 0);' "$main" grep -F 'Nothing played yet' "$render" grep -F 'B Home MENU Close START Quick' "$render" - grep -F 'A Toggle B/START Close' "$render" + grep -F 'A Toggle/Open B/START Close' "$render" grep -F 'bloom_shell_stage_executable_argument(' "$main" grep -F 'BLOOM_SHELL_BINARY, "--settings"' "$main" + grep -F 'BLOOM_SHELL_BINARY, "--settings-wifi"' "$main" grep -F 'BLOOM_SHELL_BINARY, "--root"' "$main" grep -F '"--settings-remote-keys"' "$main" grep -F '"--return-remote-keys"' "$main" diff --git a/test/test_bloom_shell_settings.cpp b/test/test_bloom_shell_settings.cpp index 41ec1f445..ce65e131c 100644 --- a/test/test_bloom_shell_settings.cpp +++ b/test/test_bloom_shell_settings.cpp @@ -379,7 +379,7 @@ TEST(BloomShellSettings, SuccessfulFixedRequestUpdatesValueAndFailureDoesNot) ASSERT_EQ(0, bloom_shell_capabilities_from_model(354, 0, &capabilities)); BloomShellQuickValues values{1, 1, 4, 9, 1, 0}; ASSERT_EQ(0, bloom_shell_quick_settings_adjust(&capabilities, &values, 1, 1, - script.c_str(), script.c_str())); + script.c_str())); EXPECT_EQ(10, values.volume); EXPECT_FALSE(values.mute); std::ifstream input(arguments); @@ -393,29 +393,22 @@ TEST(BloomShellSettings, SuccessfulFixedRequestUpdatesValueAndFailureDoesNot) } ASSERT_EQ(0, chmod(script.c_str(), 0700)); EXPECT_NE(0, bloom_shell_quick_settings_adjust(&capabilities, &values, 0, 1, - script.c_str(), script.c_str())); + script.c_str())); EXPECT_EQ(4, values.brightness); std::filesystem::remove(script); std::filesystem::remove(arguments); } -TEST(BloomShellSettings, FailedWifiApplyReloadsPersistedPreference) +TEST(BloomShellSettings, WifiAdjustmentNeverMutatesOrExecutesAnAdapter) { const auto directory = std::filesystem::temp_directory_path() / - ("bloom-shell-wifi-failure-" + std::to_string(getpid())); + ("bloom-shell-wifi-adjust-" + std::to_string(getpid())); std::filesystem::create_directory(directory); - const auto script = directory / "bloom-network"; + const auto script = directory / "adapter"; + const auto arguments = directory / "arguments"; { std::ofstream output(script); - output << "#!/bin/sh\n" - "case \"$*\" in\n" - " 'request disable') exit 1 ;;\n" - " status) printf '%s\\n' '{\"schema\":1,\"service\":\"bloom-network\"," - "\"available\":true,\"wifi_capable\":true," - "\"wifi_enabled\":false,\"associated\":false," - "\"state\":\"wifi_disabled\"}' ;;\n" - " *) exit 2 ;;\n" - "esac\n"; + output << "#!/bin/sh\nprintf '%s' \"$*\" > '" << arguments.string() << "'\n"; } ASSERT_EQ(0, chmod(script.c_str(), 0700)); BloomShellCapabilities capabilities{}; @@ -424,9 +417,9 @@ TEST(BloomShellSettings, FailedWifiApplyReloadsPersistedPreference) values.ready = 1; values.wifi_enabled = 1; EXPECT_NE(0, bloom_shell_quick_settings_adjust(&capabilities, &values, 2, -1, - script.c_str(), script.c_str())); - EXPECT_FALSE(values.wifi_enabled); - EXPECT_TRUE(values.wifi_status_ready); + script.c_str())); + EXPECT_TRUE(values.wifi_enabled); + EXPECT_FALSE(std::filesystem::exists(arguments)); std::filesystem::remove_all(directory); } @@ -514,7 +507,7 @@ TEST(BloomShellSettings, MuteToggleUsesFixedAdapterAndUpdatesOnlyAfterSuccess) std::filesystem::remove(script); } -TEST(BloomShellSettings, QuickSettingsAActivatesTogglesAndOpensFullSettings) +TEST(BloomShellSettings, QuickSettingsAOpensWifiDetailsWithoutRadioMutation) { const auto directory = std::filesystem::temp_directory_path() / ("bloom-quick-activate-" + std::to_string(getpid())); @@ -529,29 +522,25 @@ TEST(BloomShellSettings, QuickSettingsAActivatesTogglesAndOpensFullSettings) BloomShellCapabilities capabilities{}; ASSERT_EQ(0, bloom_shell_capabilities_from_model(354, 0, &capabilities)); BloomShellQuickValues values{1, 1, 7, 9, 0, 0}; - int open_settings = 0; + BloomShellQuickOpen open = BLOOM_SHELL_QUICK_OPEN_NONE; ASSERT_EQ(0, bloom_shell_quick_settings_activate(&capabilities, &values, 1, - script.c_str(), script.c_str(), - &open_settings)); + script.c_str(), &open)); EXPECT_TRUE(values.mute); - EXPECT_FALSE(open_settings); + EXPECT_EQ(BLOOM_SHELL_QUICK_OPEN_NONE, open); ASSERT_EQ(0, bloom_shell_quick_settings_activate(&capabilities, &values, 2, - script.c_str(), script.c_str(), - &open_settings)); - EXPECT_TRUE(values.wifi_enabled); - EXPECT_FALSE(open_settings); + script.c_str(), &open)); + EXPECT_FALSE(values.wifi_enabled); + EXPECT_EQ(BLOOM_SHELL_QUICK_OPEN_WIFI, open); ASSERT_EQ(0, bloom_shell_quick_settings_activate(&capabilities, &values, 4, - script.c_str(), script.c_str(), - &open_settings)); - EXPECT_TRUE(open_settings); + script.c_str(), &open)); + EXPECT_EQ(BLOOM_SHELL_QUICK_OPEN_SETTINGS, open); std::ifstream input(arguments); std::string line; ASSERT_TRUE(std::getline(input, line)); EXPECT_EQ("request mute true", line); - ASSERT_TRUE(std::getline(input, line)); - EXPECT_EQ("request enable", line); + EXPECT_FALSE(std::getline(input, line)); std::filesystem::remove_all(directory); } diff --git a/test/test_bloom_shell_wifi_ui.cpp b/test/test_bloom_shell_wifi_ui.cpp index acf5003ce..47464dced 100644 --- a/test/test_bloom_shell_wifi_ui.cpp +++ b/test/test_bloom_shell_wifi_ui.cpp @@ -1,5 +1,10 @@ #include "gtest/gtest.h" +#include +#include +#include +#include + extern "C" { #include "../src/bloomShell/bloom_shell_wifi_ui.h" } @@ -24,6 +29,39 @@ TEST(BloomShellWifiUi, ParsesOnlyCredentialFreeBoundedProfiles) &wifi)); } +TEST(BloomShellWifiUi, LoadsProfilesThroughOneFixedCredentialFreeCommand) +{ + auto directory = std::filesystem::temp_directory_path() / + ("bloom-shell-wifi-ui-load-" + std::to_string(getpid())); + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + auto script = directory / "bloomctl"; + auto arguments = directory / "arguments"; + { + std::ofstream output(script); + output << "#!/bin/sh\nprintf '%s' \"$*\" > '" << arguments.string() + << "'\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"profiles\":[{\"id\":\"home\",\"ssid\":\"Home WiFi\",\"security\":\"wpa2-psk\",\"hidden\":false,\"active\":true}]}'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + BloomShellWifiUi wifi{}; + ASSERT_EQ(0, bloom_shell_wifi_profiles_load(script.c_str(), &wifi)); + ASSERT_TRUE(wifi.ready); + ASSERT_EQ(1U, wifi.profile_count); + std::ifstream input(arguments); + std::string recorded; + std::getline(input, recorded); + EXPECT_EQ("network profile list", recorded); + + { + std::ofstream output(script); + output << "#!/bin/sh\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"profiles\":[{\"id\":\"home\",\"ssid\":\"Home WiFi\",\"security\":\"wpa2-psk\",\"hidden\":false,\"active\":true,\"psk\":\"blocked\"}]}'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + EXPECT_NE(0, bloom_shell_wifi_profiles_load(script.c_str(), &wifi)); + EXPECT_FALSE(wifi.ready); + std::filesystem::remove_all(directory); +} + TEST(BloomShellWifiUi, TracksOneBoundedAsynchronousScan) { BloomShellWifiUi wifi{};