From dd87ac81222a8dbea3eefee7f606a6c6f7fac73a Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:38:23 -0700 Subject: [PATCH] ohos: the OctoSense desktop on a HarmonyOS 2-in-1 OpenHarmony builds were the phone shell only (build.rs set mobile_only for target_env = ohos, "cannot host desktop processes"). Every native app is an in-process module there, so the desktop runs: on a MateBook Pro (HarmonyOS 7.0, API 26) all 13 dock apps open, Terminal included. - OCTOSENSE_OHOS_DESKTOP=1 (crates/shell/build.rs, phone/build.rs) builds the desktop shell for OpenHarmony; without it nothing changes. - build-home-ohos.py --desktop sets it and declares the module for 2in1 and tablet (a phone-only module runs in a phone-sized compatibility window); EntryAbility maximizes the window on a 2-in-1. - phone/Cargo.toml: an app-terminal feature (opt-in; native-apps.json keeps Terminal off the phone shell). - user_home(): the platform's data directory where it has one, so a new terminal opens in the app's folder, not in a HOME the app may not use. - build-home-ohos.py applies phone/ohos/keyboard.patch only while the framework's ArkTS bridge still drives the input method; with makepad's NDK input method there is no ArkTS keyboard code to patch. Needs makepad's OpenHarmony input (mouse, keys, scroll, NDK input method) for the desktop to take input: OctoSense-org/makepad, linked in the PR. Checked: cargo check (octosense; octosense-home --features mobile-apps), check-shell-graph for both, rom unittests; octosense-shell: 731 tests pass, 1 fails on this machine (its sandbox jail probe cannot start; this branch does not touch the sandbox). Run on the MateBook from an equivalent Linux build; build-home-ohos.py's --desktop path is unverified. Co-Authored-By: Claude Opus 5.5 --- crates/shell/build.rs | 6 +++++- crates/shell/src/lib.rs | 4 ++++ phone/Cargo.toml | 3 +++ phone/build.rs | 6 +++++- phone/ohos/EntryAbility.ets | 12 +++++++++++- rom/docs/home-build.md | 17 ++++++++++++++--- rom/scripts/build-home-ohos.py | 21 +++++++++++++++++++-- 7 files changed, 61 insertions(+), 8 deletions(-) diff --git a/crates/shell/build.rs b/crates/shell/build.rs index 4d51c035..f7128868 100644 --- a/crates/shell/build.rs +++ b/crates/shell/build.rs @@ -27,7 +27,11 @@ fn main() { if native_mobile { println!("cargo:rustc-cfg=native_mobile"); } - if feature || target_os == "android" || target_env == "ohos" { + // Experiment: `OCTOSENSE_OHOS_DESKTOP=1` builds the desktop shell for an + // OpenHarmony 2-in-1 (every native app is an in-process module there). + println!("cargo:rerun-if-env-changed=OCTOSENSE_OHOS_DESKTOP"); + let ohos_desktop = target_env == "ohos" && std::env::var_os("OCTOSENSE_OHOS_DESKTOP").is_some_and(|v| v == "1"); + if feature || target_os == "android" || (target_env == "ohos" && !ohos_desktop) { println!("cargo:rustc-cfg=mobile_only"); } // `kernel`: this build hosts the octos kernel, exactly as diff --git a/crates/shell/src/lib.rs b/crates/shell/src/lib.rs index 5c38d5d5..76170418 100644 --- a/crates/shell/src/lib.rs +++ b/crates/shell/src/lib.rs @@ -6415,6 +6415,10 @@ app_main!( /// The person's home directory (`HOME`; `USERPROFILE` on Windows), where a /// new terminal with no directory to inherit opens. fn user_home() -> Option { + // Android, iOS and OpenHarmony: `HOME` is not the app's to use. + if let Some(dir) = makepad_widgets::makepad_platform::home::platform_data_dir() { + return Some(dir); + } let var = if cfg!(windows) { "USERPROFILE" } else { "HOME" }; std::env::var_os(var).filter(|home| !home.is_empty()).map(std::path::PathBuf::from).filter(|home| home.is_dir()) } diff --git a/phone/Cargo.toml b/phone/Cargo.toml index 1a104d88..b30ac1f9 100644 --- a/phone/Cargo.toml +++ b/phone/Cargo.toml @@ -90,5 +90,8 @@ app-hub = ["dep:octosense-app-hub-app", "octosense-shell/app-hub"] # END native-apps: features app-aichat = ["dep:makepad-aichat", "octosense-shell/app-aichat"] dev-mode = ["octosense-shell/dev-mode"] +# Experiment: Terminal in the OpenHarmony desktop build (native-apps.json +# keeps it off the phone shell). +app-terminal = ["octosense-shell/app-terminal"] # The system toolbox offered to app agents (off by default; ADR 0002 §6). toolbox-peers = ["octosense-shell/toolbox-peers"] diff --git a/phone/build.rs b/phone/build.rs index 3c37f7d2..a77d8aad 100644 --- a/phone/build.rs +++ b/phone/build.rs @@ -16,7 +16,11 @@ fn main() { if native_mobile { println!("cargo:rustc-cfg=native_mobile"); } - if feature || target_os == "android" || target_env == "ohos" { + // Experiment: `OCTOSENSE_OHOS_DESKTOP=1` builds the desktop shell for an + // OpenHarmony 2-in-1 (every native app is an in-process module there). + println!("cargo:rerun-if-env-changed=OCTOSENSE_OHOS_DESKTOP"); + let ohos_desktop = target_env == "ohos" && std::env::var_os("OCTOSENSE_OHOS_DESKTOP").is_some_and(|v| v == "1"); + if feature || target_os == "android" || (target_env == "ohos" && !ohos_desktop) { println!("cargo:rustc-cfg=mobile_only"); } // The host's build id: the second this build was configured, as digits. diff --git a/phone/ohos/EntryAbility.ets b/phone/ohos/EntryAbility.ets index 29324c2c..d4cb9d90 100644 --- a/phone/ohos/EntryAbility.ets +++ b/phone/ohos/EntryAbility.ets @@ -1,7 +1,7 @@ import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { window } from '@kit.ArkUI'; -import { BusinessError } from '@kit.BasicServicesKit'; +import { BusinessError, deviceInfo } from '@kit.BasicServicesKit'; import makepad from 'libmakepad.so' import {ArkGlue} from '../makepad/makepad' @@ -52,6 +52,16 @@ export default class EntryAbility extends UIAbility { hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); }); + // A 2-in-1 starts the shell full size: its desktop build is the + // OctoSense desktop, which is its own window manager. + if (deviceInfo.deviceType === '2in1') { + windowStage.getMainWindow().then((mainWindow) => { + mainWindow.maximize().catch((e: BusinessError) => { + hilog.warn(0x0000, 'testTag', 'maximize failed: %{public}d', e.code); + }); + }); + } + // HarmonyOS lays the XComponent between its native system bars. Its // surface dimensions and local touch coordinates already exclude them. windowStage.getMainWindow().then((mainWindow) => { diff --git a/rom/docs/home-build.md b/rom/docs/home-build.md index 9d1505c5..6b791c3f 100644 --- a/rom/docs/home-build.md +++ b/rom/docs/home-build.md @@ -229,9 +229,20 @@ draggable floating ball over hosted content. Tapping it opens a compact panel with **返回首页** and **最近应用**. Dragging docks it inside the nearest side; tapping outside dismisses the panel without activating the content underneath. The ball stays in the app window and reserves no content height. -The generated ArkTS bridge also receives `phone/ohos/keyboard.patch` to coalesce -per-frame keyboard requests and serialize attach/show/hide while leaving the -pinned framework checkout intact. OpenHarmony uses only its native keyboard. +With a framework whose ArkTS bridge still drives the input method, the bridge +also receives `phone/ohos/keyboard.patch` to coalesce per-frame keyboard +requests and serialize attach/show/hide while leaving the pinned framework +checkout intact; a framework with the NDK input method (makepad's +`oh_ime.rs`) serializes them natively and gets no patch. OpenHarmony uses only +its native keyboard. +On a HarmonyOS 2-in-1, `--desktop` builds the OctoSense desktop instead of +the phone shell (`OCTOSENSE_OHOS_DESKTOP=1`, read by `crates/shell/build.rs` +and `phone/build.rs`; every native app is an in-process module there), declares +the module for `2in1` and `tablet`, and the window opens maximized. Terminal +is the opt-in `app-terminal` feature of `phone/Cargo.toml`, which this script +does not pass yet. The desktop build was run on a MateBook +Pro (HarmonyOS 7.0) from an equivalent Linux build; this script's `--desktop` +path itself is **unverified**. OpenHarmony does not recognize shell edge swipes or draw a second navigation pill; those gestures remain available to the host OS. Home paging and the central pull for the app library still work inside the content area. diff --git a/rom/scripts/build-home-ohos.py b/rom/scripts/build-home-ohos.py index 05dd95e4..82be6cbf 100644 --- a/rom/scripts/build-home-ohos.py +++ b/rom/scripts/build-home-ohos.py @@ -32,6 +32,8 @@ def main(): p.add_argument('--compatible-sdk', default='6.0.1(21)') p.add_argument('--remote-port', type=int, help='Validation only: app-owned loopback inspection') p.add_argument('--offline', action='store_true') + p.add_argument('--desktop', action='store_true', + help='The OctoSense desktop for a 2-in-1 (OCTOSENSE_OHOS_DESKTOP=1), also declared for 2in1 and tablet') args = p.parse_args() for name in ('deveco_home', 'signing_config', 'packager'): setattr(args, name, getattr(args, name).expanduser().resolve()) @@ -70,6 +72,8 @@ def main(): env.pop('MAKEPAD_REMOTE', None) if args.remote_port: env['MAKEPAD_REMOTE'] = str(args.remote_port) + if args.desktop: + env['OCTOSENSE_OHOS_DESKTOP'] = '1' cargo_args = ['-p', 'octosense-home', '--release', '--locked'] if args.offline: cargo_args.append('--offline') @@ -84,8 +88,21 @@ def main(): # keep the floating controls inside its safe area. shutil.copy2(home / 'ohos/EntryAbility.ets', project / 'entry/src/main/ets/entryability/EntryAbility.ets') - subprocess.run(['patch', '--batch', '--forward', '-p1', '-i', - str(home / 'ohos/keyboard.patch')], cwd=project, check=True) + # A framework whose ArkTS bridge still drives the input method gets the + # patch that serializes it; one with the native (NDK) input method has + # no ArkTS keyboard code left to patch. + bridge = project / 'entry/src/main/ets/makepad/makepad.ets' + if 'inputMethod' in bridge.read_text(): + subprocess.run(['patch', '--batch', '--forward', '-p1', '-i', + str(home / 'ohos/keyboard.patch')], cwd=project, check=True) + if args.desktop: + # A phone-only module runs in a phone-sized compatibility window on a + # 2-in-1; the desktop shell is declared for it (and tablets). + module_path = project / 'entry/src/main/module.json5' + module = read_json5(module_path) + types = module['module'].setdefault('deviceTypes', []) + types.extend(t for t in ('2in1', 'tablet') if t not in types) + module_path.write_text(json.dumps(module, ensure_ascii=False, indent=2) + '\n') app_path = project / 'AppScope/app.json5' app = read_json5(app_path) app['app']['bundleName'] = args.bundle_id