Skip to content

feat(ohos): pluginized bridge architecture with lifecycle serialization and plugin migrations - #46

Open
ljy9812 wants to merge 29 commits into
Eulogizethesun:ohdevfrom
ljy9812:ohdev
Open

feat(ohos): pluginized bridge architecture with lifecycle serialization and plugin migrations#46
ljy9812 wants to merge 29 commits into
Eulogizethesun:ohdevfrom
ljy9812:ohdev

Conversation

@ljy9812

@ljy9812 ljy9812 commented Aug 22, 2026

Copy link
Copy Markdown

Changes

Impact

OHOS-only repository. The pluginized architecture is the single ArkTS bridge layer all other repos (tauri/tao/wry/muda/tray-icon) consume; NAPI surface keeps camelCase naming and TSFN-based dispatch.

Testing

Verified end-to-end on HUAWEI MateBook Pro (desktop form, API 23) across the tauri api example: webview create/attach, window ops, menubar/tray, clipboard, print, geolocation permission + watchPosition streaming, notification actions. Local checklist review passed: 0 Blocker / 0 Major.

Related

Supersedes #43. Counterpart PRs: tauri#75, tao#21, wry#21, muda#5, tray-icon#9, plugins-workspace, window-vibrancy.

ddxwzc-boop and others added 29 commits August 14, 2026 11:40
The four newly-added bridge methods (requestRedraw, requestUserAttention,
setImePosition, setWindowDraggable) are invoked from Rust via synchronous
func.call (NAPI-reentrant context). Their synchronous catch blocks (and the
window-not-found early-return in setWindowDraggable) used bare hilog.warn/info,
which can throw 'Argc mismatch' and mask the original error (ohos-constraints 2.3).

Switched those sync paths to safeLogError. Async .then()/.catch() Promise
callbacks are left on hilog (not reentrant).
- window/mod.rs: bridge set_window_topmost/title/limits/ime_position/draggable/
  redraw/request_user_attention; instanceKey for specified launchType
- app.rs: notify_window_status NAPI queue + drain (windowStatusChange backfill)
- event.rs: FoldDisplayModeChange MainEvent
- ArkHelper.ets: ArkTS impls for the new window ops
- NativeAbility.ets + FloatPage.ets: windowStatusChange registration + seed;
  FloatPage title/min/max/close buttons, startMoving onTouch
- WindowManager.ets: showMainAbility(startAbility instanceKey=main),
  setPointerStyle real windowId, decoration flag interception (closable/
  maximizable/minimizable/resizable), setWindowTitle/Limits/Topmost
- type.ets: ArkHelper interface + UIAbilityWantParams.instanceKey
…otif id, startMoving guard

Addresses the 4 findings in PR#45 review #4948263922.

F1 (🟡 main-window show non-functional under standard launchType):
  showMainAbility() did startAbility(instanceKey='main'), but instanceKey is
  ignored under launchType "standard" (tauri-cli template) → every call spawned a
  DUPLICATE EntryAbility. The demo masked this (omits launchType → singleton →
  onNewWant). Replaced the main-window show path with restore() — the documented
  inverse of minimize() (the minimize() doc routes main-window restoration through
  restore(), not showWindow()). Guarded by isMinimized() so a redundant
  set_visible(true) on a visible maximized window doesn't un-maximize it. Removed
  the now-unused Want import and the misleading onAcceptWant comments.

F2 (🔵 FloatPage isMaximized local @State desyncs from system/tao triggers):
  Drive isMaximized from the existing windowStatusChange callback (+ initial seed)
  instead of an optimistic button-click toggle, so the maximize/restore icon stays
  in sync when maximize/restore is triggered by the system or the tao mirror
  (apply_window_status). isMaximized = (status === WindowStatusType.MAXIMIZE).

F3 (🔵 request_user_attention hardcoded notif id:1 collides):
  Concurrent calls shared id:1 → second overwrote first. Now uses a closure-level
  monotonic counter so each call gets a unique id. (Signature kept no-arg: tao's
  request_user_attention does not forward windowId, so per-window id needs a
  full-chain param addition — monotonic id fixes the collision today.)

F4 (🔵 FloatPage startMoving API14+ with no guard):
  startMoving is API14+ but compatibleSdkVersion is API12 → on API<14 the method is
  undefined and threw on every touch-down. Added deviceInfo.sdkApiVersion<14 guard
  (matches sibling setWindowTopmost/restoreWindow). No PanGesture fallback attached:
  a simultaneously bound PanGesture consumes the Down event and blocks startMoving,
  so the two cannot coexist — drag targets API14+ desktop.
…indowId helper

set_cursor_grab resolves tao's window id to the real OHOS window id via a
synchronous getRealWindowId ArkTS helper, then calls
OH_WindowManager_LockCursor/UnlockCursor from libnative_window_manager.so.
The library is dlopen'd lazily (never statically linked — compatibleSdk is
API 12 and older images lack the symbols; dlsym-null doubles as the version
guard → NotSupported). Typed CursorGrabError {NotSupported, OsCode, Bridge}
lets tao map errors without string matching; unlock is idempotent
(1300002 after focus-loss auto-release maps to Ok). Also declares
LOCK_WINDOW_CURSOR in the HAR module.json5 (self-documenting; HAP-side
declaration is authoritative).
…uard

0cac4c3 replaced WebViewStyle width/height with "100%" natural layout to
fix the main-webview resize relayout bug, which silently broke child
webview geometry: a child created at (x, y, w, h) rendered window-sized
at (x, y), so its bottom-right overflow was clipped by the window edge
(clipped amount == position offset).

Restore data.style.width/height in WebBuilder/EmbeddedWebBuilder, and add
a naturalLayout flag (set at creation when style.width is absent): natural
webviews — the main webview of each window — have runtime set_bounds
width/height stripped in updateWebviewStyle, keeping "100%" sizing so the
0cac4c3 fix does not regress. Explicit-bounds child webviews accept
width/height at creation and at runtime.

Verified on device (HUAWEI MateBook Pro 2in1): create_webview
multi-webview manual test renders the exact 300x200 @(50,50) rect.
…on and plugin migrations

- pluginize native bridge capabilities (harmony-contrib#67), normalize node surface (harmony-contrib#70),
  transparent XComponent background (harmony-contrib#73), webview controller release
  safety (harmony-contrib#69)
- plugin module agnostic refactor (harmony-contrib#75), serialize ability plugin
  lifecycle (harmony-contrib#74)
- account/updater bridge migration and clipboard write-html API
- dead code cleanup superseded by the bridge plugin architecture
- deps upgrade, format + git hook

Co-Authored-By: Claude <noreply@anthropic.com>
…ableDrag, fix setPointerStyle C5

- instanceKey: 'specified launchType' mechanism doesn't exist (template is
  standard, SDK 12 has no abilityStage/onAcceptWant) — delete the whole
  passthrough (window/mod.rs want.set, ArkHelper.ets, type.ets field) and
  correct the type.ets comment back to launchType:standard
- setWindowDraggable: enableDrag is API20+; add deviceInfo.sdkApiVersion < 20
  guard (safeLogError + return) matching the startMoving/setWindowTopmost pattern
- setPointerStyle: bare hilog in the NAPI-reentrant sync body throws 'Argc
  mismatch' (C5) — success log misreported failures, catch log swallowed the
  original err; switch to console fallback like safeLogError/getRealWindowId
feat(ohos): window ops bridge + status readback + FloatPage decorations
…on and plugin migrations

- pluginize native bridge capabilities (harmony-contrib#67), normalize node surface (harmony-contrib#70),
  transparent XComponent background (harmony-contrib#73), webview controller release
  safety (harmony-contrib#69)
- plugin module agnostic refactor (harmony-contrib#75), serialize ability plugin
  lifecycle (harmony-contrib#74)
- account/updater bridge migration and clipboard write-html API
- dead code cleanup superseded by the bridge plugin architecture
- deps upgrade, format + git hook

Co-Authored-By: Claude <noreply@anthropic.com>
Replace AppInner.window_rect (single shared field) with
window_rects: HashMap<i64, Rect> keyed by windowId (0 = main window,
>0 = Float sub-window via NEXT_WINDOW_ID). Add window_rect_for /
set_window_rect; release_render_owner clears only key 0.

ArkTS: wrap native window.RectChangeOptions / size-change options into
{ windowId, reason, rect } before invoking the Rust lifecycle closures
(NativeAbility.ets main window id=0, BridgeHost.ets component-window
second registration id=0). Float sub-windows never reach attachComponent
(DefaultXComponent early-returns), so register windowRectChange in
WindowManager.createSubWindow with callback injection
(registerRectChangeCallback) and off() cleanup in removeWindow.

Rust closures (lifecycle.rs) read windowId from the wrapped options and
store each rect under its own key; MainEvent::WindowResize and
ContentRect now carry window_id for tao event routing (Phase 3).

Part of openspec change p1-window-state-per-window-rect (Phase 2, D1-D5).

Co-Authored-By: Claude <noreply@anthropic.com>
…+trailing)

openspec p2-mainthread-event-hygiene layer 2: high-frequency window
operations (30+ subwindow create/destroy bursts) previously queued 12+
Immediate/Low events on the main thread and could trip the 6s watchdog
even without lock contention.

- WindowManager: per-windowId leading+trailing 16ms throttle for
  windowRectChange/windowSizeChange (rect and size have independent
  throttle state; leading keeps first-event immediacy, trailing
  guarantees final-state delivery — required for window-state cache
  freshness), WindowEntry.destroying flag drops events for windows
  being torn down, timer cleanup in removeWindow/unregisterUIAbilityStage
- NativeAbility/BridgeHost: all four registration points routed through
  the same throttledRectDispatch; RECOVER→menubar restore stays
  synchronous (not throttled)
- typed WindowSizeEventWrap/WindowRectEventWrap interfaces replace
  ESObject (arkts-limited-esobj WARN eliminated)

Verified on device: THROTTLE 104 IN / 98 OUT (5.8% suppression, 0
failures), full suite 281 pass / 1 fail / 1 skip, zero new appfreeze.

Co-Authored-By: Claude <noreply@anthropic.com>
Feature-gated behind `fault-injection` (zero overhead when off):
- Rust wire types (FaultRuleWire etc.) + OpenHarmonyApp facade
  (set_fault_rule/clear_fault_rules, auto-enables the registry on first
  call) + call_fault_injection on the bridge client
- ArkTS FaultInjection.ets: always compiled but short-circuits at
  match() when disabled — injects failures into bridge calls to light
  up error-handling branches that never run naturally

Co-Authored-By: Claude <noreply@anthropic.com>
…ge ramp-up)

Appended to existing test modules: expect_engine_phase /
engine_scheme_pairs variants, plugin-window and mouse-event
pure-transform cases — host-side inputs for branches that never occur
naturally on device.

Co-Authored-By: Claude <noreply@anthropic.com>
Rebase of upstream/ohdev (through a25848a) onto the pluginized local
architecture; upstream features were built on the old ArkHelper TSFN
framework, so they are ported semantically instead of textually:

- window/mod.rs: cursor grab FFI (dlopen libnative_window_manager.so,
  OH_WindowManager_LockCursor/UnlockCursor) with set_cursor_grab taking
  the real OHOS window id directly
- app.rs: notify_window_status NAPI + PENDING_WINDOW_STATUS queue,
  mirroring the notify_window_close drain pattern
- WindowPlugin.ets: 7 new bridge actions — set-topmost, set-title,
  set-limits (delegate to ported WindowManager methods),
  request-user-attention (static notificationManager import, monotonic
  id, 1600004 enable-retry), set-ime-position (await updateCursor,
  result returned directly — no poll), set-draggable (API20 guard),
  get-real-window-id
- plugin-window WindowClient: matching async facade methods
- DefaultWebview: naturalLayout guard ported from upstream d530828 —
  explicit child webview bounds restored, natural-layout webviews keep
  100% sizing and strip set_bounds width/height
- windowCommand 'show': delegate to WindowManager.showWindowMethod so a
  minimized main window is restored instead of no-op showWindow()

cargo check passes on host and aarch64-unknown-linux-ohos (0 errors).

Co-Authored-By: Claude <noreply@anthropic.com>
…e actions

Two facade actions tao needs beyond the PR#45 port set:

- set-cursor-icon: delegates to WindowManager.setPointerStyle (resolves
  the real OHOS window id internally, pointer.setPointerStyleSync).
  Fire-and-forget — per-window failures are logged on the ArkTS side,
  the ack only confirms dispatch (tao's setter is void).
- set-decoration-flags: applies the FLAG bit-field (closable=1,
  maximizable=2, minimizable=4, resizable=8) to a Float sub-window's
  LocalStorage; the UIAbility main window is a system-managed no-op.

WindowClient gains the matching async methods; requests are validated
windowId + integer style / flags in [0, 15].

Co-Authored-By: Claude <noreply@anthropic.com>
…e escape

pack.bat invokes this script as "...pack-plugins.ps1" "%SCRIPT_DIR%"; the
trailing backslash escapes the closing quote in cmd's parser, so
arrives with a literal trailing quote. Join-Path bakes that quote into every
derived path and Test-Path fails with ItemExistsArgumentError, aborting the
whole pack at "plugin aggregation failed". Trim both quotes and backslashes
from the incoming value.

Co-Authored-By: Claude <noreply@anthropic.com>
…usChange

The rebase-brought windowStatusChange registration in onWindowStageCreate
referenced a bare 'windowId' identifier (upstream defines it via readWindowId()
for multi-UIAbility; local single-UIAbility architecture has no such binding),
failing ArkTS compilation. The main window is always tao windowId=0 (same
sentinel as the surrounding WindowSizeEventWrap/WindowRectEventWrap wraps), and
runtime-wry routes by tao window_id, so notify with the literal 0. Float
sub-windows keep using the NEXT_WINDOW_ID(>=1) virtual id via FloatPage.

Co-Authored-By: Claude <noreply@anthropic.com>
The WM rect (windowRectChange) and the XComponent surface rect update
asynchronously; a live window_rect - content_rect diff read in the gap
produced garbage decor estimates (824/770/292 instead of the real 146),
which corrupted tao's inner_size reads and compounded through
window-state save/restore into a shrinking main window.

- latch decor height ONLY on surface events (activate/update), where both
  rects are consistent: diff==0 -> 0, 0<diff<=320 -> latch, otherwise keep
  the previous estimate
- add decor_change_callbacks: listeners fired while the app write lock is
  held whenever the latched value actually changes (contract: lock-free
  listeners only — channel sends/atomics); returning false removes the
  listener after the call
- expose register/remove_decor_change_callback for tao's event-driven
  set_inner_size self-correction
- unit tests for the latch rules and the callback firing semantics

Co-Authored-By: Claude <noreply@anthropic.com>
…en to WindowManager

1. Restore CURSOR_POSITION_X/Y atomics + update_cursor_position NAPI in
   app.rs (dropped during the pluginize refactor, restored from 5941dfb).
   The ArkTS MainPage.onMouse handler is the only viable cursor tracking
   source — the NDK DispatchMouseEvent path never fires while the cursor
   is over the WebView. tao's cursor_position() reads these atomics.
2. WindowPlugin.ets set-fullscreen: delegate to WindowManager.setFullscreen
   instead of the inline setWindowLayoutFullScreen mobile-only path the
   pluginize migration left behind. WindowManager implements the dual
   path (desktop/2in1: maximize(ENTER_IMMERSIVE) + setTitleAndDockHoverShown;
   mobile: setWindowLayoutFullScreen + setWindowSystemBarEnable with
   serializeOp race guard); the inline version was a visual no-op on
   desktop while still acking OK.

Verified on HUAWEI MateBook Pro (2in1): cursorPosition() non-zero;
fullscreen toggle enters (ENTER_IMMERSIVE) and exits (recover()).

Co-Authored-By: Claude <noreply@anthropic.com>
Remote-only commit is the pre-rebase push of work that was replayed
(and further refined) during the upstream-ohdev rebase; local content
is authoritative (-s ours). See upstream-ohdev-rebase-window-ops.
…swap

JsStatusbarManager builds its ScbServerMessageReceiver asynchronously
(~14ms) after the sync addToStatusBar() call returns. Registering
click handlers on the sync return binds them to the previous receiver,
which the new receiver then replaces - rightMenuClick and
statusBarIconClick listeners were silently dropped on re-add
(set_title/set_visible/set_quick_operation all re-run add).

Switch to the AsyncCallback overload (API 14+) and re-register in the
completion callback, which runs strictly after the new receiver
exists. The sync registration is kept as a baseline (harmless off+on)
in case the callback never fires.

Co-Authored-By: Claude <noreply@anthropic.com>
Predefined menu fullscreen (tray/menubar Fullscreen items) used an
inline implementation that only did a bare immersive maximize on
desktop - the system title bar / Dock stayed visible, diverging from
WindowPlugin set-fullscreen. Delegate both 'fullscreen' and 'recover'
predefined actions to WindowManager.setFullscreen so every entry point
shares the dual-path (desktop maximize(ENTER_IMMERSIVE) + hide title
bar/Dock hover, mobile layout fullscreen + hide system bars) and the
serializeOp race guard.

WindowManager.setFullscreen now also drives the Tauri menubar via MW-5
callbacks (macOS semantics, design.md D3): hide on entry, restore on
exit. Previously only the predefined path hid the menubar, so the
window-API Toggle Fullscreen button kept it visible - and Esc could
not exit because the MainPage Esc handler is gated on
menubar-invisible. Centralizing the callbacks makes Esc exit work for
all fullscreen flavours through the existing condition. The Esc
recoverFn (NativeAbility) delegates to setFullscreen(0, false) so the
title bar / Dock hidden on entry are restored as well.

Verified on device (MateBook Pro): button/tray/menubar fullscreen all
hide system chrome + menubar; Esc and button toggle both restore.

Co-Authored-By: Claude <noreply@anthropic.com>
Restores set_cursor_visible, which the bridge facade migration (tao
73212e1e) dropped to a no-op on the Rust side while the ArkTS
WindowManager.setPointerVisible implementation survived. Adds the
set-cursor-visible action to the WindowPlugin (delegating to
WindowManager.setPointerVisible) and a CursorVisibleRequest facade
method. The request carries no window id - pointer.setPointerVisible
is process-wide (global vs window-level semantics documented as
遗留问题六).

Verified on device (MateBook Pro): setCursorVisible(false) (3s) hides
the pointer globally and restores it after 3s.

Co-Authored-By: Claude <noreply@anthropic.com>
…store

- createSubWindow: on API19+ use createSubWindowWithOptions with
  maximizeSupported:true — sub-window win.maximize() is rejected by WMS
  (1300004) unless maximize capability was declared at creation;
  decorEnabled:false keeps FloatPage's custom title bar
- FloatPage un-maximize: use recoverWindow() (MAXIMIZE/FULL_SCREEN →
  FLOATING, the documented inverse of maximize) instead of
  restoreWindow() which is main-window-only

Co-Authored-By: Claude <noreply@anthropic.com>
…g while maximized

- WMS recover() re-anchors the floating rect to the pointer
  (GetFullScreenToFloatingRect), so programmatic restore landed the
  window near the cursor instead of its pre-maximize position.
  Snapshot the floating rect before maximize (preMaximizeRects,
  has-guard against double-maximize overwriting with the fullscreen
  rect) and moveTo() back after recover, on both the FloatPage button
  path and the WindowPlugin bridge path (shared
  snapshotPreMaximizeRect/restorePreMaximizeRect helpers; the bridge
  keeps its await-completion semantics and cannot delegate to the
  fire-and-forget maximizeWindow).
- FloatPage title-bar onTouch(Down)->startMoving() also fires for
  child button touches (ArkUI touch bubbling); while maximized it
  triggered WMS drag-off-maximize at touch-down, moving the window
  away so the touch-up landed out of region and the click gesture was
  rejected — onClick never ran. Skip startMoving while maximized
  (drag-off restore sacrificed; restore via the button).

Device-verified 2026-08-27: maximizeWindow 1 OK -> recoverWindow 1 OK
(restored pre-maximize rect), window returns to its original position;
bridge path verified via WMS rect chain [0,0]->[0,0,3120,1955]->[0,0,1140,760].

Co-Authored-By: Claude <noreply@anthropic.com>
Bridge-side counterpart to the wry https-scheme fix. The previous bridge
migration dropped the create-time https_intercept_protocols field from
WebviewCreateRequest, causing the ArkTS httpsInterceptProtocols Set to be
empty at first loadUrl and onInterceptRequest to early-return null.

- WebviewCreateRequest (lib.rs:385): add https_intercept_protocol_list:
  Option<Vec<String>>, populated by wry when use_https && protocols exist.
- WebviewCreatePayload (WebviewPlugin.ets:148): add matching
  httpsInterceptProtocolList field; seed httpsInterceptProtocols Set at
  create (WebviewPlugin.ets:1625) so the Set is non-empty before loadUrl.
- callbacks.rs / WebviewPlugin.ets: add full [bridge https-intercept] and
  [https-intercept] hilog chain (received/enter/extracted/reverted/calling/
  returned/success/passthrough) for observability — the previous path was
  silent, masking the empty-Set early-return.
- plugin-webview Cargo.toml: add log = "0.4" dependency (callbacks.rs/lib.rs
  use log::warn!/log::info! but the crate never declared log -> E0433).

Verified 2026-08-28 on HUAWEI MateBook Pro desktop with the wry fix: all
4 https-scheme cases PASS (page-load/secure-context/subresource/external).

Co-Authored-By: Claude <noreply@anthropic.com>
…t want.uri

The file manager (com.huawei.hmos.filemanager MainAbility) reads the
target from want.parameters.fileUri + want.parameters.external_storage_uuid,
NOT from top-level want.uri. The previous code put the virtual uri in
want.uri, which FM ignores -> FM defaulted to home (stayed on the homepage
instead of navigating to the parent directory). This was the real root
cause of §22-3 failing; the earlier "subPath empty" hypothesis was wrong
(subPath-non-empty paths also stayed on home).

Fix: move the mapped uri into parameters.fileUri and add
external_storage_uuid = "LOCAL" (the local disk id, verified via FM
diskNameArray; cloud = "cloud").

Device experiment (2026-08-28, aa start matrix): with parameters.fileUri +
external_storage_uuid="LOCAL", FM does a two-stage setCurrentUri (myPC
intermediate -> target uri) and navigates to Documents/Download/Desktop.
In app semantics Rust takes path.parent() so reveal always opens the
top-level public folder containing the file — this is within OHOS FM
capabilities (the launch entry only resolves top-level virtual folder
names; deeper subpaths hit isFolder error and fall back).

Note: the 2026-08-20 "PASS" was a misread — FM actually stayed on the
home/My Computer page; "empty uri when get uuid" is per-startup noise, not
a URI rejection.

Verified 2026-08-28 on HUAWEI MateBook Pro desktop: setCurrentUri reaches
file://docs/storage/Users/currentUser/Documents (was: home).

Co-Authored-By: Claude <noreply@anthropic.com>
ProcessInitializer.initialize() never set __native_module__ in AppStorage,
so every AppStorage.get(NATIVE_MODULE_STORAGE_KEY) reader (FloatPage × button
/ aboutToDisappear, menu.ets closeWindow, NativeAbility windowStatusChange)
returned undefined → notifyWindowClose / notifyWindowStatus were never
invoked. On Float sub-window close, the Rust tauri manager kept a zombie
entry → getByLabel returned a dead handle → same-label windows couldn't be
rebuilt (manual_tests §二十八 close lifecycle defect).

Fix: publish the primary native module to AppStorage at the end of
ProcessInitializer.initialize(). One write fixes all readers.

Verified on HUAWEI MateBook Pro (API 23): hilog shows "Window N close
notified via NAPI" (was "not available"); windowId increments 1→2→3 on
repeated create/close of the same-label overlay.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants