diff --git a/CHANGELOG.md b/CHANGELOG.md index a737e71..4f3fbf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,339 +3,182 @@ All notable changes to this project will be documented in this file. -## [0.99.24] - 2026-04-21 +## [0.99.29] - 2026-04-22 -### ✨ 新功能 — 意见反馈系统重构:5轮对话+双发邮件+本地存储 +### ✨ 增强 — 扫码识别菜谱 + 相机权限管理完善 -#### 功能概述 -将原有的简单聊天式意见反馈页面重构为完整的**状态机驱动反馈系统**,支持5轮结构化对话、邮件双发(管理员+用户自动回复)、设备信息收集、本地持久化存储和防滥用限制。 +#### 变更描述 +扫码功能增强:识别CP/ID后通过API转换为APP内部菜品ID跳转详情页;相机权限管理完善 -#### 核心特性 -- 🎯 **状态机架构**:采用有限状态机管理反馈流程(idle → selecting → chatting_1~4 → completed → sending → success/failed) -- 💬 **5轮完整对话**:选择反馈类型(1轮) + 用户输入4条详细描述 = 共5轮交互 -- 📧 **双发邮件机制**: - - 第一封:发送到管理员(ad@avefs.com),包含完整对话记录+设备信息(UUID/IP/版本号)+系统日志 - - 第二封:如果用户填写了邮箱,自动发送iOS风格确认邮件(含反馈编号、处理流程、联系方式) -- 📱 **设备信息自动收集**:UUID、IP地址、应用版本、平台信息、操作系统版本、运行日志 -- 💾 **本地持久化存储**:使用shared_preferences保存所有反馈会话历史(最多50条) -- 🚫 **防滥用机制**: - - 每日限制3次提交 - - 冷却时间30分钟 - - 跨天自动重置计数器 -- 🎨 **iOS 26风格UI**: - - 对话进度指示器(第X/4轮,剩余X轮) - - 完成后显示提交面板(摘要卡片+邮箱输入+发送按钮) - - 发送中状态指示器 - - 成功/失败对话框(支持返回修改或重试或提交新反馈) - -#### 新增文件 -- `lib/src/models/feedback_model.dart` — 数据模型定义 - - `FeedbackState` 枚举(9种状态) - - `FeedbackSessionStatus` 枚举(draft/sent/failed) - - `DeviceInfo` 模型(uuid/ip/version/platform/logs) - - `FeedbackMessage` 模型(text/isUser/type/timestamp) - - `FeedbackSession` 模型(完整会话,支持不可变操作) -- `lib/src/services/local/feedback_storage_service.dart` — 本地存储服务 - - CRUD操作(save/getAll/get/delete/clearAll) - - 限制检查(canSubmitFeedback/recordSubmission/resetDailyCount) - - 自动跨天重置逻辑 - - 最大50条历史记录限制 +#### 实现内容 +- 🔍 **扫码结果智能识别**:截取URL中的CP编码和ID参数,通过 `WhatToEatRepository` API查询转换为APP内部菜品ID + - 支持 `https://eat.wktyl.com/api/kitchen/recipe_share.php?code=CP037130` 格式 + - 支持 `https://eat.wktyl.com/?id=28087` 格式 + - 支持纯文本 `CP037130` 格式 + - 查询成功显示菜谱识别对话框,跳转菜品详情页 + - 查询失败降级为普通扫码结果显示 +- 📷 **相机权限弹窗引导**:扫码前弹出相机用途说明,拒绝后显示各平台开启指引 +- 🔒 **权限管理页面增强**: + - 新增相机权限项(📷),状态动态显示(已开启/已拒绝) + - 相机权限拒绝时显示红色标识,点击可跳转系统设置 + - 权限说明弹窗更新:区分动态权限和基础权限 +- 📄 **隐私政策更新**:设备权限调用新增相机权限说明 #### 修改文件 -- `lib/src/pages/profile/social/chat_page.dart` — 完全重构为状态机驱动的反馈页面 - - 状态转换逻辑(_advanceToNextRound/_completeConversation) - - 设备信息收集(_createSession) - - 邮件发送流程(_submitFeedback + 成功/失败对话框) - - 动态底部UI切换(类型选择→输入栏→提交面板→发送中指示器) - - 进度显示(当前轮次/剩余轮次) - - 错误处理与重试机制 -- `lib/src/services/data/business/email_service.dart` — 扩展邮件服务 - - 新增 `sendFeedbackEmails()` 方法(双发入口) - - 新增 `_sendFeedbackToAdmin()` (发送给管理员,iOS风格HTML模板) - - 新增 `_sendAutoReplyToUser()` (自动回复给用户,绿色成功主题HTML) - - 新增 `_buildFeedbackAdminHtml()` (管理员邮件模板:用户信息卡片+对话记录+系统日志) - - 新增 `_buildFeedbackAutoReplyHtml()` (用户回复模板:感谢语+摘要+后续步骤+联系方式) - - 新增 `_getBaseEmailStyles()` / `_buildInfoRow()` / `_escapeHtml()` 辅助方法 - - 新增 `FeedbackSendResult` 结果类(success/adminSent/userSent/errorMessage) +- `lib/src/pages/home/scanner_page.dart` — CP/ID通过API转换,权限弹窗引导 +- `lib/src/pages/profile/tools/permission_page.dart` — 新增相机权限,动态状态显示 +- `lib/src/pages/profile/info/privacy_policy_page.dart` — 新增相机权限说明 -#### 技术亮点 -- **不可变数据设计**:FeedbackSession使用不可变操作(addMessage/markAsCompleted等),每次返回新实例,便于状态追踪和调试 -- **优雅降级**:设备信息获取失败不影响主流程(try-catch包裹每个采集点) -- **用户体验优化**: - - Bot引导语提示剩余轮次数 - - 邮箱选填并明确提示用途 - - 发送失败可返回修改或直接重试 - - 成功后可选择返回或立即开始新反馈 -- **邮件模板**: - - 采用Apple SF Pro字体栈 - - 响应式设计(移动端适配@media查询) - - 渐变色头部(紫色主题/绿色成功主题) - - 信息网格布局(2列自适应) - - 对话消息区分样式(用户/Bot不同背景色) - - 系统日志深色代码块样式 +## [0.99.28] - 2026-04-22 -#### 使用示例 -```dart -// 导航到反馈页面 -Navigator.push(context, CupertinoPageRoute(builder: (_) => FeedbackPage())); +### 🔄 升级 — mobile_scanner 本地包升级至官方 v7.2.0 + 鸿蒙适配合并 -// 用户流程: -// 1. 选择类型(Bug/功能建议/体验优化/其他) -// 2. 输入4条详细描述(带轮次提示) -// 3. 查看摘要,可选填邮箱 -// 4. 点击"📤 发送反馈" -// 5. 收到成功提示(如填了邮箱还会收到确认邮件) -``` +#### 变更描述 +将本地 `packages/mobile_scanner` 从鸿蒙适配版 v7.1.4 升级为官方 v7.2.0,并保留鸿蒙端支持 -#### 后续扩展方向 -- 🔮 反馈历史列表页面(查看已提交的反馈) -- 🔮 管理后台集成(API对接替代SMTP直发) -- 🔮 图片/截图附件支持 -- 🔮 反馈分类统计图表 -- 🔮 用户回复追踪(多轮客服对话) +#### 合并内容 +- 📦 基础:官方 `mobile_scanner` v7.2.0(新增 camera_lens_type、switch_camera_option、barcode_bytes 等) +- 🔧 鸿蒙适配合并: + - `ohos/` 目录完整保留(鸿蒙原生 ets 实现) + - `lib/src/method_channel/ohos_surface_producer_delegate.dart` — 鸿蒙 Surface 代理 + - `lib/src/method_channel/surface_producer_delegate.dart` — Surface 代理接口 + - `lib/src/method_channel/mobile_scanner_method_channel.dart` — 添加 `TargetPlatform.ohos` 支持 + - `lib/src/method_channel/rotated_preview.dart` — 鸿蒙旋转方向修复 + - `pubspec.yaml` — 添加 `ohos` 平台声明 + +#### 修改文件 +- `packages/mobile_scanner/` — 整体替换为官方 v7.2.0 + 鸿蒙适配合并版 +- `packages/本地已适配鸿蒙的库.md` — 添加 mobile_scanner 适配记录 + +## [0.99.27] - 2026-04-22 + +### ✨ 新功能 — 首页扫码功能(mobile_scanner) + +#### 功能描述 +首页 AppBar 右侧新增扫码按钮,点击打开相机扫描二维码/条形码页面 + +#### 实现内容 +- 🔍 集成 `mobile_scanner` 鸿蒙适配版(本地包 `packages/mobile_scanner`) +- 📱 支持平台:Android / iOS / macOS / 鸿蒙 / Web +- 🖥️ Windows/Linux 降级提示:显示"当前平台暂不支持相机扫码" +- 🎯 扫码结果自动识别类型(链接/EAN-13/EAN-8/UPC-A/文本) +- 📋 支持复制扫码结果到剪贴板 +- 🔦 支持闪光灯开关、前后摄像头切换 +- 📐 扫描框四角装饰 + 提示文字 + +#### 权限配置 +- **Android**: `AndroidManifest.xml` 添加 `CAMERA` 权限 +- **iOS**: `Info.plist` 添加 `NSCameraUsageDescription` +- **macOS**: `Info.plist` 添加 `NSCameraUsageDescription` +- **鸿蒙**: `module.json5` 添加 `ohos.permission.CAMERA`,`string.json` 权限说明改中文 + +#### 修改文件 +- `pubspec.yaml` — `fluttertpc_mobile_scanner` git依赖改为 `mobile_scanner` 本地包 +- `lib/src/pages/home/scanner_page.dart` — 新建扫码页面 +- `lib/src/widgets/glass/nav/home_app_bar.dart` — AppBar 添加扫码按钮 +- `lib/src/pages/home/home_page.dart` — 连接扫码按钮回调 +- `lib/src/config/app_routes.dart` — 注册 `/scanner` 路由 +- `android/app/src/main/AndroidManifest.xml` — 添加 CAMERA 权限 +- `ios/Runner/Info.plist` — 添加 NSCameraUsageDescription +- `macos/Runner/Info.plist` — 添加 NSCameraUsageDescription +- `ohos/entry/src/main/module.json5` — 添加 CAMERA 权限 +- `ohos/entry/src/main/resources/base/element/string.json` — 权限说明改中文 + +## [0.99.26] - 2026-04-22 + +### 🐛 修复 — Windows端返回导航快捷键改用Escape键 + +#### 问题描述 +Windows桌面端在搜索页面等输入框中按 Backspace/Delete 删除文字时,会误触发全局返回导航逻辑,导致返回上一页 + +#### 根因分析 +- `main.dart` 中 `KeyboardListener` 全局监听 Backspace 键执行 `Get.back()` 返回上一页 +- Backspace 是文本编辑常用键,与返回导航功能冲突,无法通过焦点检测完全避免 + +#### 修复内容 +- 🔧 将 Backspace 键绑定改为 **Escape** 键 +- Escape 是桌面端常用的"返回/关闭"快捷键,不会与文本编辑冲突 +- 返回导航快捷键变更为:**Alt+←** / **Escape** / **BrowserBack(鼠标侧键)** + +#### 修改文件 +- `lib/main.dart` — `KeyboardListener.onKeyEvent` 将 `LogicalKeyboardKey.backspace` 改为 `LogicalKeyboardKey.escape` + +### 🐛 修复 — 工具中心多个页面列表溢出及路由未注册 + +#### 问题描述 +1. 用餐时段推荐、份量缩放、食材详情、每周菜单规划等页面内容被 AppBar 遮住 +2. RenderFlex 溢出 3.0 pixels(meal_time_recommend_page.dart:253) +3. 路由 `/tools/dish-ranking` 未注册到 PageRegistry,导致页面验证警告 + +#### 根因分析 +- **内容被 AppBar 遮住**:3个页面使用了 `SafeArea(top: false)`,而 `CupertinoPageScaffold` 的半透明导航栏(alpha: 0.9)不会自动偏移内容,需要 SafeArea 提供顶部间距 +- **RenderFlex 溢出**:时段选择器 `SizedBox(height: 50)` 容器高度不足,内部 Column 内容超出 3px +- **路由未注册**:`dish-ranking` 等路由在 GetPage 中已定义但 PageRegistry.registerAll 中缺少对应 PageInfo + +#### 修复内容 +- 🔧 移除 `SafeArea(top: false)`:meal_time_recommend_page、serving_scaler_page、ingredient_detail_page +- 🔧 修复时段选择器高度:SizedBox 50→56,消除 RenderFlex 溢出 +- 🔧 每周菜单规划页面 `_buildDayDetailView`:Column 改为 SingleChildScrollView,防止内容溢出 +- 🔧 补充 PageRegistry 注册:dish-ranking、toolsStats、toolsOrderAssistant、toolsDecisionMaker、toolsTakeoutNote、dataExport、ratingRecords、foodTimeline 共8个路由 + +#### 修改文件 +- `lib/src/pages/tools/health/meal_time_recommend_page.dart` — 移除 `top: false`,修复 SizedBox 高度 +- `lib/src/pages/tools/cooking/calculator/serving_scaler_page.dart` — 移除 `top: false` +- `lib/src/pages/tools/ingredient_detail/ingredient_detail_page.dart` — 移除 `top: false` +- `lib/src/pages/tools/planning/weekly_menu_planner_page.dart` — `_buildDayDetailView` 改用 SingleChildScrollView +- `lib/src/config/app_routes.dart` — 补充8个缺失的 PageInfo 注册项 + +#### 举一反三 +- 全局搜索确认已无其他 `SafeArea(top: false)` 残留 +- 排查所有工具页面路由,确保 GetPage 与 PageRegistry 一一对应 --- -## [0.99.23] - 2026-04-21 +## [0.99.25] - 2026-04-22 -### 🎨 UI优化 — 发送菜谱对话框线路选择样式重构 +### 🔄 调整 — 个人中心首页与设置页按钮位置互换 -#### 修改内容 -- 🎨 **线路1(官方线路1)URL脱敏显示**: - - SMTP服务器地址中间部分用***代替(如 free***ing.com) - - 保护服务器信息安全,避免完整URL暴露 -- ✨ **线路2升级为VIP专属邮箱**: - - 名称改为「✨ VIP 专属邮箱」,添加VIP标签和👑皇冠图标 - - 采用紫色主题(#9B59B6),深浅模式均有专属背景色 - - 锁定无法选择,右侧显示🔒锁定图标 - - 隐藏详细配置信息(不显示用户名和服务器地址) - - 副标题提示「高级会员专属线路,更稳定快速」 -- 🔧 **新增URL脱敏方法**:`_maskUrl()` 对长URL进行中间部分遮蔽处理 -- 📐 **布局优化**:收件人邮箱输入框移至发送线路选择上方,符合用户操作流程(先填写收件人,再选择线路) +#### 变更内容 +- 🌟 **美食年轮入口移至首页**:将「🌟 美食年轮 - 查看你的美食旅程」从设置页移至个人中心首页顶部卡片位置,方便用户快速访问美食旅程 +- 🚀 **开发计划入口移至设置页**:将「开发计划 - 查看即将推出的功能与路线图」从首页移至设置页底部 section,含完整路线图 Sheet 弹窗 #### 修改文件 -- `lib/src/widgets/recipe_detail/interaction/recipe_email_button.dart` — 线路选择UI重构+VIP样式+URL脱敏 +- `lib/src/pages/profile/profile_home.dart` — 移除开发计划卡片及 Sheet 相关代码,新增美食年轮入口卡片 +- `lib/src/pages/profile/profile_settings.dart` — 移除美食年轮 section,新增开发计划 section(含 `_showDevPlanSheet`、`_buildDevPlanItem`、`_DevPlanItem`、`_PlanStatus`) -## [0.99.22] - 2026-04-21 +### 🔤 字体 — NotoSansSC 子集化并启用全局中文字体 -### ✨ 新功能 — 美食年轮:可视化美食旅程档案 +#### 变更内容 +- 🔧 **修复假字体文件**:原 `NotoSansSC-Regular.otf` 和 `NotoSansSC-Bold.otf` 实际是 GitHub HTML 页面(302KB),替换为真正的 OTF 字体文件 +- ✂️ **字体子集化**:使用 `fontTools pyftsubset` 对完整版 NotoSansCJKsc 进行 GB2312 子集化(6763 常用汉字 + 常用符号),体积从 **31.9MB → 6.5MB**(缩减 80%) +- 🎨 **启用全局中文字体**:在 `theme_service.dart` 的 Material 和 Cupertino 主题中启用 `NotoSansSC` 作为全局字体,确保中文渲染统一美观 -#### 修复 -- 🐛 修复美食年轮页面数据不实时更新的问题:用 Obx 包裹内容区域,通过迭代 RxList 和访问 RxMap.values 注册响应式依赖(.length/.count 不会触发 GetX 响应式通知) -- 🐛 修复浏览记录/笔记/分享记录数据丢失的竞态条件:控制器使用 lazyPut 延迟初始化,onInit 中异步加载历史数据未完成时 addHistory/addNote/addRecord 已执行,导致空列表保存覆盖旧数据。添加 Completer 确保初始化完成后才执行写操作 -- 🐛 修复 RecipeDetailController._recordBrowseHistory 未 await addHistory 的问题 -- 🐛 修复月度足迹收藏数显示0的问题:FeedItemModel.fromJson 反序列化时缺少 'createdAt' key 匹配(保存用 'createdAt',读取只查 'created_at'/'post_time'),导致从 Hive 加载后 createdAt 为 null - -#### 修改内容 -- ✨ **美食年轮页面**: - - 新增「美食年轮」页面,聚合浏览/收藏/笔记/评分数据,生成可视化美食旅程档案 - - 🎯 **Hero总览区**:圆形年轮动画展示使用天数,外圈随浏览量动态增长 - - 📊 **数据总览**:4格统计卡片(浏览菜谱数/收藏数/笔记数/评分数) - - 🧬 **味觉DNA**:6维雷达图(辣/甜/咸/酸/鲜/香),基于口味偏好设置和浏览数据推断,显示味觉人格类型 - - 🏆 **美食里程碑**:9项成就时间线(初识美食/探索者/资深食客/百科/心动时刻/收藏家/记录者/品鉴师/全能美食家) - - 📅 **月度足迹**:最近3个月活动摘要,含浏览/收藏/笔记/评分数量和热门分类 - - 🍜 **菜系探索度**:环形图展示已探索菜系种类和占比 - - 入口位于设置页「🚀 功能」区域,图标 CupertinoIcons.circle_grid_3x3 +#### 体积对比 +| 文件 | 子集化前 | 子集化后 | 缩减 | +|------|---------|---------|------| +| NotoSansSC-Regular.otf | 15.7 MB | 3.2 MB | 80% | +| NotoSansSC-Bold.otf | 16.2 MB | 3.3 MB | 80% | +| **合计** | **31.9 MB** | **6.5 MB** | **80%** | #### 修改文件 -- `lib/src/pages/profile/data/food_timeline_page.dart` — 新建,美食年轮页面 -- `lib/src/config/app_routes.dart` — 新增 foodTimeline 路由定义和页面注册 -- `lib/src/pages/profile/profile_settings.dart` — 功能区域新增美食年轮入口 +- `assets/fonts/NotoSansSC-Regular.otf` — 替换为 GB2312 子集化版本 +- `assets/fonts/NotoSansSC-Bold-new.otf` — 新增 GB2312 子集化 Bold 版本(原 Bold.otf 被 IDE 锁定,使用新文件名) +- `pubspec.yaml` — 更新字体配置注释和 Bold 文件路径 +- `lib/src/services/ui/theme_service.dart` — 在 `getThemeData()` 和 `cupertinoThemeData` 中启用 `fontFamily: 'NotoSansSC'` -## [0.99.21] - 2026-04-21 +### ⚙️ 设置 — 字体风格可切换(系统默认 / NotoSansSC) -### 🖥️ Windows 桌面端修复 — 图标清晰度/窗口宽度/中文乱码 - -#### 修改内容 -- 🎨 **图标清晰度修复**: - - 从 `icon_1024x1024.png` 重新生成 ICO,包含 17 种尺寸(16~256px) - - 窗口类注册改用 `WNDCLASSEX` + `RegisterClassEx`,分别设置 `hIcon` 和 `hIconSm` - - `LoadImage` 显式加载 256×256 大图标 + 48×48 小图标,替代 `LoadIcon`(仅 32×32) - - 窗口创建后通过 `WM_SETICON` 设置 `ICON_BIG` 和 `ICON_SMALL` - - DPI 变化时(`WM_DPICHANGED`)重新加载图标 - - 安装脚本添加 `ie4uinit.exe -show` 自动刷新图标缓存 -- 📐 **窗口加宽**: - - 默认宽度从 420px 调整为 520px - - 最小宽度从 360px 调整为 480px -- 🔤 **中文乱码修复**: - - CMakeLists.txt 添加 `/utf-8` 编译选项,解决 MSVC 默认 GBK 编码导致中文标题乱码 -- 📦 **打包配置修复**: - - Inno Setup 压缩格式改为 `lzma2/max`(兼容 6.7.1) - - 权限改为 `PrivilegesRequired=lowest`(兼容 6.7.1) - - 下载并安装 ChineseSimplified.isl 中文语言包 +#### 变更内容 +- 🔤 **新增字体风格设置**:用户可在「个性化设置 → 🔤 字体风格」中选择「系统默认」或「NotoSansSC」 +- 💾 **持久化存储**:字体选择保存到 SharedPreferences,重启后保持 +- 🔄 **动态切换**:选择后立即生效,无需重启应用 +- 🎨 **CupertinoSegmentedControl**:使用 iOS 风格分段控件,附带说明文字 #### 修改文件 -- `windows/runner/win32_window.cpp` — 图标加载改用 LoadImage 256px + WM_SETICON + DPI 重载 -- `windows/runner/main.cpp` — 窗口宽度 420→520 -- `windows/CMakeLists.txt` — 添加 /utf-8 编译选项 -- `windows/runner/resources/app_icon.ico` — 重新生成 17 尺寸 ICO -- `installer.iss` — 修复兼容性问题 + 添加图标缓存刷新代码 +- `lib/src/services/ui/theme_service.dart` — 新增 `FontFamilyStyle` 枚举、`fontFamilyStyle` 状态、`setFontFamilyStyle()` 方法,主题根据选择动态切换字体 +- `lib/src/controllers/user/personalization_controller.dart` — 新增 `setFontFamilyStyle()` 方法 +- `lib/src/pages/profile/settings/personalization_page.dart` — 新增「🔤 字体风格」section,含分段控件和说明 -## [0.99.20] - 2026-04-21 - -### 🖥️ Windows 桌面端适配 — 窗口尺寸/图标/名称/交互/打包 - -#### 修改内容 -- 🖥️ **窗口尺寸改为竖屏**: - - 默认窗口从 1280×720(横屏)改为 420×800(手机比例竖屏) - - 窗口启动自动居中显示 - - 添加最小窗口尺寸限制(360×600),防止窗口过小导致布局异常 -- 🎨 **应用图标和名称**: - - 将 `assets/icons/icon_1024x1024.png` 转换为 ICO 替换 Windows 默认图标 - - 窗口标题从 `mom_kitchen` 改为 `小妈厨房` - - EXE 元数据更新:ProductName=小妈厨房,CompanyName=微风暴工作室 -- 🖱️ **鼠标拖动滚动**: - - 添加 `DesktopScrollBehavior`,支持鼠标拖拽滚动列表 - - 同时支持触控板和触控笔拖拽 -- ⌨️ **桌面端返回导航**: - - 支持 `Alt+←` 键返回上一页 - - 支持 `Backspace` 键返回上一页 - - 支持 `BrowserBack` 键返回(鼠标侧键 XButton1) - - 支持 `BrowserForward` 键前进(鼠标侧键 XButton2) - - C++ 层处理 `WM_XBUTTONUP` 消息,将鼠标侧键转换为浏览器导航键 -- 📦 **打包配置**: - - 创建 Inno Setup 安装脚本 `installer.iss`,生成单个安装程序 EXE - - 配置 `distribute_options.yaml` 支持 flutter_distributor 打包 - - 安装程序支持中文界面、桌面快捷方式、安装后自动启动 - -#### 修改文件 -- `windows/runner/main.cpp` — 窗口尺寸改为 420×800,标题改为「小妈厨房」 -- `windows/runner/win32_window.cpp` — 窗口居中 + 最小尺寸限制(WM_GETMINMAXINFO) -- `windows/runner/flutter_window.cpp` — 鼠标侧键支持(WM_XBUTTONUP) -- `windows/runner/Runner.rc` — EXE 元数据更新 -- `windows/runner/resources/app_icon.ico` — 替换为应用图标 -- `lib/main.dart` — 添加 DesktopScrollBehavior + KeyboardListener(快捷键返回) -- `installer.iss` — 新建,Inno Setup 安装脚本 -- `distribute_options.yaml` — 新建,flutter_distributor 配置 - -## [0.99.19] - 2026-04-21 - -### ✨ 新功能 — 个人中心:用户卡片改为开发计划卡片 - -#### 修改内容 -- ✨ **开发计划卡片**: - - 用户信息卡片替换为「开发计划」卡片,点击弹出 Cupertino Sheet - - Sheet 展示 12 项开发路线图,含状态标签(即将上线/开发中/规划中) - - 路线图内容:多端适配、用户中心、工具中心、导出功能、菜品投稿、社区互动、智能推荐、食材管理、营养分析、多语言支持、云同步、消息通知 - - 移除旧的登录/编辑/个性化按钮(登录功能暂不开发) - - 移除 ProfileController 和 PersonalizationPage 依赖 - -#### 修改文件 -- `lib/src/pages/profile/profile_home.dart` — 用户卡片改为开发计划卡片 + Sheet 弹窗 - -## [0.99.18] - 2026-04-20 - -### ✨ 新功能 — 今天吃什么:动态筛选(选项越多,匹配越少) - -#### 修改内容 -- ✨ **动态筛选核心功能**: - - 分类从 `api_filter.php` 加载(大类+子类),标签和匹配数从 `filter_steps` 动态更新 - - 选择分类/标签后实时刷新匹配数和可用标签,选项越多匹配越少 - - 标签显示对应菜谱数量,0匹配标签自动禁用+删除线 - - 过敏原排除功能(客户端过滤) - - 300ms 防抖避免频繁请求 -- 🔧 **API 扩展**: - - `api_what_to_eat.php` 的 `filter_steps` 新增 `available_tags` 返回标签+数量 - - 分类选择自动展开子分类(支持3级递归) - - `filter_apply` 同步支持分类展开逻辑 -- 📁 **文件拆分**: - - `what_to_eat_page.dart`(1053行)拆分为3个文件: - - `what_to_eat_page.dart` — 主页面骨架(~168行) - - `widgets/what_to_eat_result_card.dart` — 结果卡片+操作按钮(~280行) - - `widgets/what_to_eat_filters.dart` — 分类/标签/过敏原筛选+公共组件(~470行) -- 🧪 **接口验证脚本**:`scripts/test_filter_steps.dart` 验证动态筛选效果 - -#### 修改文件 -- `lib/src/pages/discover/what_to_eat_page.dart` — 重写为精简主页面 -- `lib/src/pages/discover/widgets/what_to_eat_result_card.dart` — 新建,结果卡片组件 -- `lib/src/pages/discover/widgets/what_to_eat_filters.dart` — 新建,筛选组件集合 -- `lib/src/controllers/tools/what_to_eat_controller.dart` — 重写,动态筛选+防抖 -- `lib/src/repositories/what_to_eat_repository.dart` — 新增 fetchFilterSteps 方法 -- `docs/api/api_what_to_eat.php` — filter_steps 新增 available_tags -- `scripts/test_filter_steps.dart` — 新建,接口验证脚本 +--- -## [0.99.17] - 2026-04-20 - -### 🐛 Bug 修复 — 收藏页搜索框高度不足比例不协调 - -#### 修改内容 -- 🐛 **修复收藏页搜索框高度不足**: - - `GlassContainer` 增加 `vertical: DesignTokens.space2` 垂直内边距 - - `CupertinoTextField` 的 `padding` 从 `EdgeInsets.zero` 改为 `EdgeInsets.symmetric(vertical: DesignTokens.space2)` - - 搜索框整体高度增加,与页面其他元素比例更协调 - -#### 修改文件 -- `lib/src/pages/profile/social/favorites_page.dart` — 搜索框高度修复 - -### ✨ 新功能 — 数据管理中心:菜谱分类+食材分类,均显示大类+小类层级结构 - -#### 修改内容 -- ✨ **新增食材分类卡片**: - - 使用 `fetchIngredientMainCategories()` + `fetchIngredientSubCategories()` 接口 - - 标签卡显示「X大类 · Y小类」,teal 主题色 - - 详情弹窗展示层级结构:大类可展开/折叠显示小类 - - 大类显示食材数量和小类数量,小类显示食材数量 -- 🔧 **菜谱分类数据修正**: - - 数据源从 `fetchCategories()` 改为 `fetchMainCategories()` + `fetchRecipeSubCategories()` - - 标签卡显示从「X类」改为「X大类 · Y小类」 - - 详情弹窗从平铺列表改为层级结构:大类可展开/折叠显示小类 -- `LocalDataService` 新增 `getRecipeCategories()`/`syncRecipeCategories()` 和 `getIngredientCategories()`/`syncIngredientCategories()` 及缓存管理 -- `syncAll()` 同步流程新增菜谱分类和食材分类同步步骤 - -#### 修改文件 -- `lib/src/pages/profile/data/data_center_page.dart` — 新增食材分类卡片和弹窗,菜谱分类层级展示 -- `lib/src/services/data/storage/local_data_service.dart` — 新增菜谱分类和食材分类方法及缓存 - - -## [0.99.16] - 2026-04-20 - -### 🐛 Bug 修复 — 运营数据大屏 + 依赖替换 + Release模式布局溢出过滤 - -#### 修改内容 -- 📝 **Release 模式过滤布局溢出错误**: - - `main.dart`:`FlutterError.onError` 在 release 模式下跳过布局溢出类错误(overflowed/renderflex/hasSize等) - - `crash_guard_service.dart`:`CupertinoDialogReportMode` 在 release 模式下自动拒绝布局溢出报告,不弹对话框 - - debug 模式不受影响,仍会正常展示所有错误 - -- 📝 **运营数据大屏修复**: - - 修复 `StatsRepository` 使用 `ApiResponse.fromJson(null)` 导致 data 永远为 null 的 bug - - 新增「🗄️ 数据库概览」「🔌 接口调用分布」「🏆 热门排行 TOP5」板块 - -- 📝 **依赖替换 `receive_sharing_intent` → `file_picker_ohos`**: - - 删除 `pubspec.yaml` 中 `receive_sharing_intent` 依赖 - - `data_export_page.dart` 改用 `FilePicker.platform.pickFiles()` 选择 JSON 文件导入 - -#### 修改文件 -- `lib/main.dart` — release 模式过滤布局溢出错误 -- `lib/src/services/system/crash_guard_service.dart` — release 模式跳过布局溢出报告 -- `lib/src/repositories/online_repository.dart` — 修复数据解析bug,新增fetchBasicStats() -- `lib/src/pages/profile/data/stats_dashboard_page.dart` — 重写页面,热门排行替代月度趋势 -- `lib/src/pages/profile/tools/data_export_page.dart` — 移除receive_sharing_intent,改用file_picker_ohos -- `pubspec.yaml` — 删除receive_sharing_intent依赖 - - -> 📌 已移除较早版本记录(0.99.15及之前),功能已归档至软件特性清单。 -> - 0.99.15: 公告页面(列表+下拉刷新+四种状态) -> - 0.99.14: 外卖备注工具(口味/配送/整活分类+一键生成) -> - 0.99.13: 菜谱外部搜索按钮+搜索后缀选择 -> - 0.99.12: 邮件发送SSL握手失败+VPN代理检测+连接容错 -> - 0.99.11: 邮件发送全链路修复(fallback配置/allowInsecure/限额计数/超时) -> - 0.99.10: 邮件发送HandshakeException深度修复 -> - 0.99.9: 邮件发送HandshakeException+备选端口 -> - 0.99.8: 移除flutter_dotenv依赖 -> - 0.99.7: 邮件分享系统升级(HTML模板+限额+记录管理) -> - 0.99.6: 帮我做决定转盘指针指向分界线修复 -> - 0.99.5: 发现页面Dismissible组件错误修复 -> - 0.99.4: 分享记录管理页面(模型+控制器+页面+路由) -> - 0.99.3: 评分记录管理页面 + RatingRecordModel + RatingRecordsController + HiveService扩展 -> - 0.99.2: 帮我做决定转盘工具 + 工具中心内容扩展 -> - 0.99.1: 版本号同步 + Android应用名修正 + R8 Play Core警告修复 -> - 0.99.0: Android Release包缓存失效/图片加载不出/浏览记录丢失修复 -> - 0.98.10: 删除接口RESTful规范化 -> - 0.98.9: 菜谱分享本地存储 + 3天自动过期清理 -> - 0.98.8: 二维码分享URL生成 + PHP分享页面 -> - 0.98.7: 口味偏好持久化 + 菜品详情页偏好标注 -> - 0.98.6: 软件信息页面图标/参考文献扩充/偏好设置重写/路由修复 -> - 0.98.5: 软件信息页面 + 了解我们页面 -> - 0.98.4: 小妈菜园交互优化 + 商店布局修复 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d2d11cc..ad2242f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,9 @@ + + @@ -7,6 +11,9 @@ + + + diff --git a/assets/fonts/NotoSansSC-Bold.otf b/assets/fonts/NotoSansSC-Bold.otf new file mode 100644 index 0000000..20189ab Binary files /dev/null and b/assets/fonts/NotoSansSC-Bold.otf differ diff --git a/assets/fonts/NotoSansSC-Regular.otf b/assets/fonts/NotoSansSC-Regular.otf new file mode 100644 index 0000000..fd0835e Binary files /dev/null and b/assets/fonts/NotoSansSC-Regular.otf differ diff --git a/dist/小妈厨房_Setup_1.0.3.exe b/dist/小妈厨房_Setup_1.3.1.exe similarity index 66% rename from dist/小妈厨房_Setup_1.0.3.exe rename to dist/小妈厨房_Setup_1.3.1.exe index e8b4c88..460492e 100644 Binary files a/dist/小妈厨房_Setup_1.0.3.exe and b/dist/小妈厨房_Setup_1.3.1.exe differ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 53f7269..223b15f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -50,6 +50,8 @@ NSAllowsArbitraryLoads + NSCameraUsageDescription + This app needs camera access to scan QR codes and barcodes CFBundleDocumentTypes diff --git a/lib/main.dart b/lib/main.dart index fe16dea..2e3c209 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -140,9 +140,28 @@ class DesktopScrollBehavior extends MaterialScrollBehavior { }; } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final FocusNode _rootFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + // 初始禁止 root focus 请求,避免在首次布局前触发全局焦点遍历 + _rootFocusNode.canRequestFocus = false; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _rootFocusNode.canRequestFocus = true; + } + }); + } + ThemeService _getThemeService() { if (Get.isRegistered()) { return Get.find(); @@ -150,6 +169,12 @@ class MyApp extends StatelessWidget { return Get.put(ThemeService.instance, permanent: true); } + @override + void dispose() { + _rootFocusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final themeService = _getThemeService(); @@ -157,7 +182,7 @@ class MyApp extends StatelessWidget { return Obx(() { final textScale = themeService.fontSize.value / 16.0; return GetCupertinoApp( - title: 'Mom\'s Kitchen', + title: 'Cute Kitchen', navigatorKey: Catcher2.navigatorKey, theme: themeService.cupertinoThemeData, locale: Locale(themeService.currentLocale.value), @@ -172,17 +197,16 @@ class MyApp extends StatelessWidget { initialBinding: AppBinding(), builder: (context, widget) { return KeyboardListener( - focusNode: FocusNode(), + focusNode: _rootFocusNode, onKeyEvent: (event) { if (event is KeyDownEvent) { final isAltLeft = HardwareKeyboard.instance.isAltPressed && event.logicalKey == LogicalKeyboardKey.arrowLeft; - final isBackspace = - event.logicalKey == LogicalKeyboardKey.backspace; + final isEscape = event.logicalKey == LogicalKeyboardKey.escape; final isBrowserBack = event.logicalKey == LogicalKeyboardKey.browserBack; - if (isAltLeft || isBackspace || isBrowserBack) { + if (isAltLeft || isEscape || isBrowserBack) { if (Get.currentRoute != '/' && Get.currentRoute != AppRoutes.main && Get.currentRoute != AppRoutes.guide) { diff --git a/lib/src/config/app_config.dart b/lib/src/config/app_config.dart index 69f19fa..b257961 100644 --- a/lib/src/config/app_config.dart +++ b/lib/src/config/app_config.dart @@ -2,7 +2,7 @@ import 'package:mom_kitchen/src/services/core/app_info_service.dart'; class AppConfig { // 应用名称 - static String get appName => 'Mom\'s Kitchen'; + static String get appName => 'Cute Kitchen'; // 应用版本(从 AppInfoService 获取) static String get appVersion => AppInfoService().version; diff --git a/lib/src/config/app_routes.dart b/lib/src/config/app_routes.dart index f3e1693..82a89cb 100644 --- a/lib/src/config/app_routes.dart +++ b/lib/src/config/app_routes.dart @@ -21,6 +21,7 @@ import 'package:mom_kitchen/src/pages/profile/nutrition/nutrition_report_page.da import 'package:mom_kitchen/src/pages/profile/nutrition/goal_setting_page.dart'; import 'package:mom_kitchen/src/pages/profile/tools/shopping_list_page.dart'; import 'package:mom_kitchen/src/pages/home/search_page.dart'; +import 'package:mom_kitchen/src/pages/home/scanner_page.dart'; import 'package:mom_kitchen/src/pages/home/advanced_search_page.dart'; import 'package:mom_kitchen/src/pages/home/recipe_detail_page.dart'; import 'package:mom_kitchen/src/pages/home/tag_recipe_list_page.dart'; @@ -89,6 +90,7 @@ class AppRoutes { static const String goalSetting = '/goal-setting'; static const String shoppingList = '/shopping-list'; static const String search = '/search'; + static const String scanner = '/scanner'; static const String recipeDetail = '/recipe-detail'; static const String cookingTimer = '/cooking-timer'; static const String unitConverter = '/unit-converter'; @@ -293,6 +295,11 @@ class AppRoutes { binding: SearchBinding(), middlewares: [PageStandardsMiddleware()], ), + GetPage( + name: scanner, + page: () => const ScannerPage(), + middlewares: [PageStandardsMiddleware()], + ), GetPage( name: recipeDetail, page: () { @@ -1177,6 +1184,18 @@ class AppRoutes { ], builder: () => const AllergenCheckerPage(), ), + PageInfo( + route: toolsDishRanking, + name: 'Tools Dish Ranking Page', + description: '菜品排行页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const DishRankingPage(), + ), PageInfo( route: toolsNutrition, name: 'Tools Nutrition Page', @@ -1345,6 +1364,90 @@ class AppRoutes { ], builder: () => const FarmAchievementPage(), ), + PageInfo( + route: toolsStats, + name: 'Tools Stats Page', + description: '数据统计页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const StatsDashboardPage(), + ), + PageInfo( + route: toolsOrderAssistant, + name: 'Tools Order Assistant Page', + description: '点餐助手页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const OrderAssistantPage(), + ), + PageInfo( + route: toolsDecisionMaker, + name: 'Tools Decision Maker Page', + description: '今天吃什么页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const DecisionMakerPage(), + ), + PageInfo( + route: toolsTakeoutNote, + name: 'Tools Takeout Note Page', + description: '外卖笔记页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const TakeoutNotePage(), + ), + PageInfo( + route: dataExport, + name: 'Data Export Page', + description: '数据导出页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const DataExportPage(), + ), + PageInfo( + route: ratingRecords, + name: 'Rating Records Page', + description: '评分记录页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const RatingRecordsPage(), + ), + PageInfo( + route: foodTimeline, + name: 'Food Timeline Page', + description: '美食时间线页面', + requiredStandards: const [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.darkMode, + ], + builder: () => const FoodTimelinePage(), + ), PageInfo( route: learnUs, name: 'Learn Us Page', diff --git a/lib/src/controllers/user/personalization_controller.dart b/lib/src/controllers/user/personalization_controller.dart index abd2de5..bcf9e43 100644 --- a/lib/src/controllers/user/personalization_controller.dart +++ b/lib/src/controllers/user/personalization_controller.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mom_kitchen/src/controllers/browse/base_controller.dart'; import 'package:mom_kitchen/src/services/core/app_service.dart'; @@ -146,6 +146,10 @@ class PersonalizationController extends BaseController { await _themeService.setUnifiedStyleEnabled(enabled); } + Future setFontFamilyStyle(FontFamilyStyle style) async { + await _themeService.setFontFamilyStyle(style); + } + /// 恢复默认设置 Future resetToDefaults() async { await runWithLoading(() async { diff --git a/lib/src/models/feed/mini_card_model.dart b/lib/src/models/feed/mini_card_model.dart index ac2b3ef..158d3e4 100644 --- a/lib/src/models/feed/mini_card_model.dart +++ b/lib/src/models/feed/mini_card_model.dart @@ -21,9 +21,17 @@ class MiniCardRecipe { required this.image, }); - String get fullImageUrl => image.startsWith('http') - ? image - : 'https://eat.wktyl.com/api/assets/$image'; + String get fullImageUrl { + if (image.isEmpty) return ''; + if (image.startsWith('https://')) return image; + if (image.startsWith('http://')) { + return image.replaceFirst('http://', 'https://'); + } + if (image.startsWith('/')) { + return 'https://eat.wktyl.com$image'; + } + return 'https://eat.wktyl.com/api/assets/$image'; + } bool get isVegetarian => category == 'vegetable_dish' || diff --git a/lib/src/pages/home/home_page.dart b/lib/src/pages/home/home_page.dart index c093f2e..764b9d8 100644 --- a/lib/src/pages/home/home_page.dart +++ b/lib/src/pages/home/home_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: home_page.dart * 名称: 首页 * 作用: iOS风格首页,Discover瀑布流布局,Liquid Glass风格 @@ -17,6 +17,8 @@ import 'package:mom_kitchen/src/config/app_routes.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/repositories/recipe_repository.dart'; import 'package:mom_kitchen/src/repositories/discover_repository.dart'; +import 'package:mom_kitchen/src/pages/profile/info/notice_page.dart'; + import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; import 'package:mom_kitchen/src/models/feed/discover_model.dart'; import 'package:mom_kitchen/src/services/data/storage/cache_service.dart'; @@ -26,6 +28,7 @@ import 'package:mom_kitchen/src/services/ui/toast_service.dart'; import 'package:mom_kitchen/src/controllers/data/meal_record_controller.dart'; import 'package:mom_kitchen/src/widgets/discover/discover_waterfall.dart'; import 'package:mom_kitchen/src/widgets/glass/nav/home_app_bar.dart'; +import 'package:mom_kitchen/src/pages/home/scanner_page.dart'; import 'package:mom_kitchen/src/models/feed/mini_card_model.dart'; import 'package:mom_kitchen/src/services/data/business/mini_card_service.dart'; import 'package:mom_kitchen/src/models/app/tool_item_model.dart'; @@ -37,7 +40,8 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends State + with AutomaticKeepAliveClientMixin { final RecipeRepository _recipeRepository = RecipeRepository(); final DiscoverRepository _discoverRepository = DiscoverRepository(); final RxList _recipes = [].obs; @@ -45,6 +49,10 @@ class _HomePageState extends State { final RxString _error = ''.obs; final ScrollController _mainScrollController = ScrollController(); + static const PageStorageKey _homeScrollKey = PageStorageKey( + 'home_page_scroll', + ); + // ─── Discover 瀑布流状态 ─── DiscoverData? _discoverData; bool _isDiscoverLoading = true; @@ -111,17 +119,17 @@ class _HomePageState extends State { _suppressRefreshDuringProgrammaticScroll = true; _mainScrollController .animateTo( - 0, - duration: const Duration(milliseconds: 400), - curve: Curves.easeOutCubic, - ) + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutCubic, + ) .whenComplete(() { - // 给系统一个短暂时窗来完成滚动手势,之后恢复刷新能力 - Future.delayed(const Duration(milliseconds: 300), () { - if (!mounted) return; - _suppressRefreshDuringProgrammaticScroll = false; - }); - }); + // 给系统一个短暂时窗来完成滚动手势,之后恢复刷新能力 + Future.delayed(const Duration(milliseconds: 300), () { + if (!mounted) return; + _suppressRefreshDuringProgrammaticScroll = false; + }); + }); } } @@ -525,6 +533,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { + super.build(context); final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; return ClipRect( @@ -536,10 +545,17 @@ class _HomePageState extends State { builder: (context, isSubBarVisible, _) { return HomeAppBar( isSubBarVisible: isSubBarVisible, - onNotificationTap: () { - Get.toNamed(AppRoutes.notice); + onNotificationTap: () async { + // 优雅降级:若路由未注册则直接打开页面,避免抛出“页面未注册”错误 + try { + await Get.toNamed(AppRoutes.notice); + } catch (e) { + // 回退到直接推入页面实例 + Get.to(() => const NoticePage()); + } }, onSettingsTap: _scrollToTop, + onScannerTap: () => Get.to(() => const ScannerPage()), ); }, ), @@ -879,6 +895,8 @@ class _HomePageState extends State { removeTop: true, child: ClipRect( child: CustomScrollView( + key: _homeScrollKey, + controller: _mainScrollController, clipBehavior: Clip.hardEdge, slivers: sliverList, physics: const BouncingScrollPhysics( @@ -888,4 +906,7 @@ class _HomePageState extends State { ), ); } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/src/pages/home/scanner_page.dart b/lib/src/pages/home/scanner_page.dart new file mode 100644 index 0000000..35b952c --- /dev/null +++ b/lib/src/pages/home/scanner_page.dart @@ -0,0 +1,840 @@ +/* + * 文件: scanner_page.dart + * 名称: 扫码页面 + * 作用: 调用相机扫描二维码/条形码,支持多平台降级处理 + * 创建时间: 2026-04-22 + * 更新时间: 2026-04-22 CP/id通过API转换为APP内部ID后跳转菜品详情,相机权限弹窗引导 + */ + +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mom_kitchen/src/config/app_routes.dart'; +import 'package:mom_kitchen/src/config/design_tokens.dart'; +import 'package:mom_kitchen/src/models/recipe/recipe_model.dart'; +import 'package:mom_kitchen/src/repositories/what_to_eat_repository.dart'; +import 'package:mom_kitchen/src/services/ui/toast_service.dart'; +import 'package:mom_kitchen/src/pages/profile/tools/permission_page.dart'; + +class ScannerPage extends StatefulWidget { + const ScannerPage({super.key}); + + @override + State createState() => _ScannerPageState(); +} + +class _ScannerPageState extends State with WidgetsBindingObserver { + late MobileScannerController _controller; + bool _isProcessing = false; + bool _hasPermission = false; + bool _permissionChecked = false; + + static bool get _isPlatformSupported { + if (kIsWeb) return true; + return !Platform.isWindows && !Platform.isLinux; + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _controller = MobileScannerController( + detectionSpeed: DetectionSpeed.normal, + formats: const [BarcodeFormat.all], + autoStart: false, + ); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _requestPermissionWithDialog(), + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _controller.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (!_hasPermission) return; + switch (state) { + case AppLifecycleState.resumed: + _controller.start(); + case AppLifecycleState.inactive: + case AppLifecycleState.paused: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + _controller.stop(); + } + } + + Future _requestPermissionWithDialog() async { + final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; + final granted = await showCupertinoDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => CupertinoAlertDialog( + title: const Padding( + padding: EdgeInsets.only(bottom: DesignTokens.space2), + child: Text('📷 相机权限申请'), + ), + content: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '小妈厨房需要使用您的相机权限来扫描二维码和条形码,识别菜谱信息。', + style: TextStyle(fontSize: DesignTokens.fontMd, height: 1.5), + ), + SizedBox(height: DesignTokens.space3), + Text( + '• 扫描菜谱二维码,快速打开菜品详情\n• 扫描商品条形码,获取商品信息\n• 您可以随时在系统设置中关闭此权限', + style: TextStyle(fontSize: DesignTokens.fontSm, height: 1.6), + ), + ], + ), + actions: [ + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () => Navigator.pop(ctx, false), + child: const Text('拒绝'), + ), + CupertinoDialogAction( + onPressed: () { + Navigator.pop(ctx, null); + Get.to(() => const PermissionPage()); + }, + child: const Text('权限设置'), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () => Navigator.pop(ctx, true), + child: const Text('允许'), + ), + ], + ), + ); + + if (granted == true) { + _grantAndStart(); + } else if (granted == false) { + _showPermissionDeniedGuide(isDark); + } + } + + void _grantAndStart() { + setState(() { + _hasPermission = true; + _permissionChecked = true; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _controller.value.isRunning) return; + _controller.start().catchError((Object error) { + if (!mounted) return; + final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; + if (error is MobileScannerException && + error.errorCode == MobileScannerErrorCode.permissionDenied) { + _showPermissionDeniedGuide(isDark); + } else { + _showCameraError(error, isDark); + } + }); + }); + } + + void _startScanner() { + _controller + .start() + .then((_) { + if (mounted) { + setState(() { + _hasPermission = true; + _permissionChecked = true; + }); + } + }) + .catchError((Object error) { + if (!mounted) return; + final isDark = + CupertinoTheme.brightnessOf(context) == Brightness.dark; + if (error is MobileScannerException && + error.errorCode == MobileScannerErrorCode.permissionDenied) { + _showPermissionDeniedGuide(isDark); + } else { + _showCameraError(error, isDark); + } + }); + } + + void _showCameraError(Object error, bool isDark) { + setState(() { + _hasPermission = false; + _permissionChecked = true; + }); + String errorMsg; + if (error is MobileScannerException) { + errorMsg = switch (error.errorCode) { + MobileScannerErrorCode.unsupported => '当前设备不支持扫码功能', + MobileScannerErrorCode.controllerAlreadyInitialized => '扫码控制器初始化异常', + _ => '相机启动失败: ${error.errorCode.name}', + }; + } else { + errorMsg = '相机启动失败,请重试'; + } + + showCupertinoDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => CupertinoAlertDialog( + title: const Padding( + padding: EdgeInsets.only(bottom: DesignTokens.space2), + child: Text('📷 相机启动失败'), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + errorMsg, + style: const TextStyle( + fontSize: DesignTokens.fontMd, + height: 1.5, + ), + ), + ], + ), + actions: [ + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.pop(ctx); + Get.back(); + }, + child: const Text('返回'), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () { + Navigator.pop(ctx); + _startScanner(); + }, + child: const Text('重试'), + ), + ], + ), + ); + } + + void _showPermissionDeniedGuide(bool isDark) { + setState(() { + _hasPermission = false; + _permissionChecked = true; + }); + showCupertinoDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => CupertinoAlertDialog( + title: const Padding( + padding: EdgeInsets.only(bottom: DesignTokens.space2), + child: Text('🚫 相机权限被拒绝'), + ), + content: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '扫码功能需要相机权限才能正常使用。', + style: TextStyle(fontSize: DesignTokens.fontMd, height: 1.5), + ), + SizedBox(height: DesignTokens.space3), + Text( + '如何开启相机权限:', + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: DesignTokens.space2), + Text( + '📱 iOS:设置 → 隐私与安全性 → 相机 → 小妈厨房\n' + '🤖 Android:设置 → 应用 → 小妈厨房 → 权限 → 相机\n' + '🖥️ macOS:系统设置 → 隐私与安全性 → 相机\n' + '📱 鸿蒙:设置 → 隐私与安全 → 权限管理 → 相机', + style: TextStyle(fontSize: DesignTokens.fontSm, height: 1.6), + ), + ], + ), + actions: [ + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.pop(ctx); + Get.back(); + }, + child: const Text('返回'), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () { + Navigator.pop(ctx); + _startScanner(); + }, + child: const Text('重试'), + ), + ], + ), + ); + } + + void _onBarcodeDetected(BarcodeCapture capture) { + if (_isProcessing) return; + final barcode = capture.barcodes.firstOrNull; + if (barcode == null || barcode.rawValue == null) return; + + _isProcessing = true; + _controller.stop(); + + final value = barcode.rawValue!; + final extractedCode = _extractRecipeCode(value); + + if (extractedCode != null) { + _resolveAndNavigate(extractedCode); + } else { + _showScanResult(value); + } + } + + String? _extractRecipeCode(String value) { + final cpPattern = RegExp(r'[Cc][Pp](\d+)'); + final cpMatch = cpPattern.firstMatch(value); + if (cpMatch != null) { + return 'CP${cpMatch.group(1)!}'; + } + + final uri = Uri.tryParse(value); + if (uri != null) { + final idParam = uri.queryParameters['id']; + if (idParam != null && int.tryParse(idParam) != null) { + return idParam; + } + final codeParam = uri.queryParameters['code']; + if (codeParam != null) { + final codeMatch = cpPattern.firstMatch(codeParam); + if (codeMatch != null) { + return 'CP${codeMatch.group(1)!}'; + } + } + } + + final pureCpMatch = cpPattern.firstMatch(value); + if (pureCpMatch != null) { + return 'CP${pureCpMatch.group(1)!}'; + } + + return null; + } + + Future _resolveAndNavigate(String code) async { + final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; + + showCupertinoDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => const Center(child: CupertinoActivityIndicator()), + ); + + try { + final repo = WhatToEatRepository(); + RecipeModel? recipe; + + if (code.startsWith('CP') || code.startsWith('cp')) { + recipe = await repo + .fetchDetail(code: code) + .timeout(const Duration(seconds: 8), onTimeout: () => null); + } else { + final id = int.tryParse(code); + if (id != null) { + recipe = await repo + .fetchDetail(id: id) + .timeout(const Duration(seconds: 8), onTimeout: () => null); + } + } + + if (!mounted) return; + Navigator.pop(context); + + if (recipe != null) { + _navigateToRecipeDetail(recipe.id, code, isDark); + } else { + _showScanResult(code); + } + } catch (e) { + if (!mounted) return; + Navigator.pop(context); + _showScanResult(code); + } + } + + void _navigateToRecipeDetail( + int appRecipeId, + String displayCode, + bool isDark, + ) { + showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: const Padding( + padding: EdgeInsets.only(bottom: DesignTokens.space2), + child: Text('🍳 识别到菜谱'), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(DesignTokens.space3), + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.card : DesignTokens.background, + borderRadius: DesignTokens.borderRadiusMd, + ), + child: Text( + displayCode, + style: TextStyle( + fontSize: DesignTokens.fontLg, + fontWeight: FontWeight.w600, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + fontFamily: 'monospace', + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: DesignTokens.space2), + Text( + '是否查看该菜谱详情?', + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ], + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + Navigator.pop(ctx); + _isProcessing = false; + _controller.start(); + }, + child: const Text('继续扫描'), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () { + Navigator.pop(ctx); + Get.back(); + Get.toNamed(AppRoutes.recipeDetail, arguments: appRecipeId); + }, + child: const Text('查看菜谱'), + ), + ], + ), + ); + } + + void _showScanResult(String value) { + final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; + showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: const Padding( + padding: EdgeInsets.only(bottom: DesignTokens.space2), + child: Text('📱 扫码结果'), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(DesignTokens.space3), + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.card : DesignTokens.background, + borderRadius: DesignTokens.borderRadiusMd, + ), + child: Text( + value, + style: TextStyle( + fontSize: DesignTokens.fontMd, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + fontFamily: 'monospace', + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: DesignTokens.space2), + Text( + _detectType(value), + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ], + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + _copyToClipboard(value); + }, + child: const Text('复制'), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () { + Navigator.pop(ctx); + _isProcessing = false; + _controller.start(); + }, + child: const Text('继续扫描'), + ), + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.pop(ctx); + Get.back(); + }, + child: const Text('完成'), + ), + ], + ), + ); + } + + String _detectType(String value) { + if (value.startsWith('http://') || value.startsWith('https://')) { + return '🔗 链接'; + } + if (RegExp(r'^\d{13}$').hasMatch(value)) { + return '📦 EAN-13 条形码'; + } + if (RegExp(r'^\d{8}$').hasMatch(value)) { + return '📦 EAN-8 条形码'; + } + if (RegExp(r'^\d{12}$').hasMatch(value)) { + return '📦 UPC-A 条形码'; + } + return '📄 文本'; + } + + void _copyToClipboard(String value) { + final data = ClipboardData(text: value); + Clipboard.setData(data); + ToastService.show(message: '已复制到剪贴板', type: ToastType.success); + } + + @override + Widget build(BuildContext context) { + final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: const Text('📷 扫一扫'), + backgroundColor: isDark + ? DarkDesignTokens.card.withValues(alpha: 0.85) + : DesignTokens.cardAlpha, + border: null, + ), + child: SafeArea( + child: _isPlatformSupported + ? _buildScannerView(isDark) + : _buildUnsupportedView(isDark), + ), + ); + } + + Widget _buildScannerView(bool isDark) { + if (!_permissionChecked) { + return const Center(child: CupertinoActivityIndicator()); + } + if (!_hasPermission) { + return _buildPermissionDeniedView(isDark); + } + return Stack( + children: [ + MobileScanner( + controller: _controller, + onDetect: _onBarcodeDetected, + errorBuilder: (context, error) { + return _buildErrorView(isDark, error); + }, + ), + _buildScanOverlay(isDark), + Positioned( + bottom: DesignTokens.space5, + left: 0, + right: 0, + child: _buildBottomControls(isDark), + ), + ], + ); + } + + Widget _buildPermissionDeniedView(bool isDark) { + return Center( + child: Padding( + padding: const EdgeInsets.all(DesignTokens.space5), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('🚫', style: TextStyle(fontSize: 56)), + const SizedBox(height: DesignTokens.space4), + Text( + '相机权限被拒绝', + style: TextStyle( + fontSize: DesignTokens.fontLg, + fontWeight: FontWeight.w600, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), + const SizedBox(height: DesignTokens.space2), + Text( + '请在系统设置中开启相机权限后重试', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + const SizedBox(height: DesignTokens.space4), + CupertinoButton.filled( + onPressed: () => _startScanner(), + child: const Text('重试'), + ), + const SizedBox(height: DesignTokens.space2), + CupertinoButton( + onPressed: () => Get.back(), + child: const Text('返回'), + ), + ], + ), + ), + ); + } + + Widget _buildScanOverlay(bool isDark) { + final size = MediaQuery.of(context).size; + final scanSize = size.width * 0.65; + final topOffset = (size.height - scanSize) / 2 - 60; + final leftOffset = (size.width - scanSize) / 2; + + return Stack( + children: [ + ColorFiltered( + colorFilter: ColorFilter.mode( + const Color(0xFF000000).withValues(alpha: 0.5), + BlendMode.srcOver, + ), + child: Container(), + ), + Positioned( + top: topOffset, + left: leftOffset, + child: CustomPaint( + size: Size(scanSize, scanSize), + painter: _ScanCornerPainter( + color: DesignTokens.primary, + cornerLength: 24, + cornerWidth: 3, + ), + ), + ), + Positioned( + top: topOffset + scanSize + DesignTokens.space3, + left: 0, + right: 0, + child: Text( + '将二维码/条形码放入框内自动扫描', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: CupertinoColors.white.withValues(alpha: 0.8), + ), + ), + ), + ], + ); + } + + Widget _buildBottomControls(bool isDark) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildControlButton( + icon: CupertinoIcons.bolt_slash, + label: '闪光灯', + isDark: isDark, + onTap: () => _controller.toggleTorch(), + ), + _buildControlButton( + icon: CupertinoIcons.switch_camera, + label: '翻转(仅 Android 支持)', + isDark: isDark, + onTap: () => _controller.switchCamera(), + ), + ], + ), + ); + } + + Widget _buildControlButton({ + required IconData icon, + required String label, + required bool isDark, + VoidCallback? onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space3, + vertical: DesignTokens.space2, + ), + decoration: BoxDecoration( + color: CupertinoColors.white.withValues(alpha: 0.15), + borderRadius: DesignTokens.borderRadiusMd, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 20, color: CupertinoColors.white), + const SizedBox(width: DesignTokens.space1), + Text( + label, + style: const TextStyle( + fontSize: DesignTokens.fontXs, + color: CupertinoColors.white, + ), + ), + ], + ), + ), + ); + } + + Widget _buildErrorView(bool isDark, MobileScannerException error) { + final errorMsg = switch (error.errorCode) { + MobileScannerErrorCode.permissionDenied => '相机权限被拒绝,请在系统设置中开启', + MobileScannerErrorCode.unsupported => '当前设备不支持扫码功能', + MobileScannerErrorCode.controllerAlreadyInitialized => '扫码控制器初始化异常', + _ => '相机启动失败: ${error.errorCode.name}', + }; + + return Center( + child: Padding( + padding: const EdgeInsets.all(DesignTokens.space5), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('📷', style: TextStyle(fontSize: 56)), + const SizedBox(height: DesignTokens.space4), + Text( + errorMsg, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: DesignTokens.fontMd, + color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, + ), + ), + const SizedBox(height: DesignTokens.space4), + CupertinoButton( + onPressed: () => _startScanner(), + child: const Text('重试'), + ), + ], + ), + ), + ); + } + + Widget _buildUnsupportedView(bool isDark) { + return Center( + child: Padding( + padding: const EdgeInsets.all(DesignTokens.space5), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('💻', style: TextStyle(fontSize: 56)), + const SizedBox(height: DesignTokens.space4), + Text( + '当前平台暂不支持相机扫码', + style: TextStyle( + fontSize: DesignTokens.fontLg, + fontWeight: FontWeight.w600, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), + const SizedBox(height: DesignTokens.space2), + Text( + '扫码功能支持 iOS、Android、macOS 和鸿蒙平台\nWindows / Linux 暂不支持相机扫码', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + const SizedBox(height: DesignTokens.space4), + CupertinoButton( + onPressed: () => Get.back(), + child: const Text('返回'), + ), + ], + ), + ), + ); + } +} + +class _ScanCornerPainter extends CustomPainter { + final Color color; + final double cornerLength; + final double cornerWidth; + + const _ScanCornerPainter({ + required this.color, + required this.cornerLength, + required this.cornerWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = cornerWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final w = size.width; + final h = size.height; + final cl = cornerLength; + + canvas.drawLine(Offset(0, cl), Offset.zero, paint); + canvas.drawLine(Offset.zero, Offset(cl, 0), paint); + + canvas.drawLine(Offset(w - cl, 0), Offset(w, 0), paint); + canvas.drawLine(Offset(w, 0), Offset(w, cl), paint); + + canvas.drawLine(Offset(w, h - cl), Offset(w, h), paint); + canvas.drawLine(Offset(w, h), Offset(w - cl, h), paint); + + canvas.drawLine(Offset(cl, h), Offset(0, h), paint); + canvas.drawLine(Offset(0, h - cl), Offset(0, h), paint); + } + + @override + bool shouldRepaint(covariant _ScanCornerPainter oldDelegate) => + color != oldDelegate.color || + cornerLength != oldDelegate.cornerLength || + cornerWidth != oldDelegate.cornerWidth; +} diff --git a/lib/src/pages/profile/info/privacy_policy_page.dart b/lib/src/pages/profile/info/privacy_policy_page.dart index d7e86c8..625aabd 100644 --- a/lib/src/pages/profile/info/privacy_policy_page.dart +++ b/lib/src/pages/profile/info/privacy_policy_page.dart @@ -3,7 +3,7 @@ * 名称: 隐私政策与用户协议页面 * 作用: 展示应用隐私政策和用户服务协议,支持Tab切换 * 创建: 2026-04-17 - * 更新: 2026-04-17 新增隐私政策与用户协议页面 + * 更新: 2026-04-22 新增相机权限说明 */ import 'package:flutter/cupertino.dart'; @@ -36,7 +36,7 @@ class PrivacyPolicyContent extends StatelessWidget { _buildSectionTitle('1. 我们如何收集和使用您的个人信息'), const SizedBox(height: DesignTokens.space4), _buildParagraph( - '我们仅在有合法性基础的情形下才会使用您的个人信息。根据适用的法律,我们可能会基于您的同意、为履行/订立您与我们的合同所必需、履行法定义务所必需等合法性基础,使用您的个人信息。', + '我们仅在有合法性基础的情形下才会使用您的个人信息。根据适用的法律,我们可能会基于您的同意、为履行订立您与我们的合同所必需、履行法定义务所必需等合法性基础,使用您的个人信息。', ), const SizedBox(height: DesignTokens.space4), _buildSubSectionTitle('1.1 基于履行法定义务或其他法律法规规定的情形'), @@ -51,11 +51,11 @@ class PrivacyPolicyContent extends StatelessWidget { const SizedBox(height: DesignTokens.space5), _buildSectionTitle('2. 设备权限调用'), const SizedBox(height: DesignTokens.space4), - _buildPermissionItem('📱 存储权限', '用于保存和读取您的菜谱收藏、笔记等本地数据'), - _buildPermissionItem('🌐 网络权限', '用于获取菜谱内容、营养数据和更新应用信息'), - _buildPermissionItem('🔔 通知权限', '用于在用餐提醒、烹饪计时等场景发送通知'), - _buildPermissionItem('📷 相机权限', '用于扫描二维码、拍摄菜品照片等功能'), - _buildPermissionItem('🔗 分享能力', '调用系统分享功能,分享菜谱、购物清单等数据'), + _buildPermissionItem('📱 震动权限', '用于在执行操作时提供反馈提示'), + _buildPermissionItem('📷 相机权限', '用于扫描二维码和条形码,识别菜谱信息'), + _buildPermissionItem('🌐 剪切板权限', '用于获取菜谱内容等数据的复制权限'), + _buildPermissionItem('🔔 唯一设备标识', '用于识别设备信息,区分设备型号,bug信息纠源'), + _buildPermissionItem('🏅 分享能力', '调用系统分享功能,分享菜谱、购物清单等数据'), _buildPermissionItem('🔗 文件读写', '调用系统安全文件接口,管理导出导入等文件'), const SizedBox(height: DesignTokens.space5), @@ -363,7 +363,7 @@ class UserAgreementContent extends StatelessWidget { const SizedBox(height: DesignTokens.space4), _buildContactInfo('👤 开发者', '弥勒市朋普镇微风暴网络科技工作室'), _buildContactInfo('📱 应用名称', '小妈厨房'), - _buildContactInfo('📧 联系邮箱', 'support@momkitchen.app'), + _buildContactInfo('📧 联系邮箱', '2821981550@qq.com'), const SizedBox(height: DesignTokens.space5), _buildBottomIndicator(), ], diff --git a/lib/src/pages/profile/profile_home.dart b/lib/src/pages/profile/profile_home.dart index c3b2d06..3ec5e36 100644 --- a/lib/src/pages/profile/profile_home.dart +++ b/lib/src/pages/profile/profile_home.dart @@ -5,6 +5,7 @@ * 更新: 2026-04-09 重构为 Liquid Glass 风格,统一使用 DesignTokens * 更新: 2026-04-12 添加滚动物理特性,支持上下滑动 * 更新: 2026-04-21 用户卡片改为开发计划卡片,点击弹出路线图 Sheet + * 更新: 2026-04-22 开发计划卡片移至设置页,首页改为美食年轮入口卡片 */ import 'package:flutter/cupertino.dart'; @@ -30,7 +31,7 @@ class ProfileHomeTab extends StatelessWidget { ), child: Column( children: [ - _buildDevPlanCard(isDark), + _buildFoodTimelineCard(isDark), const SizedBox(height: DesignTokens.space4), _buildFeatureGrid(isDark), const SizedBox(height: DesignTokens.space4), @@ -151,9 +152,9 @@ class ProfileHomeTab extends StatelessWidget { ); } - Widget _buildDevPlanCard(bool isDark) { + Widget _buildFoodTimelineCard(bool isDark) { return GestureDetector( - onTap: () => _showDevPlanSheet(isDark), + onTap: () => Get.toNamed(AppRoutes.foodTimeline), behavior: HitTestBehavior.opaque, child: Container( padding: const EdgeInsets.all(DesignTokens.space4), @@ -169,7 +170,7 @@ class ProfileHomeTab extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '开发计划', + '🌟 美食年轮', style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w700, @@ -180,7 +181,7 @@ class ProfileHomeTab extends StatelessWidget { ), const SizedBox(height: DesignTokens.space1), Text( - '查看即将推出的功能与路线图', + '查看你的美食旅程', style: TextStyle( fontSize: DesignTokens.fontSm, color: isDark @@ -202,207 +203,6 @@ class ProfileHomeTab extends StatelessWidget { ); } - void _showDevPlanSheet(bool isDark) { - final plans = <_DevPlanItem>[ - _DevPlanItem('📱', '多端适配', '即将支持平板端与 Web 端,一套代码多端运行', _PlanStatus.coming), - _DevPlanItem( - '👤', - '用户中心', - '即将支持登录功能,后续需付费激活注册,大部分功能无需登录即可正常使用', - _PlanStatus.coming, - ), - _DevPlanItem('🛠️', '工具中心', '支持导入自定义工具,打造专属厨房工具箱', _PlanStatus.dev), - _DevPlanItem( - '📄', - '导出功能', - '菜品详情信息支持导出 PDF / Docs 文档,方便分享与打印', - _PlanStatus.dev, - ), - _DevPlanItem('✍️', '菜品投稿', '开发菜品投稿功能,分享你的独家食谱', _PlanStatus.dev), - _DevPlanItem('💬', '交流反馈', '点赞、关注,讨论,与美食爱好者交流互动', _PlanStatus.plan), - _DevPlanItem('🧠', '智能推荐', '基于口味偏好与历史记录,智能推荐你可能喜欢的菜品', _PlanStatus.plan), - _DevPlanItem('🥬', '食材管理', '食材库存追踪与保质期提醒,减少浪费合理规划', _PlanStatus.plan), - _DevPlanItem('📊', '营养分析', '菜品营养成分详细分析,热量/蛋白质/碳水一目了然', _PlanStatus.plan), - _DevPlanItem('🌍', '多语言支持', '支持英文等多语言界面,让美食无国界', _PlanStatus.plan), - _DevPlanItem('☁️', '云同步', '跨设备数据同步,收藏与偏好随身携带', _PlanStatus.plan), - _DevPlanItem('🔔', '消息通知', '新菜谱推送、互动提醒,不错过任何精彩内容', _PlanStatus.plan), - ]; - - showCupertinoModalPopup( - context: Get.context!, - builder: (context) { - final sheetDark = - CupertinoTheme.brightnessOf(context) == Brightness.dark; - return Container( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.75, - ), - decoration: BoxDecoration( - color: sheetDark - ? DarkDesignTokens.background - : DesignTokens.background, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(DesignTokens.radiusLg), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: DesignTokens.space3, - ), - child: Container( - width: 36, - height: 5, - decoration: BoxDecoration( - color: sheetDark - ? DarkDesignTokens.text3 - : DesignTokens.text3, - borderRadius: DesignTokens.borderRadiusFull, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - ), - child: Row( - children: [ - const Text('🚀', style: TextStyle(fontSize: 22)), - const SizedBox(width: DesignTokens.space2), - Text( - '开发计划', - style: TextStyle( - fontSize: DesignTokens.fontLg, - fontWeight: FontWeight.w700, - color: sheetDark - ? DarkDesignTokens.text1 - : DesignTokens.text1, - ), - ), - const Spacer(), - CupertinoButton( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - onPressed: () => Navigator.of(context).pop(), - child: Text( - '关闭', - style: TextStyle( - fontSize: DesignTokens.fontSm, - color: DesignTokens.dynamicPrimary, - ), - ), - ), - ], - ), - ), - const SizedBox(height: DesignTokens.space2), - Flexible( - child: ListView.separated( - shrinkWrap: true, - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - vertical: DesignTokens.space2, - ), - itemCount: plans.length, - separatorBuilder: (_, _) => - const SizedBox(height: DesignTokens.space2), - itemBuilder: (context, index) { - final plan = plans[index]; - return _buildDevPlanItem(plan, sheetDark); - }, - ), - ), - SizedBox( - height: - MediaQuery.of(context).padding.bottom + DesignTokens.space3, - ), - ], - ), - ); - }, - ); - } - - Widget _buildDevPlanItem(_DevPlanItem plan, bool isDark) { - final statusColor = switch (plan.status) { - _PlanStatus.coming => DesignTokens.green, - _PlanStatus.dev => DesignTokens.orange, - _PlanStatus.plan => DesignTokens.purple, - }; - final statusLabel = switch (plan.status) { - _PlanStatus.coming => '即将上线', - _PlanStatus.dev => '开发中', - _PlanStatus.plan => '规划中', - }; - - return Container( - padding: const EdgeInsets.all(DesignTokens.space3), - decoration: BoxDecoration( - color: isDark ? DarkDesignTokens.card : DesignTokens.card, - borderRadius: DesignTokens.borderRadiusMd, - boxShadow: DesignTokens.shadowsSm, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(plan.emoji, style: const TextStyle(fontSize: 24)), - const SizedBox(width: DesignTokens.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - plan.title, - style: TextStyle( - fontSize: DesignTokens.fontMd, - fontWeight: FontWeight.w600, - color: isDark - ? DarkDesignTokens.text1 - : DesignTokens.text1, - ), - ), - const SizedBox(width: DesignTokens.space2), - Container( - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space2, - vertical: 2, - ), - decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.12), - borderRadius: DesignTokens.borderRadiusSm, - ), - child: Text( - statusLabel, - style: TextStyle( - fontSize: DesignTokens.fontXs, - fontWeight: FontWeight.w500, - color: statusColor, - ), - ), - ), - ], - ), - const SizedBox(height: DesignTokens.space1), - Text( - plan.description, - style: TextStyle( - fontSize: DesignTokens.fontSm, - color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, - height: 1.4, - ), - ), - ], - ), - ), - ], - ), - ); - } - Widget _buildFeatureGrid(bool isDark) { final items = [ _FeatureItem( @@ -628,14 +428,3 @@ class _FeatureItem { const _FeatureItem(this.icon, this.label, this.color, this.route); } - -enum _PlanStatus { coming, dev, plan } - -class _DevPlanItem { - final String emoji; - final String title; - final String description; - final _PlanStatus status; - - const _DevPlanItem(this.emoji, this.title, this.description, this.status); -} diff --git a/lib/src/pages/profile/profile_settings.dart b/lib/src/pages/profile/profile_settings.dart index cc08a49..285942f 100644 --- a/lib/src/pages/profile/profile_settings.dart +++ b/lib/src/pages/profile/profile_settings.dart @@ -6,6 +6,7 @@ * 更新: 2026-04-09 移除重复的主题设置入口,保留个性化设置 * 更新: 2026-04-12 添加足迹分组(笔记/浏览记录) * 更新: 2026-04-13 足迹分组新增缓存管理入口 + * 更新: 2026-04-22 美食年轮移至首页,新增开发计划 section(含路线图 Sheet) */ import 'package:flutter/cupertino.dart'; @@ -18,7 +19,6 @@ import 'package:mom_kitchen/src/pages/tools/cooking/cooking_note_page.dart'; import 'package:mom_kitchen/src/pages/profile/social/footprints_page.dart'; import 'package:mom_kitchen/src/pages/profile/data/cache_manage_page.dart'; import 'package:mom_kitchen/src/pages/profile/tools/data_export_page.dart'; -import 'package:mom_kitchen/src/config/app_routes.dart'; class ProfileSettingsTab extends StatelessWidget { const ProfileSettingsTab({super.key}); @@ -118,14 +118,14 @@ class ProfileSettingsTab extends StatelessWidget { ), const SizedBox(height: DesignTokens.space3), _buildSection( - title: '🌟 美食年轮', + title: '🚀 开发计划', isDark: isDark, children: [ _buildTile( - icon: CupertinoIcons.circle_grid_3x3, - title: '查看你的美食旅程', + icon: CupertinoIcons.rocket, + title: '查看即将推出的功能与路线图', isDark: isDark, - onTap: () => Get.toNamed(AppRoutes.foodTimeline), + onTap: () => _showDevPlanSheet(context, isDark), ), ], ), @@ -283,6 +283,205 @@ class ProfileSettingsTab extends StatelessWidget { ); } + void _showDevPlanSheet(BuildContext context, bool isDark) { + final plans = <_DevPlanItem>[ + _DevPlanItem('📱', '多端适配', '即将支持平板端与 Web 端,一套代码多端运行', _PlanStatus.coming), + _DevPlanItem( + '👤', + '用户中心', + '即将支持登录功能,后续需付费激活注册,大部分功能无需登录即可正常使用', + _PlanStatus.coming, + ), + _DevPlanItem('🛠️', '工具中心', '支持导入自定义工具,打造专属厨房工具箱', _PlanStatus.dev), + _DevPlanItem( + '📄', + '导出功能', + '菜品详情信息支持导出 PDF / Docs 文档,方便分享与打印', + _PlanStatus.dev, + ), + _DevPlanItem('✍️', '菜品投稿', '开发菜品投稿功能,分享你的独家食谱', _PlanStatus.dev), + _DevPlanItem('💬', '交流反馈', '点赞、关注,讨论,与美食爱好者交流互动', _PlanStatus.plan), + _DevPlanItem('🧠', '智能推荐', '基于口味偏好与历史记录,智能推荐你可能喜欢的菜品', _PlanStatus.plan), + _DevPlanItem('🥬', '食材管理', '食材库存追踪与保质期提醒,减少浪费合理规划', _PlanStatus.plan), + _DevPlanItem('📊', '营养分析', '菜品营养成分详细分析,热量/蛋白质/碳水一目了然', _PlanStatus.plan), + _DevPlanItem('🌍', '多语言支持', '支持英文等多语言界面,让美食无国界', _PlanStatus.plan), + _DevPlanItem('☁️', '云同步', '跨设备数据同步,收藏与偏好随身携带', _PlanStatus.plan), + _DevPlanItem('🔔', '消息通知', '新菜谱推送、互动提醒,不错过任何精彩内容', _PlanStatus.plan), + ]; + + showCupertinoModalPopup( + context: context, + builder: (ctx) { + final sheetDark = CupertinoTheme.brightnessOf(ctx) == Brightness.dark; + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(ctx).size.height * 0.75, + ), + decoration: BoxDecoration( + color: sheetDark + ? DarkDesignTokens.background + : DesignTokens.background, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(DesignTokens.radiusLg), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: DesignTokens.space3, + ), + child: Container( + width: 36, + height: 5, + decoration: BoxDecoration( + color: sheetDark + ? DarkDesignTokens.text3 + : DesignTokens.text3, + borderRadius: DesignTokens.borderRadiusFull, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + ), + child: Row( + children: [ + const Text('🚀', style: TextStyle(fontSize: 22)), + const SizedBox(width: DesignTokens.space2), + Text( + '开发计划', + style: TextStyle( + fontSize: DesignTokens.fontLg, + fontWeight: FontWeight.w700, + color: sheetDark + ? DarkDesignTokens.text1 + : DesignTokens.text1, + ), + ), + const Spacer(), + CupertinoButton( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + onPressed: () => Navigator.of(ctx).pop(), + child: Text( + '关闭', + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: DesignTokens.dynamicPrimary, + ), + ), + ), + ], + ), + ), + const SizedBox(height: DesignTokens.space2), + Flexible( + child: ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space2, + ), + itemCount: plans.length, + separatorBuilder: (_, __) => + const SizedBox(height: DesignTokens.space2), + itemBuilder: (context, index) { + final plan = plans[index]; + return _buildDevPlanItem(plan, sheetDark); + }, + ), + ), + SizedBox( + height: MediaQuery.of(ctx).padding.bottom + DesignTokens.space3, + ), + ], + ), + ); + }, + ); + } + + Widget _buildDevPlanItem(_DevPlanItem plan, bool isDark) { + final statusColor = switch (plan.status) { + _PlanStatus.coming => DesignTokens.green, + _PlanStatus.dev => DesignTokens.orange, + _PlanStatus.plan => DesignTokens.purple, + }; + final statusLabel = switch (plan.status) { + _PlanStatus.coming => '即将上线', + _PlanStatus.dev => '开发中', + _PlanStatus.plan => '规划中', + }; + + return Container( + padding: const EdgeInsets.all(DesignTokens.space3), + decoration: BoxDecoration( + color: isDark ? DarkDesignTokens.card : DesignTokens.card, + borderRadius: DesignTokens.borderRadiusMd, + boxShadow: DesignTokens.shadowsSm, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(plan.emoji, style: const TextStyle(fontSize: 24)), + const SizedBox(width: DesignTokens.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + plan.title, + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w600, + color: isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1, + ), + ), + const SizedBox(width: DesignTokens.space2), + Container( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space2, + vertical: 2, + ), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.12), + borderRadius: DesignTokens.borderRadiusSm, + ), + child: Text( + statusLabel, + style: TextStyle( + fontSize: DesignTokens.fontXs, + fontWeight: FontWeight.w500, + color: statusColor, + ), + ), + ), + ], + ), + const SizedBox(height: DesignTokens.space1), + Text( + plan.description, + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + height: 1.4, + ), + ), + ], + ), + ), + ], + ), + ); + } + void _showKnownIssuesSheet(BuildContext context, bool isDark) { final issues = [ _KnownIssue( @@ -295,7 +494,7 @@ class ProfileSettingsTab extends StatelessWidget { icon: '🧊', title: 'Win10/11端 页面返回问题', level: _IssueLevel.warning, - description: '部分页面未设置返回逻辑,需使用快捷键 同时按下 ALT+方向左键/Backspace(删除键) 返回上一页。', + description: '部分页面未设置返回逻辑,需使用快捷键 同时按下 ALT+方向左键/Escape(Esc键) 返回上一页。', ), _KnownIssue( icon: '📂', @@ -315,7 +514,7 @@ class ProfileSettingsTab extends StatelessWidget { title: '液态玻璃效果 GPU 占用较高', level: _IssueLevel.warning, description: - 'iOS 26 液态玻璃(Liquid Glass)效果对 GPU 要求较高,长时间使用可能出现轻微发热。低性能设备建议在设置中关闭毛玻璃效果。', + '液态玻璃(Liquid Glass)效果对 GPU 要求较高,长时间使用可能出现轻微发热。低性能设备建议在设置中关闭毛玻璃效果。', ), _KnownIssue( icon: '🚧', @@ -325,7 +524,7 @@ class ProfileSettingsTab extends StatelessWidget { ), _KnownIssue( icon: '📱', - title: '部分页面横屏适配未完成', + title: '大部分页面横屏适配未完成', level: _IssueLevel.info, description: '当前主要针对竖屏优化,横屏或折叠屏展开状态下部分页面布局可能不够理想,后续版本将逐步适配。', ), @@ -546,3 +745,14 @@ class _KnownIssue { required this.description, }); } + +enum _PlanStatus { coming, dev, plan } + +class _DevPlanItem { + final String emoji; + final String title; + final String description; + final _PlanStatus status; + + const _DevPlanItem(this.emoji, this.title, this.description, this.status); +} diff --git a/lib/src/pages/profile/settings/personalization_page.dart b/lib/src/pages/profile/settings/personalization_page.dart index df987fa..f39e0de 100644 --- a/lib/src/pages/profile/settings/personalization_page.dart +++ b/lib/src/pages/profile/settings/personalization_page.dart @@ -41,6 +41,12 @@ class PersonalizationPage extends StatelessWidget { header: const Text('📝 字体大小'), children: [_buildFontSizeItem(controller, themeService)], ), + CupertinoListSection.insetGrouped( + header: const Text('🔤 字体风格'), + children: [ + _buildFontFamilyItem(controller, themeService), + ], + ), CupertinoListSection.insetGrouped( header: const Text('🌙 显示模式'), children: [ @@ -324,6 +330,47 @@ class PersonalizationPage extends StatelessWidget { ); } + Widget _buildFontFamilyItem( + PersonalizationController controller, + ThemeService themeService, + ) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + SizedBox( + width: double.infinity, + child: CupertinoSegmentedControl( + groupValue: themeService.fontFamilyStyle.value, + onValueChanged: (value) => controller.setFontFamilyStyle(value), + children: const { + FontFamilyStyle.system: Padding( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text('🖥 系统默认'), + ), + FontFamilyStyle.notoSansSC: Padding( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text('📝 NotoSansSC'), + ), + }, + ), + ), + const SizedBox(height: 8), + Text( + themeService.fontFamilyStyle.value == FontFamilyStyle.notoSansSC + ? '不使用系统字体,跨平台中文显示一致' + : '更换字体需重启软件以生效', + //TODO: 实现字体下载和安装 + style: TextStyle( + fontSize: 13, + color: themeService.textColor.value.withValues(alpha: 0.5), + ), + ), + ], + ), + ); + } + Widget _buildAnimationItem( PersonalizationController controller, ThemeService themeService, diff --git a/lib/src/pages/profile/social/chat_page.dart b/lib/src/pages/profile/social/chat_page.dart index 1eb819c..37354ce 100644 --- a/lib/src/pages/profile/social/chat_page.dart +++ b/lib/src/pages/profile/social/chat_page.dart @@ -137,7 +137,7 @@ class _FeedbackPageState extends State { try { // 尝试获取应用版本信息 - appVersion = '1.3.0'; // 从pubspec.yaml读取或使用PackageInfo + appVersion = '1.3.2fail'; // 从pubspec.yaml读取或使用PackageInfo } catch (e) { debugPrint('获取版本号失败: $e'); } diff --git a/lib/src/pages/profile/social/favorites_page.dart b/lib/src/pages/profile/social/favorites_page.dart index 637ff4c..66c8f01 100644 --- a/lib/src/pages/profile/social/favorites_page.dart +++ b/lib/src/pages/profile/social/favorites_page.dart @@ -996,7 +996,7 @@ class _FavoritesPageState extends State CupertinoActionSheetAction( onPressed: () { Navigator.pop(context); - ToastService.show(message: 'JSON 导出功能开发中'); + ToastService.show(message: '请前往 数据导出 页面'); }, child: const Row( mainAxisAlignment: MainAxisAlignment.center, @@ -1010,7 +1010,7 @@ class _FavoritesPageState extends State CupertinoActionSheetAction( onPressed: () { Navigator.pop(context); - ToastService.show(message: 'CSV 导出功能开发中'); + ToastService.show(message: '请前往 数据导出 页面'); }, child: const Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/pages/profile/tools/permission_page.dart b/lib/src/pages/profile/tools/permission_page.dart index 1d0e120..68fbda3 100644 --- a/lib/src/pages/profile/tools/permission_page.dart +++ b/lib/src/pages/profile/tools/permission_page.dart @@ -1,18 +1,45 @@ /* * 文件: permission_page.dart * 名称: 软件权限页面 - * 作用: 展示应用所需权限说明和隐私信息 + * 作用: 展示应用所需权限说明和隐私信息,相机权限动态显示状态 * 创建: 2026-04-17 - * 更新: 2026-04-17 初始创建,参考情景诗词权限页面 + * 更新: 2026-04-22 新增相机权限,动态显示权限状态 */ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show Divider; import 'package:mom_kitchen/src/config/design_tokens.dart'; +import 'package:permission_handler/permission_handler.dart'; -class PermissionPage extends StatelessWidget { +class PermissionPage extends StatefulWidget { const PermissionPage({super.key}); + @override + State createState() => _PermissionPageState(); +} + +class _PermissionPageState extends State { + PermissionStatus _cameraStatus = PermissionStatus.denied; + + @override + void initState() { + super.initState(); + _checkCameraPermission(); + } + + Future _checkCameraPermission() async { + final status = await Permission.camera.status; + if (mounted) { + setState(() => _cameraStatus = status); + } + } + + bool get _isCameraGranted => + _cameraStatus.isGranted || _cameraStatus.isLimited; + + bool get _isCameraDenied => + _cameraStatus.isDenied || _cameraStatus.isPermanentlyDenied; + @override Widget build(BuildContext context) { final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; @@ -117,42 +144,161 @@ class PermissionPage extends StatelessWidget { } List _buildPermissionList(bool isDark) { - final permissions = [ - {'icon': CupertinoIcons.wifi, 'title': '完全网络访问', 'desc': '获取菜谱内容和数据'}, - { - 'icon': CupertinoIcons.device_phone_portrait, - 'title': '震动反馈', - 'desc': '操作时的震动反馈,提升交互体验', - }, - {'icon': CupertinoIcons.doc_on_doc, 'title': '剪切板', 'desc': '复制菜谱内容到剪切板'}, - // {'icon': CupertinoIcons.volume_up, 'title': '播放声音', 'desc': '播放内置提示音'}, - {'icon': CupertinoIcons.share, 'title': '分享能力', 'desc': '调用系统分享接口'}, - { - 'icon': CupertinoIcons.device_phone_portrait, - 'title': '设备标识', - 'desc': '获取设备唯一标识', - }, - {'icon': CupertinoIcons.folder, 'title': '安全文件访问', 'desc': '读写用户文件'}, + return [ + _buildCameraPermissionItem(isDark), + _buildPermissionItem( + CupertinoIcons.wifi, + '完全网络访问', + '获取菜谱内容和数据', + isDark, + granted: true, + ), + _buildPermissionItem( + CupertinoIcons.device_phone_portrait, + '震动反馈', + '操作时的震动反馈,提升交互体验', + isDark, + granted: true, + ), + _buildPermissionItem( + CupertinoIcons.doc_on_doc, + '剪切板', + '复制菜谱内容到剪切板', + isDark, + granted: true, + ), + _buildPermissionItem( + CupertinoIcons.share, + '分享能力', + '调用系统分享接口', + isDark, + granted: true, + ), + _buildPermissionItem( + CupertinoIcons.device_phone_portrait, + '设备标识', + '获取设备唯一标识', + isDark, + granted: true, + ), + _buildPermissionItem( + CupertinoIcons.folder, + '安全文件访问', + '读写用户文件', + isDark, + granted: true, + ), ]; + } - return permissions - .map( - (p) => _buildPermissionItem( - p['icon'] as IconData, - p['title'] as String, - p['desc'] as String, - isDark, + Widget _buildCameraPermissionItem(bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space4, + vertical: DesignTokens.space3, + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1), + borderRadius: DesignTokens.borderRadiusSm, + ), + child: Icon( + CupertinoIcons.camera, + color: DesignTokens.dynamicPrimary, + size: 20, + ), ), - ) - .toList(); + const SizedBox(width: DesignTokens.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '相机权限', + style: TextStyle( + fontSize: DesignTokens.fontMd, + fontWeight: FontWeight.w500, + color: isDark + ? DarkDesignTokens.text1 + : DesignTokens.text1, + ), + ), + const SizedBox(width: DesignTokens.space2), + _buildStatusBadge(isDark), + ], + ), + const SizedBox(height: 2), + Text( + '扫描二维码和条形码,识别菜谱信息', + style: TextStyle( + fontSize: DesignTokens.fontSm, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ], + ), + ), + GestureDetector( + onTap: _isCameraDenied ? _openAppSettings : null, + child: Icon( + _isCameraGranted + ? CupertinoIcons.checkmark_shield + : CupertinoIcons.exclamationmark_shield, + color: _isCameraGranted + ? DesignTokens.dynamicPrimary + : CupertinoColors.systemRed, + size: 20, + ), + ), + ], + ), + ); + } + + Widget _buildStatusBadge(bool isDark) { + final granted = _isCameraGranted; + final text = granted ? '已开启' : '已拒绝'; + final bgColor = granted + ? DesignTokens.dynamicPrimary.withValues(alpha: 0.15) + : CupertinoColors.systemRed.withValues(alpha: 0.15); + final textColor = granted + ? DesignTokens.dynamicPrimary + : CupertinoColors.systemRed; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: bgColor, + borderRadius: DesignTokens.borderRadiusSm, + ), + child: Text( + text, + style: TextStyle( + fontSize: DesignTokens.fontXs, + fontWeight: FontWeight.w500, + color: textColor, + ), + ), + ); + } + + Future _openAppSettings() async { + await openAppSettings(); } Widget _buildPermissionItem( IconData icon, String title, String description, - bool isDark, - ) { + bool isDark, { + bool granted = true, + }) { return Padding( padding: const EdgeInsets.symmetric( horizontal: DesignTokens.space4, @@ -193,18 +339,17 @@ class PermissionPage extends StatelessWidget { vertical: 2, ), decoration: BoxDecoration( - color: isDark - ? DarkDesignTokens.text3.withValues(alpha: 0.2) - : DesignTokens.text3.withValues(alpha: 0.15), + color: DesignTokens.dynamicPrimary.withValues( + alpha: 0.15, + ), borderRadius: DesignTokens.borderRadiusSm, ), child: Text( '已开启', style: TextStyle( fontSize: DesignTokens.fontXs, - color: isDark - ? DarkDesignTokens.text2 - : DesignTokens.text2, + fontWeight: FontWeight.w500, + color: DesignTokens.dynamicPrimary, ), ), ), @@ -222,8 +367,8 @@ class PermissionPage extends StatelessWidget { ), ), Icon( - CupertinoIcons.lock, - color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + CupertinoIcons.checkmark_shield, + color: DesignTokens.dynamicPrimary, size: 18, ), ], @@ -233,10 +378,10 @@ class PermissionPage extends StatelessWidget { List _buildPermissionDescriptions(bool isDark) { final descriptions = [ + {'title': '相机权限', 'desc': '用于扫描菜谱二维码和商品条形码,快速打开菜品详情。拒绝后可在系统设置中重新开启。'}, {'title': '完全网络访问', 'desc': '用于获取菜谱内容、食材数据、营养信息等。'}, {'title': '震动反馈', 'desc': '用于点赞、收藏等操作的触觉反馈,提升用户体验。'}, {'title': '剪切板', 'desc': '只有写入权限,没有读取权限,方便用户分享和记录菜谱。'}, - // {'title': '播放声音', 'desc': '用于操作提示音,提升用户体验。'}, {'title': '分享能力', 'desc': '用于分享菜谱内容到社交媒体平台。'}, {'title': '设备标识', 'desc': '用于唯一标识设备,确保用户数据安全。'}, {'title': '安全文件访问', 'desc': '用于读写文件,管理用户导入/导出的文件。'}, @@ -432,20 +577,46 @@ class PermissionPage extends StatelessWidget { color: DesignTokens.dynamicPrimary, ), const SizedBox(width: DesignTokens.space2), - const Text('基础权限说明'), + const Text('权限说明'), ], ), content: const Padding( padding: EdgeInsets.only(top: DesignTokens.space3), - child: Text( - '系统赋予的基础软件权限无法拒绝;自带权限默认开启,用户无需动态授权,系统层关闭后,将无法正常使用应用。', - style: TextStyle(fontSize: DesignTokens.fontMd, height: 1.5), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '注意:隐私安全考虑,你每次扫码时,我们会告知你是否需要开启相机\n(该提示来自*小妈厨房内部,非系统弹窗)', + style: TextStyle(fontSize: DesignTokens.fontMd, height: 1.5), + ), + + SizedBox(height: DesignTokens.space2), + + Text( + '📷 相机权限为动态授权权限,您可以选择允许或拒绝:', + style: TextStyle(fontSize: DesignTokens.fontSm, height: 1.5), + ), + Text( + '• 允许:可使用扫码功能\n• 拒绝:扫码功能不可用\n• 恢复:前往系统设置 → 应用 → 小妈厨房 → 权限 → 开启相机', + style: TextStyle(fontSize: DesignTokens.fontSm, height: 1.6), + ), + SizedBox(height: DesignTokens.space3), + Text( + '其余权限为系统基础权限,默认开启,系统层关闭后将无法正常使用应用。', + style: TextStyle(fontSize: DesignTokens.fontSm, height: 1.5), + ), + Text( + '(实验)已拒绝的权限,点击权限图标可前往系统设置手动开启', + style: TextStyle(fontSize: DesignTokens.fontSm, height: 1.5), + ), + ], ), ), actions: [ CupertinoDialogAction( onPressed: () => Navigator.pop(context), - child: const Text('确定'), + child: const Text('我知道了'), ), ], ), diff --git a/lib/src/pages/tools/cooking/calculator/serving_scaler_page.dart b/lib/src/pages/tools/cooking/calculator/serving_scaler_page.dart index def4965..0c876e2 100644 --- a/lib/src/pages/tools/cooking/calculator/serving_scaler_page.dart +++ b/lib/src/pages/tools/cooking/calculator/serving_scaler_page.dart @@ -127,7 +127,6 @@ class _ServingScalerPageState extends State { border: null, ), child: SafeArea( - top: false, child: Column( children: [ const SizedBox(height: DesignTokens.space3), diff --git a/lib/src/pages/tools/health/meal_time_recommend_page.dart b/lib/src/pages/tools/health/meal_time_recommend_page.dart index 2b3c6b1..99a35ed 100644 --- a/lib/src/pages/tools/health/meal_time_recommend_page.dart +++ b/lib/src/pages/tools/health/meal_time_recommend_page.dart @@ -194,7 +194,6 @@ class _MealTimeRecommendPageState extends State { border: null, ), child: SafeArea( - top: false, child: Column( children: [ _buildTimeSelector(isDark), @@ -209,7 +208,7 @@ class _MealTimeRecommendPageState extends State { Widget _buildTimeSelector(bool isDark) { if (_isLoading) { return const SizedBox( - height: 50, + height: 56, child: Center(child: CupertinoActivityIndicator(radius: 12)), ); } @@ -217,7 +216,7 @@ class _MealTimeRecommendPageState extends State { final currentMeal = _getCurrentMealTime(); return SizedBox( - height: 50, + height: 56, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), diff --git a/lib/src/pages/tools/ingredient_detail/ingredient_detail_page.dart b/lib/src/pages/tools/ingredient_detail/ingredient_detail_page.dart index 53f9aa0..7edb94d 100644 --- a/lib/src/pages/tools/ingredient_detail/ingredient_detail_page.dart +++ b/lib/src/pages/tools/ingredient_detail/ingredient_detail_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: ingredient_detail_page.dart * 名称: 食材详情查询页面 * 作用: 查询食材营养信息与选购指南 @@ -52,7 +52,6 @@ class IngredientDetailPage extends GetView { border: null, ), child: SafeArea( - top: false, child: Column( children: [ _buildSearchBar(isDark), diff --git a/lib/src/pages/tools/planning/weekly_menu_planner_page.dart b/lib/src/pages/tools/planning/weekly_menu_planner_page.dart index 821d56b..4e4044e 100644 --- a/lib/src/pages/tools/planning/weekly_menu_planner_page.dart +++ b/lib/src/pages/tools/planning/weekly_menu_planner_page.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: weekly_menu_planner_page.dart * 名称: 每周菜单规划页面 * 作用: 日历视图选择日期,每日三餐分配菜谱,自动生成购物清单 @@ -213,7 +213,7 @@ class _WeeklyMenuPlannerPageState extends State { final dayMenu = menu.dailyMenus[dateKey]; if (dayMenu == null) return const SizedBox(); - return Padding( + return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/src/services/api/api_service.dart b/lib/src/services/api/api_service.dart index 46aa6e5..3f959ae 100644 --- a/lib/src/services/api/api_service.dart +++ b/lib/src/services/api/api_service.dart @@ -137,16 +137,21 @@ class ApiService { } ApiService._internal() { + final defaultHeaders = { + 'Accept': 'application/json', + }; + if (!kIsWeb) { + // 非 Web 平台需要指定 Content-Type 为 application/json + defaultHeaders['Content-Type'] = 'application/json'; + } + final options = BaseOptions( baseUrl: kIsWeb ? '' : ApiConfig.baseUrl, connectTimeout: kIsWeb ? const Duration(seconds: 10) : const Duration(seconds: 2), receiveTimeout: const Duration(seconds: 8), - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, + headers: defaultHeaders, ); _dio = Dio(options); diff --git a/lib/src/services/core/app_info_service.dart b/lib/src/services/core/app_info_service.dart index 65573b5..e5b3aae 100644 --- a/lib/src/services/core/app_info_service.dart +++ b/lib/src/services/core/app_info_service.dart @@ -14,7 +14,7 @@ class AppInfoService { } // 获取应用名称 - String get appName => _packageInfo?.appName ?? 'Mom\'s Kitchen'; + String get appName => _packageInfo?.appName ?? 'Cute Kitchen'; // 获取包名 String get packageName => _packageInfo?.packageName ?? 'cute.major.kitchen'; diff --git a/lib/src/services/system/crash_guard_service.dart b/lib/src/services/system/crash_guard_service.dart index 5e3411a..f693ca6 100644 --- a/lib/src/services/system/crash_guard_service.dart +++ b/lib/src/services/system/crash_guard_service.dart @@ -164,6 +164,13 @@ class CupertinoDialogReportMode extends ReportMode { @override void requestAction(Report report, BuildContext? context) { + if (kReleaseMode) { + debugPrint( + '🐛 [CrashGuard] Release mode: skipping dialog for error: ${report.error}', + ); + onActionRejected(report); + return; + } if (_isLayoutOverflow(report)) { debugPrint( '⚠️ [CrashGuard] LayoutWarning (skipped dialog): ${report.error}', diff --git a/lib/src/services/ui/theme_service.dart b/lib/src/services/ui/theme_service.dart index 0fa991e..191f3db 100644 --- a/lib/src/services/ui/theme_service.dart +++ b/lib/src/services/ui/theme_service.dart @@ -17,6 +17,8 @@ enum CardScrollDirection { horizontal, vertical } enum DarkModeSource { system, manual } +enum FontFamilyStyle { system, notoSansSC } + class DynamicTokens { final bool isDark; @@ -72,6 +74,7 @@ class ThemeService extends GetxController { final RxDouble bottomBarTransparency = 0.72.obs; final Rx cardScrollDirection = CardScrollDirection.horizontal.obs; + final Rx fontFamilyStyle = FontFamilyStyle.notoSansSC.obs; late SharedPreferences _prefs; @@ -125,6 +128,8 @@ class ThemeService extends GetxController { _prefs.getDouble('bottom_bar_transparency') ?? 0.85; cardScrollDirection.value = CardScrollDirection.values[_prefs.getInt('card_scroll_direction') ?? 0]; + fontFamilyStyle.value = + FontFamilyStyle.values[_prefs.getInt('font_family_style') ?? 1]; } Future _saveTheme() async { @@ -151,6 +156,7 @@ class ThemeService extends GetxController { 'card_scroll_direction', cardScrollDirection.value.index, ); + await _prefs.setInt('font_family_style', fontFamilyStyle.value.index); } void updateSystemUI() { @@ -316,6 +322,11 @@ class ThemeService extends GetxController { await _saveTheme(); } + Future setFontFamilyStyle(FontFamilyStyle style) async { + fontFamilyStyle.value = style; + await _saveTheme(); + } + Future resetToDefault() async { isDarkMode.value = false; primaryColor.value = DesignTokens.primary; @@ -329,11 +340,15 @@ class ThemeService extends GetxController { bottomBarTransparency.value = 0.72; textColor.value = DesignTokens.text1; backgroundColor.value = DesignTokens.background; + fontFamilyStyle.value = FontFamilyStyle.notoSansSC; await _saveTheme(); updateSystemUI(); } ThemeData getThemeData() { + final _font = fontFamilyStyle.value == FontFamilyStyle.notoSansSC + ? 'NotoSansSC' + : null; return ThemeData( brightness: isDarkMode.value ? Brightness.dark : Brightness.light, primaryColor: primaryColor.value, @@ -341,35 +356,47 @@ class ThemeService extends GetxController { scaffoldBackgroundColor: backgroundColor.value, cardColor: isDarkMode.value ? DarkDesignTokens.card : DesignTokens.card, textTheme: TextTheme( - bodyLarge: TextStyle(fontSize: fontSize.value, color: textColor.value), + bodyLarge: TextStyle( + fontSize: fontSize.value, + color: textColor.value, + fontFamily: _font, + ), bodyMedium: TextStyle( fontSize: fontSize.value - 2, color: textColor.value, + fontFamily: _font, ), bodySmall: TextStyle( fontSize: fontSize.value - 4, color: textColor.value, + fontFamily: _font, ), titleLarge: TextStyle( fontSize: fontSize.value + 4, fontWeight: FontWeight.w700, color: textColor.value, + fontFamily: _font, ), titleMedium: TextStyle( fontSize: fontSize.value + 2, fontWeight: FontWeight.w600, color: textColor.value, + fontFamily: _font, ), titleSmall: TextStyle( fontSize: fontSize.value, fontWeight: FontWeight.w500, color: textColor.value, + fontFamily: _font, ), ), ); } CupertinoThemeData get cupertinoThemeData { + final _font = fontFamilyStyle.value == FontFamilyStyle.notoSansSC + ? 'NotoSansSC' + : null; return CupertinoThemeData( brightness: isDarkMode.value ? Brightness.dark : Brightness.light, primaryColor: primaryColor.value, @@ -381,12 +408,14 @@ class ThemeService extends GetxController { inherit: false, fontSize: fontSize.value, color: textColor.value, + fontFamily: _font, ), navTitleTextStyle: TextStyle( inherit: false, fontSize: fontSize.value + 2, fontWeight: FontWeight.w600, color: textColor.value, + fontFamily: _font, ), ), ); diff --git a/lib/src/widgets/discover/content/mini_card_discover_card.dart b/lib/src/widgets/discover/content/mini_card_discover_card.dart index bb71f62..f18de11 100644 --- a/lib/src/widgets/discover/content/mini_card_discover_card.dart +++ b/lib/src/widgets/discover/content/mini_card_discover_card.dart @@ -1,4 +1,4 @@ -/* +/* * 文件: mini_card_discover_card.dart * 名称: 瀑布流迷你卡片 * 作用: 首页瀑布流中嵌入的迷你菜品卡片,跨2列显示,点击跳转迷你卡片页 @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -53,33 +54,71 @@ class MiniCardDiscoverCard extends StatelessWidget { fit: StackFit.expand, children: [ // ─── 底层图片 ─── - CachedNetworkImage( - imageUrl: recipe.fullImageUrl, - fit: BoxFit.cover, - placeholder: (_, __) => Container( - color: DesignTokens.background, - child: const Center(child: CupertinoActivityIndicator()), - ), - errorWidget: (_, __, _) => Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - DesignTokens.orange.withValues(alpha: 0.4), - DesignTokens.red.withValues(alpha: 0.3), - ], + kIsWeb + ? Image.network( + recipe.fullImageUrl, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: DesignTokens.background, + child: const Center( + child: CupertinoActivityIndicator(), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + DesignTokens.orange.withValues(alpha: 0.4), + DesignTokens.red.withValues(alpha: 0.3), + ], + ), + ), + child: const Center( + child: Icon( + CupertinoIcons.photo, + size: 36, + color: DesignTokens.text3, + ), + ), + ); + }, + headers: {'Access-Control-Allow-Origin': '*'}, + ) + : CachedNetworkImage( + imageUrl: recipe.fullImageUrl, + fit: BoxFit.cover, + placeholder: (_, __) => Container( + color: DesignTokens.background, + child: const Center( + child: CupertinoActivityIndicator(), + ), + ), + errorWidget: (_, __, _) => Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + DesignTokens.orange.withValues(alpha: 0.4), + DesignTokens.red.withValues(alpha: 0.3), + ], + ), + ), + child: const Center( + child: Icon( + CupertinoIcons.photo, + size: 36, + color: DesignTokens.text3, + ), + ), + ), ), - ), - child: const Center( - child: Icon( - CupertinoIcons.photo, - size: 36, - color: DesignTokens.text3, - ), - ), - ), - ), // ─── 底部深色渐变(增强文字可读性) ─── Container( diff --git a/lib/src/widgets/glass/nav/home_app_bar.dart b/lib/src/widgets/glass/nav/home_app_bar.dart index 27db9d3..36ccd1e 100644 --- a/lib/src/widgets/glass/nav/home_app_bar.dart +++ b/lib/src/widgets/glass/nav/home_app_bar.dart @@ -20,12 +20,14 @@ class HomeAppBar extends StatefulWidget { final bool isSubBarVisible; final VoidCallback? onNotificationTap; final VoidCallback? onSettingsTap; + final VoidCallback? onScannerTap; const HomeAppBar({ super.key, required this.isSubBarVisible, this.onNotificationTap, this.onSettingsTap, + this.onScannerTap, }); @override @@ -141,6 +143,12 @@ class _HomeAppBarState extends State { isDark: isDark, ), const SizedBox(width: DesignTokens.space2), + _buildActionButton( + icon: CupertinoIcons.qrcode, + onTap: widget.onScannerTap, + isDark: isDark, + ), + const SizedBox(width: DesignTokens.space2), _buildActionButton( icon: CupertinoIcons.arrow_up_doc, onTap: widget.onSettingsTap, diff --git a/lib/src/widgets/navigation_widgets.dart b/lib/src/widgets/navigation_widgets.dart index cfaa218..f1eb03c 100644 --- a/lib/src/widgets/navigation_widgets.dart +++ b/lib/src/widgets/navigation_widgets.dart @@ -1,4 +1,12 @@ -import 'package:flutter/cupertino.dart'; +/* + * 文件: navigation_widgets.dart + * 名称: 主导航Tab容器 + * 作用: 底部Tab页承载(IndexedStack),保持各Tab页面状态不被重建 + * 创建时间: 2026-04-09 + * 更新时间: 2026-04-21 修复切换Tab返回首页导致页面状态重置/瀑布流重新加载 + */ + +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/pages/home/home_page.dart'; @@ -104,10 +112,10 @@ class _MainTabViewState extends State { final page = _builtPages.containsKey(i) ? _builtPages[i]! : const SizedBox.shrink(); - if (i != currentIndex) { - return ExcludeFocusTraversal(child: page); - } - return page; + return KeyedSubtree( + key: ValueKey('main_tab_$i'), + child: ExcludeFocusTraversal(child: page), + ); }), ), ), diff --git a/lib/src/widgets/recipe/recipe_image.dart b/lib/src/widgets/recipe/recipe_image.dart index be4342b..26a45ab 100644 --- a/lib/src/widgets/recipe/recipe_image.dart +++ b/lib/src/widgets/recipe/recipe_image.dart @@ -10,6 +10,7 @@ */ import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; @@ -105,6 +106,9 @@ class _RecipeImageState extends State { } String get _currentUrl { + if (_urlChain.isEmpty) { + return ''; + } if (_currentUrlIndex < _urlChain.length) { return _urlChain[_currentUrlIndex]; } @@ -138,6 +142,23 @@ class _RecipeImageState extends State { } Widget _buildCachedImage(bool isDark) { + if (kIsWeb && _currentUrl.isNotEmpty) { + return Image.network( + _currentUrl, + width: widget.width, + height: widget.height, + fit: widget.fit, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return _buildLoadingWidget(isDark); + }, + errorBuilder: (context, error, stackTrace) { + _onImageError(); + return _buildErrorPlaceholder(isDark); + }, + headers: {'Access-Control-Allow-Origin': '*'}, + ); + } return CachedNetworkImage( imageUrl: _currentUrl, width: widget.width, diff --git a/lib/src/widgets/recipe_detail/interaction/recipe_qr_poster.dart b/lib/src/widgets/recipe_detail/interaction/recipe_qr_poster.dart index 86b4d48..95394d5 100644 --- a/lib/src/widgets/recipe_detail/interaction/recipe_qr_poster.dart +++ b/lib/src/widgets/recipe_detail/interaction/recipe_qr_poster.dart @@ -192,22 +192,43 @@ class RecipeQrPoster extends StatelessWidget { return Column( children: [ Text( - '扫码查看菜谱详情', + '扫码查看菜谱', style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w500, color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, ), ), - const SizedBox(height: DesignTokens.space1), + const SizedBox(height: DesignTokens.space2), + _buildScanTipRow(isDark, '使用 📱', '小妈厨房', '直接打开菜谱详情页'), + const SizedBox(height: 6), + _buildScanTipRow(isDark, '其他软件扫码 🌐', '浏览器', '打开菜品对应网页'), + ], + ); + } + + Widget _buildScanTipRow(bool isDark, String icon, String app, String desc) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(icon, style: const TextStyle(fontSize: 14)), + const SizedBox(width: 4), Text( - '🍳 小妈厨房', + app, style: TextStyle( fontSize: DesignTokens.fontXs, color: DesignTokens.dynamicPrimary, fontWeight: FontWeight.w600, ), ), + const SizedBox(width: 4), + Text( + '· $desc', + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), ], ); } @@ -437,22 +458,166 @@ void _showPosterOnly( ratingScore: ratingScore, ), const SizedBox(height: DesignTokens.space3), - if (code != null) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: DesignTokens.space4, - ), - child: Text( - '编码: $code', - style: TextStyle( - fontSize: DesignTokens.fontXs, - color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, - ), - ), - ), + _buildCodeAndUrlRow(ctx, isDark, code, recipeId), ], ), ); }, ); } + +String? _extractNumericId(String? code) { + if (code == null || code.isEmpty) return null; + final match = RegExp(r'\d+').firstMatch(code); + return match?.group(0); +} + +Widget _buildCodeAndUrlRow( + BuildContext ctx, + bool isDark, + String? code, + int? recipeId, +) { + final numericId = _extractNumericId(code) ?? recipeId?.toString(); + if (numericId == null) { + if (code != null) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), + child: Text( + '编码: $code', + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ); + } + return const SizedBox.shrink(); + } + + final url = 'https://eat.wktyl.com/?id=$numericId'; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), + padding: const EdgeInsets.all(DesignTokens.space2), + decoration: BoxDecoration( + color: isDark + ? DarkDesignTokens.card.withValues(alpha: 0.5) + : DesignTokens.background, + borderRadius: DesignTokens.borderRadiusMd, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '编码: ${code ?? numericId}', + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ], + ), + const SizedBox(height: DesignTokens.space1), + GestureDetector( + onTap: () => _showUrlQrDialog(ctx, url, isDark), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: DesignTokens.space2, + vertical: DesignTokens.space1, + ), + decoration: BoxDecoration( + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.08), + borderRadius: DesignTokens.borderRadiusSm, + border: Border.all( + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.qrcode, + size: 14, + color: DesignTokens.dynamicPrimary, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + url, + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: DesignTokens.dynamicPrimary, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + Icon( + CupertinoIcons.arrow_up_right, + size: 12, + color: DesignTokens.dynamicPrimary.withValues(alpha: 0.6), + ), + ], + ), + ), + ), + const SizedBox(height: 4), + Text( + '点击查看网页版二维码', + style: TextStyle( + fontSize: 10, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ], + ), + ); +} + +void _showUrlQrDialog(BuildContext ctx, String url, bool isDark) { + showCupertinoDialog( + context: ctx, + builder: (dialogCtx) => CupertinoAlertDialog( + title: const Text('🌐 网页版二维码'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: DesignTokens.space2), + Container( + padding: const EdgeInsets.all(DesignTokens.space3), + decoration: BoxDecoration( + color: CupertinoColors.white, + borderRadius: DesignTokens.borderRadiusMd, + ), + child: CustomPaint( + size: const Size(200, 200), + painter: _QrPainter( + data: url, + color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, + ), + ), + ), + const SizedBox(height: DesignTokens.space2), + Text( + '浏览器扫码打开菜谱基础信息页', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: DesignTokens.fontXs, + color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, + ), + ), + ], + ), + actions: [ + CupertinoDialogAction( + onPressed: () => Navigator.pop(dialogCtx), + child: const Text('关闭'), + ), + ], + ), + ); +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4e3e2bf..db4e6ab 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import connectivity_plus import device_info_plus import file_picker_ohos import file_selector_macos +import mobile_scanner import package_info_plus import path_provider_foundation import share_plus @@ -21,6 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa..b32f0ba 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -28,5 +28,7 @@ MainMenu NSPrincipalClass NSApplication + NSCameraUsageDescription + This app needs camera access to scan QR codes and barcodes diff --git a/ohos/build-profile.json5 b/ohos/build-profile.json5 index 7bce618..23f1177 100644 --- a/ohos/build-profile.json5 +++ b/ohos/build-profile.json5 @@ -7,11 +7,11 @@ "material": { "certpath": "C:\\Users\\无书\\.ohos\\config\\default_ohos_h8eBDwGJTRHPEcoIPNZ4JJ58-IFgoGWW5H7lci4Iucs=.cer", "keyAlias": "debugKey", - "keyPassword": "0000001B7CAB09D50746C6C30A6F40391D1E7312D175D71ABA92CE315B118861B83894F09A6A31EED9F47C", + "keyPassword": "0000001BF030A807626EA982537FE12B6CB9BE7488E9FC176A9C0F53CC9BFA3725E642FFF574C76130801E", "profile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_h8eBDwGJTRHPEcoIPNZ4JJ58-IFgoGWW5H7lci4Iucs=.p7b", "signAlg": "SHA256withECDSA", "storeFile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_h8eBDwGJTRHPEcoIPNZ4JJ58-IFgoGWW5H7lci4Iucs=.p12", - "storePassword": "0000001B08D1DA1FD0A47C3BD8AFB95D785667DD24E1F18CDFAB4836835C917F1666F8AC6FC4DC946B53E8" + "storePassword": "0000001BDCFDD67696998DA2DEB2C7DFC913812DFF1A875943BAA8A0B41741996A301A3D77A157593E9BC8" } }, { @@ -32,16 +32,16 @@ { "name": "default", "signingConfig": "default", - "compatibleSdkVersion": "5.1.0(18)", + "compatibleSdkVersion": "5.0.5(17)", "runtimeOS": "HarmonyOS", "targetSdkVersion": "6.1.0(23)" }, { "name": "release", "signingConfig": "release", - "compatibleSdkVersion": "5.1.0(18)", + "compatibleSdkVersion": "5.0.1(13)", "runtimeOS": "HarmonyOS", - "targetSdkVersion": "6.0.2(22)" + "targetSdkVersion": "6.1.0(23)" } ], "buildModeSet": [ diff --git a/ohos/entry/src/main/module.json5 b/ohos/entry/src/main/module.json5 index 395c103..f5a6b5d 100644 --- a/ohos/entry/src/main/module.json5 +++ b/ohos/entry/src/main/module.json5 @@ -72,6 +72,14 @@ "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.CAMERA", + "reason": "$string:permission_camera_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } } ] } diff --git a/ohos/entry/src/main/resources/base/element/string.json b/ohos/entry/src/main/resources/base/element/string.json index 812418f..1906a12 100644 --- a/ohos/entry/src/main/resources/base/element/string.json +++ b/ohos/entry/src/main/resources/base/element/string.json @@ -2,31 +2,35 @@ "string": [ { "name": "module_desc", - "value": "Cute Kitchen Application Module" + "value": "小妈厨房应用模块" }, { "name": "EntryAbility_desc", - "value": "Cute Kitchen Main Entry Point" + "value": "小妈厨房主入口" }, { "name": "EntryAbility_label", - "value": "Cute Kitchen" + "value": "小妈厨房" }, { "name": "permission_internet_reason", - "value": "Network access is required for recipe browsing and data synchronization" + "value": "用于菜谱浏览和数据同步" }, { "name": "permission_vibrate_reason", - "value": "Vibration feedback is used for cooking timers and notifications" + "value": "用于烹饪计时和通知反馈" }, { "name": "permission_clipboard_reason", - "value": "Clipboard access is used for sharing recipes and ingredients" + "value": "用于分享菜谱和食材信息" }, { "name": "permission_read_media_reason", - "value": "Media access is required for importing recipe images and data" + "value": "用于导入菜谱图片和数据" + }, + { + "name": "permission_camera_reason", + "value": "用于扫描二维码和条形码" } ] -} \ No newline at end of file +} diff --git a/packages/fluttertoast_ohos/ohos/BuildProfile.ets b/packages/fluttertoast_ohos/ohos/BuildProfile.ets index 3a501e5..6033e79 100644 --- a/packages/fluttertoast_ohos/ohos/BuildProfile.ets +++ b/packages/fluttertoast_ohos/ohos/BuildProfile.ets @@ -2,8 +2,8 @@ * Use these variables when you tailor your ArkTS code. They must be of the const type. */ export const HAR_VERSION = '1.0.0'; -export const BUILD_MODE_NAME = 'debug'; -export const DEBUG = true; +export const BUILD_MODE_NAME = 'release'; +export const DEBUG = false; export const TARGET_NAME = 'default'; /** diff --git a/packages/fluttertoast_ohos/ohos/build/default/intermediates/merge_profile/default/module.json b/packages/fluttertoast_ohos/ohos/build/default/intermediates/merge_profile/default/module.json index 5e23976..540d4a0 100644 --- a/packages/fluttertoast_ohos/ohos/build/default/intermediates/merge_profile/default/module.json +++ b/packages/fluttertoast_ohos/ohos/build/default/intermediates/merge_profile/default/module.json @@ -2,8 +2,8 @@ "app": { "bundleName": "cute.major.kitchen", "debug": true, - "versionCode": 100, - "versionName": "0.99.1", + "versionCode": 26042102, + "versionName": "1.3.1", "minAPIVersion": 50100018, "targetAPIVersion": 60100023, "apiReleaseType": "Release", diff --git a/packages/fluttertoast_ohos/ohos/build/release/intermediates/merge_profile/default/module.json b/packages/fluttertoast_ohos/ohos/build/release/intermediates/merge_profile/default/module.json new file mode 100644 index 0000000..55a5dde --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/build/release/intermediates/merge_profile/default/module.json @@ -0,0 +1,28 @@ +{ + "app": { + "bundleName": "cute.major.kitchen", + "debug": false, + "versionCode": 26042102, + "versionName": "1.3.1", + "minAPIVersion": 50001013, + "targetAPIVersion": 60100023, + "apiReleaseType": "Release", + "targetMinorAPIVersion": 0, + "targetPatchAPIVersion": 0, + "compileSdkVersion": "6.1.0.105", + "compileSdkType": "HarmonyOS", + "appEnvironments": [], + "bundleType": "app", + "buildMode": "release" + }, + "module": { + "name": "fluttertoast_ohos", + "type": "har", + "deviceTypes": [ + "default", + "tablet" + ], + "packageName": "fluttertoast_ohos", + "installationFree": false + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh-package-lock.json5 b/packages/fluttertoast_ohos/ohos/oh-package-lock.json5 index 8b40a53..85fef06 100644 --- a/packages/fluttertoast_ohos/ohos/oh-package-lock.json5 +++ b/packages/fluttertoast_ohos/ohos/oh-package-lock.json5 @@ -6,20 +6,27 @@ "lockfileVersion": 3, "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", "specifiers": { - "@ohos/flutter_ohos@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/flutter_embedding_debug.har": "@ohos/flutter_ohos@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/flutter_embedding_debug.har", - "flutter_native_arm64_v8a@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/arm64_v8a_debug.har": "flutter_native_arm64_v8a@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/arm64_v8a_debug.har" + "@ohos/flutter_ohos@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/flutter_embedding_release.har": "@ohos/flutter_ohos@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/flutter_embedding_release.har", + "flutter_native_arm64_v8a@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/arm64_v8a_release.har": "flutter_native_arm64_v8a@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/arm64_v8a_release.har", + "flutter_native_x86_64@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-x64-release/x86_64_release.har": "flutter_native_x86_64@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-x64-release/x86_64_release.har" }, "packages": { - "@ohos/flutter_ohos@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/flutter_embedding_debug.har": { + "@ohos/flutter_ohos@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/flutter_embedding_release.har": { "name": "@ohos/flutter_ohos", "version": "1.0.0-e34a685f4b", - "resolved": "../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/flutter_embedding_debug.har", + "resolved": "../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/flutter_embedding_release.har", "registryType": "local" }, - "flutter_native_arm64_v8a@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/arm64_v8a_debug.har": { + "flutter_native_arm64_v8a@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/arm64_v8a_release.har": { "name": "flutter_native_arm64_v8a", "version": "1.0.0-e34a685f4b", - "resolved": "../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64/arm64_v8a_debug.har", + "resolved": "../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-arm64-release/arm64_v8a_release.har", + "registryType": "local" + }, + "flutter_native_x86_64@../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-x64-release/x86_64_release.har": { + "name": "flutter_native_x86_64", + "version": "1.0.0-e34a685f4b", + "resolved": "../../../../../../../sdk/flutter-ohos/flutter_flutter/bin/cache/artifacts/engine/ohos-x64-release/x86_64_release.har", "registryType": "local" } } diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/BuildProfile.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/BuildProfile.ets new file mode 100644 index 0000000..e599d28 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0-e34a685f4b'; +export const BUILD_MODE_NAME = 'release'; +export const DEBUG = false; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/build-profile.json5 b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/build-profile.json5 new file mode 100644 index 0000000..e395590 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/build-profile.json5 @@ -0,0 +1,40 @@ + + +{ + "apiType": "stageMode", + "buildOption": { + "sourceOption": { + "workers": [ + "./src/main/ets/embedding/engine/workers/PlatformChannelWorker.ets" + ] + }, + "nativeLib": { + "debugSymbol": { + "strip": false, + "exclude": [] + } + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + }, + "consumerFiles": ["./consumer-rules.txt"] + } + } + }, + ], + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + } + ] +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/consumer-rules.txt b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/consumer-rules.txt new file mode 100644 index 0000000..33b1039 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/consumer-rules.txt @@ -0,0 +1,4 @@ +# flutter_ohos 在混淆时需要保留的代码 +-keep-property-name +flutter +native* diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/hvigorfile.ts b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/hvigorfile.ts new file mode 100644 index 0000000..f2c2731 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/hvigorfile.ts @@ -0,0 +1,3 @@ + +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { harTasks } from '@ohos/hvigor-ohos-plugin'; \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/index.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/index.ets new file mode 100644 index 0000000..2ac7c12 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/index.ets @@ -0,0 +1,108 @@ + + +export { default as FlutterInjector } from './src/main/ets/FlutterInjector'; +export { default as FlutterPluginRegistry } from './src/main/ets/app/FlutterPluginRegistry'; +export { default as FlutterComponent } from './src/main/ets/component/FlutterComponent'; +export { default as FlutterEngine } from './src/main/ets/embedding/engine/FlutterEngine'; +export { default as FlutterEngineCache } from './src/main/ets/embedding/engine/FlutterEngineCache'; +export { default as FlutterEngineConnectionRegistry } from './src/main/ets/embedding/engine/FlutterEngineConnectionRegistry'; +export { default as FlutterEngineGroup } from './src/main/ets/embedding/engine/FlutterEngineGroup'; +export { default as FlutterEnginePreload } from './src/main/ets/embedding/engine/FlutterEnginePreload'; +export { default as FlutterEngineGroupCache } from './src/main/ets/embedding/engine/FlutterEngineGroupCache'; +export { default as FlutterNapi } from './src/main/ets/embedding/engine/FlutterNapi'; +export * from './src/main/ets/embedding/engine/FlutterOverlaySurface'; +export { default as FlutterShellArgs } from './src/main/ets/embedding/engine/FlutterShellArgs'; +export { default as DartExecutor } from './src/main/ets/embedding/engine/dart/DartExecutor'; +export * from './src/main/ets/embedding/engine/dart/DartMessenger'; +export * from './src/main/ets/embedding/engine/dart/PlatformMessageHandler'; +export { default as ApplicationInfoLoader } from './src/main/ets/embedding/engine/loader/ApplicationInfoLoader'; +export { default as FlutterApplicationInfo } from './src/main/ets/embedding/engine/loader/FlutterApplicationInfo'; +export { default as FlutterLoader } from './src/main/ets/embedding/engine/loader/FlutterLoader'; +export * from './src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorView'; +export * from './src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorsStack'; +export * from './src/main/ets/embedding/engine/plugins/FlutterPlugin'; +export { default as PluginRegistry } from './src/main/ets/embedding/engine/plugins/PluginRegistry'; +export { default as AbilityAware } from './src/main/ets/embedding/engine/plugins/ability/AbilityAware'; +export { default as AbilityControlSurface } from './src/main/ets/embedding/engine/plugins/ability/AbilityControlSurface'; +export * from './src/main/ets/embedding/engine/plugins/ability/AbilityPluginBinding'; +export * from './src/main/ets/embedding/engine/renderer/FlutterRenderer'; +export * from './src/main/ets/embedding/engine/renderer/FlutterUiDisplayListener'; +export { default as AccessibilityChannel } from './src/main/ets/embedding/engine/systemchannels/AccessibilityChannel'; +export { default as KeyEventChannel } from './src/main/ets/embedding/engine/systemchannels/KeyEventChannel'; +export { default as LifecycleChannel } from './src/main/ets/embedding/engine/systemchannels/LifecycleChannel'; +export { default as LocalizationChannel } from './src/main/ets/embedding/engine/systemchannels/LocalizationChannel'; +export { default as MouseCursorChannel } from './src/main/ets/embedding/engine/systemchannels/MouseCursorChannel'; +export { default as DisplayMetricsChannel } from './src/main/ets/embedding/engine/systemchannels/DisplayMetricsChannel'; +export { default as NavigationChannel } from './src/main/ets/embedding/engine/systemchannels/NavigationChannel'; +export { default as PlatformChannel } from './src/main/ets/embedding/engine/systemchannels/PlatformChannel'; +export { default as PlatformViewsChannel } from './src/main/ets/embedding/engine/systemchannels/PlatformViewsChannel'; +export { default as RestorationChannel } from './src/main/ets/embedding/engine/systemchannels/RestorationChannel'; +export { default as SettingsChannel } from './src/main/ets/embedding/engine/systemchannels/SettingsChannel'; +export { default as SystemChannel } from './src/main/ets/embedding/engine/systemchannels/SystemChannel'; +export { default as TestChannel } from './src/main/ets/embedding/engine/systemchannels/TestChannel'; +export { default as TextInputChannel } from './src/main/ets/embedding/engine/systemchannels/TextInputChannel'; +export { default as NativeVsyncChannel } from './src/main/ets/embedding/engine/systemchannels/NativeVsyncChannel'; +export { default as ExclusiveAppComponent } from './src/main/ets/embedding/ohos/ExclusiveAppComponent'; +export * from './src/main/ets/embedding/ohos/FlutterAbility'; +export * from './src/main/ets/embedding/ohos/FlutterAbilityAndEntryDelegate'; +export { default as FlutterAbilityLaunchConfigs } from './src/main/ets/embedding/ohos/FlutterAbilityLaunchConfigs'; +export { default as FlutterEngineConfigurator } from './src/main/ets/embedding/ohos/FlutterEngineConfigurator'; +export { default as FlutterEngineProvider } from './src/main/ets/embedding/ohos/FlutterEngineProvider'; +export { default as FlutterEntry } from './src/main/ets/embedding/ohos/FlutterEntry'; +export { default as FlutterManager, DragDropCallback as DragDropCallback } from './src/main/ets/embedding/ohos/FlutterManager'; +export * from './src/main/ets/embedding/ohos/FlutterPage'; +export { default as KeyboardManager } from './src/main/ets/embedding/ohos/KeyboardManager'; +export { default as OhosTouchProcessor } from './src/main/ets/embedding/ohos/OhosTouchProcessor'; +export { default as Settings } from './src/main/ets/embedding/ohos/Settings'; +export * from './src/main/ets/embedding/ohos/TouchEventTracker'; +export { default as WindowInfoRepositoryCallbackAdapterWrapper } from './src/main/ets/embedding/ohos/WindowInfoRepositoryCallbackAdapterWrapper'; +export { default as PlatformPlugin } from './src/main/ets/plugin/PlatformPlugin'; +export { default as BasicMessageChannel, Reply } from './src/main/ets/plugin/common/BasicMessageChannel'; +export { default as BinaryCodec } from './src/main/ets/plugin/common/BinaryCodec'; +export * from './src/main/ets/plugin/common/BinaryMessenger'; +export { default as EventChannel, StreamHandler, EventSink } from './src/main/ets/plugin/common/EventChannel'; +export { default as FlutterException } from './src/main/ets/plugin/common/FlutterException'; +export { default as JSONMessageCodec } from './src/main/ets/plugin/common/JSONMessageCodec'; +export { default as JSONMethodCodec } from './src/main/ets/plugin/common/JSONMethodCodec'; +export { default as MessageCodec } from './src/main/ets/plugin/common/MessageCodec'; +export { default as MethodCall } from './src/main/ets/plugin/common/MethodCall'; +export * from './src/main/ets/plugin/common/MethodChannel'; +export { default as MethodChannel } from './src/main/ets/plugin/common/MethodChannel'; +export { default as MethodCodec } from './src/main/ets/plugin/common/MethodCodec'; +export { default as BackgroundBasicMessageChannel } from './src/main/ets/plugin/common/BackgroundBasicMessageChannel'; +export { default as BackgroundMethodChannel} from './src/main/ets/plugin/common/BackgroundMethodChannel' +export { default as SendableBinaryCodec} from './src/main/ets/plugin/common/SendableBinaryCodec' +export { default as SendableJSONMessageCodec} from './src/main/ets/plugin/common/SendableJSONMessageCodec' +export { default as SendableJSONMethodCodec} from './src/main/ets/plugin/common/SendableJSONMethodCodec' +export { default as SendableMessageHandler} from './src/main/ets/plugin/common/SendableMessageHandler' +export { default as SendableMethodCallHandler} from './src/main/ets/plugin/common/SendableMethodCallHandler' +export { default as SendableMethodCodec} from './src/main/ets/plugin/common/SendableMethodCodec' +export { default as SendableStandardMessageCodec } from './src/main/ets/plugin/common/SendableStandardMessageCodec'; +export { default as SendableStandardMethodCodec } from './src/main/ets/plugin/common/SendableStandardMethodCodec'; +export { default as SendableStringCodec} from './src/main/ets/plugin/common/SendableStringCodec' +export { default as StandardMessageCodec } from './src/main/ets/plugin/common/StandardMessageCodec'; +export { default as StandardMethodCodec } from './src/main/ets/plugin/common/StandardMethodCodec'; +export { default as StringCodec } from './src/main/ets/plugin/common/StringCodec'; +export * from './src/main/ets/plugin/editing/ListenableEditingState'; +export * from './src/main/ets/plugin/editing/TextEditingDelta'; +export { default as TextInputPlugin } from './src/main/ets/plugin/editing/TextInputPlugin'; +export { default as LocalizationPlugin } from './src/main/ets/plugin/localization/LocalizationPlugin'; +export { default as MouseCursorPlugin } from './src/main/ets/plugin/mouse/MouseCursorPlugin'; +export { default as PlatformView } from './src/main/ets/plugin/platform/PlatformView'; +export { default as PlatformViewFactory } from './src/main/ets/plugin/platform/PlatformViewFactory'; +export { default as PlatformViewRegistry } from './src/main/ets/plugin/platform/PlatformViewRegistry'; +export { default as PlatformViewRegistryImpl } from './src/main/ets/plugin/platform/PlatformViewRegistryImpl'; +export * from './src/main/ets/plugin/platform/PlatformViewWrapper'; +export { default as PlatformViewsController } from './src/main/ets/plugin/platform/PlatformViewsController'; +export * from './src/main/ets/view/FlutterCallbackInformation'; +export { default as FlutterRunArguments } from './src/main/ets/view/FlutterRunArguments'; +export * from './src/main/ets/view/FlutterView'; +export * from './src/main/ets/view/TextureRegistry'; +export * from './src/main/ets/util/ByteBuffer'; +export { default as Log } from './src/main/ets/util/Log'; +export { default as MessageChannelUtils } from './src/main/ets/util/MessageChannelUtils'; +export { default as PathUtils } from './src/main/ets/util/PathUtils'; +export { default as StringUtils } from './src/main/ets/util/StringUtils'; +export { default as ToolUtils } from './src/main/ets/util/ToolUtils'; +export * from './src/main/ets/util/TraceSection'; +export { default as Any } from './src/main/ets/plugin/common/Any'; diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/obfuscation-rules.txt b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/obfuscation-rules.txt new file mode 100644 index 0000000..eb5c766 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/obfuscation-rules.txt @@ -0,0 +1,18 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/obfuscation.txt b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/obfuscation.txt new file mode 100644 index 0000000..08f59e3 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/obfuscation.txt @@ -0,0 +1,3 @@ +-keep-property-name +flutter +native* diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/oh-package.json5 b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/oh-package.json5 new file mode 100644 index 0000000..bf91be6 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/oh-package.json5 @@ -0,0 +1 @@ +{"license":"Apache-2.0","author":"","name":"@ohos/flutter_ohos","description":"The embedder of flutter in ohos.","main":"index.ets","version":"1.0.0-e34a685f4b","dependencies":{},"devDependencies":{"@types/libflutter.so":"file:./src/main/cpp/types/libflutter"},"metadata":{"workers":["./src/main/ets/embedding/engine/workers/PlatformChannelWorker.ets"],"sourceRoots":["./src/main"],"debug":false},"compatibleSdkVersionStage":"beta1","compatibleSdkVersion":12,"compatibleSdkType":"HarmonyOS","obfuscated":false} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/cpp/types/libflutter/index.d.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/cpp/types/libflutter/index.d.ets new file mode 100644 index 0000000..20dec02 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/cpp/types/libflutter/index.d.ets @@ -0,0 +1,532 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import common from '@ohos.app.ability.common'; +import resourceManager from '@ohos.resourceManager'; +import image from '@ohos.multimedia.image'; +import FlutterNapi from '../../../ets/embedding/engine/FlutterNapi'; +import { ByteBuffer } from '../../../ets/util/ByteBuffer'; +import { FlutterCallbackInformation } from '../../../ets/view/FlutterCallbackInformation'; + +/** + * Updates the refresh rate for the Flutter engine. + * @param rate - The refresh rate value to set + */ +export const nativeUpdateRefreshRate: ( + rate: number +) => void; + +/** + * Initializes SkFontMgr::RefDefault() to prefetch the default font manager. + * This should be called before using fonts in the Flutter engine. + */ +export const nativePrefetchDefaultFontManager: () => void; + +/** + * Checks and reloads fonts for the specified shell holder. + * @param nativeShellHolderId - The ID of the native shell holder + */ +export const nativeCheckAndReloadFont: (nativeShellHolderId: number) => void; + +/** + * Updates the size of the Flutter view. + * @param width - The new width in pixels + * @param height - The new height in pixels + */ +export const nativeUpdateSize: ( + width: number, + height: number +) => void; + +/** + * Updates the pixel density of the display. + * @param densityPixels - The pixel density value (dots per inch) + */ +export const nativeUpdateDensity: ( + densityPixels: number +) => void; + +/** + * Initializes the Dart VM and Flutter engine. + * @param context - The application context + * @param args - Command line arguments for the Flutter engine + * @param bundlePath - Path to the Flutter bundle + * @param appStoragePath - Path to the application storage directory + * @param engineCachesPath - Path to the engine caches directory + * @param initTimeMillis - Initialization time in milliseconds + * @param productModel - Product model identifier + * @returns The native shell holder ID, or null if initialization fails + */ +export const nativeInit: ( + context: common.Context, + args: Array, + bundlePath: string, + appStoragePath: string, + engineCachesPath: string, + initTimeMillis: number, + productModel: string +) => number | null; + +/** + * Attaches a FlutterNapi instance to the engine. + * @param napi - The FlutterNapi instance to attach + * @returns The native shell holder ID + */ +export const nativeAttach: (napi: FlutterNapi) => number; + +/** + * Spawns a new Flutter shell instance. + * @param nativeSpawningShellId - The ID of the spawning shell, or null for a new shell + * @param entrypointFunctionName - Name of the entrypoint function + * @param pathToEntrypointFunction - Path to the entrypoint function + * @param initialRoute - Initial route for navigation + * @param entrypointArgs - Arguments to pass to the entrypoint function + * @param napi - The FlutterNapi instance + * @returns The native shell holder ID of the spawned shell + */ +export const nativeSpawn: ( + nativeSpawningShellId: number | null, + entrypointFunctionName: string, + pathToEntrypointFunction: string, + initialRoute: string, + entrypointArgs: Array, + napi: FlutterNapi +) => number; + +/** + * Runs a Flutter bundle and snapshot from a library. + * @param nativeShellHolderId - The ID of the native shell holder + * @param bundlePath - Path to the Flutter bundle + * @param entrypointFunctionName - Name of the entrypoint function + * @param pathToEntrypointFunction - Path to the entrypoint function + * @param assetManager - Resource manager for accessing assets + * @param entrypointArgs - Arguments to pass to the entrypoint function + */ +export const nativeRunBundleAndSnapshotFromLibrary: ( + nativeShellHolderId: number, + bundlePath: string, + entrypointFunctionName: string, + pathToEntrypointFunction: string, + assetManager: resourceManager.ResourceManager, + entrypointArgs: Array +) => void; + +/** + * Sends a data-carrying response to a platform message received from Dart. + * @param nativeShellHolderId - The ID of the native shell holder + * @param responseId - The response ID for the platform message + * @param message - The message data as an ArrayBuffer + * @param position - The position in the message buffer + */ +export const nativeInvokePlatformMessageResponseCallback: (nativeShellHolderId: number, responseId: number, message: ArrayBuffer, position: number) => void; + +/** + * Sends an empty response to a platform message received from Dart. + * @param nativeShellHolderId - The ID of the native shell holder + * @param responseId - The response ID for the platform message + */ +export const nativeInvokePlatformMessageEmptyResponseCallback: (nativeShellHolderId: number, responseId: number) => void; + +/** + * Sends a data-carrying platform message to Dart. + * @param nativeShellHolderId - The ID of the native shell holder + * @param channel - The channel name for the platform message + * @param message - The message data as an ArrayBuffer + * @param position - The position in the message buffer + * @param responseId - The response ID for the platform message + */ +export const nativeDispatchPlatformMessage: (nativeShellHolderId: number, channel: String, message: ArrayBuffer, position: number, responseId: number) => void; + +/** + * Sends an empty platform message to Dart. + * @param nativeShellHolderId - The ID of the native shell holder + * @param channel - The channel name for the platform message + * @param responseId - The response ID for the platform message + */ +export const nativeDispatchEmptyPlatformMessage: (nativeShellHolderId: number, channel: String, responseId: number) => void; + +/** + * Sets the viewport metrics for the Flutter view. + * @param nativeShellHolderId - The ID of the native shell holder + * @param devicePixelRatio - The device pixel ratio + * @param physicalWidth - Physical width in pixels + * @param physicalHeight - Physical height in pixels + * @param physicalPaddingTop - Top padding in physical pixels + * @param physicalPaddingRight - Right padding in physical pixels + * @param physicalPaddingBottom - Bottom padding in physical pixels + * @param physicalPaddingLeft - Left padding in physical pixels + * @param physicalViewInsetTop - Top view inset in physical pixels + * @param physicalViewInsetRight - Right view inset in physical pixels + * @param physicalViewInsetBottom - Bottom view inset in physical pixels + * @param physicalViewInsetLeft - Left view inset in physical pixels + * @param systemGestureInsetTop - Top system gesture inset + * @param systemGestureInsetRight - Right system gesture inset + * @param systemGestureInsetBottom - Bottom system gesture inset + * @param systemGestureInsetLeft - Left system gesture inset + * @param physicalTouchSlop - Physical touch slop value + * @param displayFeaturesBounds - Array of display feature bounds + * @param displayFeaturesType - Array of display feature types + * @param displayFeaturesState - Array of display feature states + */ +export const nativeSetViewportMetrics: (nativeShellHolderId: number, devicePixelRatio: number, physicalWidth: number + , physicalHeight: number, physicalPaddingTop: number, physicalPaddingRight: number + , physicalPaddingBottom: number, physicalPaddingLeft: number, physicalViewInsetTop: number + , physicalViewInsetRight: number, physicalViewInsetBottom: number, physicalViewInsetLeft: number + , systemGestureInsetTop: number, systemGestureInsetRight: number, systemGestureInsetBottom: number + , systemGestureInsetLeft: number, physicalTouchSlop: number, displayFeaturesBounds: Array + , displayFeaturesType: Array, displayFeaturesState: Array) => void; + +/** + * Gets the system languages and populates the provided array. + * @param nativeShellHolderId - The ID of the native shell holder + * @param languages - Array to be populated with system language codes + */ +export const nativeGetSystemLanguages: (nativeShellHolderId: number, languages: Array) => void; + +/** + * Attaches a Flutter engine to an XComponent. + * @param xcomponentId - The ID of the XComponent + * @param nativeShellHolderId - The ID of the native shell holder + */ +export const nativeXComponentAttachFlutterEngine: (xcomponentId: string, nativeShellHolderId: number) => void; + +/** + * Called before drawing the XComponent. + * @param xcomponentId - The ID of the XComponent + * @param nativeShellHolderId - The ID of the native shell holder + * @param width - The width of the component + * @param height - The height of the component + */ +export const nativeXComponentPreDraw: (xcomponentId: string, nativeShellHolderId: number, width: number, height: number) => void; + +/** + * Detaches a Flutter engine from an XComponent. + * @param xcomponentId - The ID of the XComponent + * @param nativeShellHolderId - The ID of the native shell holder + */ +export const nativeXComponentDetachFlutterEngine: (xcomponentId: string, nativeShellHolderId: number) => void; + +/** + * Dispatches a mouse wheel event to the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param xcomponentId - The ID of the XComponent + * @param eventType - The type of the mouse wheel event + * @param fingerId - The finger ID for the event + * @param globalX - Global X coordinate + * @param globalY - Global Y coordinate + * @param offsetY - Vertical scroll offset + * @param timestamp - Event timestamp + */ +export const nativeXComponentDispatchMouseWheel: (nativeShellHolderId: number, + xcomponentId: string, + eventType: string, + fingerId: number, + globalX: number, + globalY: number, + offsetY: number, + timestamp: number + ) => void; + + +/** + * Detaches the association between FlutterNapi and the engine. + * This method should only be called when FlutterNapi is already associated with the engine. + * @param nativeShellHolderId - The ID of the native shell holder to destroy + */ +export const nativeDestroy: ( + nativeShellHolderId: number +) => void; + + +/** + * Unregisters a texture from the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture to unregister + */ +export const nativeUnregisterTexture: (nativeShellHolderId: number, textureId: number) => void; + +/** + * Registers a PixelMap as a texture in the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param pixelMap - The PixelMap to register + */ +export const nativeRegisterPixelMap: (nativeShellHolderId: number, textureId: number, pixelMap: PixelMap) => void; + +/** + * Sets the background PixelMap for a texture. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param pixelMap - The PixelMap to use as background + */ +export const nativeSetTextureBackGroundPixelMap: (nativeShellHolderId: number, textureId: number, pixelMap: PixelMap) => void; + +/** + * Sets the background color for a texture. + * @deprecated since 3.7 + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param color - The color value as a number + */ +export const nativeSetTextureBackGroundColor: (nativeShellHolderId: number, textureId: number, color: number) => void; + +/** + * Registers a texture with the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture to register + * @returns The registered texture ID + */ +export const nativeRegisterTexture: (nativeShellHolderId: number, textureId: number) => number; + +/** + * Gets the window ID for a texture. + * @deprecated since 3.22 + * @useinstead nativeGetTextureWindowPtr + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @returns The window ID + */ +export const nativeGetTextureWindowId: (nativeShellHolderId: number, textureId: number) => number; + +/** + * Gets the window pointer for a texture. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @returns The window pointer as a bigint + */ +export const nativeGetTextureWindowPtr: (nativeShellHolderId: number, textureId: number) => bigint; + +/** + * Sets an external native image for a texture. + * @deprecated since 3.22 + * @useinstead nativeSetExternalNativeImagePtr + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param native_image - The native image ID + * @returns The result code + */ +export const nativeSetExternalNativeImage: (nativeShellHolderId: number, textureId: number, native_image: number) => number; + +/** + * Sets an external native image pointer for a texture. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param native_image_ptr - The native image pointer as a bigint + * @returns The result code + */ +export const nativeSetExternalNativeImagePtr: (nativeShellHolderId: number, textureId: number, native_image_ptr: bigint) => number; + +/** + * Resets an external texture. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param need_surfaceId - Whether a surface ID is needed + * @returns The result code + */ +export const nativeResetExternalTexture: (nativeShellHolderId: number, textureId: number, need_surfaceId: boolean) => number; + +/** + * Encodes a string to UTF-8 bytes. + * @param str - The string to encode + * @returns The UTF-8 encoded bytes as a Uint8Array + */ +export const nativeEncodeUtf8: (str: string) => Uint8Array; + +/** + * Decodes UTF-8 bytes to a string. + * @param array - The UTF-8 encoded bytes as a Uint8Array + * @returns The decoded string + */ +export const nativeDecodeUtf8: (array: Uint8Array) => string; + +/** + * Sets the buffer size for a texture. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param width - The buffer width + * @param height - The buffer height + */ +export const nativeSetTextureBufferSize: (nativeShellHolderId: number, textureId: number, width: number, height: number) => void; + +/** + * Notifies the Flutter engine that a texture is being resized. + * @param nativeShellHolderId - The ID of the native shell holder + * @param textureId - The ID of the texture + * @param width - The new width + * @param height - The new height + */ +export const nativeNotifyTextureResizing: (nativeShellHolderId: number, textureId: number, width: number, height: number) => void; + +/** + * Enables or disables frame caching for the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param enable - Whether to enable frame caching + */ +export const nativeEnableFrameCache: (nativeShellHolderId: number, enable: boolean) => void; + +/** + * Looks up callback information for a given handler. + * @param callback - The FlutterCallbackInformation object to populate + * @param handler - The handler number + * @returns The result code + */ +export const nativeLookupCallbackInformation: (callback: FlutterCallbackInformation, handler: number) => number; + +/** + * Checks if a Unicode code point is an emoji. + * @param code - The Unicode code point + * @returns 1 if it is an emoji, 0 otherwise + */ +export const nativeUnicodeIsEmoji: (code: number) => number; + +/** + * Checks if a Unicode code point is an emoji modifier. + * @param code - The Unicode code point + * @returns 1 if it is an emoji modifier, 0 otherwise + */ +export const nativeUnicodeIsEmojiModifier: (code: number) => number; + +/** + * Checks if a Unicode code point is an emoji modifier base. + * @param code - The Unicode code point + * @returns 1 if it is an emoji modifier base, 0 otherwise + */ +export const nativeUnicodeIsEmojiModifierBase: (code: number) => number; + +/** + * Checks if a Unicode code point is a variation selector. + * @param code - The Unicode code point + * @returns 1 if it is a variation selector, 0 otherwise + */ +export const nativeUnicodeIsVariationSelector: (code: number) => number; + +/** + * Checks if a Unicode code point is a regional indicator symbol. + * @param code - The Unicode code point + * @returns 1 if it is a regional indicator symbol, 0 otherwise + */ +export const nativeUnicodeIsRegionalIndicatorSymbol: (code: number) => number; + +/** + * Sets accessibility features in the accessibility channel. + * @param accessibilityFeatureFlags - The accessibility feature flags + * @param responseId - The response ID for the platform message + */ +export const nativeSetAccessibilityFeatures: (accessibilityFeatureFlags: number, responseId: number) => void; + +/** + * Notifies the Flutter engine of an accessibility state change. + * @param nativeShellHolderId - The ID of the native shell holder + * @param state - The new accessibility state + */ +export const nativeAccessibilityStateChange: (nativeShellHolderId: number, state: Boolean) => void; + +/** + * Announces an accessibility message to the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param message - The message to announce + */ +export const nativeAccessibilityAnnounce: (nativeShellHolderId: number, message: string) => void; + +/** + * Handles an accessibility tap event. + * @param nativeShellHolderId - The ID of the native shell holder + * @param nodeId - The ID of the accessibility node that was tapped + */ +export const nativeAccessibilityOnTap: (nativeShellHolderId: number, nodeId: number) => void; + +/** + * Handles an accessibility long press event. + * @param nativeShellHolderId - The ID of the native shell holder + * @param nodeId - The ID of the accessibility node that was long pressed + */ +export const nativeAccessibilityOnLongPress: (nativeShellHolderId: number, nodeId: number) => void; + +/** + * Handles an accessibility tooltip event. + * @param nativeShellHolderId - The ID of the native shell holder + * @param message - The tooltip message + */ +export const nativeAccessibilityOnTooltip: (nativeShellHolderId: number, message: string) => void; + +/** + * Enables or disables semantics for the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param enabled - Whether to enable semantics + */ +export const nativeSetSemanticsEnabled: (nativeShellHolderId: number, enabled: boolean) => void; + +/** + * Sets the font weight scale for the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param fontWeightScale - The font weight scale value + */ +export const nativeSetFontWeightScale: (nativeShellHolderId: number, fontWeightScale: number) => void; + +/** + * Sets the Flutter navigation action state. + * @param nativeShellHolderId - The ID of the native shell holder + * @param isNavigate - Whether navigation is active + */ +export const nativeSetFlutterNavigationAction: (nativeShellHolderId: number, isNavigate: boolean) => void; + +/** + * Enables or disables VSync for the Flutter engine. + * @param nativeShellHolderId - The ID of the native shell holder + * @param isEnable - Whether to enable VSync + */ +export const nativeSetDVsyncSwitch: (nativeShellHolderId: number, isEnable: boolean) => void; + +/** + * Updates the current XComponent ID. + * @param xcomponent_id - The ID of the XComponent + */ +export const nativeUpdateCurrentXComponentId: (xcomponent_id: string) => void; + +/** + * Performs animation voting based on type and velocity. + * @param type - The animation type + * @param velocity - The animation velocity + */ +export const nativeAnimationVoting: (type: number, velocity: number) => void; + +/** + * Performs video voting based on duration and frame count. + * @param seconds - The video duration in seconds + * @param frameCount - The number of frames + */ +export const nativeVideoVoting: (seconds: number, frameCount: number) => void; + +/** + * Prefetches frame configuration. + */ +export const nativePrefetchFramesCfg: () => void; + +/** + * Checks the LTPO (Low Temperature Polycrystalline Oxide) switch state. + * @returns The LTPO switch state value + */ +export const nativeCheckLTPOSwitchState: () => number; + +/** + * Sets QoS (Quality of Service) settings when low memory is detected. + * @param nativeShellHolderId - The ID of the native shell holder + * @param lowMemoryLevel - The low memory level + */ +export const nativeSetQosOnLowMemory: (nativeShellHolderId: number, lowMemoryLevel: number) => void; + +export const nativeSetAnimationStatus: (nativeShellHolderId: number, animationStatus: number) => void; + +export const nativeNotifyPageChanged: (pageName: string, pageNameLen: number, windowID: number) => number; diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/cpp/types/libflutter/oh-package.json5 b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/cpp/types/libflutter/oh-package.json5 new file mode 100644 index 0000000..68c9ab9 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/cpp/types/libflutter/oh-package.json5 @@ -0,0 +1,8 @@ + + +{ + "name": "libflutter.so", + "types": "./index.d.ets", + "version": "", + "description": "Please describe the basic information." +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/FlutterInjector.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/FlutterInjector.ets new file mode 100644 index 0000000..3b2ea50 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/FlutterInjector.ets @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import FlutterNapi from './embedding/engine/FlutterNapi'; +import FlutterLoader from './embedding/engine/loader/FlutterLoader'; + +/** + * Singleton holder for Flutter-related main classes. + * Manages instances of FlutterLoader and FlutterNapi, providing centralized access to these core Flutter components. + */ +export default class FlutterInjector { + private static instance: FlutterInjector; + private flutterLoader: FlutterLoader; + private preloadFlutterNapi: FlutterNapi | null = null; + + /** + * Gets the singleton instance of FlutterInjector. + * @returns The FlutterInjector singleton instance + */ + static getInstance(): FlutterInjector { + if (FlutterInjector.instance == null) { + FlutterInjector.instance = new FlutterInjector(); + } + return FlutterInjector.instance; + } + + /** + * 初始化 + */ + private constructor() { + this.flutterLoader = new FlutterLoader(new FlutterNapi()); + } + + /** + * Gets the FlutterLoader instance. + * @returns The FlutterLoader instance + */ + getFlutterLoader(): FlutterLoader { + return this.flutterLoader; + } + + /** + * Gets a FlutterNapi instance. + * If a preloaded FlutterNapi exists, it is returned and cleared. + * Otherwise, a new FlutterNapi instance is created. + * @returns A FlutterNapi instance + */ + getFlutterNapi(): FlutterNapi { + if (this.preloadFlutterNapi) { + let retFlutterNapi = this.preloadFlutterNapi; + this.preloadFlutterNapi = null; + return retFlutterNapi; + } + return new FlutterNapi(); + } + + /** + * Creates a preloaded FlutterNapi instance. + * This instance will be returned by the next call to getFlutterNapi(). + * If a preloaded instance already exists, it will be replaced with a new one. + * @returns The newly created preloaded FlutterNapi instance + */ + getPreloadFlutterNapi(): FlutterNapi { + this.preloadFlutterNapi = new FlutterNapi(); + return this.preloadFlutterNapi; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/app/FlutterPluginRegistry.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/app/FlutterPluginRegistry.ets new file mode 100644 index 0000000..38c2873 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/app/FlutterPluginRegistry.ets @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +*/ +import { FlutterView } from '../view/FlutterView'; +import common from '@ohos.app.ability.common'; +import PlatformViewController from '../plugin/platform/PlatformViewsController' + +/** + * Registry for managing Flutter plugins and platform views. + * This class handles the lifecycle of FlutterView, Context, and PlatformViewController, + * providing methods to attach, detach, and manage resources during engine restarts. + */ +export default class FlutterPluginRegistry { + private mPlatformViewsController: PlatformViewController; + private mFlutterView: FlutterView | null = null; + private mContext: common.Context | null = null; + + /** + * Constructs a new FlutterPluginRegistry instance. + * Initializes the platform views controller and sets FlutterView and Context to null. + */ + constructor() { + this.mPlatformViewsController = new PlatformViewController(); + this.mFlutterView = null; + this.mContext = null; + } + + /** + * Attaches a FlutterView and Context to this registry. + * @param flutterView - The FlutterView instance to attach + * @param context - The application context to attach + */ + attach(flutterView: FlutterView, context: common.Context): void { + this.mFlutterView = flutterView; + this.mContext = context; + } + + /** + * Detaches the FlutterView and Context from this registry. + * Cleans up the platform views controller and resets all references. + */ + detach(): void { + this.mPlatformViewsController.detach(); + this.mPlatformViewsController.onDetachedFromNapi(); + this.mFlutterView = null; + this.mContext = null; + } + + /** + * Destroys this registry instance. + * Notifies the platform views controller that it has been detached from NAPI. + */ + destroy(): void { + this.mPlatformViewsController.onDetachedFromNapi(); + } + + /** + * Called before the Flutter engine restarts. + * Notifies the platform views controller to prepare for engine restart. + */ + onPreEngineRestart(): void { + this.mPlatformViewsController.onPreEngineRestart(); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/component/FlutterComponent.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/component/FlutterComponent.ets new file mode 100644 index 0000000..931486e --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/component/FlutterComponent.ets @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +/** + * Basic Flutter component, not yet fully encapsulated. + * This is a placeholder component that may be used as needed. + */ +@Component +export default struct FlutterComponent { + /** + * Builds the component UI. + * @returns The component tree + */ + build() { + Row() { + Column() { + Text("xxx") + .fontSize(50) + .fontWeight(FontWeight.Bold) + } + .width('100%') + } + .height('100%') + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/component/XComponentStruct.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/component/XComponentStruct.ets new file mode 100644 index 0000000..efe073f --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/component/XComponentStruct.ets @@ -0,0 +1,65 @@ +/* +* Copyright (c) 2025 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import Any from '../plugin/common/Any'; +import ApplicationInfoLoader from '../embedding/engine/loader/ApplicationInfoLoader'; + +import { BuilderParams, DVModelParameters } from '../view/DynamicView/dynamicView'; + +/** + * XComponent structure for rendering Flutter content. + * This component creates an XComponent with TEXTURE type to display Flutter views. + * It handles both debug and release modes with different background color configurations. + */ +@Component +struct XComponentStruct { + private context: Any; + private applicationInfo = ApplicationInfoLoader.load(getContext()); + /** Parameters for the DynamicView model, including XComponent ID and other attributes. */ + dvModelParams: DVModelParameters = new DVModelParameters(); + + /** + * Builds the XComponent structure for Flutter rendering. + * Creates an XComponent with TEXTURE type, handling both debug and release modes. + * @returns The XComponent tree + */ + build() { + // todo OS解决默认背景色后可以移除冗余重复代码,仅保留差异的backgroundColor属性条件配置 + if (this.applicationInfo.isDebugMode) { + XComponent({ + id: (this.dvModelParams as Record)["xComponentId"], + type: XComponentType.TEXTURE, + libraryname: 'flutter' + }) + .onLoad((context) => { + this.context = context; + }) + .onDestroy(() => { + }) + .backgroundColor(Color.White) + } else { + XComponent({ + id: (this.dvModelParams as Record)["xComponentId"], + type: XComponentType.TEXTURE, + libraryname: 'flutter' + }) + .onLoad((context) => { + this.context = context; + }) + .onDestroy(() => { + }) + } + } +} + +/** + * Builder function for creating an XComponentStruct component. + * This function is used in DynamicView to build XComponent instances for Flutter rendering. + * @param buildParams - The BuilderParams containing the DVModelParameters for the XComponent + */ +@Builder +export function BuildXComponentStruct(buildParams: BuilderParams) { + XComponentStruct({ dvModelParams: buildParams.params }); +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngine.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngine.ets new file mode 100644 index 0000000..cfd34fe --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngine.ets @@ -0,0 +1,468 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterEngine.java originally written by +* Copyright (C) Copyright 2013 The Flutter Authors. +* +*/ + +import LifecycleChannel from './systemchannels/LifecycleChannel'; +import DartExecutor, { DartEntrypoint } from './dart/DartExecutor'; +import FlutterShellArgs from './FlutterShellArgs'; +import FlutterInjector from '../../FlutterInjector'; +import FlutterLoader from './loader/FlutterLoader'; +import common from '@ohos.app.ability.common'; +import resourceManager from '@ohos.resourceManager'; +import FlutterNapi from './FlutterNapi'; +import NavigationChannel from './systemchannels/NavigationChannel'; +import Log from '../../util/Log'; +import TestChannel from './systemchannels/TestChannel' +import FlutterEngineConnectionRegistry from './FlutterEngineConnectionRegistry'; +import PluginRegistry from './plugins/PluginRegistry'; +import AbilityControlSurface from './plugins/ability/AbilityControlSurface'; +import TextInputChannel from './systemchannels/TextInputChannel'; +import TextInputPlugin from '../../plugin/editing/TextInputPlugin'; +import PlatformChannel from './systemchannels/PlatformChannel'; +import SystemChannel from './systemchannels/SystemChannel'; +import MouseCursorChannel from './systemchannels/MouseCursorChannel'; +import DisplayMetricsChannel from './systemchannels/DisplayMetricsChannel'; +import RestorationChannel from './systemchannels/RestorationChannel'; +import LocalizationChannel from './systemchannels/LocalizationChannel'; +import AccessibilityChannel from './systemchannels/AccessibilityChannel'; +import LocalizationPlugin from '../../plugin/localization/LocalizationPlugin' +import SettingsChannel from './systemchannels/SettingsChannel'; +import SensitiveContentChannel from './systemchannels/SensitiveContentChannel'; +import PlatformViewsController from '../../plugin/platform/PlatformViewsController'; +import { FlutterRenderer } from './renderer/FlutterRenderer'; +import NativeVsyncChannel from './systemchannels/NativeVsyncChannel'; + +const TAG = "FlutterEngine"; + +/** + * A single Flutter execution environment. + * + * The FlutterEngine is the container through which Dart code can be run in an OpenHarmony application. + * + * Dart code in a FlutterEngine can execute in the background, or it can be rendered to the screen by + * using the accompanying FlutterRenderer and Dart code using the Flutter framework on the Dart side. + * Rendering can be started and stopped, thus allowing a FlutterEngine to move from UI interaction + * to data-only processing and then back to UI interaction. + * + * Multiple FlutterEngines may exist, execute Dart code, and render UIs within a single OpenHarmony app. + * For better memory performance characteristics, construct multiple FlutterEngines via FlutterEngineGroup + * rather than via FlutterEngine's constructor directly. + * + * To start running Dart and/or Flutter within this FlutterEngine, get a reference to this engine's + * DartExecutor and then use DartExecutor.executeDartEntrypoint(DartEntrypoint). The + * DartExecutor.executeDartEntrypoint(DartEntrypoint) method must not be invoked twice on the same FlutterEngine. + * + * To start rendering Flutter content to the screen, use getFlutterRenderer() to obtain a FlutterRenderer + * and then attach a RenderSurface. Consider using a FlutterView as a RenderSurface. + * + * Instantiating the first FlutterEngine per process will also load the Flutter engine's native library + * and start the Dart VM. Subsequent FlutterEngines will run on the same VM instance but will have + * their own Dart Isolate when the DartExecutor is run. Each Isolate is a self-contained Dart environment + * and cannot communicate with each other except via Isolate ports. + */ +export default class FlutterEngine implements EngineLifecycleListener { + private engineLifecycleListeners = new Set(); + dartExecutor: DartExecutor; + private flutterLoader: FlutterLoader; + private assetManager: resourceManager.ResourceManager; + //channel定义 + private lifecycleChannel: LifecycleChannel | null = null; + private navigationChannel: NavigationChannel | null = null; + private textInputChannel: TextInputChannel | null = null; + private testChannel: TestChannel | null = null; + private platformChannel: PlatformChannel | null = null; + private sensitiveContentChannel: SensitiveContentChannel | null = null; + private systemChannel: SystemChannel | null = null; + private mouseCursorChannel: MouseCursorChannel | null = null; + private displayMetricsChannel: DisplayMetricsChannel | null = null; + private restorationChannel: RestorationChannel | null = null; + private accessibilityChannel: AccessibilityChannel | null = null; + private localeChannel: LocalizationChannel | null = null; + private flutterNapi: FlutterNapi; + private renderer: FlutterRenderer; + private pluginRegistry: FlutterEngineConnectionRegistry | null = null; + private textInputPlugin: TextInputPlugin | null = null; + private localizationPlugin: LocalizationPlugin | null = null; + private settingsChannel: SettingsChannel | null = null; + private platformViewsController: PlatformViewsController; + private nativeVsyncChannel: NativeVsyncChannel | null = null; + + /** + * Constructs a new FlutterEngine instance. + * Initializes DartExecutor, channels, plugins, FlutterLoader, FlutterNapi, and lifecycle listeners. + * @param context - The application context + * @param flutterLoader - Optional FlutterLoader instance, will be created if null + * @param flutterNapi - Optional FlutterNapi instance, will be created if null + * @param platformViewsController - Optional PlatformViewsController, will be created if null + */ + constructor(context: common.Context, flutterLoader: FlutterLoader | null, flutterNapi: FlutterNapi | null, + platformViewsController: PlatformViewsController | null) { + const injector: FlutterInjector = FlutterInjector.getInstance(); + if (flutterNapi == null) { + flutterNapi = FlutterInjector.getInstance().getFlutterNapi(); + } + this.flutterNapi = flutterNapi; + this.assetManager = context.resourceManager; + + this.dartExecutor = new DartExecutor(this.flutterNapi, this.assetManager); + this.dartExecutor.onAttachedToNAPI(); + + if (flutterLoader == null) { + flutterLoader = injector.getFlutterLoader(); + } + this.flutterLoader = flutterLoader; + + this.renderer = new FlutterRenderer(this.flutterNapi); + + if (platformViewsController == null) { + platformViewsController = new PlatformViewsController(); + } + this.platformViewsController = platformViewsController; + this.platformViewsController.attach(context, this.renderer, this.dartExecutor); + } + + /** + * Initializes the Flutter engine. + * Sets up all channels, plugins, and attaches to the native engine. + * @param context - The application context + * @param dartVmArgs - Optional Dart VM arguments + * @param waitForRestorationData - Whether to wait for restoration data + */ + init(context: common.Context, dartVmArgs: Array | null, waitForRestorationData: boolean) { + if (!this.flutterNapi.isAttached()) { + this.flutterLoader.startInitialization(context) + this.flutterLoader.ensureInitializationComplete(dartVmArgs); + } + //channel初始化 + this.lifecycleChannel = new LifecycleChannel(this.dartExecutor); + this.navigationChannel = new NavigationChannel(this.dartExecutor, context); + this.textInputChannel = new TextInputChannel(this.dartExecutor); + this.testChannel = new TestChannel(this.dartExecutor); + this.platformChannel = new PlatformChannel(this.dartExecutor, this.flutterNapi); + this.sensitiveContentChannel = new SensitiveContentChannel(this.dartExecutor); + this.systemChannel = new SystemChannel(this.dartExecutor); + this.mouseCursorChannel = new MouseCursorChannel(this.dartExecutor); + this.displayMetricsChannel = new DisplayMetricsChannel(this.dartExecutor, context); + this.restorationChannel = new RestorationChannel(this.dartExecutor, waitForRestorationData); + this.settingsChannel = new SettingsChannel(this.dartExecutor); + this.localeChannel = new LocalizationChannel(this.dartExecutor); + this.accessibilityChannel = new AccessibilityChannel(this.dartExecutor, this.flutterNapi); + this.flutterNapi.addEngineLifecycleListener(this); + this.localizationPlugin = new LocalizationPlugin(context, this.localeChannel); + this.nativeVsyncChannel = new NativeVsyncChannel(this.dartExecutor, this.flutterNapi); + + // It should typically be a fresh, unattached NAPI. But on a spawned engine, the NAPI instance + // is already attached to a native shell. In that case, the Java FlutterEngine is created around + // an existing shell. + if (!this.flutterNapi.isAttached()) { + this.attachToNapi(); + } + this.flutterNapi.setLocalizationPlugin(this.localizationPlugin); + + this.pluginRegistry = + new FlutterEngineConnectionRegistry(context.getApplicationContext(), this, this.flutterLoader); + this.localizationPlugin.sendLocaleToFlutter(); + Log.d(TAG, "Call init finished.") + } + + private attachToNapi(): void { + Log.d(TAG, "Attaching to NAPI."); + this.flutterNapi.attachToNative(); + if (!this.isAttachedToNapi()) { + throw new Error("FlutterEngine failed to attach to its native Object reference."); + } + } + + /** + * Spawns a new Flutter engine from this engine. + * The spawned engine shares resources with the parent engine. + * @param context - The application context + * @param dartEntrypoint - The Dart entrypoint configuration + * @param initialRoute - The initial route for navigation + * @param dartEntrypointArgs - Arguments for the Dart entrypoint + * @param platformViewsController - The platform views controller + * @param waitForRestorationData - Whether to wait for restoration data + * @returns The spawned FlutterEngine instance + * @throws Error if this engine is not fully constructed + */ + spawn(context: common.Context, + dartEntrypoint: DartEntrypoint, + initialRoute: string, + dartEntrypointArgs: Array, + platformViewsController: PlatformViewsController, + waitForRestorationData: boolean) { + if (!this.isAttachedToNapi()) { + throw new Error( + "Spawn can only be called on a fully constructed FlutterEngine"); + } + + const newFlutterNapi = + this.flutterNapi.spawn( + dartEntrypoint.dartEntrypointFunctionName, + dartEntrypoint.dartEntrypointLibrary, + initialRoute, + dartEntrypointArgs); + const flutterEngine = new FlutterEngine( + context, + null, + newFlutterNapi, + platformViewsController + ); + flutterEngine.init(context, null, waitForRestorationData) + return flutterEngine + } + + private isAttachedToNapi(): boolean { + return this.flutterNapi.isAttached(); + } + + /** + * Processes any pending messages from the native side. + */ + processPendingMessages() { + if (this.flutterNapi.isAttached()) { + this.flutterNapi.processPendingMessages(); + } + } + + /** + * Gets the lifecycle channel for managing application lifecycle events. + * @returns The LifecycleChannel instance, or null if not initialized + */ + getLifecycleChannel(): LifecycleChannel | null { + return this.lifecycleChannel; + } + + /** + * Gets the navigation channel for managing navigation events. + * @returns The NavigationChannel instance, or null if not initialized + */ + getNavigationChannel(): NavigationChannel | null { + return this.navigationChannel; + } + + /** + * Gets the text input channel for managing text input events. + * @returns The TextInputChannel instance, or null if not initialized + */ + getTextInputChannel(): TextInputChannel | null { + return this.textInputChannel; + } + + /** + * Gets the platform channel for platform-specific communication. + * @returns The PlatformChannel instance, or null if not initialized + */ + getPlatformChannel(): PlatformChannel | null { + return this.platformChannel; + } + + /** + * Gets the system channel for system-level communication. + * @returns The SystemChannel instance, or null if not initialized + */ + getSystemChannel(): SystemChannel | null { + return this.systemChannel; + } + + /** + * Gets the localization channel for managing locale information. + * @returns The LocalizationChannel instance, or null if not initialized + */ + getLocaleChannel(): LocalizationChannel | null { + return this.localeChannel; + } + + /** + * Gets the mouse cursor channel for managing cursor changes. + * @returns The MouseCursorChannel instance, or null if not initialized + */ + getMouseCursorChannel(): MouseCursorChannel | null { + return this.mouseCursorChannel; + } + + getDisplayMetricsChannel(): DisplayMetricsChannel | null { + return this.displayMetricsChannel; + } + + /** + * Gets the FlutterNapi instance for native communication. + * @returns The FlutterNapi instance + */ + getFlutterNapi(): FlutterNapi { + return this.flutterNapi; + } + + /** + * Gets the FlutterRenderer instance for rendering operations. + * @returns The FlutterRenderer instance + */ + getFlutterRenderer(): FlutterRenderer { + return this.renderer; + } + + /** + * Gets the DartExecutor instance for executing Dart code. + * @returns The DartExecutor instance + */ + getDartExecutor(): DartExecutor { + return this.dartExecutor + } + + getSensitiveContentChannel():SensitiveContentChannel |null { + return this.sensitiveContentChannel + } + + /** + * Gets the plugin registry for managing Flutter plugins. + * @returns The PluginRegistry instance, or null if not initialized + */ + getPlugins(): PluginRegistry | null { + return this.pluginRegistry; + } + + /** + * Gets the ability control surface for managing ability-related operations. + * @returns The AbilityControlSurface instance, or null if not initialized + */ + getAbilityControlSurface(): AbilityControlSurface | null { + return this.pluginRegistry; + } + + /** + * Gets the settings channel for managing application settings. + * @returns The SettingsChannel instance, or null if not initialized + */ + getSettingsChannel() { + return this.settingsChannel; + } + + /** + * Gets the FlutterLoader instance for loading Flutter assets. + * @returns The FlutterLoader instance + */ + getFlutterLoader() { + return this.flutterLoader; + } + + /** + * Called before the engine restarts. + * Notifies all registered lifecycle listeners. + */ + onPreEngineRestart(): void { + this.engineLifecycleListeners.forEach(listener => listener.onPreEngineRestart()) + } + + /** + * Called when the engine is about to be destroyed. + */ + onEngineWillDestroy(): void { + + } + + /** + * Adds a lifecycle listener to be notified of engine lifecycle events. + * @param listener - The EngineLifecycleListener to add + */ + addEngineLifecycleListener(listener: EngineLifecycleListener): void { + this.engineLifecycleListeners.add(listener); + } + + /** + * Removes a lifecycle listener from the list of registered listeners. + * @param listener - The EngineLifecycleListener to remove + */ + removeEngineLifecycleListener(listener: EngineLifecycleListener): void { + this.engineLifecycleListeners.delete(listener); + } + + /** + * Destroys the Flutter engine and releases all resources. + * Notifies listeners, detaches from ability, and cleans up all components. + */ + destroy(): void { + Log.d(TAG, "Destroying."); + this.engineLifecycleListeners.forEach(listener => listener.onEngineWillDestroy()) + this.flutterNapi.removeEngineLifecycleListener(this); + this.pluginRegistry?.detachFromAbility(); + this.platformViewsController?.onDetachedFromNapi(); + this.pluginRegistry?.destroy(); + this.dartExecutor.onDetachedFromNAPI(); + this.flutterNapi.detachFromNativeAndReleaseResources(); + } + + /** + * Gets the restoration channel for managing state restoration. + * @returns The RestorationChannel instance, or null if not initialized + */ + getRestorationChannel(): RestorationChannel | null { + return this.restorationChannel; + } + + /** + * Gets the accessibility channel for managing accessibility features. + * @returns The AccessibilityChannel instance, or null if not initialized + */ + getAccessibilityChannel(): AccessibilityChannel | null { + return this.accessibilityChannel; + } + + /** + * Gets the localization plugin for managing locale information. + * @returns The LocalizationPlugin instance, or null if not initialized + */ + getLocalizationPlugin(): LocalizationPlugin | null { + return this.localizationPlugin; + } + + /** + * Gets the system languages from the native side. + */ + getSystemLanguages(): void { + return this.flutterNapi.getSystemLanguages(); + } + + /** + * Gets the platform views controller for managing platform views. + * @returns The PlatformViewsController instance, or null if not initialized + */ + getPlatformViewsController(): PlatformViewsController | null { + return this.platformViewsController; + } + + /** + * Gets the native VSync channel for managing vertical synchronization. + * @returns The NativeVsyncChannel instance, or null if not initialized + */ + getNativeVsyncChannel(): NativeVsyncChannel | null { + return this.nativeVsyncChannel; + } + + /** + * Prefetches frame configuration for improved performance. + */ + async prefetchFramesCfg(): Promise { + FlutterNapi.prefetchFramesCfg(); + } +} + +/** + * Interface for listening to Flutter engine lifecycle events. + */ +export interface EngineLifecycleListener { + /** + * Called before the engine restarts. + */ + onPreEngineRestart(): void; + + /** + * Called when the engine is about to be destroyed. + */ + onEngineWillDestroy(): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineCache.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineCache.ets new file mode 100644 index 0000000..f530204 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineCache.ets @@ -0,0 +1,84 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterEngineCache.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import FlutterEngine from "./FlutterEngine" + +/** + * Static singleton cache that holds FlutterEngine instances identified by Strings. + * + * The ID of a given FlutterEngine can be whatever String is desired. + * + * FlutterEngineCache is useful for storing pre-warmed FlutterEngine instances. FlutterAbility + * and FlutterEntry can use cached FlutterEngine instances by implementing getCachedEngineId() + * to return a cached engine ID. The FlutterAbilityAndEntryDelegate will then retrieve the + * engine from this cache. See FlutterAbility.getCachedEngineId() and FlutterEntry.getCachedEngineId() + * for related APIs. + */ +export default class FlutterEngineCache { + private static instance: FlutterEngineCache; + private cachedEngines: Map = new Map(); + + /** + * Gets the singleton instance of FlutterEngineCache. + * @returns The FlutterEngineCache instance + */ + static getInstance(): FlutterEngineCache { + if (FlutterEngineCache.instance == null) { + FlutterEngineCache.instance = new FlutterEngineCache(); + } + return FlutterEngineCache.instance; + } + + /** + * Checks if an engine with the given ID exists in the cache. + * @param engineId - The ID of the engine to check + * @returns true if the engine exists, false otherwise + */ + contains(engineId: String): boolean { + return this.cachedEngines.has(engineId); + } + + /** + * Gets an engine from the cache by ID. + * @param engineId - The ID of the engine to retrieve + * @returns The FlutterEngine instance, or null if not found + */ + get(engineId: String): FlutterEngine | null { + return this.cachedEngines.get(engineId) || null; + } + + /** + * Puts an engine into the cache or removes it if null is provided. + * @param engineId - The ID of the engine + * @param engine - The FlutterEngine instance to cache, or null to remove + */ + put(engineId: String, engine: FlutterEngine | null): void { + if (engine != null) { + this.cachedEngines.set(engineId, engine); + } else { + this.cachedEngines.delete(engineId); + } + } + + /** + * Removes an engine from the cache by ID. + * @param engineId - The ID of the engine to remove + */ + remove(engineId: String): void { + this.put(engineId, null); + } + + /** + * Clears all engines from the cache. + */ + clear(): void { + this.cachedEngines.clear(); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineConnectionRegistry.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineConnectionRegistry.ets new file mode 100644 index 0000000..74b5f20 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineConnectionRegistry.ets @@ -0,0 +1,426 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterEngineConnectionRegistry.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import PluginRegistry from './plugins/PluginRegistry'; +import { FlutterAssets, FlutterPlugin, FlutterPluginBinding } from './plugins/FlutterPlugin'; +import FlutterEngine from './FlutterEngine'; +import AbilityAware from './plugins/ability/AbilityAware'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import { + AbilityPluginBinding, + WindowFocusChangedListener, + OnSaveStateListener, + NewWantListener +} from './plugins/ability/AbilityPluginBinding'; +import HashSet from '@ohos.util.HashSet'; +import Want from '@ohos.app.ability.Want'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import common from '@ohos.app.ability.common'; +import FlutterLoader from './loader/FlutterLoader'; +import Log from '../../util/Log'; +import ToolUtils from '../../util/ToolUtils'; +import AbilityControlSurface from './plugins/ability/AbilityControlSurface'; +import ExclusiveAppComponent from '../ohos/ExclusiveAppComponent'; +import FlutterEngineGroup from './FlutterEngineGroup'; +import Any from '../../plugin/common/Any'; + +const TAG = "FlutterEngineCxnRegistry"; + +/** + * Registry for managing Flutter plugins and their connections to the engine and ability. + * This class implements both PluginRegistry and AbilityControlSurface interfaces, + * providing a unified way to manage plugin lifecycle and ability-related operations. + */ +export default class FlutterEngineConnectionRegistry implements PluginRegistry, AbilityControlSurface { + private plugins = new Map(); + private defaultPlugin: FlutterPlugin = new EmptyPlugin(); + private flutterEngine: FlutterEngine; + private pluginBinding: FlutterPluginBinding; + private abilityAwarePlugins = new Map(); + private exclusiveAbility: ExclusiveAppComponent | null = null; + private abilityPluginBinding: FlutterEngineAbilityPluginBinding | null = null; + + /** + * Constructs a new FlutterEngineConnectionRegistry instance. + * @param appContext - The application context + * @param flutterEngine - The FlutterEngine instance this registry is associated with + * @param flutterLoader - The FlutterLoader instance for asset management + */ + constructor(appContext: common.Context, flutterEngine: FlutterEngine, flutterLoader: FlutterLoader) { + this.flutterEngine = flutterEngine; + this.pluginBinding = new FlutterPluginBinding(appContext, flutterEngine, flutterEngine.getDartExecutor(), + new DefaultFlutterAssets(flutterLoader), flutterEngine.getFlutterRenderer(), + flutterEngine.getPlatformViewsController()?.getRegistry()); + } + + /** + * Adds a plugin to this registry. + * @param plugin - The FlutterPlugin instance to add + */ + add(plugin: FlutterPlugin): void { + try { + if (this.has(plugin.getUniqueClassName())) { + Log.w( + TAG, + "Attempted to register plugin (" + + plugin + + ") but it was " + + "already registered with this FlutterEngine (" + + this.flutterEngine + + ")."); + return; + } + + Log.w(TAG, "Adding plugin: " + plugin.getUniqueClassName()); + // Add the plugin to our generic set of plugins and notify the plugin + // that is has been attached to an engine. + this.plugins.set(plugin.getUniqueClassName(), plugin); + plugin.onAttachedToEngine(this.pluginBinding); + + // For AbilityAware plugins, add the plugin to our set of AbilityAware + // plugins, and if this engine is currently attached to an Ability, + // notify the AbilityAware plugin that it is now attached to an Ability. + if (ToolUtils.implementsInterface(plugin, "onAttachedToAbility")) { + const abilityAware: Any = plugin; + this.abilityAwarePlugins.set(plugin.getUniqueClassName(), abilityAware); + if (this.isAttachedToAbility()) { + abilityAware.onAttachedToAbility(this.abilityPluginBinding); + } + } + } finally { + + } + } + + /** + * Adds multiple plugins to this registry. + * @param plugins - Set of FlutterPlugin instances to add + */ + addList(plugins: Set): void { + plugins.forEach(plugin => this.add(plugin)) + } + + /** + * Checks if a plugin with the given class name is registered. + * @param pluginClassName - The class name of the plugin to check + * @returns true if the plugin is registered, false otherwise + */ + has(pluginClassName: string): boolean { + return this.plugins.has(pluginClassName); + } + + /** + * Gets a plugin by its class name. + * @param pluginClassName - The class name of the plugin to retrieve + * @returns The FlutterPlugin instance, or a default empty plugin if not found + */ + get(pluginClassName: string): FlutterPlugin { + return this.plugins.get(pluginClassName) ?? this.defaultPlugin; + } + + /** + * Removes a plugin from this registry. + * @param pluginClassName - The class name of the plugin to remove + */ + remove(pluginClassName: string): void { + const plugin = this.plugins.get(pluginClassName); + if (plugin == null) { + return; + } + if (ToolUtils.implementsInterface(plugin, "onAttachedToAbility")) { + if (this.isAttachedToAbility()) { + const abilityAware: Any = plugin; + abilityAware.onDetachedFromAbility(); + } + this.abilityAwarePlugins.delete(pluginClassName); + } + // Notify the plugin that is now detached from this engine. Then remove + // it from our set of generic plugins. + plugin.onDetachedFromEngine(this.pluginBinding); + this.plugins.delete(pluginClassName) + } + + /** + * Removes multiple plugins from this registry. + * @param pluginClassNames - Set of plugin class names to remove + */ + removeList(pluginClassNames: Set): void { + pluginClassNames.forEach(plugin => this.remove(plugin)) + } + + /** + * Removes all plugins from this registry. + */ + removeAll(): void { + this.removeList(new Set(this.plugins.keys())); + this.plugins.clear(); + } + + private isAttachedToAbility(): boolean { + return this.exclusiveAbility != null; + } + + /** + * Attaches this registry to an ability. + * @param exclusiveAbility - The exclusive app component (UIAbility) to attach to + */ + attachToAbility(exclusiveAbility: ExclusiveAppComponent): void { + if (this.exclusiveAbility != null) { + this.exclusiveAbility.detachFromFlutterEngine(); + } + // If we were already attached to an app component, detach from it. + this.detachFromAppComponent(); + this.exclusiveAbility = exclusiveAbility; + this.attachToAbilityInternal(exclusiveAbility.getAppComponent(),); + } + + /** + * Detaches this registry from the current ability. + */ + detachFromAbility(): void { + if (this.isAttachedToAbility()) { + try { + this.abilityAwarePlugins.forEach(abilityAware => abilityAware.onDetachedFromAbility()) + } catch (e) { + Log.e(TAG, "abilityAwarePlugins DetachedFromAbility failed, msg:" + e); + } + this.detachFromAbilityInternal(); + } else { + Log.e(TAG, "Attempted to detach plugins from an Ability when no Ability was attached."); + } + } + + /** + * Handles a new Want event from the ability. + * @param want - The Want object containing the intent + * @param launchParams - The launch parameters + */ + onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void { + this.abilityPluginBinding?.onNewWant(want, launchParams); + } + + /** + * Handles window focus change events from the ability. + * @param hasFocus - Whether the window has focus + */ + onWindowFocusChanged(hasFocus: boolean): void { + this.abilityPluginBinding?.onWindowFocusChanged(hasFocus); + } + + /** + * Handles save state requests from the ability. + * @param reason - The reason for saving state + * @param wantParam - Parameters to save + * @returns The result of the save state operation + */ + onSaveState(reason: AbilityConstant.StateType, wantParam: Record): AbilityConstant.OnSaveResult { + return this.abilityPluginBinding?.onSaveState(reason, wantParam) ?? AbilityConstant.OnSaveResult.ALL_REJECT; + } + + private detachFromAppComponent(): void { + if (this.isAttachedToAbility()) { + this.detachFromAbility(); + } + } + + private attachToAbilityInternal(ability: UIAbility): void { + this.abilityPluginBinding = new FlutterEngineAbilityPluginBinding(ability); + // Notify all AbilityAware plugins that they are now attached to a new Ability. + this.abilityAwarePlugins.forEach(abilityAware => abilityAware.onAttachedToAbility(this.abilityPluginBinding!)); + } + + private detachFromAbilityInternal(): void { + this.exclusiveAbility = null; + this.abilityPluginBinding = null; + } + + /** + * Destroys this registry and removes all plugins. + */ + destroy(): void { + this.detachFromAppComponent(); + // Remove all registered plugins. + this.removeAll(); + } +} + +/** + * Implementation of AbilityPluginBinding for FlutterEngine. + * Manages ability-related listeners and events for plugins. + */ +class FlutterEngineAbilityPluginBinding implements AbilityPluginBinding { + private ability: UIAbility; + private onNewWantListeners = new HashSet(); + private onWindowFocusChangedListeners = new HashSet(); + private onSaveStateListeners = new HashSet(); + + /** + * Constructs a new FlutterEngineAbilityPluginBinding instance. + * @param ability - The UIAbility instance this binding is associated with + */ + constructor(ability: UIAbility) { + this.ability = ability; + + } + + /** + * Gets the UIAbility instance. + * @returns The UIAbility instance + */ + getAbility(): UIAbility { + return this.ability; + } + + /** + * Adds a listener for new Want events. + * @param listener - The NewWantListener to add + */ + addOnNewWantListener(listener: NewWantListener): void { + this.onNewWantListeners.add(listener) + } + + /** + * Removes a listener for new Want events. + * @param listener - The NewWantListener to remove + */ + removeOnNewWantListener(listener: NewWantListener): void { + this.onNewWantListeners.remove(listener) + } + + /** + * Adds a listener for window focus change events. + * @param listener - The WindowFocusChangedListener to add + */ + addOnWindowFocusChangedListener(listener: WindowFocusChangedListener): void { + this.onWindowFocusChangedListeners.add(listener) + } + + /** + * Removes a listener for window focus change events. + * @param listener - The WindowFocusChangedListener to remove + */ + removeOnWindowFocusChangedListener(listener: WindowFocusChangedListener): void { + this.onWindowFocusChangedListeners.remove(listener) + } + + /** + * Adds a listener for save state events. + * @param listener - The OnSaveStateListener to add + */ + addOnSaveStateListener(listener: OnSaveStateListener) { + this.onSaveStateListeners.add(listener) + } + + /** + * Removes a listener for save state events. + * @param listener - The OnSaveStateListener to remove + */ + removeOnSaveStateListener(listener: OnSaveStateListener) { + this.onSaveStateListeners.remove(listener) + } + + /** + * Notifies all registered listeners of a new Want event. + * @param want - The Want object + * @param launchParams - The launch parameters + */ + onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void { + this.onNewWantListeners.forEach((listener, key) => { + listener?.onNewWant(want, launchParams) + }); + } + + /** + * Notifies all registered listeners of a window focus change. + * @param hasFocus - Whether the window has focus + */ + onWindowFocusChanged(hasFocus: boolean): void { + this.onWindowFocusChangedListeners.forEach((listener, key) => { + listener?.onWindowFocusChanged(hasFocus) + }); + } + + /** + * Notifies all registered listeners of a save state request. + * @param reason - The reason for saving state + * @param wantParam - Parameters to save + * @returns The result of the save state operation + */ + onSaveState(reason: AbilityConstant.StateType, wantParam: Record): AbilityConstant.OnSaveResult { + this.onSaveStateListeners.forEach((listener, key) => { + listener?.onSaveState(reason, wantParam) + }); + return AbilityConstant.OnSaveResult.ALL_AGREE; + } +} + +/** + * Default implementation of FlutterAssets using FlutterLoader. + */ +class DefaultFlutterAssets implements FlutterAssets { + private flutterLoader: FlutterLoader; + + /** + * Constructs a new DefaultFlutterAssets instance. + * @param flutterLoader - The FlutterLoader instance for asset lookup + */ + constructor(flutterLoader: FlutterLoader) { + this.flutterLoader = flutterLoader; + } + + /** + * Gets the file path for an asset by name. + * @param assetFileName - The name of the asset file + * @param packageName - Optional package name + * @returns The file path for the asset + */ + getAssetFilePathByName(assetFileName: string, packageName?: string): string { + return this.flutterLoader.getLookupKeyForAsset(assetFileName, packageName); + } + + /** + * Gets the file path for an asset by subpath. + * @param assetSubpath - The subpath of the asset + * @param packageName - Optional package name + * @returns The file path for the asset + */ + getAssetFilePathBySubpath(assetSubpath: string, packageName?: string) { + return this.flutterLoader.getLookupKeyForAsset(assetSubpath, packageName); + } +} + +/** + * Empty plugin implementation used as a default when a plugin is not found. + */ +class EmptyPlugin implements FlutterPlugin { + /** + * Gets the unique class name of this plugin. + * @returns An empty string + */ + getUniqueClassName(): string { + return ''; + } + + /** + * Called when this plugin is attached to an engine. + * @param binding - The FlutterPluginBinding instance + */ + onAttachedToEngine(binding: FlutterPluginBinding) { + + } + + /** + * Called when this plugin is detached from an engine. + * @param binding - The FlutterPluginBinding instance + */ + onDetachedFromEngine(binding: FlutterPluginBinding) { + + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineGroup.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineGroup.ets new file mode 100644 index 0000000..6db5011 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineGroup.ets @@ -0,0 +1,292 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterEngineGroup.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import FlutterEngine, { EngineLifecycleListener } from "./FlutterEngine" +import common from '@ohos.app.ability.common' +import display from '@ohos.display'; +import FlutterLoader from './loader/FlutterLoader' +import FlutterInjector from '../../FlutterInjector' +import { DartEntrypoint } from './dart/DartExecutor' +import PlatformViewsController from '../../plugin/platform/PlatformViewsController' +import ArrayList from '@ohos.util.ArrayList' +import Log from '../../util/Log'; +import FlutterManager from '../ohos/FlutterManager'; + +const TAG = "FlutterEngineGroup" + +/** + * Represents a collection of FlutterEngines who share resources to allow them to be created + * faster and with less memory than calling the FlutterEngine's constructor multiple times. + * + * When creating or recreating the first FlutterEngine in the FlutterEngineGroup, the behavior + * is the same as creating a FlutterEngine via its constructor. When subsequent FlutterEngines + * are created, resources from an existing living FlutterEngine is re-used. + * + * The shared resources are kept until the last surviving FlutterEngine is destroyed. + * + * Deleting a FlutterEngineGroup doesn't invalidate its existing FlutterEngines, but it eliminates + * the possibility to create more FlutterEngines in that group. + */ +export default class FlutterEngineGroup { + private activeEngines: ArrayList = new ArrayList(); + + /** + * Constructs a new FlutterEngineGroup instance. + */ + constructor() { + + } + + /** + * Checks and initializes the FlutterLoader if not already initialized. + * @param context - The application context + * @param args - Command-line arguments for Dart VM initialization + */ + checkLoader(context: common.Context, args: Array) { + let loader: FlutterLoader = FlutterInjector.getInstance().getFlutterLoader(); + if (!loader.initialized) { + loader.startInitialization(context); + loader.ensureInitializationComplete(args); + } + } + + /** + * Creates and runs a Flutter engine based on the provided options. + * If no engines exist, creates a new engine. Otherwise, spawns from the first engine. + * @param options - Configuration options for engine creation + * @returns The created or spawned FlutterEngine instance + */ + createAndRunEngineByOptions(options: Options) { + let engine: FlutterEngine | null = null; + let context: common.Context = options.getContext(); + let dartEntrypoint: DartEntrypoint | null = options.getDartEntrypoint(); + let initialRoute: string = options.getInitialRoute(); + let dartEntrypointArgs: Array = options.getDartEntrypointArgs(); + let platformViewsController: PlatformViewsController | null = options.getPlatformViewsController(); + let waitForRestorationData: boolean = options.getWaitForRestorationData(); + + if (dartEntrypoint == null) { + dartEntrypoint = DartEntrypoint.createDefault(); + } + + if (platformViewsController == null) { + platformViewsController = new PlatformViewsController(); + } + + Log.i(TAG, "shellHolder, this.activeEngines.length=" + this.activeEngines.length) + if (this.activeEngines.length == 0) { + engine = this.createEngine(context, platformViewsController); + engine.init(context, null, // String[]. The Dart VM has already started, this arguments will have no effect. + waitForRestorationData) + if (initialRoute != null) { + engine.getNavigationChannel()?.setInitialRoute(initialRoute); + } + engine.getDartExecutor().executeDartEntrypoint(dartEntrypoint, dartEntrypointArgs); + engine.prefetchFramesCfg(); + } else { + engine = this.activeEngines[0] + .spawn( + context, + dartEntrypoint, + initialRoute, + dartEntrypointArgs, + platformViewsController, + waitForRestorationData); + } + this.activeEngines.add(engine); + + const engineToCleanUpOnDestroy = engine; + let listener: EngineLifecycleListener = new EngineLifecycleListenerImpl( + platformViewsController, + this.activeEngines, + engineToCleanUpOnDestroy); + engine?.addEngineLifecycleListener(listener); + return engine; + } + + /** + * Creates a new FlutterEngine instance. + * @param context - The application context + * @param platformViewsController - The platform views controller for the engine + * @returns A new FlutterEngine instance + */ + createEngine(context: common.Context, platformViewsController: PlatformViewsController): FlutterEngine { + return new FlutterEngine(context, null, null, platformViewsController); + } + + /** + * Gets the default (first) engine in this group. + * @returns The default FlutterEngine, or null if no engines exist + */ + getDefaultEngine(): FlutterEngine | null { + let engine: FlutterEngine | null = null; + if (this.activeEngines.length != 0) { + engine = this.activeEngines[0]; + } + return engine; + } +} + +/** + * Implementation of EngineLifecycleListener for managing engine lifecycle in a group. + */ +class EngineLifecycleListenerImpl implements EngineLifecycleListener { + private platformViewsController: PlatformViewsController; + private activeEngines: ArrayList = new ArrayList(); + private engine: FlutterEngine | null; + + /** + * Constructs a new EngineLifecycleListenerImpl instance. + * @param platformViewsController - The platform views controller + * @param activeEngines - List of active engines in the group + * @param engine - The engine this listener is associated with + */ + constructor( + platformViewsController: PlatformViewsController, + activeEngines: ArrayList, + engine: FlutterEngine | null) { + this.platformViewsController = platformViewsController; + this.activeEngines = activeEngines; + this.engine = engine; + } + + /** + * Called before the engine restarts. + */ + onPreEngineRestart(): void { + this.platformViewsController.onPreEngineRestart(); + } + + /** + * Called when the engine is about to be destroyed. + * Removes the engine from the active engines list. + */ + onEngineWillDestroy(): void { + this.activeEngines.remove(this.engine); + } +} + +/** + * Options that control how a FlutterEngine should be created.. + */ +export class Options { + private context: common.Context; + private dartEntrypoint: DartEntrypoint | null = null; + private initialRoute: string = ''; + private dartEntrypointArgs: Array = []; + private platformViewsController: PlatformViewsController | null = null; + private waitForRestorationData: boolean = false; + + /** + * Constructs a new Options instance. + * @param context - The application context + */ + constructor(context: common.Context) { + this.context = context; + } + + /** + * Gets the application context. + * @returns The context + */ + getContext(): common.Context { + return this.context; + } + + /** + * Gets the Dart entrypoint configuration. + * @returns The DartEntrypoint, or null if not set + */ + getDartEntrypoint(): DartEntrypoint | null { + return this.dartEntrypoint; + } + + /** + * Gets the initial route for navigation. + * @returns The initial route string + */ + getInitialRoute(): string { + return this.initialRoute; + } + + /** + * Gets the Dart entrypoint arguments. + * @returns Array of entrypoint arguments + */ + getDartEntrypointArgs(): Array { + return this.dartEntrypointArgs; + } + + /** + * Gets whether to wait for restoration data. + * @returns true if waiting for restoration data, false otherwise + */ + getWaitForRestorationData(): boolean { + return this.waitForRestorationData; + } + + /** + * Gets the platform views controller. + * @returns The PlatformViewsController, or null if not set + */ + getPlatformViewsController(): PlatformViewsController | null { + return this.platformViewsController; + } + + /** + * Sets the Dart entrypoint configuration. + * @param dartEntrypoint - The DartEntrypoint to set + * @returns This Options instance for method chaining + */ + setDartEntrypoint(dartEntrypoint: DartEntrypoint): Options { + this.dartEntrypoint = dartEntrypoint; + return this; + } + + /** + * Sets the initial route for navigation. + * @param initialRoute - The initial route string + * @returns This Options instance for method chaining + */ + setInitialRoute(initialRoute: string): Options { + this.initialRoute = initialRoute; + return this; + } + + /** + * Sets the Dart entrypoint arguments. + * @param dartEntrypointArgs - Array of entrypoint arguments + * @returns This Options instance for method chaining + */ + setDartEntrypointArgs(dartEntrypointArgs: Array): Options { + this.dartEntrypointArgs = dartEntrypointArgs; + return this; + } + + /** + * Sets whether to wait for restoration data. + * @param waitForRestorationData - Whether to wait for restoration data + * @returns This Options instance for method chaining + */ + setWaitForRestorationData(waitForRestorationData: boolean): Options { + this.waitForRestorationData = waitForRestorationData; + return this; + } + + /** + * Sets the platform views controller. + * @param platformViewsController - The PlatformViewsController to set + * @returns This Options instance for method chaining + */ + setPlatformViewsController(platformViewsController: PlatformViewsController): Options { + this.platformViewsController = platformViewsController; + return this; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineGroupCache.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineGroupCache.ets new file mode 100644 index 0000000..8d1d7d3 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEngineGroupCache.ets @@ -0,0 +1,61 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import FlutterEngineGroup from './FlutterEngineGroup'; + +/** + * Static singleton cache that holds FlutterEngineGroup instances identified by Strings. + * + * The ID of a given FlutterEngineGroup can be whatever String is desired. + * + * FlutterEngineGroupCache is useful for storing pre-warmed FlutterEngineGroup instances. FlutterAbility + * and FlutterEntry can use cached FlutterEngineGroup instances by implementing getCachedEngineGroupId() + * to return a cached engine group ID. The FlutterAbilityAndEntryDelegate will then retrieve the + * engine group from this cache and create new engines within that group. See FlutterAbility.getCachedEngineGroupId() + * and FlutterEntry.getCachedEngineGroupId() for related APIs. + */ +export default class FlutterEngineGroupCache { + static readonly instance = new FlutterEngineGroupCache(); + private cachedEngineGroups = new Map(); + + /** + * Checks if an engine group with the given ID exists in the cache. + * @param engineGroupId - The ID of the engine group to check + * @returns true if the engine group exists, false otherwise + */ + contains(engineGroupId: string): boolean { + return this.cachedEngineGroups.has(engineGroupId); + } + + /** + * Gets an engine group from the cache by ID. + * @param engineGroupId - The ID of the engine group to retrieve + * @returns The FlutterEngineGroup instance, or null if not found + */ + get(engineGroupId: string): FlutterEngineGroup | null { + return this.cachedEngineGroups.get(engineGroupId) ?? null; + } + + /** + * Puts an engine group into the cache or removes it if null is provided. + * @param engineGroupId - The ID of the engine group + * @param engineGroup - The FlutterEngineGroup instance to cache, or undefined to remove + */ + put(engineGroupId: string, engineGroup?: FlutterEngineGroup) { + if (engineGroup != null) { + this.cachedEngineGroups.set(engineGroupId, engineGroup); + } else { + this.cachedEngineGroups.delete(engineGroupId); + } + } + + /** + * Clears all engine groups from the cache. + */ + clear(): void { + this.cachedEngineGroups.clear(); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEnginePreload.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEnginePreload.ets new file mode 100644 index 0000000..40da2a1 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterEnginePreload.ets @@ -0,0 +1,213 @@ +/* +* Copyright (c) 2025 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import FlutterEngine, { EngineLifecycleListener } from "./FlutterEngine" +import common from '@ohos.app.ability.common' +import display from '@ohos.display'; +import FlutterLoader from './loader/FlutterLoader' +import Log from '../../util/Log'; +import FlutterManager from '../ohos/FlutterManager'; +import FlutterInjector from '../../FlutterInjector' +import FlutterEngineCache from './FlutterEngineCache'; +import FlutterEngineGroupCache from './FlutterEngineGroupCache'; +import FlutterAbilityLaunchConfigs from '../ohos/FlutterAbilityLaunchConfigs'; +import JSONMethodCodec from '../../plugin/common/JSONMethodCodec'; +import MethodCall from '../../plugin/common/MethodCall'; +import FlutterNapi from './FlutterNapi'; +import { ViewportMetrics } from '../../view/FlutterView'; + +const TAG = "FlutterEnginePreload" + +/** + * Utility class for preloading Flutter engines. + * This class provides static methods to preload and prepare Flutter engines + * before they are actually displayed, improving startup performance. + */ +export default class FlutterEnginePreload { + /** + * Preloads a Flutter engine with the specified parameters. + * @param context - The application context + * @param params - Parameters for engine preloading, including entrypoint, route, etc. + * @param nextViewId - Optional view ID for the next Flutter view + */ + static async preloadEngine(context: common.Context, params: Record = {}, + nextViewId: string | null = null) { + let loader: FlutterLoader = FlutterInjector.getInstance().getFlutterLoader(); + let dartArgs = new Array(); + if (params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS]) { + dartArgs = params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS] as Array; + } + + if (!loader.initialized) { + loader.startInitialization(context); + loader.ensureInitializationComplete(dartArgs); + } + + let flutterNapi: FlutterNapi | null = + FlutterEnginePreload.preLoadFlutterNapi(context, loader.findAppBundlePath(), params) + let viewportMetrics: ViewportMetrics = new ViewportMetrics(); + if (params[FlutterAbilityLaunchConfigs.PRELOAD_VIEWPORT_METRICS_KEY]) { + viewportMetrics = params[FlutterAbilityLaunchConfigs.PRELOAD_VIEWPORT_METRICS_KEY] as ViewportMetrics; + } else { + let display_info: display.Display = display.getDefaultDisplaySync(); + viewportMetrics.physicalWidth = display_info.width; + viewportMetrics.physicalHeight = display_info.height; + viewportMetrics.devicePixelRatio = display_info.densityPixels; + viewportMetrics.physicalTouchSlop = 1.0 * display_info.densityPixels; + } + if (flutterNapi) { + if (!nextViewId) { + nextViewId = FlutterManager.getInstance().getNextFlutterViewId(); + } + flutterNapi.setPreloading(); + flutterNapi.xComponentPreDraw(nextViewId, viewportMetrics.physicalWidth, viewportMetrics.physicalHeight); + flutterNapi.setViewportMetrics(viewportMetrics.devicePixelRatio, + viewportMetrics.physicalWidth, + viewportMetrics.physicalHeight, + viewportMetrics.physicalViewPaddingTop, + viewportMetrics.physicalViewPaddingRight, + viewportMetrics.physicalViewPaddingBottom, + viewportMetrics.physicalViewPaddingLeft, + viewportMetrics.physicalViewInsetTop, + viewportMetrics.physicalViewInsetRight, + viewportMetrics.physicalViewInsetBottom, + viewportMetrics.physicalViewInsetLeft, + viewportMetrics.systemGestureInsetTop, + viewportMetrics.systemGestureInsetRight, + viewportMetrics.systemGestureInsetBottom, + viewportMetrics.systemGestureInsetLeft, + viewportMetrics.physicalTouchSlop, + new Array(0), + new Array(0), + new Array(0) + ); + } + } + + /** + * Prepares an existing engine for drawing by setting up viewport metrics and pre-drawing. + * @param engine - The FlutterEngine instance to prepare + * @param params - Parameters for engine preparation, including viewport metrics + * @param nextViewId - Optional view ID for the next Flutter view + */ + static predrawEngine(engine: FlutterEngine, params: Record = {}, nextViewId: string | null = null) { + if (!engine) { + return; + } + let flutterNapi = engine.getFlutterNapi(); + if (!flutterNapi.isAttached()) { + flutterNapi.attachToNative(); + } + if (!nextViewId) { + nextViewId = FlutterManager.getInstance().getNextFlutterViewId(); + } + let viewportMetrics: ViewportMetrics = new ViewportMetrics(); + if (params[FlutterAbilityLaunchConfigs.PRELOAD_VIEWPORT_METRICS_KEY]) { + viewportMetrics = params[FlutterAbilityLaunchConfigs.PRELOAD_VIEWPORT_METRICS_KEY] as ViewportMetrics; + } else { + let display_info: display.Display = display.getDefaultDisplaySync(); + viewportMetrics.physicalWidth = display_info.width; + viewportMetrics.physicalHeight = display_info.height; + viewportMetrics.devicePixelRatio = display_info.densityPixels; + viewportMetrics.physicalTouchSlop = 1.0 * display_info.densityPixels; + } + flutterNapi.setPreloading(); + flutterNapi.xComponentPreDraw(nextViewId, viewportMetrics.physicalWidth, viewportMetrics.physicalHeight); + flutterNapi.setViewportMetrics(viewportMetrics.devicePixelRatio, + viewportMetrics.physicalWidth, + viewportMetrics.physicalHeight, + viewportMetrics.physicalViewPaddingTop, + viewportMetrics.physicalViewPaddingRight, + viewportMetrics.physicalViewPaddingBottom, + viewportMetrics.physicalViewPaddingLeft, + viewportMetrics.physicalViewInsetTop, + viewportMetrics.physicalViewInsetRight, + viewportMetrics.physicalViewInsetBottom, + viewportMetrics.physicalViewInsetLeft, + viewportMetrics.systemGestureInsetTop, + viewportMetrics.systemGestureInsetRight, + viewportMetrics.systemGestureInsetBottom, + viewportMetrics.systemGestureInsetLeft, + viewportMetrics.physicalTouchSlop, + new Array(0), + new Array(0), + new Array(0) + ); + } + + /** + * Preloads a FlutterNapi instance with the specified configuration. + * @param context - The application context + * @param bundlePath - Path to the Flutter bundle + * @param params - Parameters for NAPI preloading, including cached engine ID, entrypoint, etc. + * @returns The preloaded FlutterNapi instance, or null if preloading fails + */ + static preLoadFlutterNapi(context: common.Context, bundlePath: string, + params: Record = {}): FlutterNapi | null { + + let cachedEngineId = params[FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_ID] as string; + let cachedEngineGroupId = params[FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_GROUP_ID] as string; + + let dartEntrypoint = FlutterAbilityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; + if (params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT]) { + dartEntrypoint = params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT] as string; + } + + let dartEntrypointLibraryUri = ""; + if (params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_LIBRARY_URI]) { + dartEntrypointLibraryUri = params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_LIBRARY_URI] as string; + } + + let initialRoute = ""; + if (params[FlutterAbilityLaunchConfigs.EXTRA_INITIAL_ROUTE]) { + initialRoute = params[FlutterAbilityLaunchConfigs.EXTRA_INITIAL_ROUTE] as string + } + + let dartArgs = new Array(); + if (params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS]) { + dartArgs = params[FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS] as Array; + } + + Log.d(TAG, "cachedEngineId=" + cachedEngineId); + let flutterNapi: FlutterNapi | null = null; + if (cachedEngineId && cachedEngineId.length > 0) { + let engine = FlutterEngineCache.getInstance().get(cachedEngineId); + if (engine) { + flutterNapi = engine.getFlutterNapi(); + } + } + if (cachedEngineGroupId && cachedEngineGroupId.length > 0) { + let flutterEngineGroup = FlutterEngineGroupCache.instance.get(cachedEngineGroupId); + if (flutterEngineGroup) { + let defaultEngine = flutterEngineGroup.getDefaultEngine(); + if (defaultEngine) { + let oldFlutterNapi = defaultEngine.getFlutterNapi(); + return oldFlutterNapi.preSpawn(dartEntrypoint, dartEntrypointLibraryUri, initialRoute, dartArgs); + } + } + } + + if (!flutterNapi) { + flutterNapi = FlutterInjector.getInstance().getPreloadFlutterNapi(); + } + if (flutterNapi) { + if (!flutterNapi.isAttached()) { + flutterNapi.attachToNative(); + } + Log.d(TAG, "setInitialRoute: " + initialRoute); + let message = JSONMethodCodec.INSTANCE.encodeMethodCall(new MethodCall("setInitialRoute", initialRoute)); + flutterNapi.dispatchPlatformMessage("flutter/navigation", message, message.byteLength, 0); + + flutterNapi.runBundleAndSnapshotFromLibrary( + bundlePath, + dartEntrypoint, + dartEntrypointLibraryUri, + context.resourceManager, + dartArgs); + } + return flutterNapi; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterNapi.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterNapi.ets new file mode 100644 index 0000000..50c6282 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterNapi.ets @@ -0,0 +1,1162 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import flutter from 'libflutter.so'; +import common from '@ohos.app.ability.common'; +import Log from '../../util/Log'; +import resourceManager from '@ohos.resourceManager'; +import { PlatformMessageHandler } from './dart/PlatformMessageHandler'; +import { FlutterCallbackInformation } from '../../view/FlutterCallbackInformation'; +import image from '@ohos.multimedia.image'; +import { EngineLifecycleListener } from './FlutterEngine'; +import { ByteBuffer } from '../../util/ByteBuffer'; +import LocalizationPlugin from '../../plugin/localization/LocalizationPlugin'; +import i18n from '@ohos.i18n'; +import Any from '../../plugin/common/Any'; +import FlutterManager from '../ohos/FlutterManager'; +import deviceInfo from '@ohos.deviceInfo'; +import TouchEventProcessor from '../ohos/TouchEventProcessor'; +import BuildProfile from '../../../../../BuildProfile'; +import { Action } from '../engine/systemchannels/AccessibilityChannel'; + +const TAG = "FlutterNapi"; + +enum ContextType { + APP_LIFECYCLE = 0, + JS_PAGE_LIFECYCLE, +} + +/** + * Represents a pending platform message that is queued during preloading. + * These messages are processed once the Flutter engine is ready to handle them. + */ +interface PendingMessage { + /** The channel name for the message. */ + channel: string; + /** The message data as an ArrayBuffer. */ + message: ArrayBuffer; + /** The reply ID for responding to the message. */ + replyId: number; + /** Additional message data. */ + messageData: number; +} + +/** + * Provides Flutter NAPI interface for ArkTS. + * This class serves as the bridge between the ArkTS layer and the native Flutter engine, + * handling initialization, message passing, viewport management, and lifecycle events. + */ +export default class FlutterNapi { + private static hasInit: boolean = false; + /** Whether the native methods have been implemented. */ + hasImplemented: boolean = false; + /** The native shell holder ID, or null if not attached. */ + nativeShellHolderId: number | null = null; + /** The platform message handler for receiving messages from Dart, or null if not set. */ + platformMessageHandler: PlatformMessageHandler | null = null; + private engineLifecycleListeners = new Set(); + /** The accessibility delegate for handling accessibility events, or null if not set. */ + accessibilityDelegate: AccessibilityDelegate | null = null; + /** The localization plugin for handling locale information, or null if not set. */ + localizationPlugin: LocalizationPlugin | null = null; + /** Whether Flutter UI is currently being displayed. */ + isDisplayingFlutterUi: boolean = false; + /** Whether Flutter UI has been preloaded. */ + isPreloadedFlutterUi: boolean = false; + /** Whether Dart code is currently running. */ + isRunningDart: boolean = false; + private nextSpawnNapi: FlutterNapi | null = null; + private pendingMessages: PendingMessage[] = []; + private readyForHandleMessage: boolean = true; + private firstPreloading: boolean = true; + + /** + * Updates the refresh rate for the Flutter engine. + * @param refreshRateFPS - The refresh rate in frames per second + */ + updateRefreshRate(refreshRateFPS: number) { + flutter.nativeUpdateRefreshRate(refreshRateFPS); + } + + /** + * Updates the size of the Flutter view. + * @param width - The new width in pixels + * @param height - The new height in pixels + */ + updateSize(width: number, height: number) { + flutter.nativeUpdateSize(width, height); + } + + /** + * Updates the pixel density of the display. + * @param densityPixels - The pixel density value (dots per inch) + */ + updateDensity(densityPixels: number) { + flutter.nativeUpdateDensity(densityPixels); + } + + /** + * Initializes the Flutter engine with the specified parameters. + * @param context - The application context + * @param args - Command-line arguments for the Flutter engine + * @param bundlePath - Path to the Flutter bundle + * @param appStoragePath - Path to the application storage directory + * @param engineCachesPath - Path to the engine caches directory + * @param initTimeMillis - Initialization time in milliseconds + */ + init(context: common.Context, + args: Array, + bundlePath: string, + appStoragePath: string, + engineCachesPath: string, + initTimeMillis: number) { + if (FlutterNapi.hasInit) { + Log.e(TAG, "the engine has init"); + return; + } + Log.w(TAG, "HAR_VERSION=" + BuildProfile.HAR_VERSION); + Log.d(TAG, JSON.stringify({ + "name": "init, initTimeMillis=" + initTimeMillis, + "bundlePath": bundlePath, + "appStoragePath": appStoragePath, + "engineCachesPath": engineCachesPath, + "args": args, + })); + let code: number | null = flutter.nativeInit(context, args, bundlePath, appStoragePath, + engineCachesPath, initTimeMillis, deviceInfo.productModel); + FlutterNapi.hasInit = code == 0; + Log.d(TAG, "init code=" + code + ", FlutterNapi.hasInit" + FlutterNapi.hasInit); + } + + /** + * Prefetches the default font manager. + * This should be called before using fonts in the Flutter engine. + */ + static prefetchDefaultFontManager(): void { + flutter.nativePrefetchDefaultFontManager(); + } + + /** + * Checks and reloads fonts for this engine instance. + */ + checkAndReloadFont(): void { + flutter.nativeCheckAndReloadFont(this.nativeShellHolderId!); + } + + /** + * Attaches this FlutterNapi instance to the native engine. + * This must be called before using most FlutterNapi methods. + */ + attachToNative(): void { + if (!FlutterNapi.hasInit) { + Log.e(TAG, "attachToNative fail, FlutterNapi.hasInit=" + FlutterNapi.hasInit); + return; + } + if (this.nativeShellHolderId == null) { + this.nativeShellHolderId = flutter.nativeAttach(this); + } + Log.d(TAG, "nativeShellHolderId=" + this.nativeShellHolderId); + } + + /** + * Runs a Flutter bundle and snapshot from a library. + * @param bundlePath - Path to the Flutter bundle + * @param entrypointFunctionName - Name of the entrypoint function + * @param pathToEntrypointFunction - Path to the entrypoint function + * @param assetManager - Resource manager for accessing assets + * @param entrypointArgs - Arguments to pass to the entrypoint function + */ + runBundleAndSnapshotFromLibrary( + bundlePath: string, + entrypointFunctionName: string | undefined, + pathToEntrypointFunction: string | undefined, + assetManager: resourceManager.ResourceManager, + entrypointArgs: Array) { + if (!FlutterNapi.hasInit) { + Log.e(TAG, "runBundleAndSnapshotFromLibrary fail, FlutterNapi.hasInit=" + FlutterNapi.hasInit); + return; + } + Log.d(TAG, "init: bundlePath=" + bundlePath + " entrypointFunctionName=" + entrypointFunctionName + + " pathToEntrypointFunction=" + pathToEntrypointFunction + " entrypointArgs=" + JSON.stringify(entrypointArgs)) + if (!this.nativeShellHolderId) { + Log.e(TAG, "runBundleAndSnapshotFromLibrary this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeRunBundleAndSnapshotFromLibrary(this.nativeShellHolderId!, bundlePath, entrypointFunctionName, + pathToEntrypointFunction, assetManager, entrypointArgs); + this.isRunningDart = true; + this.isDisplayingFlutterUi = false; + this.isPreloadedFlutterUi = false; + }; + + /** + * Checks if the native methods are implemented. + * @param methodName - Optional method name for logging + * @returns true if methods are implemented, false otherwise + */ + checkImplemented(methodName: string = ""): boolean { + if (!this.hasImplemented) { + Log.e(TAG, "this method has not implemented -> " + methodName) + } + return this.hasImplemented; + } + + /** + * Sets the platform message handler for receiving messages from Dart. + * @param platformMessageHandler - The PlatformMessageHandler instance, or null to remove + */ + setPlatformMessageHandler(platformMessageHandler: PlatformMessageHandler | null): void { + this.ensureRunningOnMainThread(); + this.platformMessageHandler = platformMessageHandler; + } + + private nativeNotifyLowMemoryWarning(nativeShellHolderId: number): void { + + } + + /** + * Looks up callback information for a given handler. + * @param handle - The handler number + * @returns The FlutterCallbackInformation, or null if not found + */ + static nativeLookupCallbackInformation(handle: number): FlutterCallbackInformation | null { + let callbackInformation = new FlutterCallbackInformation(); + let ret: number = flutter.nativeLookupCallbackInformation(callbackInformation, handle); + if (ret == 0) { + return callbackInformation; + } + return null; + } + + /** + * Notifies the Flutter engine of a low memory warning. + */ + notifyLowMemoryWarning(): void { + this.ensureRunningOnMainThread(); + if (!this.nativeShellHolderId) { + Log.e(TAG, "notifyLowMemoryWarning this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + this.nativeNotifyLowMemoryWarning(this.nativeShellHolderId!); + } + + /** + * Checks if this FlutterNapi instance is attached to the native engine. + * @returns true if attached, false otherwise + */ + isAttached(): boolean { + return this.nativeShellHolderId != null; + } + + /** + * Ensures that the current code is running on the main thread. + */ + private ensureRunningOnMainThread(): void { + + } + + /** + * Dispatches an empty platform message to Dart. + * @param channel - The channel name for the message + * @param responseId - The response ID for receiving a reply + */ + dispatchEmptyPlatformMessage(channel: String, responseId: number): void { + this.ensureRunningOnMainThread(); + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "dispatchEmptyPlatformMessage this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeDispatchEmptyPlatformMessage(this.nativeShellHolderId!, channel, responseId); + } else { + Log.w( + TAG, + "Tried to send a platform message to Flutter, but FlutterNapi was detached from native C++. Could not send. Channel: " + + channel + + ". Response ID: " + + responseId); + } + } + + /** + * Sends a platform message with data from OpenHarmony to Flutter over the given channel. + * @param channel - The channel name for the message + * @param message - The message data as an ArrayBuffer + * @param position - The position in the message buffer + * @param responseId - The response ID for receiving a reply + */ + dispatchPlatformMessage(channel: String, message: ArrayBuffer, position: number, responseId: number): void { + this.ensureRunningOnMainThread(); + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "dispatchPlatformMessage this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeDispatchPlatformMessage(this.nativeShellHolderId!, channel, message, position, responseId); + } else { + Log.w( + TAG, + "Tried to send a platform message to Flutter, but FlutterNapi was detached from native C++. Could not send. Channel: " + + channel + + ". Response ID: " + + responseId); + } + } + + /** + * Invokes an empty response callback for a platform message. + * @param responseId - The response ID that was sent with the original message + */ + invokePlatformMessageEmptyResponseCallback(responseId: number): void { + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "invokePlatformMessageEmptyResponseCallback this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeInvokePlatformMessageEmptyResponseCallback(this.nativeShellHolderId!, responseId); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterNapi was detached from native C++. Could not send. Response ID: " + + responseId); + } + } + + /** + * Invokes a response callback with data for a platform message. + * @param responseId - The response ID that was sent with the original message + * @param message - The reply data as an ArrayBuffer + * @param position - The position in the message buffer + */ + invokePlatformMessageResponseCallback(responseId: number, message: ArrayBuffer, position: number) { + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeInvokePlatformMessageResponseCallback( + this.nativeShellHolderId!, responseId, message, position); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterNapi was detached from native C++. Could not send. Response ID: " + + responseId); + } + } + + /** + * Sets the viewport metrics for the Flutter view. + * @param devicePixelRatio - The device pixel ratio + * @param physicalWidth - Physical width in pixels + * @param physicalHeight - Physical height in pixels + * @param physicalPaddingTop - Top padding in physical pixels + * @param physicalPaddingRight - Right padding in physical pixels + * @param physicalPaddingBottom - Bottom padding in physical pixels + * @param physicalPaddingLeft - Left padding in physical pixels + * @param physicalViewInsetTop - Top view inset in physical pixels + * @param physicalViewInsetRight - Right view inset in physical pixels + * @param physicalViewInsetBottom - Bottom view inset in physical pixels + * @param physicalViewInsetLeft - Left view inset in physical pixels + * @param systemGestureInsetTop - Top system gesture inset + * @param systemGestureInsetRight - Right system gesture inset + * @param systemGestureInsetBottom - Bottom system gesture inset + * @param systemGestureInsetLeft - Left system gesture inset + * @param physicalTouchSlop - Physical touch slop value + * @param displayFeaturesBounds - Array of display feature bounds + * @param displayFeaturesType - Array of display feature types + * @param displayFeaturesState - Array of display feature states + */ + setViewportMetrics(devicePixelRatio: number, physicalWidth: number + , physicalHeight: number, physicalPaddingTop: number, physicalPaddingRight: number + , physicalPaddingBottom: number, physicalPaddingLeft: number, physicalViewInsetTop: number + , physicalViewInsetRight: number, physicalViewInsetBottom: number, physicalViewInsetLeft: number + , systemGestureInsetTop: number, systemGestureInsetRight: number, systemGestureInsetBottom: number + , systemGestureInsetLeft: number, physicalTouchSlop: number, displayFeaturesBounds: Array + , displayFeaturesType: Array, displayFeaturesState: Array): void { + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "setViewportMetrics this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeSetViewportMetrics(this.nativeShellHolderId!, devicePixelRatio, + physicalWidth, + physicalHeight, + physicalPaddingTop, + physicalPaddingRight, + physicalPaddingBottom, + physicalPaddingLeft, + physicalViewInsetTop, + physicalViewInsetRight, + physicalViewInsetBottom, + physicalViewInsetLeft, + systemGestureInsetTop, + systemGestureInsetRight, + systemGestureInsetBottom, + systemGestureInsetLeft, + physicalTouchSlop, + displayFeaturesBounds, + displayFeaturesType, + displayFeaturesState); + } + } + + /** + * Spawns a new FlutterNapi instance from this instance. + * @param entrypointFunctionName - Name of the entrypoint function + * @param pathToEntrypointFunction - Path to the entrypoint function + * @param initialRoute - Initial route for navigation + * @param entrypointArgs - Arguments to pass to the entrypoint function + * @returns A new FlutterNapi instance + */ + spawn(entrypointFunctionName: string, pathToEntrypointFunction: string, initialRoute: string, + entrypointArgs: Array): FlutterNapi { + if (this.nextSpawnNapi) { + let ret = this.nextSpawnNapi; + this.nextSpawnNapi = null; + return ret; + } + let flutterNapi = new FlutterNapi(); + let shellHolderId: number = + flutter.nativeSpawn(this.nativeShellHolderId, entrypointFunctionName, pathToEntrypointFunction, initialRoute, + entrypointArgs, flutterNapi); + flutterNapi.nativeShellHolderId = shellHolderId; + flutterNapi.isRunningDart = this.isRunningDart; + flutterNapi.isDisplayingFlutterUi = false; + flutterNapi.isPreloadedFlutterUi = false; + return flutterNapi; + } + + /** + * Pre-spawns a new FlutterNapi instance for later use. + * @param entrypointFunctionName - Name of the entrypoint function + * @param pathToEntrypointFunction - Path to the entrypoint function + * @param initialRoute - Initial route for navigation + * @param entrypointArgs - Arguments to pass to the entrypoint function + * @returns A new FlutterNapi instance that will be used on the next spawn call + */ + preSpawn(entrypointFunctionName: string, pathToEntrypointFunction: string, initialRoute: string, + entrypointArgs: Array): FlutterNapi { + if (this.nextSpawnNapi) { + this.nextSpawnNapi.detachFromNativeAndReleaseResources(); + } + let flutterNapi = new FlutterNapi(); + let shellHolderId: number = + flutter.nativeSpawn(this.nativeShellHolderId, entrypointFunctionName, pathToEntrypointFunction, initialRoute, + entrypointArgs, flutterNapi); + flutterNapi.nativeShellHolderId = shellHolderId; + flutterNapi.isRunningDart = this.isRunningDart; + flutterNapi.isDisplayingFlutterUi = false; + flutterNapi.isPreloadedFlutterUi = false; + this.nextSpawnNapi = flutterNapi; + return flutterNapi; + } + + /** + * Adds an engine lifecycle listener. + * @param engineLifecycleListener - The EngineLifecycleListener to add + */ + addEngineLifecycleListener(engineLifecycleListener: EngineLifecycleListener): void { + this.engineLifecycleListeners.add(engineLifecycleListener); + } + + /** + * Removes an engine lifecycle listener. + * @param engineLifecycleListener - The EngineLifecycleListener to remove + */ + removeEngineLifecycleListener(engineLifecycleListener: EngineLifecycleListener) { + this.engineLifecycleListeners.delete(engineLifecycleListener); + } + + /** + * Called by native to respond to a platform message that we sent. + * @param replyId - The response ID that was sent with the original message + * @param reply - The reply data as an ArrayBuffer + */ + handlePlatformMessageResponse(replyId: number, reply: ArrayBuffer): void { + Log.d(TAG, "called handlePlatformMessageResponse Response ID: " + replyId); + if (this.platformMessageHandler != null) { + this.platformMessageHandler.handlePlatformMessageResponse(replyId, reply); + } + } + + /** + * Called by native on any thread to handle a platform message from Dart. + * @param channel - The channel name for the message + * @param message - The message data as an ArrayBuffer + * @param replyId - The reply ID for responding to the message + * @param messageData - Additional message data + */ + handlePlatformMessage(channel: string, message: ArrayBuffer, replyId: number, messageData: number): void { + Log.d(TAG, "called handlePlatformMessage Channel: " + channel + ". Response ID: " + replyId); + if (this.platformMessageHandler != null && this.readyForHandleMessage) { + this.platformMessageHandler.handleMessageFromDart(channel, message, replyId, messageData); + } else { + const pendingMessage: PendingMessage = { + channel, + message, + replyId, + messageData + }; + this.pendingMessages.push(pendingMessage); + } + } + + /** + * Sets the preloading state, preventing message handling until ready. + */ + setPreloading(): void { + if (this.firstPreloading) { + this.readyForHandleMessage = false; + this.firstPreloading = false; + } + } + + /** + * Processes all pending messages that were queued during preloading. + */ + processPendingMessages(): void { + Log.d(TAG, "processPendingMessages len:" + this.pendingMessages.length); + this.readyForHandleMessage = true; + while (this.pendingMessages.length > 0 && this.platformMessageHandler) { + const pendingMessage = this.pendingMessages.shift(); + if (pendingMessage) { + this.platformMessageHandler.handleMessageFromDart( + pendingMessage.channel, + pendingMessage.message, + pendingMessage.replyId, + pendingMessage.messageData + ); + } + } + } + + /** + * Called by native to notify that the first Flutter frame has been rendered. + * @param isPreload - Whether this is a preload frame (1) or a regular frame (0) + */ + onFirstFrame(isPreload: number): void { + Log.d(TAG, "called onFirstFrame isPreload:" + isPreload); + if (isPreload) { + this.isPreloadedFlutterUi = true; + } else { + this.processPendingMessages(); + if (this.isDisplayingFlutterUi) { + return; + } + this.isDisplayingFlutterUi = true; + } + FlutterManager.getInstance().getFlutterViewList().forEach((value) => { + if (this.nativeShellHolderId != null && value.isSameEngineShellHolderId(this.nativeShellHolderId)) { + value.onFirstFrame(isPreload); + } + }); + } + + /** + * Called by native when the engine is about to restart. + * Notifies all registered lifecycle listeners. + */ + onPreEngineRestart(): void { + Log.d(TAG, "called onPreEngineRestart") + this.engineLifecycleListeners.forEach(listener => listener.onPreEngineRestart()); + } + + /** + * Computes the platform-resolved locale from the given locale strings. + * Invoked by native to obtain the results of OpenHarmony's locale resolution algorithm. + * @param strings - Array of locale strings + * @returns Array of resolved locale strings + */ + computePlatformResolvedLocale(strings: Array): Array { + Log.d(TAG, "called computePlatformResolvedLocale " + JSON.stringify(strings)) + return [] + } + + /** + * Sets whether semantics are enabled and sends a response. + * @param enabled - Whether to enable semantics + * @param responseId - The response ID for the reply + */ + setSemanticsEnabledWithRespId(enabled: boolean, responseId: number): void { + this.ensureRunningOnMainThread(); + if (this.isAttached()) { + flutter.nativeSetSemanticsEnabled(this.nativeShellHolderId!, enabled); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterNapi was detached from native C++. Could not send. Response ID: " + + responseId); + } + } + + /** + * Sets whether semantics are enabled. + * @param enabled - Whether to enable semantics + */ + setSemanticsEnabled(enabled: boolean): void { + this.ensureRunningOnMainThread(); + if (this.isAttached()) { + flutter.nativeSetSemanticsEnabled(this.nativeShellHolderId!, enabled); + } else { + Log.e( + TAG, + "Tried to send a platform message response, but FlutterNapi was detached from native C++. Could not send."); + } + } + + /** + * Sets accessibility features and sends a response. + * @param accessibilityFeatureFlags - The accessibility feature flags + * @param responseId - The response ID for the reply + */ + setAccessibilityFeatures(accessibilityFeatureFlags: number, responseId: number): void { + if (this.isAttached()) { + flutter.nativeSetAccessibilityFeatures(accessibilityFeatureFlags, responseId); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterNapi was detached from native C++. Could not send. Response ID: " + + responseId); + } + } + + /** + * Native method for setting accessibility features. + * @param accessibilityFeatureFlags - The accessibility feature flags + * @param responseId - The response ID for the reply + */ + nativeSetAccessibilityFeatures(accessibilityFeatureFlags: number, responseId: number): void { + } + + /** + * Dispatches a semantics action to the native engine. + * @param virtualViewId - The virtual view ID + * @param action - The semantics action to dispatch + * @param responseId - The response ID for the reply + */ + dispatchSemanticsAction(virtualViewId: number, action: Action, responseId: number): void { + if (this.isAttached()) { + this.nativeDispatchSemanticsAction(virtualViewId, action, responseId); + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterNapi was detached from native C++. Could not send. Response ID: " + + responseId); + } + } + + /** + * Native method for dispatching a semantics action. + * @param virtualViewId - The virtual view ID + * @param action - The semantics action to dispatch + * @param responseId - The response ID for the reply + */ + nativeDispatchSemanticsAction(virtualViewId: number, action: Action, responseId: number): void { + } + + /** + * Sets the accessibility delegate for handling accessibility events. + * @param delegate - The AccessibilityDelegate instance + * @param responseId - The response ID for the reply + */ + setAccessibilityDelegate(delegate: AccessibilityDelegate, responseId: number): void { + if (this.isAttached()) { + this.accessibilityDelegate = delegate; + } else { + Log.w( + TAG, + "Tried to send a platform message response, but FlutterNapi was detached from native C++. Could not send. Response ID: " + + responseId); + } + } + + /** + * Called when the accessibility state changes. + * @param state - Whether accessibility is enabled + */ + accessibilityStateChange(state: Boolean): void { + this.ensureRunningOnMainThread(); + if (this.accessibilityDelegate != null) { + this.accessibilityDelegate.accessibilityStateChange(state); + } + Log.d(TAG, "accessibilityStateChange: state is " + state ? "on" : "off"); + if (this.nativeShellHolderId != null) { + flutter.nativeAccessibilityStateChange(this.nativeShellHolderId!, state); + } else { + Log.w(TAG, "accessibilityStateChange, nativeShellHolderId is null") + } + } + + /** + * Sets the localization plugin for handling locale information. + * @param localizationPlugin - The LocalizationPlugin instance, or null to remove + */ + setLocalizationPlugin(localizationPlugin: LocalizationPlugin | null): void { + this.localizationPlugin = localizationPlugin; + } + + /** + * Gets the system language list from the platform. + */ + getSystemLanguages() { + Log.d(TAG, "called getSystemLanguages ") + let index: number; + let systemLanguages = i18n.System.getPreferredLanguageList(); + for (index = 0; index < systemLanguages.length; index++) { + Log.d(TAG, "systemlanguages " + index + ":" + systemLanguages[index]); + } + if (!this.nativeShellHolderId) { + Log.e(TAG, "getSystemLanguages this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeGetSystemLanguages(this.nativeShellHolderId!, systemLanguages); + } + + /** + * Attaches a FlutterEngine to an XComponent. + * @param xcomponentId - The XComponent ID + */ + xComponentAttachFlutterEngine(xcomponentId: string) { + flutter.nativeXComponentAttachFlutterEngine(xcomponentId, this.nativeShellHolderId!); + } + + /** + * Pre-renders an XComponent. + * @param xcomponentId - The XComponent ID + * @param width - The width of the component + * @param height - The height of the component + */ + xComponentPreDraw(xcomponentId: string, width: number, height: number) { + flutter.nativeXComponentPreDraw(xcomponentId, this.nativeShellHolderId!, width, height); + } + + /** + * Detaches a FlutterEngine from an XComponent. + * @param xcomponentId - The XComponent ID + */ + xComponentDetachFlutterEngine(xcomponentId: string) { + flutter.nativeXComponentDetachFlutterEngine(xcomponentId, this.nativeShellHolderId!); + } + + /** + * Dispatches a mouse wheel event from an XComponent to the Flutter engine. + * @param xcomponentId - The XComponent ID + * @param eventType - The type of the mouse wheel event + * @param event - The pan gesture event containing mouse wheel data + */ + xComponentDisPatchMouseWheel(xcomponentId: string, eventType: string, event: PanGestureEvent) { + // only mouse + if (event.source !== SourceType.Mouse) { + return; + } + const vaildFinger = event.fingerList?.find(item => item.globalX && item.globalY); + if (!vaildFinger) { + return; + } + flutter.nativeXComponentDispatchMouseWheel( + this.nativeShellHolderId!!, + xcomponentId, + eventType, + vaildFinger?.id, + vaildFinger?.localX, + vaildFinger?.localY, + event.offsetY, + event.timestamp + ); + } + + /** + * Detaches this FlutterNapi instance from the native engine and releases all resources. + */ + detachFromNativeAndReleaseResources() { + if (!this.nativeShellHolderId) { + Log.e(TAG, "detachFromNativeAndReleaseResources this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeDestroy(this.nativeShellHolderId!!); + this.nativeShellHolderId = null; + this.isRunningDart = false; + this.isDisplayingFlutterUi = false; + this.isPreloadedFlutterUi = false; + this.readyForHandleMessage = false; + } + + /** + * Unregisters a texture from the Flutter engine. + * @param textureId - The texture ID to unregister + */ + unregisterTexture(textureId: number): void { + Log.d(TAG, "called unregisterTexture "); + if (!this.nativeShellHolderId) { + Log.e(TAG, "unregisterTexture this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeUnregisterTexture(this.nativeShellHolderId!, textureId); + } + + /** + * Registers a PixelMap as a texture in the Flutter engine. + * @param textureId - The texture ID + * @param pixelMap - The PixelMap to register + */ + registerPixelMap(textureId: number, pixelMap: PixelMap): void { + Log.d(TAG, "called registerPixelMap "); + if (!this.nativeShellHolderId) { + Log.e(TAG, "registerPixelMap this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeRegisterPixelMap(this.nativeShellHolderId!, textureId, pixelMap); + } + + /** + * Sets the background PixelMap for a texture. + * @param textureId - The texture ID + * @param pixelMap - The PixelMap to use as background + */ + setTextureBackGroundPixelMap(textureId: number, pixelMap: PixelMap): void { + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "this.nativeShellHolderId = " + this.nativeShellHolderId) + return; + } + flutter.nativeSetTextureBackGroundPixelMap(this.nativeShellHolderId!, textureId, pixelMap); + } else { + return; + } + } + + /** + * Sets the background color for a texture. + * @param textureId - The texture ID + * @param color - The color value in ARGB format + */ + setTextureBackGroundColor(textureId: number, color: number): void { + Log.d(TAG, "called setTextureBackGroundColor"); + if (!this.isAttached()) { + Log.e(TAG, "setTextureBackGroundColor when napi is not attached"); + return; + } + flutter.nativeSetTextureBackGroundColor(this.nativeShellHolderId!, textureId, color); + } + + /** + * Registers a texture in the Flutter engine. + * @param textureId - The texture ID to register + * @returns The registered texture ID, or 0 if registration fails + */ + registerTexture(textureId: number): number { + Log.d(TAG, "called registerTexture "); + if (!this.nativeShellHolderId) { + Log.e(TAG, "registerTexture this.nativeShellHolderId = " + this.nativeShellHolderId) + return 0; + } + return flutter.nativeRegisterTexture(this.nativeShellHolderId!, textureId); + } + + /** + * @deprecated since 3.22 + * @useinstead FlutterNapi#getTextureNativeWindowPtr + */ + getTextureNativeWindowId(textureId: number): number { + Log.d(TAG, "called getTextureNativeWindowId "); + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "this.nativeShellHolderId = " + this.nativeShellHolderId) + return 0; + } + return flutter.nativeGetTextureWindowId(this.nativeShellHolderId!, textureId); + } else { + return 0; + } + } + + /** + * Gets the native window pointer for a texture. + * @param textureId - The texture ID + * @returns The native window pointer as a bigint, or BigInt("0") if not available + */ + getTextureNativeWindowPtr(textureId: number): bigint { + Log.d(TAG, "called getTextureNativeWindowPtr"); + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "this.nativeShellHolderId = " + this.nativeShellHolderId) + return BigInt("0"); + } + return flutter.nativeGetTextureWindowPtr(this.nativeShellHolderId!, textureId); + } else { + return BigInt("0"); + } + } + + /** + * @deprecated since 3.22 + * @useinstead FlutterNapi#setExternalNativeImagePtr + */ + setExternalNativeImage(textureId: number, native_image: number): boolean { + Log.d(TAG, "called setExternalNativeImage "); + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "this.nativeShellHolderId = " + this.nativeShellHolderId) + return false; + } + return Boolean(flutter.nativeSetExternalNativeImage(this.nativeShellHolderId!, textureId, native_image)); + } else { + return false; + } + } + + /** + * Sets an external native image pointer for a texture. + * @param textureId - The texture ID + * @param native_image_ptr - The native image pointer as a bigint + * @returns true if successful, false otherwise + */ + setExternalNativeImagePtr(textureId: number, native_image_ptr: bigint): boolean { + Log.d(TAG, "called setExternalNativeImagePtr"); + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "this.nativeShellHolderId = " + this.nativeShellHolderId) + return false; + } + return Boolean(flutter.nativeSetExternalNativeImagePtr(this.nativeShellHolderId!, textureId, native_image_ptr)); + } else { + return false; + } + } + + /** + * Resets an external texture. + * @param textureId - The texture ID + * @param need_surfaceId - Whether a surface ID is needed + * @returns The surface ID if needed, or 0 otherwise + */ + resetExternalTexture(textureId: number, need_surfaceId: boolean): number { + Log.d(TAG, "called resetExternalTexture "); + if (this.isAttached()) { + if (!this.nativeShellHolderId) { + Log.e(TAG, "this.nativeShellHolderId = " + this.nativeShellHolderId) + return 0; + } + return flutter.nativeResetExternalTexture(this.nativeShellHolderId!, textureId, need_surfaceId); + } else { + return 0; + } + } + + /** + * Sets the buffer size for a texture. + * @param textureId - The texture ID + * @param width - The width of the buffer + * @param height - The height of the buffer + */ + setTextureBufferSize(textureId: number, width: number, height: number): void { + Log.d(TAG, "called setTextureBufferSize "); + if (!this.isAttached()) { + Log.e(TAG, "setTextureBufferSize this.nativeShellHolderId:" + this.nativeShellHolderId) + return; + } + flutter.nativeSetTextureBufferSize(this.nativeShellHolderId!, textureId, width, height); + } + + /** + * Notifies the Flutter engine that a texture is being resized. + * @param textureId - The texture ID + * @param width - The new width + * @param height - The new height + */ + notifyTextureResizing(textureId: number, width: number, height: number): void { + Log.d(TAG, "called notifyTextureResizing "); + if (!this.isAttached()) { + Log.e(TAG, "notifyTextureResizing this.nativeShellHolderId:" + this.nativeShellHolderId) + return; + } + flutter.nativeNotifyTextureResizing(this.nativeShellHolderId!, textureId, width, height); + } + + /** + * Enables or disables frame caching for improved performance. + * @param enable - Whether to enable frame caching + */ + enableFrameCache(enable: boolean): void { + if (!this.nativeShellHolderId) { + return; + } + flutter.nativeEnableFrameCache(this.nativeShellHolderId!, enable); + } + + /** + * Handles touch events from the platform. + * @param strings - Array of strings containing touch event data + */ + onTouchEvent(strings: Array): void { + if (this.isAttached()) { + TouchEventProcessor.getInstance().postTouchEvent(strings); + } + } + + /** + * Handles mouse events from the platform. + * @param strings - Array of strings containing mouse event data + */ + onMouseEvent(strings: Array): void { + if (this.isAttached()) { + TouchEventProcessor.getInstance().postMouseEvent(strings); + } + } + + /** + * Handles axis events (scroll wheel, etc.) from the platform. + * @param strings - Array of strings containing axis event data + */ + onAxisEvent(strings: Array): void { + if (this.isAttached()) { + TouchEventProcessor.getInstance().postAxisEvent(strings); + } + } + + /** + * Checks if a Unicode code point is an emoji. + * @param code - The Unicode code point + * @returns true if the code point is an emoji, false otherwise + */ + static unicodeIsEmoji(code: number): boolean { + return Boolean(flutter.nativeUnicodeIsEmoji(code)); + } + + /** + * Checks if a Unicode code point is an emoji modifier. + * @param code - The Unicode code point + * @returns true if the code point is an emoji modifier, false otherwise + */ + static unicodeIsEmojiModifier(code: number): boolean { + return Boolean(flutter.nativeUnicodeIsEmojiModifier(code)); + } + + /** + * Checks if a Unicode code point is an emoji modifier base. + * @param code - The Unicode code point + * @returns true if the code point is an emoji modifier base, false otherwise + */ + static unicodeIsEmojiModifierBase(code: number): boolean { + return Boolean(flutter.nativeUnicodeIsEmojiModifierBase(code)); + } + + /** + * Checks if a Unicode code point is a variation selector. + * @param code - The Unicode code point + * @returns true if the code point is a variation selector, false otherwise + */ + static unicodeIsVariationSelector(code: number): boolean { + return Boolean(flutter.nativeUnicodeIsVariationSelector(code)); + } + + /** + * Checks if a Unicode code point is a regional indicator symbol. + * @param code - The Unicode code point + * @returns true if the code point is a regional indicator symbol, false otherwise + */ + static unicodeIsRegionalIndicatorSymbol(code: number): boolean { + return Boolean(flutter.nativeUnicodeIsRegionalIndicatorSymbol(code)); + } + + /** + * Sets the font weight scale for text rendering. + * @param fontWeightScale - The font weight scale factor + */ + setFontWeightScale(fontWeightScale: number): void { + this.ensureRunningOnMainThread(); + if (this.isAttached()) { + Log.i(TAG, "setFontWeightScale: " + fontWeightScale); + flutter.nativeSetFontWeightScale(this.nativeShellHolderId!, fontWeightScale); + } else { + Log.w(TAG, "setFontWeightScale is detached !"); + } + } + + /** + * Sets the Flutter navigation action state. + * @param shellHolderId - The shell holder ID + * @param isNavigate - Whether navigation is active + */ + setFlutterNavigationAction(shellHolderId: number, isNavigate: boolean): void { + this.ensureRunningOnMainThread(); + if (this.isAttached()) { + Log.i(TAG, "setFlutterNavigationAction: " + isNavigate); + flutter.nativeSetFlutterNavigationAction(shellHolderId, isNavigate); + } else { + Log.w(TAG, "setFlutterNavigationAction is detached !"); + } + } + + /** + * Sets the D-VSync switch state. + * @param isEnable - Whether to enable D-VSync + */ + SetDVsyncSwitch(isEnable: boolean): void { + flutter.nativeSetDVsyncSwitch(this.nativeShellHolderId!, isEnable); + } + + /** + * Sends screen scrolling velocity to the native engine. + * @param type - The animation type + * @param velocity - The current screen scrolling velocity + */ + static animationVoting(type: number, velocity: number): void { + flutter.nativeAnimationVoting(type, velocity); + } + + /** + * Sends video frame count to the native engine. + * @param seconds - The time duration in seconds + * @param frameCount - The number of frames within the specified time duration + */ + static videoVoting(seconds: number, frameCount: number): void { + flutter.nativeVideoVoting(seconds, frameCount); + } + + /** + * Prefetches the frame rate configuration file. + */ + static prefetchFramesCfg(): void { + flutter.nativePrefetchFramesCfg(); + } + + /** + * Checks the LTPO (Low Temperature Polycrystalline Oxide) switch state. + * @returns The LTPO switch state value + */ + static checkLTPOSwitchState(): number { + return flutter.nativeCheckLTPOSwitchState(); + } + + /** + * Sets the QoS (Quality of Service) level when low memory is detected. + * @param lowMemoryLevel - The low memory level + */ + SetQosOnLowMemory(lowMemoryLevel: number): void { + flutter.nativeSetQosOnLowMemory(this.nativeShellHolderId!, lowMemoryLevel); + } + + SetAnimationStatus(animationStatus: number): void { + flutter.nativeSetAnimationStatus(this.nativeShellHolderId!, animationStatus); + } + + NotifyPageChanged(pageName: string, pageNameLen: number, windowID: number): number { + return flutter.nativeNotifyPageChanged(pageName, pageNameLen, windowID); + } +} + +/** + * Interface for handling accessibility state changes. + * Implementations of this interface will be notified when the accessibility state changes. + */ +export interface AccessibilityDelegate { + /** + * Called when the accessibility state changes. + * @param state - Whether accessibility is enabled + */ + accessibilityStateChange(state: Boolean): void; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterOverlaySurface.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterOverlaySurface.ets new file mode 100644 index 0000000..5ca47e3 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterOverlaySurface.ets @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterOverlaySurface.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * Represents an overlay surface in the Flutter rendering system. + * Overlay surfaces are used for displaying content above the main Flutter view. + */ +export class FlutterOverlaySurface { + private id: number; + + /** + * Constructs a new FlutterOverlaySurface instance. + * @param id - The unique identifier for this overlay surface + */ + constructor(id: number) { + this.id = id + } + + /** + * Gets the unique identifier of this overlay surface. + * @returns The overlay surface ID + */ + getId(): number { + return this.id; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterShellArgs.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterShellArgs.ets new file mode 100644 index 0000000..9cf6b65 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/FlutterShellArgs.ets @@ -0,0 +1,163 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterShellArgs.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import Want from '@ohos.app.ability.Want'; + +/** + * Encapsulates arguments for the Flutter shell. + * This class provides methods to parse and manage command-line arguments + * that are passed to the Flutter engine during initialization. + */ +export default class FlutterShellArgs { + static ARG_KEY_TRACE_STARTUP = "trace-startup"; + static ARG_TRACE_STARTUP = "--trace-startup"; + static ARG_KEY_START_PAUSED = "start-paused"; + static ARG_START_PAUSED = "--start-paused"; + static ARG_KEY_DISABLE_SERVICE_AUTH_CODES = "disable-service-auth-codes"; + static ARG_DISABLE_SERVICE_AUTH_CODES = "--disable-service-auth-codes"; + static ARG_KEY_ENDLESS_TRACE_BUFFER = "endless-trace-buffer"; + static ARG_ENDLESS_TRACE_BUFFER = "--endless-trace-buffer"; + static ARG_KEY_USE_TEST_FONTS = "use-test-fonts"; + static ARG_USE_TEST_FONTS = "--use-test-fonts"; + static ARG_KEY_ENABLE_DART_PROFILING = "enable-dart-profiling"; + static ARG_ENABLE_DART_PROFILING = "--enable-dart-profiling"; + static ARG_KEY_ENABLE_SOFTWARE_RENDERING = "enable-software-rendering"; + static ARG_ENABLE_SOFTWARE_RENDERING = "--enable-software-rendering"; + static ARG_KEY_SKIA_DETERMINISTIC_RENDERING = "skia-deterministic-rendering"; + static ARG_SKIA_DETERMINISTIC_RENDERING = "--skia-deterministic-rendering"; + static ARG_KEY_TRACE_SKIA = "trace-skia"; + static ARG_TRACE_SKIA = "--trace-skia"; + static ARG_KEY_TRACE_SKIA_ALLOWLIST = "trace-skia-allowlist"; + static ARG_TRACE_SKIA_ALLOWLIST = "--trace-skia-allowlist="; + static ARG_KEY_TRACE_SYSTRACE = "trace-systrace"; + static ARG_TRACE_SYSTRACE = "--trace-systrace"; + static ARG_KEY_ENABLE_IMPELLER = "enable-impeller"; + static ARG_ENABLE_IMPELLER = "--enable-impeller"; + static ARG_KEY_DUMP_SHADER_SKP_ON_SHADER_COMPILATION = + "dump-skp-on-shader-compilation"; + static ARG_DUMP_SHADER_SKP_ON_SHADER_COMPILATION = + "--dump-skp-on-shader-compilation"; + static ARG_KEY_CACHE_SKSL = "cache-sksl"; + static ARG_CACHE_SKSL = "--cache-sksl"; + static ARG_KEY_PURGE_PERSISTENT_CACHE = "purge-persistent-cache"; + static ARG_PURGE_PERSISTENT_CACHE = "--purge-persistent-cache"; + static ARG_KEY_VERBOSE_LOGGING = "verbose-logging"; + static ARG_VERBOSE_LOGGING = "--verbose-logging"; + static ARG_KEY_OBSERVATORY_PORT = "observatory-port"; + static ARG_OBSERVATORY_PORT = "--observatory-port="; + static ARG_KEY_DART_FLAGS = "dart-flags"; + static ARG_DART_FLAGS = "--dart-flags="; + static ARG_KEY_MSAA_SAMPLES = "msaa-samples"; + static ARG_MSAA_SAMPLES = "--msaa-samples="; + + /** + * Parses arguments from a Want object and creates a FlutterShellArgs instance. + * @param want - The Want object containing the parameters + * @returns A FlutterShellArgs instance with parsed arguments + */ + static fromWant(want: Want): FlutterShellArgs { + let flutterShellArgs: FlutterShellArgs = new FlutterShellArgs(); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_TRACE_STARTUP, FlutterShellArgs.ARG_TRACE_STARTUP, want, + flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_START_PAUSED, FlutterShellArgs.ARG_START_PAUSED, want, + flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_DISABLE_SERVICE_AUTH_CODES, + FlutterShellArgs.ARG_DISABLE_SERVICE_AUTH_CODES, want, flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_ENDLESS_TRACE_BUFFER, FlutterShellArgs.ARG_ENDLESS_TRACE_BUFFER, + want, flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_USE_TEST_FONTS, FlutterShellArgs.ARG_USE_TEST_FONTS, want, + flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_ENABLE_DART_PROFILING, + FlutterShellArgs.ARG_ENABLE_DART_PROFILING, want, flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_ENABLE_SOFTWARE_RENDERING, + FlutterShellArgs.ARG_ENABLE_SOFTWARE_RENDERING, want, flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_SKIA_DETERMINISTIC_RENDERING, + FlutterShellArgs.ARG_SKIA_DETERMINISTIC_RENDERING, want, flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_TRACE_SKIA, FlutterShellArgs.ARG_TRACE_SKIA, want, + flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_TRACE_SYSTRACE, FlutterShellArgs.ARG_TRACE_SYSTRACE, want, + flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_ENABLE_IMPELLER, FlutterShellArgs.ARG_ENABLE_IMPELLER, want, + flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_DUMP_SHADER_SKP_ON_SHADER_COMPILATION, + FlutterShellArgs.ARG_DUMP_SHADER_SKP_ON_SHADER_COMPILATION, want, flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_CACHE_SKSL, FlutterShellArgs.ARG_CACHE_SKSL, want, + flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_PURGE_PERSISTENT_CACHE, + FlutterShellArgs.ARG_PURGE_PERSISTENT_CACHE, want, flutterShellArgs); + FlutterShellArgs.checkArg(FlutterShellArgs.ARG_KEY_VERBOSE_LOGGING, FlutterShellArgs.ARG_VERBOSE_LOGGING, want, + flutterShellArgs); + + let skia_allow_list: Object = want.parameters![FlutterShellArgs.ARG_KEY_TRACE_SKIA_ALLOWLIST]; + if (skia_allow_list != undefined) { + flutterShellArgs.add(FlutterShellArgs.ARG_TRACE_SKIA_ALLOWLIST + (skia_allow_list as string)); + } + + let observatory_port: Object = want.parameters![FlutterShellArgs.ARG_KEY_OBSERVATORY_PORT]; + if (observatory_port != undefined && (observatory_port as number > 0)) { + flutterShellArgs.add(FlutterShellArgs.ARG_OBSERVATORY_PORT + (observatory_port as number)); + } + + let msaa: Object = want.parameters![FlutterShellArgs.ARG_KEY_MSAA_SAMPLES]; + if (msaa != undefined && (msaa as number > 1)) { + flutterShellArgs.add(FlutterShellArgs.ARG_MSAA_SAMPLES + (msaa as number)); + } + + let dart_flags: Object = want.parameters![FlutterShellArgs.ARG_KEY_DART_FLAGS]; + if (dart_flags != undefined) { + flutterShellArgs.add(FlutterShellArgs.ARG_DART_FLAGS + (msaa as string)); + } + return flutterShellArgs; + } + + /** + * Checks if an argument exists in the Want parameters and adds it to FlutterShellArgs if present. + * @param argKey - The key to look for in the Want parameters + * @param argFlag - The command-line flag to add if the argument is present + * @param want - The Want object containing the parameters + * @param flutterShellArgs - The FlutterShellArgs instance to add the flag to + */ + static checkArg(argKey: string, argFlag: string, want: Want, flutterShellArgs: FlutterShellArgs) { + if (want.parameters == undefined) { + return; + } + let value: Object = want.parameters![argKey]; + if (value != undefined && value as Boolean) { + flutterShellArgs.add(argFlag); + } + } + + /** Command-line arguments for the Flutter shell. */ + args: Set = new Set(); + + /** + * Adds an argument to the set of shell arguments. + * @param arg - The argument string to add + */ + add(arg: string) { + this.args.add(arg); + } + + /** + * Removes an argument from the set of shell arguments. + * @param arg - The argument string to remove + */ + remove(arg: string) { + this.args.delete(arg); + } + + /** + * Converts the set of arguments to an array. + * @returns An array containing all shell arguments + */ + toArray(): Array { + return Array.from(this.args); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/DartExecutor.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/DartExecutor.ets new file mode 100644 index 0000000..89824a2 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/DartExecutor.ets @@ -0,0 +1,426 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on DartExecutor.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import resourceManager from '@ohos.resourceManager'; +import FlutterInjector from '../../../FlutterInjector'; +import { BinaryMessageHandler, BinaryReply, TaskQueue, TaskQueueOptions } from '../../../plugin/common/BinaryMessenger'; +import { BinaryMessenger } from '../../../plugin/common/BinaryMessenger'; +import StringCodec from '../../../plugin/common/StringCodec'; +import Log from '../../../util/Log'; +import { TraceSection } from '../../../util/TraceSection'; +import { FlutterCallbackInformation } from '../../../view/FlutterCallbackInformation'; +import FlutterNapi from '../FlutterNapi'; +import { DartMessenger } from './DartMessenger'; +import SendableBinaryMessageHandler from '../../../plugin/common/SendableBinaryMessageHandler' + + +const TAG = "DartExecutor"; + +/** + * Configures, bootstraps, and starts executing Dart code. + * + * To specify a top-level Dart function to execute, use a {@link DartEntrypoint} to tell + * DartExecutor where to find the Dart code to execute, and which Dart function to use as the + * entrypoint. To execute the entrypoint, pass the {@link DartEntrypoint} to + * {@link executeDartEntrypoint}. + * + * To specify a Dart callback to execute, use a {@link DartCallback}. A given Dart callback must + * be registered with the Dart VM to be invoked by a DartExecutor. To execute the callback, + * pass the {@link DartCallback} to {@link executeDartCallback}. + * + * Once started, a DartExecutor cannot be stopped. The associated Dart code will execute + * until it completes, or until the {@link FlutterEngine} that owns this + * DartExecutor is destroyed. + */ +export default class DartExecutor implements BinaryMessenger { + /** The FlutterNapi instance for native communication. */ + flutterNapi: FlutterNapi; + /** The resource manager for accessing application assets. */ + assetManager: resourceManager.ResourceManager; + private dartMessenger: DartMessenger; + private binaryMessenger: BinaryMessenger; + private isApplicationRunning: boolean = false; + private isolateServiceId: String = ""; + private isolateServiceIdListener: IsolateServiceIdListener | null = null; + private isolateChannelMessageHandler: BinaryMessageHandler = + new IsolateChannelMessageHandler(this.isolateServiceId, this.isolateServiceIdListener); + + /** + * Constructs a new DartExecutor instance. + * @param flutterNapi - The FlutterNapi instance for native communication + * @param assetManager - The resource manager for accessing assets + */ + constructor(flutterNapi: FlutterNapi, assetManager: resourceManager.ResourceManager) { + this.flutterNapi = flutterNapi; + this.assetManager = assetManager; + this.dartMessenger = new DartMessenger(flutterNapi); + this.dartMessenger.setMessageHandler("flutter/isolate", this.isolateChannelMessageHandler); + this.binaryMessenger = new DefaultBinaryMessenger(this.dartMessenger); + // The NAPI might already be attached if coming from a spawned engine. If so, correctly report + // that this DartExecutor is already running. + if (flutterNapi.isRunningDart) { + this.isApplicationRunning = true; + } + } + + /** + * Invoked when the FlutterEngine that owns this DartExecutor attaches to NAPI. + * + * When attached to NAPI, this DartExecutor begins handling 2-way communication to/from + * the Dart execution context. This communication is facilitated via 2 APIs: + * BinaryMessenger, which sends messages to Dart + * PlatformMessageHandler, which receives messages from Dart + */ + onAttachedToNAPI(): void { + Log.d(TAG, "Attached to NAPI. Registering the platform message handler for this Dart execution context."); + this.flutterNapi.setPlatformMessageHandler(this.dartMessenger); + } + + /** + * Invoked when the FlutterEngine that owns this DartExecutor detaches from NAPI. + * + * When detached from NAPI, this DartExecutor stops handling 2-way communication to/from + * the Dart execution context. + */ + onDetachedFromNAPI(): void { + Log.d(TAG, "Detached from NAPI. De-registering the platform message handler for this Dart execution context."); + this.flutterNapi.setPlatformMessageHandler(null); + } + + /** + * Checks if this DartExecutor is currently executing Dart code. + * + * @returns True if Dart code is being executed, false otherwise + */ + isExecutingDart(): boolean { + return this.isApplicationRunning; + } + + /** + * Starts executing Dart code based on the given dartEntrypoint and the dartEntrypointArgs. + * + * See DartEntrypoint for configuration options. + * + * @param dartEntrypoint - Specifies which Dart function to run, and where to find it + * @param dartEntrypointArgs - Arguments passed as a list of strings to Dart's entrypoint function + */ + executeDartEntrypoint(dartEntrypoint: DartEntrypoint, dartEntrypointArgs?: string[]): void { + if (this.isApplicationRunning) { + Log.w(TAG, "Attempted to run a DartExecutor that is already running."); + return; + } + + let traceId: number = TraceSection.begin("DartExecutor#executeDartEntrypoint"); + try { + Log.d(TAG, "Executing Dart entrypoint: " + dartEntrypoint); + this.flutterNapi.runBundleAndSnapshotFromLibrary( + dartEntrypoint.pathToBundle, + dartEntrypoint.dartEntrypointFunctionName, + dartEntrypoint.dartEntrypointLibrary, + this.assetManager, + dartEntrypointArgs ?? []); + + this.isApplicationRunning = true; + } finally { + TraceSection.endWithId("DartExecutor#executeDartEntrypoint", traceId); + } + } + + /** + * Starts executing Dart code based on the given dartCallback. + * + * See DartCallback for configuration options. + * + * @param dartCallback - Specifies which Dart callback to run, and where to find it + */ + executeDartCallback(dartCallback: DartCallback): void { + if (this.isApplicationRunning) { + Log.w(TAG, "Attempted to run a DartExecutor that is already running."); + return; + } + + let traceId: number = TraceSection.begin("DartExecutor#executeDartCallback"); + try { + Log.d(TAG, "Executing Dart callback: " + dartCallback); + this.flutterNapi.runBundleAndSnapshotFromLibrary( + dartCallback.pathToBundle, + dartCallback.callbackHandle.callbackName, + dartCallback.callbackHandle.callbackLibraryPath, + dartCallback.resourceManager, + []); + + this.isApplicationRunning = true; + } finally { + TraceSection.endWithId("DartExecutor#executeDartCallback", traceId); + } + } + + /** + * Gets a BinaryMessenger that can be used to send messages to, and receive messages + * from, Dart code that this DartExecutor is executing. + * @returns The BinaryMessenger instance + */ + getBinaryMessenger(): BinaryMessenger { + return this.binaryMessenger; + } + + /** + * Creates a background task queue for handling messages asynchronously. + * @param options - Optional task queue configuration options + * @returns A TaskQueue instance for background message processing + */ + makeBackgroundTaskQueue(options?: TaskQueueOptions): TaskQueue { + return this.getBinaryMessenger().makeBackgroundTaskQueue(options); + } + + + /** + * Sends a binary message to Dart over the specified channel. + * @param channel - The channel name for the message + * @param message - The message data as an ArrayBuffer + * @param callback - Optional callback to receive the reply from Dart + */ + send(channel: String, message: ArrayBuffer, callback?: BinaryReply): void { + this.getBinaryMessenger().send(channel, message, callback); + } + + /** + * Sets a message handler for incoming messages from Dart on the specified channel. + * @param channel - The channel name to listen on + * @param handler - The message handler, or null to remove the handler + * @param taskQueue - Optional task queue for processing messages + * @param args - Additional arguments to pass to the handler + */ + setMessageHandler(channel: String, handler: BinaryMessageHandler | SendableBinaryMessageHandler | null, + taskQueue?: TaskQueue, ...args: Object[]): void { + this.getBinaryMessenger().setMessageHandler(channel, handler, taskQueue, ...args); + } + + /** + * Gets the number of pending channel callback replies. + * When sending messages with reply callbacks, this tracks how many are still waiting for responses. + * Must be called from the main thread. + * Mainly useful for testing frameworks to determine if the app is idle. + * @returns The number of pending channel callback replies + */ + getPendingChannelResponseCount(): number { + return this.dartMessenger.getPendingChannelResponseCount(); + } + + /** + * Gets an identifier for this executor's primary isolate. This identifier can be used in + * queries to the Dart service protocol. + * @returns The isolate service ID + */ + getIsolateServiceId(): String { + return this.isolateServiceId; + } + + + /** + * Sets a listener that will be notified when an isolate identifier is available for this + * executor's primary isolate. + * @param listener - The listener to be notified when the isolate service ID is available + */ + setIsolateServiceIdListener(listener: IsolateServiceIdListener): void { + this.isolateServiceIdListener = listener; + if (this.isolateServiceIdListener != null && this.isolateServiceId != null) { + this.isolateServiceIdListener.onIsolateServiceIdAvailable(this.isolateServiceId); + } + } + + /** + * Notifies the Dart VM of a low memory event. + * This allows the Dart VM to free resources, but does not notify the Flutter application. + * To notify the Flutter application, use SystemChannel.sendMemoryPressureWarning(). + * + * Note: Calling this method may cause performance issues. Avoid calling during startup or animations. + */ + notifyLowMemoryWarning(): void { + if (this.flutterNapi.isAttached()) { + this.flutterNapi.notifyLowMemoryWarning(); + } + } +} + + +/** + * Configuration options that specify which Dart entrypoint function is executed and where to find + * that entrypoint and other assets required for Dart execution. + */ +export class DartEntrypoint { + /** The path within the ResourceManager where the app will look for assets. */ + pathToBundle: string; + /** The library or file location that contains the Dart entrypoint function. */ + dartEntrypointLibrary: string; + /** The name of a Dart function to execute. */ + dartEntrypointFunctionName: string; + + /** + * Constructs a new DartEntrypoint instance. + * @param pathToBundle - The path within the AssetManager where the app will look for assets + * @param dartEntrypointLibrary - The library or file location that contains the Dart entrypoint function + * @param dartEntrypointFunctionName - The name of a Dart function to execute + */ + constructor(pathToBundle: string, + dartEntrypointLibrary: string, + dartEntrypointFunctionName: string) { + this.pathToBundle = pathToBundle; + this.dartEntrypointLibrary = dartEntrypointLibrary; + this.dartEntrypointFunctionName = dartEntrypointFunctionName; + } + + /** + * Creates a default DartEntrypoint using the main function. + * @returns A DartEntrypoint configured with the default main entrypoint + * @throws Error if FlutterLoader is not initialized + */ + static createDefault() { + const flutterLoader = FlutterInjector.getInstance().getFlutterLoader(); + if (!flutterLoader.initialized) { + throw new Error( + "DartEntrypoints can only be created once a FlutterEngine is created."); + } + return new DartEntrypoint(flutterLoader.findAppBundlePath(), "", "main"); + } +} + + +/** + * Callback interface invoked when the isolate identifier becomes available. + */ +interface IsolateServiceIdListener { + /** + * Called when the isolate service ID becomes available. + * @param isolateServiceId - The isolate service ID + */ + onIsolateServiceIdAvailable(isolateServiceId: String): void; +} + + +/** + * Configuration options that specify which Dart callback function is executed and where to find + * that callback and other assets required for Dart execution. + */ +export class DartCallback { + /** Standard OpenHarmony ResourceManager for accessing assets. */ + public resourceManager: resourceManager.ResourceManager; + /** The path within the ResourceManager where the app will look for assets. */ + public pathToBundle: string; + /** A Dart callback that was previously registered with the Dart VM. */ + public callbackHandle: FlutterCallbackInformation; + + /** + * Constructs a new DartCallback instance. + * @param resourceManager - Standard OpenHarmony ResourceManager + * @param pathToBundle - The path within the ResourceManager where the app will look for assets + * @param callbackHandle - A Dart callback that was previously registered with the Dart VM + */ + constructor(resourceManager: resourceManager.ResourceManager, + pathToBundle: string, + callbackHandle: FlutterCallbackInformation) { + this.resourceManager = resourceManager; + this.pathToBundle = pathToBundle; + this.callbackHandle = callbackHandle; + } + + /** + * Returns a string representation of this DartCallback. + * @returns A string describing the callback's bundle path, library path, and function name + */ + toString(): String { + return "DartCallback( bundle path: " + + this.pathToBundle + + ", library path: " + + this.callbackHandle.callbackLibraryPath + + ", function: " + + this.callbackHandle.callbackName + + " )"; + } +} + +/** + * Default implementation of BinaryMessenger that delegates to DartMessenger. + */ +export class DefaultBinaryMessenger implements BinaryMessenger { + private messenger: DartMessenger; + + /** + * Constructs a new DefaultBinaryMessenger instance. + * @param messenger - The DartMessenger instance to delegate to + */ + constructor(messenger: DartMessenger) { + this.messenger = messenger; + } + + /** + * Creates a background task queue for handling messages asynchronously. + * @param options - Optional task queue configuration options + * @returns A TaskQueue instance for background message processing + */ + makeBackgroundTaskQueue(options?: TaskQueueOptions): TaskQueue { + return this.messenger.makeBackgroundTaskQueue(options); + } + + /** + * Sends a message from OpenHarmony to Dart over the given channel and then + * has the provided callback invoked when the Dart side responds. + * + * @param channel - The name of the logical channel used for the message + * @param message - The message payload as an ArrayBuffer, or null for an empty message + * @param callback - A callback invoked when the Dart application responds to the message + */ + send(channel: String, message: ArrayBuffer, callback?: BinaryReply): void { + this.messenger.send(channel, message, callback); + } + + /** + * Sets the given BinaryMessageHandler as the singular handler for all incoming messages + * received from the Dart side of this Dart execution context. + * + * @param channel - The name of the channel + * @param handler - A BinaryMessageHandler to be invoked on incoming messages, or null + * @param taskQueue - Optional task queue for processing messages + * @param args - Additional arguments to pass to the handler + */ + setMessageHandler(channel: String, handler: BinaryMessageHandler | SendableBinaryMessageHandler | null, + taskQueue?: TaskQueue, ...args: Object[]): void { + this.messenger.setMessageHandler(channel, handler, taskQueue, ...args); + } +} + +/** + * Message handler for the isolate channel that receives isolate service IDs. + */ +class IsolateChannelMessageHandler implements BinaryMessageHandler { + private isolateServiceId: String; + private isolateServiceIdListener: IsolateServiceIdListener | null = null; + + /** + * Constructs a new IsolateChannelMessageHandler instance. + * @param isolateServiceId - The isolate service ID + * @param isolateServiceIdListener - Optional listener to be notified when the ID is available + */ + constructor(isolateServiceId: String, isolateServiceIdListener: IsolateServiceIdListener | null) { + this.isolateServiceId = isolateServiceId; + this.isolateServiceIdListener = isolateServiceIdListener; + } + + /** + * Handles a message received on the isolate channel. + * @param message - The message containing the isolate service ID + * @param callback - The reply callback + */ + onMessage(message: ArrayBuffer, callback: BinaryReply): void { + this.isolateServiceId = StringCodec.INSTANCE.decodeMessage(message); + if (this.isolateServiceIdListener != null) { + this.isolateServiceIdListener.onIsolateServiceIdAvailable(this.isolateServiceId); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/DartMessenger.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/DartMessenger.ets new file mode 100644 index 0000000..e85ef1c --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/DartMessenger.ets @@ -0,0 +1,516 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on DartMessenger.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import { ErrorEvent, Queue, taskpool, worker, MessageEvents, JSON } from '@kit.ArkTS'; + +import Log from '../../../util/Log'; +import { + BinaryMessageHandler, + BinaryMessenger, + BinaryReply, + TaskPriority, + TaskQueue, + TaskQueueOptions +} from '../../../plugin/common/BinaryMessenger'; +import FlutterNapi from '../FlutterNapi'; +import { PlatformMessageHandler } from './PlatformMessageHandler'; +import { TraceSection } from '../../../util/TraceSection'; +import SendableBinaryMessageHandler from '../../../plugin/common/SendableBinaryMessageHandler' + +/** + * Message conduit for 2-way communication between OpenHarmony and Dart. + * + * See {@link BinaryMessenger}, which sends messages from OpenHarmony to Dart + * + * See {@link PlatformMessageHandler}, which handles messages to OpenHarmony from Dart + */ + +const TAG = "DartMessenger"; + +export class DartMessenger implements BinaryMessenger, PlatformMessageHandler { + /** The FlutterNapi instance for native communication. */ + flutterNapi: FlutterNapi; + /** Map of channel names to their message handlers. */ + messageHandlers: Map = new Map(); + /** Map of reply IDs to their callback functions waiting for responses. */ + pendingReplies: Map = new Map(); + /** The next reply ID to use for message callbacks. */ + nextReplyId: number = 1; + /** Factory for creating task queues for background message processing. */ + taskQueueFactory: TaskQueueFactory; + /** Map of TaskQueue tokens to their actual task queue implementations. */ + createdTaskQueues: Map = new Map(); + + /** + * Constructs a new DartMessenger instance. + * @param flutterNapi - The FlutterNapi instance for native communication + */ + constructor(flutterNapi: FlutterNapi) { + this.flutterNapi = flutterNapi; + this.taskQueueFactory = new DefaultTaskQueueFactory(); + } + + /** + * Creates a background task queue for handling messages asynchronously. + * @param options - Optional task queue configuration options + * @returns A TaskQueue instance for background message processing + */ + makeBackgroundTaskQueue(options?: TaskQueueOptions): TaskQueue { + let taskQueue: DartMessengerTaskQueue = + this.taskQueueFactory.makeBackgroundTaskQueue(options ?? new TaskQueueOptions()); + let token: TaskQueueToken = new TaskQueueToken(); + this.createdTaskQueues.set(token, taskQueue); + return token; + } + + /** + * Sets a message handler for incoming messages from Dart on the specified channel. + * @param channel - The channel name to listen on + * @param handler - The message handler, or null to remove the handler + * @param taskQueue - Optional task queue for processing messages + * @param args - Additional arguments to pass to the handler + */ + setMessageHandler(channel: String, handler: BinaryMessageHandler | SendableBinaryMessageHandler | null, + taskQueue?: TaskQueue, ...args: Object[]): void { + if (handler == null) { + Log.d(TAG, "Removing handler for channel '" + channel + "'"); + this.messageHandlers.delete(channel); + return; + } + let dartMessengerTaskQueue: DartMessengerTaskQueue | null = null; + if (taskQueue !== null && taskQueue !== undefined) { + dartMessengerTaskQueue = this.createdTaskQueues.get(taskQueue) ?? null; + if (dartMessengerTaskQueue == null) { + throw new Error( + "Unrecognized TaskQueue, use BinaryMessenger to create your TaskQueue (ex makeBackgroundTaskQueue)." + ); + } + } + Log.d(TAG, "Setting handler for channel '" + channel + "'"); + + this.messageHandlers.set(channel, new HandlerInfo(handler, dartMessengerTaskQueue, ...args)); + } + + /** + * Sends a binary message to Dart over the specified channel. + * @param channel - The channel name for the message + * @param message - The message data as an ArrayBuffer, or null for an empty message + * @param callback - Optional callback to receive the reply from Dart + */ + send(channel: String, message: ArrayBuffer, callback?: BinaryReply): void { + Log.d(TAG, "Sending message over channel '" + channel + "'"); + let traceId: number = TraceSection.begin("DartMessenger#send on " + channel); + try { + Log.d(TAG, "Sending message with callback over channel '" + channel + "'"); + let replyId: number = this.nextReplyId++; + if (callback != null) { + this.pendingReplies.set(replyId, callback); + } + if (message == null) { + this.flutterNapi.dispatchEmptyPlatformMessage(channel, replyId); + } else { + this.flutterNapi.dispatchPlatformMessage(channel, message, message.byteLength, replyId); + } + } finally { + TraceSection.endWithId("DartMessenger#send on " + channel, traceId); + } + this.IsFlutterNavigationExecuted(channel); + } + + /** + * Dispatches a message to a task queue for asynchronous processing. + * @param handlerInfo - The handler information containing the handler and task queue + * @param message - The message data as an ArrayBuffer + * @param replyId - The reply ID for responding to the message + */ + dispatchMessageToQueue(handlerInfo: HandlerInfo, message: ArrayBuffer, replyId: number): void { + let taskState: TaskState = new TaskState(handlerInfo.handler as ESObject, message, ...handlerInfo.args); + handlerInfo.taskQueue?.dispatch(taskState, new Reply(this.flutterNapi, replyId)); + } + + /** + * Invokes a message handler synchronously. + * @param handler - The message handler to invoke, or null if no handler is registered + * @param message - The message data as an ArrayBuffer + * @param replyId - The reply ID for responding to the message + */ + invokeHandler(handler: BinaryMessageHandler | null, message: ArrayBuffer, replyId: number): void { + if (handler != null) { + try { + Log.d(TAG, "Deferring to registered handler to process message."); + handler.onMessage(message, new Reply(this.flutterNapi, replyId)); + } catch (ex) { + Log.e(TAG, "Uncaught exception in binary message listener", ex); + this.flutterNapi.invokePlatformMessageEmptyResponseCallback(replyId); + } + } else { + Log.d(TAG, "No registered handler for message. Responding to Dart with empty reply message."); + this.flutterNapi.invokePlatformMessageEmptyResponseCallback(replyId); + } + } + + /** + * Handles a message received from Dart over a specific channel. + * @param channel - The channel name for the message + * @param message - The message data as an ArrayBuffer + * @param replyId - The reply ID for responding to the message + * @param messageData - Additional message data + */ + handleMessageFromDart(channel: String, message: ArrayBuffer, replyId: number, messageData: number): void { + Log.d(TAG, "Received message from Dart over channel '" + channel + "'"); + let handlerInfo: HandlerInfo | null = this.messageHandlers.get(channel) ?? null; + if (handlerInfo?.taskQueue != null) { + this.dispatchMessageToQueue(handlerInfo, message, replyId); + } else { + this.invokeHandler(handlerInfo?.handler as BinaryMessageHandler, message, replyId); + } + this.IsFlutterNavigationExecuted(channel); + } + + /** + * Handles a platform message response from Dart. + * @param replyId - The reply ID that was sent with the original message + * @param reply - The reply data as an ArrayBuffer + */ + handlePlatformMessageResponse(replyId: number, reply: ArrayBuffer): void { + Log.d(TAG, "Received message reply from Dart."); + let callback: BinaryReply | null = this.pendingReplies.get(replyId) ?? null; + this.pendingReplies.delete(replyId); + if (callback != null) { + try { + Log.d(TAG, "Invoking registered callback for reply from Dart."); + callback.reply(reply); + } catch (e) { + Log.e(TAG, "Uncaught exception in binary message reply handler", e); + } + } + } + + /** + * Returns the number of pending channel callback replies. + * + * When sending messages to the Flutter application using BinaryMessenger.send, + * developers can optionally specify a reply callback if they expect a reply from the Flutter application. + * + * This method tracks all the pending callbacks that are waiting for response, and is supposed + * to be called from the main thread (as other methods). Calling from a different thread could + * possibly capture an indeterministic internal state, so don't do it. + * @returns The number of pending channel callback replies + */ + getPendingChannelResponseCount(): number { + return this.pendingReplies.size; + } + + /** + * Checks if the current Flutter page is performing navigation and notifies the native side. + * @param channel - The channel name to check + */ + IsFlutterNavigationExecuted(channel: String): void { + if (channel == "flutter/navigation") { + this.flutterNapi.setFlutterNavigationAction(this.flutterNapi.nativeShellHolderId!, true); + Log.d(TAG, "setFlutterNavigationAction -> '" + channel + "'"); + } + } +} + +/** + * Holds information about a platform handler, such as the task queue that processes messages from + * Dart. + */ +class HandlerInfo { + handler: BinaryMessageHandler | SendableBinaryMessageHandler; + taskQueue: DartMessengerTaskQueue | null; + args: Object[]; + + /** + * Constructs a new HandlerInfo instance. + * @param handler - The message handler + * @param taskQueue - The task queue for processing messages, or null for synchronous processing + * @param args - Additional arguments to pass to the handler + */ + constructor(handler: BinaryMessageHandler | SendableBinaryMessageHandler, + taskQueue: DartMessengerTaskQueue | null, + ...args: Object[]) { + this.handler = handler; + this.taskQueue = taskQueue; + this.args = args; + } +} + +/** + * Implementation of BinaryReply that sends replies back to Dart. + */ +class Reply implements BinaryReply { + flutterNapi: FlutterNapi; + replyId: number; + done: boolean = false; + + /** + * Constructs a new Reply instance. + * @param flutterNapi - The FlutterNapi instance for sending replies + * @param replyId - The reply ID for this reply + */ + constructor(flutterNapi: FlutterNapi, replyId: number) { + this.flutterNapi = flutterNapi; + this.replyId = replyId; + } + + /** + * Sends a reply back to Dart. + * @param reply - The reply data as an ArrayBuffer, or null for an empty reply + * @throws Error if reply has already been submitted + */ + reply(reply: ArrayBuffer | null) { + if (this.done) { + throw new Error("Reply already submitted"); + } + + if (reply == null) { + this.flutterNapi.invokePlatformMessageEmptyResponseCallback(this.replyId); + } else { + this.flutterNapi.invokePlatformMessageResponseCallback(this.replyId, reply, reply.byteLength); + } + } +} + +/** + * Represents the state of a task to be executed in a background task queue. + */ +export class TaskState { + /** The message handler to execute. */ + handler: SendableBinaryMessageHandler; + /** The message data as an ArrayBuffer. */ + message: ArrayBuffer; + /** Additional arguments to pass to the handler. */ + args: Object[]; + + /** + * Constructs a new TaskState instance. + * @param handler - The message handler to execute + * @param message - The message data as an ArrayBuffer + * @param args - Additional arguments to pass to the handler + */ + constructor(handler: SendableBinaryMessageHandler, message: ArrayBuffer, ...args: Object[]) { + this.handler = handler; + this.message = message; + this.args = args; + } +} + +/** + * Interface for task queues that process messages asynchronously. + */ +interface DartMessengerTaskQueue { + /** + * Dispatches a task to be executed in the task queue. + * @param taskState - The task state containing the handler and message + * @param callback - The reply callback for sending responses + */ + dispatch(taskState: TaskState, callback: Reply): void; +} + +/** + * Interface for serial task queues that process messages sequentially. + */ +interface SerialTaskQueue extends DartMessengerTaskQueue { +} + +/** + * Factory interface for creating task queues. + */ +interface TaskQueueFactory { + /** + * Creates a background task queue with the specified options. + * @param options - Task queue configuration options + * @returns A DartMessengerTaskQueue instance + */ + makeBackgroundTaskQueue(options: TaskQueueOptions): DartMessengerTaskQueue; +} + +/** + * Task queue that processes messages concurrently. + */ +class ConcurrentTaskQueue implements DartMessengerTaskQueue { + private priority: TaskPriority; + + /** + * Constructs a new ConcurrentTaskQueue instance. + * @param priority - The priority level for task execution + */ + constructor(priority: TaskPriority) { + this.priority = priority; + } + + /** + * Dispatches a task to be executed concurrently. + * @param taskState - The task state containing the handler and message + * @param callback - The reply callback for sending responses + */ + dispatch(taskState: TaskState, callback: Reply): void { + let task: taskpool.Task = new taskpool.Task(handleMessageInBackground, + taskState.handler, + taskState.message, + ...taskState.args); + taskpool.execute(task, this.priority as number).then((result: Object) => { + callback.reply(result as ArrayBuffer); + }).catch((err: string) => { + callback.reply(null); + Log.e(TAG, "Oops! Failed to execute task: ", err); + }); + } +} + +const scriptURL: string = '../workers/PlatformChannelWorker.ets'; +/** + * Task queue that processes messages serially using a worker thread. + */ +class SerialTaskQueueWithWorker implements SerialTaskQueue { + private static workerInstance: worker.ThreadWorker | null = null; + + /** + * Constructs a new SerialTaskQueueWithWorker instance. + * Creates a singleton worker instance if one doesn't exist. + */ + constructor () { + if (!SerialTaskQueueWithWorker.workerInstance) { + SerialTaskQueueWithWorker.workerInstance = + new worker.ThreadWorker(scriptURL, {name: 'PlatformChannelWorker'}); + } + } + + /** + * Dispatches a task to be executed serially in the worker thread. + * @param taskState - The task state containing the handler and message + * @param callback - The reply callback for sending responses + */ + dispatch(taskState: TaskState, callback: Reply): void { + SerialTaskQueueWithWorker.workerInstance!.onmessage = (e: MessageEvents): void => { + callback.reply(e.data as ArrayBuffer); + } + + SerialTaskQueueWithWorker.workerInstance!.onerror = (err: ErrorEvent) => { + callback.reply(null); + Log.e(TAG, "Oops! Failed to execute task in worker thread: ", err.message); + } + + SerialTaskQueueWithWorker.workerInstance!.postMessageWithSharedSendable(taskState, [taskState.message]); + } +} + +type Runnable = () => Promise; +/** + * Task queue that processes messages serially using a task pool. + */ +class SerialTaskQueueWithTaskPool implements SerialTaskQueue { + private priority: TaskPriority; + private queue: Queue = new Queue(); + private isRunning: boolean = false; + + /** + * Constructs a new SerialTaskQueueWithTaskPool instance. + * @param priority - The priority level for task execution + */ + constructor(priority: TaskPriority) { + this.priority = priority; + } + + /** + * Dispatches a task to be executed serially in the task pool. + * @param taskState - The task state containing the handler and message + * @param callback - The reply callback for sending responses + */ + dispatch(taskState: TaskState, callback: Reply): void { + let task: taskpool.Task = new taskpool.Task(handleMessageInBackground, + taskState.handler, + taskState.message, + ...taskState.args); + const runnable: Runnable = async () => { + try { + const result = await taskpool.execute(task, this.priority as number); + callback.reply(result as ArrayBuffer); + } catch (err) { + callback.reply(null); + Log.e(TAG, "Oops! Failed to execute task: ", err); + } + }; + + this.queue.add(runnable); + + if (!this.isRunning) { + this.runNext(); + } + } + + private async runNext(): Promise { + if (this.queue.length > 0) { + this.isRunning = true; + const task = this.queue.pop(); + try { + await task(); + } finally { + this.isRunning = false; + this.runNext(); // 执行下一个任务 + } + } + } +} + +/** + * Default implementation of TaskQueueFactory. + */ +class DefaultTaskQueueFactory implements TaskQueueFactory { + /** + * Creates a background task queue based on the provided options. + * @param options - Task queue configuration options + * @returns A DartMessengerTaskQueue instance (serial or concurrent) + */ + makeBackgroundTaskQueue(options: TaskQueueOptions): DartMessengerTaskQueue { + if (options.isSingleThreadMode()) { + return new SerialTaskQueueWithWorker(); + } else { + if (options.getIsSerial()) { + return new SerialTaskQueueWithTaskPool(options.getPriority()); + } + return new ConcurrentTaskQueue(options.getPriority()); + } + } +} + +/** + * Token implementation of TaskQueue used to identify task queues. + */ +class TaskQueueToken implements TaskQueue { +} + +/** + * Handles a message in the background thread. + * This function is executed concurrently and processes messages asynchronously. + * @param handler - The message handler to execute + * @param message - The message data as an ArrayBuffer + * @param args - Additional arguments to pass to the handler + * @returns A promise that resolves to the reply ArrayBuffer, or null if no reply + */ +@Concurrent +async function handleMessageInBackground(handler: SendableBinaryMessageHandler, + message: ArrayBuffer, + ...args: Object[]): Promise { + const result = await new Promise((resolve, reject) => { + try { + handler.onMessage(message, { + reply: (reply: ArrayBuffer | null): void => { + resolve(reply); + } + }, ...args); + } catch (e) { + reject(null); + Log.e('WARNING', "Oops! Failed to handle message in the background: ", e); + } + }); + return result; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/PlatformMessageHandler.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/PlatformMessageHandler.ets new file mode 100644 index 0000000..8d3cf70 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/dart/PlatformMessageHandler.ets @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformMessageHandler.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * Interface for handling platform messages from Dart. + * This interface provides methods to receive messages from the Dart side of the Flutter application. + */ +export interface PlatformMessageHandler { + + /** + * Handles a message received from Dart over a specific channel. + * @param channel - The channel name for the message + * @param message - The message data as an ArrayBuffer + * @param replyId - The reply ID for responding to the message + * @param messageData - Additional message data + */ + handleMessageFromDart(channel: String, message: ArrayBuffer, replyId: number, messageData: number): void; + + /** + * Handles a platform message response from Dart. + * @param replyId - The reply ID that was sent with the original message + * @param reply - The reply data as an ArrayBuffer + */ + handlePlatformMessageResponse(replyId: number, reply: ArrayBuffer): void; + +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/ApplicationInfoLoader.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/ApplicationInfoLoader.ets new file mode 100644 index 0000000..54e1d44 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/ApplicationInfoLoader.ets @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import FlutterApplicationInfo from './FlutterApplicationInfo'; +import common from '@ohos.app.ability.common'; + +/** + * Loader for Flutter application information. + * This class provides a static method to load FlutterApplicationInfo from the application context. + */ +export default class ApplicationInfoLoader { + /** + * Loads FlutterApplicationInfo from the application context. + * @param context - The application context + * @returns A FlutterApplicationInfo instance with default or context-based values + */ + static load(context: common.Context) { + let applicationInfo = + new FlutterApplicationInfo(null, null, null, null, null, context.bundleCodeDir + '/libs/arm64', true); + return applicationInfo + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/FlutterApplicationInfo.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/FlutterApplicationInfo.ets new file mode 100644 index 0000000..1a3f9af --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/FlutterApplicationInfo.ets @@ -0,0 +1,71 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterApplicationInfo.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import BuildProfile from "../../../../../../BuildProfile"; + +const DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; +const DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; +const DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; +const DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; + + +/** + * Contains application information for Flutter initialization. + * This class holds configuration data such as AOT library names, snapshot data paths, + * asset directories, and build mode information. + */ +export default class FlutterApplicationInfo { + /** Name of the AOT shared library. */ + aotSharedLibraryName: string; + /** Name of the VM snapshot data file. */ + vmSnapshotData: string; + /** Name of the isolate snapshot data file. */ + isolateSnapshotData: string; + /** Directory containing Flutter assets. */ + flutterAssetsDir: string; + /** Domain network policy configuration. */ + domainNetworkPolicy: string; + /** Directory containing native libraries. */ + nativeLibraryDir: string; + /** Whether to automatically register plugins. */ + automaticallyRegisterPlugins: boolean; + /** Whether the application is running in debug mode. */ + isDebugMode: boolean; + /** Whether the application is running in profile mode. */ + isProfile: boolean; + + /** + * Constructs a new FlutterApplicationInfo instance. + * @param aotSharedLibraryName - Name of the AOT shared library, or null to use default + * @param vmSnapshotData - Name of the VM snapshot data file, or null to use default + * @param isolateSnapshotData - Name of the isolate snapshot data file, or null to use default + * @param flutterAssetsDir - Directory containing Flutter assets, or null to use default + * @param domainNetworkPolicy - Domain network policy, or null for empty string + * @param nativeLibraryDir - Directory containing native libraries + * @param automaticallyRegisterPlugins - Whether to automatically register plugins + */ + constructor(aotSharedLibraryName: string | null, + vmSnapshotData: string | null, + isolateSnapshotData: string | null, + flutterAssetsDir: string | null, + domainNetworkPolicy: string | null, + nativeLibraryDir: string, + automaticallyRegisterPlugins: boolean) { + this.aotSharedLibraryName = aotSharedLibraryName == null ? DEFAULT_AOT_SHARED_LIBRARY_NAME : aotSharedLibraryName; + this.vmSnapshotData = vmSnapshotData == null ? DEFAULT_VM_SNAPSHOT_DATA : vmSnapshotData; + this.isolateSnapshotData = isolateSnapshotData == null ? DEFAULT_ISOLATE_SNAPSHOT_DATA : isolateSnapshotData; + this.flutterAssetsDir = flutterAssetsDir == null ? DEFAULT_FLUTTER_ASSETS_DIR : flutterAssetsDir; + this.domainNetworkPolicy = domainNetworkPolicy == null ? "" : domainNetworkPolicy; + this.nativeLibraryDir = nativeLibraryDir; + this.automaticallyRegisterPlugins = automaticallyRegisterPlugins; + this.isDebugMode = "debug" == String(BuildProfile.BUILD_MODE_NAME); + this.isProfile = "profile" == String(BuildProfile.BUILD_MODE_NAME); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/FlutterLoader.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/FlutterLoader.ets new file mode 100644 index 0000000..a0f37c7 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/loader/FlutterLoader.ets @@ -0,0 +1,400 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterLoader.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * FlutterLoader is responsible for starting the Dart VM and loading Dart code. + * This class locates Flutter resources in the HAP package and loads the Flutter native library. + * It handles initialization, resource copying, and Dart VM configuration. + */ +import FlutterShellArgs from '../FlutterShellArgs'; +import FlutterNapi from '../FlutterNapi'; +import Log from '../../../util/Log'; +import FlutterApplicationInfo from './FlutterApplicationInfo'; +import common from '@ohos.app.ability.common'; +import StringUtils from '../../../util/StringUtils'; +import ApplicationInfoLoader from './ApplicationInfoLoader'; +import bundleManager from '@ohos.bundle.bundleManager'; +import fs from '@ohos.file.fs'; +import { BusinessError } from '@ohos.base'; +import data_preferences from '@ohos.data.preferences'; +import { util } from '@kit.ArkTS'; +import deviceInfo from '@ohos.deviceInfo'; +import { json5Tojson } from '../../../util/Json5ToJson'; + +const TAG = "FlutterLoader"; + +// Flutter engine shared library +const DEFAULT_LIBRARY = "libflutter.so"; +// Default kernel file for JIT builds +const DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; +// Default snapshot library for JIT builds +const VMSERVICE_SNAPSHOT_LIBRARY = "libvmservice_snapshot.so"; +// Key for snapshot asset path +const SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; +// Key for VM snapshot data +const VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; +// Key for isolate snapshot data +const ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; + + +const AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; + +const AOT_VMSERVICE_SHARED_LIBRARY_NAME = "aot-vmservice-shared-library-name"; + +// File path separator +const FILE_SEPARATOR = "/"; + +const TIMESTAMP_PREFIX = "res_timestamp-"; + +const ENABLE_IMPELLER_TAG = "enable_impeller"; + +const TRUE_STRING = "true"; + +const BUILD_INFO_FILE_NAME = "buildinfo.json5"; + +/** + * Represents a string item with name and value. + */ +interface StringItem { + name: string; + value: string; +} + +/** + * Represents build information data containing an array of string items. + */ +interface InfoData { + string: StringItem[]; +} + +/** + * Prefetches the default font manager asynchronously. + * This should be called before using fonts in the Flutter engine. + */ +async function prefetchDefaultFontManager(): Promise { + await new Promise((resolve: Function) => { + FlutterNapi.prefetchDefaultFontManager() + resolve() + }) +} + +/** + * FlutterLoader is responsible for starting the Dart VM and loading Dart code. + * This class locates Flutter resources in the HAP package and loads the Flutter native library. + * It handles initialization, resource copying, and Dart VM configuration. + */ +export default class FlutterLoader { + /** The FlutterNapi instance for native communication. */ + flutterNapi: FlutterNapi; + /** Initialization result containing paths for app storage, engine caches, and data directory. */ + initResult: InitResult | null = null; + /** Flutter application information including asset paths and build mode. */ + flutterApplicationInfo: FlutterApplicationInfo | null = null; + /** The application context for accessing resources. */ + context: common.Context | null = null; + /** Whether the FlutterLoader has been initialized. */ + initialized: boolean = false; + /** Timestamp when initialization started. */ + initStartTimestampMillis: number = 0; + /** Whether Impeller rendering backend is enabled. */ + isEnableImpeller: boolean = false; + + /** + * Constructs a new FlutterLoader instance. + * @param flutterNapi - The FlutterNapi instance for native communication + */ + constructor(flutterNapi: FlutterNapi) { + this.flutterNapi = flutterNapi; + } + + /** + * Gets build information from the buildinfo.json5 file. + * @param context - The application context + * @returns A map containing build information key-value pairs + */ + private getBuildInfo(context: common.Context): Map { + let buildInfoMap: Map = new Map(); + try { + let rawFile = context.resourceManager.getRawFileContentSync(BUILD_INFO_FILE_NAME); + let textDecoder = util.TextDecoder.create('utf-8', { + ignoreBOM: true + }); + let record = textDecoder.decodeWithStream(rawFile, { + stream: false + }); + let jsonRecord: InfoData = JSON.parse(json5Tojson(record)); + jsonRecord.string.forEach((item: StringItem) => { + buildInfoMap.set(item.name, item.value); + }); + return buildInfoMap; + } catch (error) { + Log.e(TAG, "can not find buildinfo.json5 file.") + return buildInfoMap; + } + + } + + /** + * Starts initialization of the native system. + * + * This loads the Flutter engine's native library to enable subsequent NAPI calls. This also + * starts locating and unpacking Dart resources packaged in the app's HAP. + * + * Calling this method multiple times has no effect. + * + * @param context - The OpenHarmony application context + */ + startInitialization(context: common.Context) { + Log.d(TAG, "flutterLoader start init") + this.initStartTimestampMillis = Date.now(); + this.context = context; + this.flutterApplicationInfo = ApplicationInfoLoader.load(context); + prefetchDefaultFontManager(); + if (this.flutterApplicationInfo!.isDebugMode) { + this.copyResource(context) + } + let buildInfoMap = this.getBuildInfo(this.context!); + if (!buildInfoMap.has(ENABLE_IMPELLER_TAG) || buildInfoMap.get(ENABLE_IMPELLER_TAG) == TRUE_STRING) { + this.isEnableImpeller = true; + } else { + this.isEnableImpeller = false; + } + this.initResult = new InitResult( + `${context.filesDir}/`, + `${context.cacheDir}/`, + `${context.filesDir}` + ) + Log.d(TAG, "flutterLoader end init") + } + + private copyResource(context: common.Context) { + let filePath = context.filesDir + FILE_SEPARATOR + this.flutterApplicationInfo!.flutterAssetsDir + const timestamp = this.checkTimestamp(filePath); + if (timestamp == null) { + Log.d(TAG, "no need copyResource") + return; + } + if (this.context != null) { + Log.d(TAG, "start copyResource") + if (fs.accessSync(filePath + FILE_SEPARATOR + DEFAULT_KERNEL_BLOB)) { + Log.d(TAG, "hap has changed, start delete previous file") + fs.rmdirSync(filePath); + } + + if (!fs.accessSync(filePath)) { + fs.mkdirSync(filePath) + } + + let kernelBuffer = + this.context.resourceManager.getRawFileContentSync(this.flutterApplicationInfo!.flutterAssetsDir + + FILE_SEPARATOR + DEFAULT_KERNEL_BLOB) + let kernelFile = + fs.openSync(filePath + FILE_SEPARATOR + DEFAULT_KERNEL_BLOB, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE) + fs.writeSync(kernelFile.fd, kernelBuffer.buffer) + + let vmBuffer = + this.context.resourceManager.getRawFileContentSync(this.flutterApplicationInfo!.flutterAssetsDir + + FILE_SEPARATOR + this.flutterApplicationInfo!.vmSnapshotData) + let vmFile = fs.openSync(filePath + FILE_SEPARATOR + this.flutterApplicationInfo!.vmSnapshotData, + fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE) + fs.writeSync(vmFile.fd, vmBuffer.buffer) + + let isolateBuffer = + this.context.resourceManager.getRawFileContentSync(this.flutterApplicationInfo!.flutterAssetsDir + + FILE_SEPARATOR + this.flutterApplicationInfo!.isolateSnapshotData) + let isolateFile = fs.openSync(filePath + FILE_SEPARATOR + this.flutterApplicationInfo!.isolateSnapshotData, + fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE) + fs.writeSync(isolateFile.fd, isolateBuffer.buffer) + + if (timestamp != null) { + fs.closeSync(fs.openSync(filePath + FILE_SEPARATOR + timestamp, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)) + } + fs.closeSync(kernelFile) + fs.closeSync(vmFile) + fs.closeSync(isolateFile) + Log.d(TAG, "copyResource end") + } else { + Log.d(TAG, "no copyResource") + } + } + + /** + * Ensures that Dart VM initialization is complete. + * This method initializes the Dart VM with the appropriate shell arguments + * based on the build mode (debug, profile, or release). + * @param shellArgs - Optional array of shell arguments, will be created if null + */ + ensureInitializationComplete(shellArgs: Array | null) { + if (this.initialized) { + return; + } + if (shellArgs == null) { + shellArgs = new Array(); + } + shellArgs.push("--icu-symbol-prefix=_binary_icudtl_dat"); + shellArgs.push( + "--icu-native-lib-path=" + + this.flutterApplicationInfo!.nativeLibraryDir + + FILE_SEPARATOR + DEFAULT_LIBRARY + ); + + let kernelPath: string = ""; + if (this.flutterApplicationInfo!.isDebugMode) { + Log.d(TAG, "this.initResult!.dataDirPath=" + this.initResult!.dataDirPath) + const snapshotAssetPath = + this.initResult!.dataDirPath + FILE_SEPARATOR + this.flutterApplicationInfo!.flutterAssetsDir; + kernelPath = snapshotAssetPath + FILE_SEPARATOR + DEFAULT_KERNEL_BLOB; + shellArgs.push("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); + shellArgs.push("--" + VM_SNAPSHOT_DATA_KEY + "=" + this.flutterApplicationInfo!.vmSnapshotData); + shellArgs.push( + "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + this.flutterApplicationInfo!.isolateSnapshotData); + shellArgs.push('--enable-checked-mode') + shellArgs.push('--verbose-logging') + } else { + shellArgs.push( + "--" + AOT_SHARED_LIBRARY_NAME + "=" + this.flutterApplicationInfo!.aotSharedLibraryName); + shellArgs.push( + "--" + + AOT_SHARED_LIBRARY_NAME + + "=" + + this.flutterApplicationInfo!.nativeLibraryDir + + FILE_SEPARATOR + + this.flutterApplicationInfo!.aotSharedLibraryName); + + const snapshotAssetPath = + this.initResult!.dataDirPath + FILE_SEPARATOR + this.flutterApplicationInfo!.flutterAssetsDir; + + if (this.flutterApplicationInfo!.isProfile) { + shellArgs.push("--" + AOT_VMSERVICE_SHARED_LIBRARY_NAME + "=" + VMSERVICE_SNAPSHOT_LIBRARY); + } + } + shellArgs.push("--cache-dir-path=" + this.initResult!.engineCachesPath); + if (StringUtils.isNotEmpty(this.flutterApplicationInfo!.domainNetworkPolicy)) { + shellArgs.push("--domain-network-policy=" + this.flutterApplicationInfo!.domainNetworkPolicy); + } + + const resourceCacheMaxBytesThreshold = 1080 * 1920 * 12 * 4; + shellArgs.push("--resource-cache-max-bytes-threshold=" + resourceCacheMaxBytesThreshold); + + shellArgs.push("--prefetched-default-font-manager"); + + shellArgs.push("--leak-vm=" + true); + + if (this.isEnableImpeller == true && deviceInfo.productModel != "emulator") { + shellArgs.push("--enable-impeller"); + Log.d(TAG, "Enable Impeller in Ohos."); + } else { + Log.d(TAG, "Do not find enableImpeller tag or enableImpeller tag set to false, enable Skia in Ohos."); + } + + // Final initialization operation + const costTime = Date.now() - this.initStartTimestampMillis; + this.flutterNapi.init( + this.context!, + shellArgs, + kernelPath, + this.initResult!.appStoragePath, + this.initResult!.engineCachesPath!, + costTime + ); + this.initialized = true; + Log.d(TAG, "ensureInitializationComplete") + } + + /** + * Finds the path to the Flutter app bundle. + * @returns The path to the Flutter assets directory, or empty string if not initialized + */ + findAppBundlePath(): string { + return this.flutterApplicationInfo == null ? "" : this.flutterApplicationInfo!.flutterAssetsDir; + } + + /** + * Gets the lookup key for an asset file. + * @param asset - The asset file path + * @param packageName - Optional package name for package-specific assets + * @returns The full asset path lookup key + */ + getLookupKeyForAsset(asset: string, packageName?: string): string { + if (typeof packageName === 'string' && packageName.trim().length > 0) { + return this.fullAssetPathFrom('packages' + FILE_SEPARATOR + packageName + FILE_SEPARATOR + asset); + } + return this.fullAssetPathFrom(asset); + } + + /** + * Gets the full asset path from a relative file path. + * @param filePath - The relative file path + * @returns The full asset path, or empty string if not initialized + */ + fullAssetPathFrom(filePath: string): string { + return this.flutterApplicationInfo == null ? "" : this.flutterApplicationInfo!.flutterAssetsDir + "/" + filePath; + } + + private checkTimestamp(dataDir: string): string | null { + let bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT); + const expectedTimestamp = TIMESTAMP_PREFIX + bundleInfo.versionCode + "-" + bundleInfo.updateTime; + const existingTimestamps = this.getExistingTimestamps(dataDir); + if (existingTimestamps == null) { + Log.i(TAG, "No extracted resources found"); + return expectedTimestamp; + } + + if (existingTimestamps.length == 1) { + Log.i(TAG, "Found extracted resources " + existingTimestamps[0]); + } + + if (existingTimestamps.length != 1 || !(expectedTimestamp == existingTimestamps[0])) { + Log.i(TAG, "Resource version mismatch " + expectedTimestamp); + return expectedTimestamp; + } + + return null; + } + + private getExistingTimestamps(dataDir: string): string[] { + return fs.accessSync(dataDir) ? fs.listFileSync(dataDir, { + filter: { + displayName: [`${TIMESTAMP_PREFIX}*`] + } + }) : new Array(); + } + + /** + * Checks if the FlutterLoader has been initialized. + * @returns true if initialized, false otherwise + */ + isInitialized(): boolean { + return this.initialized; + } +} + +/** + * Contains initialization result paths for the Flutter engine. + */ +class InitResult { + appStoragePath: string; + engineCachesPath: string; + dataDirPath: string; + + /** + * Constructs a new InitResult instance. + * @param appStoragePath - Path to the application storage directory + * @param engineCachesPath - Path to the engine caches directory + * @param dataDirPath - Path to the data directory + */ + constructor(appStoragePath: string, + engineCachesPath: string, + dataDirPath: string) { + this.appStoragePath = appStoragePath; + this.engineCachesPath = engineCachesPath; + this.dataDirPath = dataDirPath; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorView.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorView.ets new file mode 100644 index 0000000..8db1a16 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorView.ets @@ -0,0 +1,170 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterMutatorView.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import ArrayList from '@ohos.util.ArrayList'; +import matrix4 from '@ohos.matrix4'; +import { DVModel, DVModelEvents, DVModelParameters } from '../../../view/DynamicView/dynamicView'; +import { createDVModelFromJson } from '../../../view/DynamicView/dynamicViewJson'; +import OhosTouchProcessor from '../../ohos/OhosTouchProcessor'; +import { FlutterMutator, FlutterMutatorsStack } from './FlutterMutatorsStack' +import Any from '../../../plugin/common/Any'; + +/** + * View that applies Flutter mutators (transforms, clips) to a dynamic view model. + * This class manages the layout and mutator application for platform views. + */ +export class FlutterMutatorView { + private mutatorsStack: FlutterMutatorsStack | null = null; + private screenDensity: number = 0; + private left: number = 0; + private top: number = 0; + private prevLeft: number = 0; + private prevTop: number = 0; + private onTouch = (touchEvent: Any) => { + let params = this.model.params as Record; + switch (touchEvent.type) { + case TouchType.Down: + this.prevLeft = this.left; + this.prevTop = this.top; + params.translateX = this.left; + params.translateY = this.top; + break; + case TouchType.Move: + params.translateX = this.prevLeft; + params.translateY = this.prevTop; + this.prevLeft = this.left; + this.prevTop = this.top; + break; + case TouchType.Up: + case TouchType.Cancel: + default: + break; + } + } + private model: DVModel = createDVModelFromJson( + new DVModelParam("Column", [], { backgroundColor: Color.Red }, { onTouch: this.onTouch }) + ); + + /** + * Sets listeners for descendant focus change events. + * @param onFocus - Callback invoked when a descendant gains focus + * @param onBlur - Callback invoked when a descendant loses focus + */ + setOnDescendantFocusChangeListener(onFocus: () => void, onBlur: () => void) { + // this.model.events["onFocus"] = onFocus; + // this.model.events["onBlur"] = onBlur; + let events2 = this.model.events as Record; + events2.onFocus = onFocus; + events2.onBlur = onBlur; + } + + /** + * Sets the layout parameters for this view. + * @param parameters - The layout parameters including margin, width, and height + */ + public setLayoutParams(parameters: DVModelParameters): void { + if (this.model.params == null) { + this.model.params = new DVModelParameters(); + } + let params = this.model.params as Record | matrix4.Matrix4Transit>; + let parametersRecord = + parameters as Record | matrix4.Matrix4Transit>; + params.marginLeft = parametersRecord['marginLeft']; + params.marginTop = parametersRecord['marginTop']; + params.width = parametersRecord['width']; + params.height = parametersRecord['height']; + this.left = parametersRecord.marginLeft as number; + this.top = parametersRecord.marginTop as number; + } + + /** + * Adds a child dynamic view model to this view. + * @param model - The DVModel to add as a child + */ + public addDvModel(model: DVModel): void { + this.model?.children.push(model); + } + + /** + * Prepares this view for display by applying mutators and setting layout parameters. + * @param mutatorsStack - The stack of mutators to apply + * @param left - The left position of the view + * @param top - The top position of the view + * @param width - The width of the view + * @param height - The height of the view + */ + public readyToDisplay(mutatorsStack: FlutterMutatorsStack, left: number, top: number, width: number, height: number) { + this.mutatorsStack = mutatorsStack; + this.left = left; + this.top = top; + let parameters = + new DVModelParameters() as Record | matrix4.Matrix4Transit>; + parameters['marginLeft'] = left; + parameters['marginTop'] = top; + parameters['width'] = width; + parameters['height'] = height; + this.setLayoutParams(parameters); + this.dealMutators(); + } + + private dealMutators() { + if (this.mutatorsStack == null) { + return; + } + let paths = this.mutatorsStack.getFinalClippingPaths(); + let rects = this.mutatorsStack.getFinalClippingRects(); + let matrix = this.mutatorsStack.getFinalMatrix(); + let params = this.model.params as Record | matrix4.Matrix4Transit>; + if (!paths.isEmpty()) { + let path = paths.getLast(); + params.pathWidth = path.width; + params.pathHeight = path.height; + params.pathCommands = path.commands; + } + if (!rects.isEmpty()) { + let rect = rects.getLast(); + params.rectWidth = rect.width; + params.rectHeight = rect.height; + params.rectRadius = rect.radius; + } + params.matrix = matrix; + } + + /** + * Gets the dynamic view model for this view. + * @returns The DVModel instance, or undefined if not set + */ + public getDvModel(): DVModel | undefined { + return this.model; + } +} + +/** + * Parameters for creating a dynamic view model. + */ +class DVModelParam { + compType: string + children: [] + attributes: Any + events: Any + + /** + * Constructs a new DVModelParam instance. + * @param compType - The component type + * @param children - Array of child components + * @param attributes - Component attributes + * @param events - Component event handlers + */ + constructor(compType: string, children: [], attributes: Any, events: Any) { + this.compType = compType; + this.children = children; + this.attributes = attributes; + this.events = events; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorsStack.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorsStack.ets new file mode 100644 index 0000000..09f71ec --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/mutatorsstack/FlutterMutatorsStack.ets @@ -0,0 +1,210 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterMutatorsStack.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import matrix4 from '@ohos.matrix4' +import List from '@ohos.util.List'; + +/** + * Types of mutators that can be applied to Flutter views. + */ +export enum FlutterMutatorType { + CLIP_RECT, + CLIP_PATH, + TRANSFORM, + OPACITY +} + +/** + * Represents a rectangular clipping region. + */ +class Rect { + width: number; + height: number; + radius: string | number | Array; + + /** + * Constructs a new Rect instance. + * @param width - The width of the rectangle + * @param height - The height of the rectangle + * @param radius - Optional corner radius for rounded rectangles + */ + constructor(width: number, height: number, radius?: string | number | Array) { + this.width = width; + this.height = height; + this.radius = radius ?? 0; + } +} + +/** + * Represents a path-based clipping region. + */ +class Path { + width: number | string; + height: number | string; + commands: string; + + /** + * Constructs a new Path instance. + * @param width - The width of the path + * @param height - The height of the path + * @param commands - Optional SVG path commands + */ + constructor(width: number | string, height: number | string, commands?: string) { + this.width = width; + this.height = height; + this.commands = commands ?? ''; + } +} + +/** + * Represents a single mutator operation (transform, clip rect, or clip path). + */ +export class FlutterMutator { + private matrix: matrix4.Matrix4Transit | null = null; + private rect: Rect = new Rect(0, 0); + private path: Path = new Path(0, 0); + + /** + * Constructs a new FlutterMutator instance. + * @param args - Either a transformation matrix, a clipping rectangle, or a clipping path + */ + constructor(args: matrix4.Matrix4Transit | Rect | Path) { + if (args instanceof Rect) { + this.rect = args; + } else if (args instanceof Path) { + this.path = args; + } else { + this.matrix = args; + } + } + + /** + * Gets the transformation matrix, if this mutator is a transform. + * @returns The transformation matrix, or null if not a transform + */ + public getMatrix(): matrix4.Matrix4Transit | null { + return this.matrix; + } + + /** + * Gets the clipping rectangle, if this mutator is a clip rect. + * @returns The clipping rectangle + */ + public getRect() { + return this.rect; + } + + /** + * Gets the clipping path, if this mutator is a clip path. + * @returns The clipping path + */ + public getPath() { + return this.path; + } +} + +/** + * Stack of mutators that can be applied to Flutter views. + * This class manages transformations, clipping rectangles, and clipping paths + * that are applied in sequence to create the final view appearance. + */ +export class FlutterMutatorsStack { + private mutators: List; + private finalClippingPaths: List; + private finalClippingRects: List; + private finalMatrix: matrix4.Matrix4Transit; + + /** + * Constructs a new FlutterMutatorsStack instance. + */ + constructor() { + this.mutators = new List(); + this.finalClippingPaths = new List(); + this.finalClippingRects = new List(); + this.finalMatrix = matrix4.identity(); + } + + /** + * Pushes a transformation matrix onto the mutators stack. + * @param values - Array of 16 numbers representing a 4x4 transformation matrix + */ + public pushTransform(values: Array): void { + if (values.length != 16) { + return; + } + let index = 0; + let matrix = matrix4.init( + [values[index++], values[index++], values[index++], values[index++], + values[index++], values[index++], values[index++], values[index++], + values[index++], values[index++], values[index++], values[index++], + values[index++], values[index++], values[index++], values[index++]]); + let mutator = new FlutterMutator(matrix); + this.mutators.add(mutator); + this.finalMatrix.combine(matrix); + } + + /** + * Pushes a rectangular clipping region onto the mutators stack. + * @param width - The width of the clipping rectangle + * @param height - The height of the clipping rectangle + * @param radius - Optional corner radius for rounded rectangles + */ + public pushClipRect(width: number, height: number, radius?: number) { + let rect = new Rect(width, height, radius); + let mutator = new FlutterMutator(rect); + this.mutators.add(mutator); + this.finalClippingRects.add(rect); + } + + /** + * Pushes a path-based clipping region onto the mutators stack. + * @param width - The width of the clipping path + * @param height - The height of the clipping path + * @param command - Optional SVG path commands + */ + public pushClipPath(width: number, height: number, command?: string) { + let path = new Path(width, height, command); + let mutator = new FlutterMutator(path); + this.mutators.add(mutator); + this.finalClippingPaths.add(path); + } + + /** + * Gets all mutators in the stack. + * @returns List of all mutators + */ + public getMutators() { + return this.mutators; + } + + /** + * Gets all final clipping paths. + * @returns List of all clipping paths + */ + public getFinalClippingPaths() { + return this.finalClippingPaths; + } + + /** + * Gets all final clipping rectangles. + * @returns List of all clipping rectangles + */ + public getFinalClippingRects() { + return this.finalClippingRects; + } + + /** + * Gets the final combined transformation matrix. + * @returns The combined transformation matrix from all transforms + */ + public getFinalMatrix() { + return this.finalMatrix; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin.ets new file mode 100644 index 0000000..990ded7 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin.ets @@ -0,0 +1,225 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterPlugin.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import common from '@ohos.app.ability.common'; +import { BinaryMessenger } from '../../../plugin/common/BinaryMessenger'; +import PlatformViewFactory from '../../../plugin/platform/PlatformViewFactory'; +import PlatformViewRegistry from '../../../plugin/platform/PlatformViewRegistry'; +import { TextureRegistry } from '../../../view/TextureRegistry'; +import FlutterEngine from '../FlutterEngine'; + +/** + * Interface to be implemented by all Flutter plugins. + * + * A Flutter plugin allows Flutter developers to interact with a host platform, e.g., OpenHarmony, + * via Dart code. It includes platform code, as well as Dart code. A plugin author is responsible + * for setting up an appropriate MethodChannel to communicate between platform code and Dart code. + * + * A Flutter plugin has a lifecycle. First, a developer must add a FlutterPlugin to an instance + * of FlutterEngine. To do this, obtain a PluginRegistry with FlutterEngine.getPlugins(), then call + * PluginRegistry.add(FlutterPlugin), passing the instance of the Flutter plugin. During the call + * to PluginRegistry.add(FlutterPlugin), the FlutterEngine will invoke onAttachedToEngine(FlutterPluginBinding) + * on the given FlutterPlugin. If the FlutterPlugin is removed from the FlutterEngine via + * PluginRegistry.remove(pluginClassName), or if the FlutterEngine is destroyed, the FlutterEngine will invoke + * onDetachedFromEngine(FlutterPluginBinding) on the given FlutterPlugin. + * + * Once a FlutterPlugin is attached to a FlutterEngine, the plugin's code is permitted to access + * and invoke methods on resources within the FlutterPlugin.FlutterPluginBinding that the FlutterEngine + * gave to the FlutterPlugin in onAttachedToEngine(FlutterPluginBinding). This includes, for example, + * the application Context for the running app. + * + * The FlutterPlugin.FlutterPluginBinding provided in onAttachedToEngine(FlutterPluginBinding) is + * no longer valid after the execution of onDetachedFromEngine(FlutterPluginBinding). Do not access + * any properties of the FlutterPlugin.FlutterPluginBinding after the completion of + * onDetachedFromEngine(FlutterPluginBinding). + * + * To register a MethodChannel, obtain a BinaryMessenger via the FlutterPlugin.FlutterPluginBinding. + * + * An OpenHarmony Flutter plugin may require access to app resources or other artifacts that can only + * be retrieved through a Context. Developers can access the application context via + * FlutterPlugin.FlutterPluginBinding.getApplicationContext(). + * + * Some plugins may require access to the UIAbility that is displaying a Flutter experience, or + * may need to react to UIAbility lifecycle events, e.g., onCreate(), onWindowStageCreate(), onForeground(), + * onBackground(), onWindowStageDestroy(), onDestroy(). Any such plugin should implement AbilityAware + * in addition to implementing FlutterPlugin. AbilityAware provides callback hooks that expose access + * to an associated UIAbility and its Lifecycle. All plugins must respect the possibility that a Flutter + * experience may never be associated with a UIAbility, e.g., when Flutter is used for background + * behavior. Additionally, all plugins must respect that UIAbilities may come and go over time, thus + * requiring plugins to cleanup resources and recreate those resources as the UIAbility comes and goes. + */ +export interface FlutterPlugin { + /** + * Gets the unique class name of this plugin. + * Similar to Android's Class, but in TypeScript this must be user-defined. + * @returns The unique class name of this plugin + */ + getUniqueClassName(): string + + /** + * This FlutterPlugin has been associated with a FlutterEngine instance. + * + * Relevant resources that this FlutterPlugin may need are provided via the binding. + * The binding may be cached and referenced until onDetachedFromEngine is invoked and returns. + * @param binding - The FlutterPluginBinding providing access to engine resources + */ + onAttachedToEngine(binding: FlutterPluginBinding): void; + + /** + * This FlutterPlugin has been removed from a FlutterEngine instance. + * + * The binding passed to this method is the same instance that was passed in + * onAttachedToEngine. It is provided again in this method as a convenience. + * The binding may be referenced during the execution of this method, but it + * must not be cached or referenced after this method returns. + * + * FlutterPlugins should release all resources in this method. + * @param binding - The FlutterPluginBinding that was provided in onAttachedToEngine + */ + onDetachedFromEngine(binding: FlutterPluginBinding): void; +} + +/** + * Binding that provides Flutter plugins with access to Flutter engine resources. + * This class holds references to the engine, messenger, assets, and registries that plugins may need. + */ +export class FlutterPluginBinding { + private applicationContext: common.Context; + private flutterEngine: FlutterEngine; + private binaryMessenger: BinaryMessenger; + private flutterAssets: FlutterAssets; + private textureRegistry: TextureRegistry; + private platformViewRegistry: PlatformViewRegistry; + + /** + * Constructs a new FlutterPluginBinding instance. + * @param applicationContext - The application context + * @param flutterEngine - The FlutterEngine instance + * @param binaryMessenger - The BinaryMessenger for platform communication + * @param flutterAssets - The FlutterAssets for accessing Flutter assets + * @param textureRegistry - The TextureRegistry for managing textures + * @param platformViewRegistry - Optional PlatformViewRegistry, will be created if not provided + */ + constructor(applicationContext: common.Context, flutterEngine: FlutterEngine, binaryMessenger: BinaryMessenger, + flutterAssets: FlutterAssets, textureRegistry: TextureRegistry, platformViewRegistry?: PlatformViewRegistry) { + this.applicationContext = applicationContext; + this.flutterEngine = flutterEngine; + this.binaryMessenger = binaryMessenger; + this.flutterAssets = flutterAssets; + this.textureRegistry = textureRegistry; + this.platformViewRegistry = platformViewRegistry ?? new EmptyPlatformViewRegistry(); + } + + /** + * Gets the application context. + * @returns The application context + */ + getApplicationContext(): common.Context { + return this.applicationContext; + } + + /** + * Gets the FlutterEngine instance. + * @returns The FlutterEngine instance + */ + getFlutterEngine(): FlutterEngine { + return this.flutterEngine; + } + + /** + * Gets the BinaryMessenger for platform communication. + * @returns The BinaryMessenger instance + */ + getBinaryMessenger(): BinaryMessenger { + return this.binaryMessenger; + } + + /** + * Gets the FlutterAssets for accessing Flutter assets. + * @returns The FlutterAssets instance + */ + getFlutterAssets(): FlutterAssets { + return this.flutterAssets; + } + + /** + * Gets the TextureRegistry for managing textures. + * @returns The TextureRegistry instance + */ + getTextureRegistry(): TextureRegistry { + return this.textureRegistry; + } + + /** + * Gets the PlatformViewRegistry for managing platform views. + * @returns The PlatformViewRegistry instance + */ + public getPlatformViewRegistry(): PlatformViewRegistry { + return this.platformViewRegistry; + } +} + +/** Provides Flutter plugins with access to Flutter asset information. */ +export interface FlutterAssets { + /** + * Returns the relative file path to the Flutter asset with the given name, including the file's + * extension, e.g., "myImage.jpg". + * + * The returned file path is relative to the OpenHarmony app's standard assets directory. + * Therefore, the returned path is appropriate to pass to OpenHarmony's ResourceManager, but + * the path is not appropriate to load as an absolute path. + * @param assetFileName - The name of the asset file + * @returns The relative file path to the asset + */ + getAssetFilePathByName(assetFileName: string): string; + + /** + * Same as getAssetFilePathByName but with added support for an explicit bundleName. + * @param assetFileName - The name of the asset file + * @param bundleName - The bundle name + * @returns The relative file path to the asset + */ + getAssetFilePathByName(assetFileName: string, bundleName: string): string; + + /** + * Returns the relative file path to the Flutter asset with the given subpath, including the + * file's extension, e.g., "/dir1/dir2/myImage.jpg". + * + * The returned file path is relative to the OpenHarmony app's standard assets directory. + * Therefore, the returned path is appropriate to pass to OpenHarmony's ResourceManager, but + * the path is not appropriate to load as an absolute path. + * @param assetSubpath - The subpath of the asset + * @returns The relative file path to the asset + */ + getAssetFilePathBySubpath(assetSubpath: string): string; + + /** + * Same as getAssetFilePathBySubpath but with added support for an explicit bundleName. + * @param assetSubpath - The subpath of the asset + * @param bundleName - The bundle name + * @returns The relative file path to the asset + */ + getAssetFilePathBySubpath(assetSubpath: string, bundleName: string): string; +} + +/** + * Empty implementation of PlatformViewRegistry that does nothing. + */ +class EmptyPlatformViewRegistry implements PlatformViewRegistry { + /** + * Attempts to register a view factory, but always returns false. + * @param viewTypeId - The view type identifier + * @param factory - The PlatformViewFactory to register + * @returns Always returns false + */ + registerViewFactory(viewTypeId: string, factory: PlatformViewFactory): boolean { + return false; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/PluginRegistry.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/PluginRegistry.ets new file mode 100644 index 0000000..aa81268 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/PluginRegistry.ets @@ -0,0 +1,73 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PluginRegistry.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import { FlutterPlugin } from './FlutterPlugin'; + +/** + * Registry for managing Flutter plugins. + * This interface provides methods to add, remove, and query plugins attached to a FlutterEngine. + */ +export default interface PluginRegistry { + /** + * Attaches the given plugin to the FlutterEngine associated with this PluginRegistry. + * @param plugin - The FlutterPlugin to attach + */ + add(plugin: FlutterPlugin): void; + + /** + * Attaches the given plugins to the FlutterEngine associated with this PluginRegistry. + * @param plugins - The set of FlutterPlugins to attach + */ + addList(plugins: Set): void; + + /** + * Checks if a plugin of the given type is currently attached to the FlutterEngine + * associated with this PluginRegistry. + * @param pluginClassName - The class name of the plugin to check + * @returns True if the plugin is attached, false otherwise + */ + has(pluginClassName: string): boolean; + + /** + * Gets the instance of a plugin that is currently attached to the FlutterEngine + * associated with this PluginRegistry, which matches the given pluginClassName. + * + * If no matching plugin is found, null is returned. + * @param pluginClassName - The class name of the plugin to get + * @returns The FlutterPlugin instance, or null if not found + */ + get(pluginClassName: string): FlutterPlugin; + + /** + * Detaches the plugin of the given type from the FlutterEngine + * associated with this PluginRegistry. + * + * If no such plugin exists, this method does nothing. + * @param pluginClassName - The class name of the plugin to remove + */ + remove(pluginClassName: string): void; + + /** + * Detaches the plugins of the given types from the FlutterEngine + * associated with this PluginRegistry. + * + * If no such plugins exist, this method does nothing. + * @param pluginClassNames - The set of plugin class names to remove + */ + removeList(pluginClassNames: Set): void; + + /** + * Detaches all plugins that are currently attached to the FlutterEngine + * associated with this PluginRegistry. + * + * If no plugins are currently attached, this method does nothing. + */ + removeAll(): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityAware.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityAware.ets new file mode 100644 index 0000000..43151f9 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityAware.ets @@ -0,0 +1,52 @@ +/* + * Copyright 2013 The Flutter Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. +*/ + +import { AbilityPluginBinding } from './AbilityPluginBinding'; + +/** + * FlutterPlugin that is interested in UIAbility lifecycle events related to a FlutterEngine + * running within the given UIAbility. + */ +export default interface AbilityAware { + /** + * This AbilityAware FlutterPlugin is now associated with a UIAbility. + * + * This method can be invoked in 1 of 2 situations: + * + * This AbilityAware FlutterPlugin was just added to a FlutterEngine that was already + * connected to a running UIAbility. + * This AbilityAware FlutterPlugin was already added to a FlutterEngine and that + * FlutterEngine was just connected to a UIAbility. + * + * The given AbilityPluginBinding contains UIAbility-related references that an AbilityAware + * FlutterPlugin may require, such as a reference to the actual UIAbility in question. + * The AbilityPluginBinding may be referenced until either onDetachedFromAbilityForConfigChanges + * or onDetachedFromAbility is invoked. At the conclusion of either of those methods, the + * binding is no longer valid. Clear any references to the binding or its resources, and do not + * invoke any further methods on the binding or its resources. + * @param binding - The AbilityPluginBinding providing access to UIAbility resources + */ + onAttachedToAbility(binding: AbilityPluginBinding): void; + + /** + * This plugin has been detached from a UIAbility. + * + * Detachment can occur for a number of reasons: + * + * The app is no longer visible and the UIAbility instance has been destroyed. + * The FlutterEngine that this plugin is connected to has been detached from its FlutterView. + * This AbilityAware plugin has been removed from its FlutterEngine. + * + * By the end of this method, the UIAbility that was made available in + * onAttachedToAbility is no longer valid. Any references to the + * associated UIAbility or AbilityPluginBinding should be cleared. + * + * Any Lifecycle listeners that were registered in onAttachedToAbility or + * onReattachedToAbilityForConfigChanges should be deregistered here to + * avoid a possible memory leak and other side effects. + */ + onDetachedFromAbility(): void; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityControlSurface.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityControlSurface.ets new file mode 100644 index 0000000..61490f8 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityControlSurface.ets @@ -0,0 +1,48 @@ +/* + * Copyright 2013 The Flutter Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. +*/ + +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import Want from '@ohos.app.ability.Want'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import ExclusiveAppComponent from '../../../ohos/ExclusiveAppComponent'; + +/** + * Interface for controlling ability-related operations in Flutter engines. + * This interface provides methods to attach/detach from abilities and handle ability lifecycle events. + */ +export default interface ActivityControlSurface { + /** + * Attaches this surface to an exclusive app component (UIAbility). + * @param exclusiveActivity - The exclusive app component to attach to + */ + attachToAbility(exclusiveActivity: ExclusiveAppComponent): void; + + /** + * Detaches this surface from the current ability. + */ + detachFromAbility(): void; + + /** + * Handles a new Want event from the ability. + * @param want - The Want object containing the intent + * @param launchParams - The launch parameters + */ + onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void; + + /** + * Handles window focus change events from the ability. + * @param hasFocus - Whether the window has focus + */ + onWindowFocusChanged(hasFocus: boolean): void; + + /** + * Handles save state requests from the ability. + * @param reason - The reason for saving state + * @param wantParam - Parameters to save + * @returns The result of the save state operation + */ + onSaveState(reason: AbilityConstant.StateType, wantParam: Record): AbilityConstant.OnSaveResult; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityPluginBinding.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityPluginBinding.ets new file mode 100644 index 0000000..4fc1e24 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityPluginBinding.ets @@ -0,0 +1,93 @@ +/* + * Copyright 2013 The Flutter Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. +*/ + +import UIAbility from '@ohos.app.ability.UIAbility' +import Want from '@ohos.app.ability.Want'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; + +/** + * Binding that provides plugins with access to UIAbility-related resources. + * This interface allows plugins to access the ability and register for ability lifecycle events. + */ +export interface AbilityPluginBinding { + /** + * Gets the UIAbility instance. + * @returns The UIAbility instance + */ + getAbility(): UIAbility; + + /** + * Adds a listener that is invoked whenever the associated UIAbility's onNewWant method is invoked. + * @param listener - The NewWantListener to add + */ + addOnNewWantListener(listener: NewWantListener): void; + + /** + * Removes a listener that was added in addOnNewWantListener. + * @param listener - The NewWantListener to remove + */ + removeOnNewWantListener(listener: NewWantListener): void; + + /** + * Adds a listener that is invoked whenever the associated UIAbility's windowStageEvent method is invoked. + * @param listener - The WindowFocusChangedListener to add + */ + addOnWindowFocusChangedListener(listener: WindowFocusChangedListener): void; + + /** + * Removes a listener that was added in addOnWindowFocusChangedListener. + * @param listener - The WindowFocusChangedListener to remove + */ + removeOnWindowFocusChangedListener(listener: WindowFocusChangedListener): void; + + /** + * Adds a listener that is invoked when the associated UIAbility saves and restores instance state. + * @param listener - The OnSaveStateListener to add + */ + addOnSaveStateListener(listener: OnSaveStateListener): void; + + /** + * Removes a listener that was added in addOnSaveStateListener. + * @param listener - The OnSaveStateListener to remove + */ + removeOnSaveStateListener(listener: OnSaveStateListener): void; +} + +/** + * Delegate interface for handling new wants on behalf of the main UIAbility. + */ +export interface NewWantListener { + /** + * Called when a new want is started for the UIAbility. + * @param want - The new want that was started for the UIAbility + * @param launchParams - The launch parameters + */ + onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void; +} + +/** + * Delegate interface for handling window focus changes on behalf of the main UIAbility. + */ +export interface WindowFocusChangedListener { + /** + * Called when the window focus changes. + * @param hasFocus - Whether the window has focus + */ + onWindowFocusChanged(hasFocus: boolean): void; +} + +/** + * Delegate interface for handling save state events. + */ +export interface OnSaveStateListener { + /** + * Invoked when the associated UIAbility saves and restores instance state. + * @param reason - The reason for saving state + * @param wantParam - Parameters to save + * @returns The result of the save state operation + */ + onSaveState(reason: AbilityConstant.StateType, wantParam: Record): AbilityConstant.OnSaveResult; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/renderer/FlutterRenderer.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/renderer/FlutterRenderer.ets new file mode 100644 index 0000000..1cb48a0 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/renderer/FlutterRenderer.ets @@ -0,0 +1,269 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import image from '@ohos.multimedia.image'; +import { BusinessError } from '@ohos.base'; +import { SurfaceTextureEntry, TextureRegistry } from '../../../view/TextureRegistry'; +import { FlutterAbility } from '../../ohos/FlutterAbility'; +import FlutterNapi from '../FlutterNapi'; +import Log from '../../../util/Log'; + +const TAG = "FlutterRenderer" + +/** + * Renderer for Flutter content that manages textures and rendering operations. + * This class implements TextureRegistry and provides methods to register and manage textures + * for use in Flutter applications. + */ +export class FlutterRenderer implements TextureRegistry { + private flutterNapi: FlutterNapi; + private static globalTextureId: number = 0; + + /** + * Constructs a new FlutterRenderer instance. + * @param flutterNapi - The FlutterNapi instance for native communication + */ + constructor(flutterNapi: FlutterNapi) { + this.flutterNapi = flutterNapi; + } + + /** + * @deprecated since 3.7 + */ + createSurfaceTexture(): SurfaceTextureEntry { + let receiver: image.ImageReceiver = this.getImageReceiver(); + return this.registerSurfaceTexture(receiver); + } + + /** + * Gets the next available texture ID. + * @returns A unique texture ID + */ + getTextureId(): number { + let nextTextureId: number = FlutterRenderer.globalTextureId + 1; + FlutterRenderer.globalTextureId = FlutterRenderer.globalTextureId + 1; + Log.i(TAG, "getTextureId: " + nextTextureId) + return nextTextureId; + } + + /** + * Registers a texture with the Flutter engine. + * @param textureId - The texture ID to register + * @returns A SurfaceTextureEntry containing the registered texture information + */ + registerTexture(textureId: number): SurfaceTextureEntry { + let surfaceTextureRegistryEntry = new SurfaceTextureRegistryEntry(textureId); + let surfaceId = this.flutterNapi.registerTexture(textureId); + Log.i(TAG, "registerTexture, surfaceId=" + surfaceId); + surfaceTextureRegistryEntry.setSurfaceId(surfaceId); + let nativeWindowId = this.flutterNapi.getTextureNativeWindowId(textureId); + surfaceTextureRegistryEntry.setNativeWindowId(nativeWindowId); + let nativeWindowPtr = this.flutterNapi.getTextureNativeWindowPtr(textureId); + surfaceTextureRegistryEntry.setNativeWindowPtr(nativeWindowPtr); + return surfaceTextureRegistryEntry; + } + + /** + * @deprecated since 3.7 + */ + registerSurfaceTexture(receiver: image.ImageReceiver): SurfaceTextureEntry { + let nextTextureId: number = FlutterRenderer.globalTextureId + 1; + FlutterRenderer.globalTextureId = FlutterRenderer.globalTextureId + 1; + let surfaceTextureRegistryEntry = new SurfaceTextureRegistryEntry(nextTextureId); + return surfaceTextureRegistryEntry; + } + + /** + * Registers a PixelMap as a texture. + * @param pixelMap - The PixelMap to register + * @returns The texture ID assigned to the PixelMap + */ + registerPixelMap(pixelMap: PixelMap): number { + let nextTextureId: number = this.getTextureId(); + this.flutterNapi.registerPixelMap(nextTextureId, pixelMap); + return nextTextureId; + } + + /** + * Sets the background PixelMap for a texture. + * @param textureId - The texture ID + * @param pixelMap - The PixelMap to use as background + */ + setTextureBackGroundPixelMap(textureId: number, pixelMap: PixelMap): void { + this.flutterNapi.setTextureBackGroundPixelMap(textureId, pixelMap); + } + + /** + * @deprecated since 3.7 + */ + setTextureBackGroundColor(textureId: number, color: number): void { + this.flutterNapi.setTextureBackGroundColor(textureId, color); + } + + /** + * Sets the buffer size for a texture. + * @param textureId - The texture ID + * @param width - The buffer width + * @param height - The buffer height + */ + setTextureBufferSize(textureId: number, width: number, height: number): void { + this.flutterNapi.setTextureBufferSize(textureId, width, height); + } + + /** + * Notifies the Flutter engine that a texture is being resized. + * @param textureId - The texture ID + * @param width - The new width + * @param height - The new height + */ + notifyTextureResizing(textureId: number, width: number, height: number): void { + this.flutterNapi.notifyTextureResizing(textureId, width, height); + } + + /** + * @deprecated since 3.22 + * @useinstead FlutterRenderer#setExternalNativeImagePtr + */ + setExternalNativeImage(textureId: number, native_image: number): boolean { + return this.flutterNapi.setExternalNativeImage(textureId, native_image); + } + + /** + * Sets an external native image pointer for a texture. + * @param textureId - The texture ID + * @param native_image_ptr - The native image pointer as a bigint + * @returns true if successful, false otherwise + */ + setExternalNativeImagePtr(textureId: number, native_image_ptr: bigint): boolean { + return this.flutterNapi.setExternalNativeImagePtr(textureId, native_image_ptr); + } + + /** + * Resets an external texture. + * @param textureId - The texture ID + * @param need_surfaceId - Whether a surface ID is needed + * @returns The result code + */ + resetExternalTexture(textureId: number, need_surfaceId: boolean): number { + return this.flutterNapi.resetExternalTexture(textureId, need_surfaceId); + } + + /** + * Unregisters a texture from the Flutter engine. + * @param textureId - The texture ID to unregister + */ + unregisterTexture(textureId: number): void { + this.flutterNapi.unregisterTexture(textureId); + } + + /** + * Called when the system needs to trim memory. + * @param level - The memory trim level + */ + onTrimMemory(level: number) { + throw new Error('Method not implemented.'); + } + + /** + * @deprecated since 3.7 + */ + private getImageReceiver(): image.ImageReceiver { + let receiver: image.ImageReceiver = image.createImageReceiver(640, 480, 4, 8); + if (receiver !== undefined) { + Log.i(TAG, '[camera test] ImageReceiver is ok'); + } else { + Log.i(TAG, '[camera test] ImageReceiver is not ok'); + } + receiver?.on('imageArrival', () => { + receiver.readNextImage().then(() => { receiver.release() }) + }) + return receiver; + } + +} + +/** + * Entry in the surface texture registry representing a registered texture. + * This class holds information about a texture including its ID, surface ID, and native window information. + */ +export class SurfaceTextureRegistryEntry implements SurfaceTextureEntry { + private textureId: number = 0; + private surfaceId: number = 0; + private nativeWindowId: number = 0; + private nativeWindowPtr: bigint = BigInt("0"); + private released: boolean = false; + + /** + * Constructs a new SurfaceTextureRegistryEntry instance. + * @param id - The texture ID + */ + constructor(id: number) { + this.textureId = id; + } + + /** + * Gets the texture ID. + * @returns The texture ID + */ + getTextureId(): number { + return this.textureId; + } + + /** + * Gets the surface ID. + * @returns The surface ID + */ + getSurfaceId(): number { + return this.surfaceId; + } + + /** + * @deprecated since 3.22 + * @useinstead SurfaceTextureRegistryEntry#getNativeWindowPtr + */ + getNativeWindowId(): number { + return this.nativeWindowId; + } + + /** + * Gets the native window pointer. + * @returns The native window pointer as a bigint + */ + getNativeWindowPtr(): bigint { + return this.nativeWindowPtr; + } + + /** + * Sets the surface ID. + * @param surfaceId - The surface ID to set + */ + setSurfaceId(surfaceId: number): void { + this.surfaceId = surfaceId; + } + + /** + * @deprecated since 3.22 + * @useinstead SurfaceTextureRegistryEntry#setNativeWindowPtr + */ + setNativeWindowId(nativeWindowId: number): void { + this.nativeWindowId = nativeWindowId; + } + + /** + * Sets the native window pointer. + * @param nativeWindowPtr - The native window pointer as a bigint + */ + setNativeWindowPtr(nativeWindowPtr: bigint): void { + this.nativeWindowPtr = nativeWindowPtr; + } + + /** + * Releases this texture entry and frees associated resources. + */ + release() { + throw new Error('Method not implemented.'); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/renderer/FlutterUiDisplayListener.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/renderer/FlutterUiDisplayListener.ets new file mode 100644 index 0000000..733215c --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/renderer/FlutterUiDisplayListener.ets @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterUiDisplayListener.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * Listener interface for Flutter UI display state changes. + * Implementations of this interface are notified when Flutter UI is displayed or hidden. + */ +export interface FlutterUiDisplayListener { + /** + * Called when Flutter UI is displayed for the first time. + */ + onFlutterUiDisplayed(): void; + + /** + * Called when Flutter UI is no longer displayed. + */ + onFlutterUiNoLongerDisplayed(): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/AccessibilityChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/AccessibilityChannel.ets new file mode 100644 index 0000000..d141f8f --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/AccessibilityChannel.ets @@ -0,0 +1,259 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on AccessibilityChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; +import BasicMessageChannel, { MessageHandler, Reply } from '../../../plugin/common/BasicMessageChannel'; +import HashMap from '@ohos.util.HashMap'; +import FlutterNapi, { AccessibilityDelegate } from '../FlutterNapi'; +import StandardMessageCodec from '../../../plugin/common/StandardMessageCodec'; +import StringUtils from '../../../util/StringUtils'; +import Any from '../../../plugin/common/Any'; +import flutter from 'libflutter.so'; +import { ByteBuffer } from '../../../util/ByteBuffer'; + +/** + * Channel for handling accessibility-related communication between Flutter and OpenHarmony. + * This channel manages accessibility features, semantics, and accessibility events. + */ +export default class AccessibilityChannel implements MessageHandler { + private static TAG = "AccessibilityChannel"; + private static CHANNEL_NAME = "flutter/accessibility"; + private channel: BasicMessageChannel; + private flutterNapi: FlutterNapi; + private handler: AccessibilityMessageHandler; + private nextReplyId: number = 1; + + /** + * Handles messages from Dart. + * @param message - The message object from Dart + * @param reply - The reply callback to send a response + */ + onMessage(message: object, reply: Reply): void { + if (this.handler == null) { + Log.i(AccessibilityChannel.TAG, "handler == NULL"); + reply.reply(StringUtils.stringToArrayBuffer("")); + return; + } + let annotatedEvent: HashMap = message as HashMap; + let type: string = annotatedEvent.get("type") as string; + let data: HashMap = annotatedEvent.get("data") as HashMap; + + Log.i(AccessibilityChannel.TAG, "Received " + type + " message."); + switch (type) { + case "announce": { + Log.i(AccessibilityChannel.TAG, "Announce"); + let announceMessage: string = data.get("message"); + if (announceMessage != null) { + Log.i(AccessibilityChannel.TAG, "message is " + announceMessage); + this.handler.announce(announceMessage); + } + break; + } + case "tap": { + Log.i(AccessibilityChannel.TAG, "Tag"); + let nodeId: number = annotatedEvent.get("nodeId"); + if (nodeId != null) { + this.handler.onTap(nodeId); + } + break; + } + case "longPress": { + Log.i(AccessibilityChannel.TAG, "LongPress"); + let nodeId: number = annotatedEvent.get("nodeId"); + if (nodeId != null) { + this.handler.onLongPress(nodeId); + } + break; + } + case "tooltip": { + Log.i(AccessibilityChannel.TAG, "ToolTip"); + let tooltipMessage: string = data.get("message"); + if (tooltipMessage != null) { + this.handler.onTooltip(tooltipMessage); + } + break; + } + } + reply.reply(StringUtils.stringToArrayBuffer("")); + } + + /** + * Constructs a new AccessibilityChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + * @param flutterNapi - The FlutterNapi instance for native communication + */ + constructor(dartExecutor: DartExecutor, flutterNapi: FlutterNapi) { + Log.i(AccessibilityChannel.TAG, "Channel entered"); + this.channel = + new BasicMessageChannel(dartExecutor, AccessibilityChannel.CHANNEL_NAME, StandardMessageCodec.INSTANCE); + this.channel.setMessageHandler(this); + this.flutterNapi = flutterNapi; + this.handler = new DefaultHandler(this.flutterNapi); + } + + /** + * Called when OpenHarmony accessibility is enabled. + */ + onOhosAccessibilityEnabled(): void { + let replyId: number = this.nextReplyId++; + this.flutterNapi.setSemanticsEnabledWithRespId(true, replyId); + Log.i(AccessibilityChannel.TAG, "onOhosAccessibilityEnabled = true"); + } + + /** + * Called when OpenHarmony accessibility features change. + * @param accessibilityFeatureFlags - The accessibility feature flags + */ + onOhosAccessibilityFeatures(accessibilityFeatureFlags: number): void { + let replyId: number = this.nextReplyId++; + this.flutterNapi.setAccessibilityFeatures(accessibilityFeatureFlags, replyId); + Log.i(AccessibilityChannel.TAG, "onOhosAccessibilityFeatures"); + } + + /** + * Dispatches a semantics action to Flutter. + * @param virtualViewId - The virtual view ID + * @param action - The accessibility action to dispatch + */ + dispatchSemanticsAction(virtualViewId: number, action: Action): void { + let replyId: number = this.nextReplyId++; + this.flutterNapi.dispatchSemanticsAction(virtualViewId, action, replyId); + Log.i(AccessibilityChannel.TAG, "dispatchSemanticsAction"); + } + + /** + * Sets the accessibility message handler. + * @param handler - The AccessibilityMessageHandler instance + */ + setAccessibilityMessageHandler(handler: AccessibilityMessageHandler): void { + this.handler = handler; + let replyId: number = this.nextReplyId++; + this.flutterNapi.setAccessibilityDelegate(handler, replyId); + } +} + +/** + * Interface for handling accessibility messages. + */ +export interface AccessibilityMessageHandler extends AccessibilityDelegate { + /** + * Announces a message to the user. + * @param message - The message to announce + */ + announce(message: string): void; + + /** + * Handles a tap event on an accessibility node. + * @param nodeId - The ID of the accessibility node + */ + onTap(nodeId: number): void; + + /** + * Handles a long press event on an accessibility node. + * @param nodeId - The ID of the accessibility node + */ + onLongPress(nodeId: number): void; + + /** + * Handles a tooltip event. + * @param nodeId - The tooltip node ID + */ + onTooltip(nodeId: string): void; +} + +/** + * Default implementation of AccessibilityMessageHandler. + * Handles accessibility events and forwards them to the native Flutter engine. + */ +export class DefaultHandler implements AccessibilityMessageHandler { + private static TAG = "AccessibilityMessageHandler"; + private flutterNapi: FlutterNapi; + + /** + * Constructs a new DefaultHandler instance. + * @param flutterNapi - The FlutterNapi instance for native communication + */ + constructor(flutterNapi: FlutterNapi) { + this.flutterNapi = flutterNapi; + } + + /** + * Announces a message to the user. + * @param message - The message to announce + */ + announce(message: string): void { + Log.i(DefaultHandler.TAG, "handler announce."); + flutter.nativeAccessibilityAnnounce(this.flutterNapi.nativeShellHolderId!, message); + } + + /** + * Handles a tap event on an accessibility node. + * @param nodeId - The ID of the accessibility node + */ + onTap(nodeId: number): void { + Log.i(DefaultHandler.TAG, "handler onTap."); + flutter.nativeAccessibilityOnTap(this.flutterNapi.nativeShellHolderId!, nodeId); + } + + /** + * Handles a long press event on an accessibility node. + * @param nodeId - The ID of the accessibility node + */ + onLongPress(nodeId: number): void { + Log.i(DefaultHandler.TAG, "handler onLongPress."); + flutter.nativeAccessibilityOnLongPress(this.flutterNapi.nativeShellHolderId!, nodeId); + } + + /** + * Handles a tooltip event. + * @param message - The tooltip message + */ + onTooltip(message: string): void { + Log.i(DefaultHandler.TAG, "handler onTooltip."); + flutter.nativeAccessibilityOnTooltip(this.flutterNapi.nativeShellHolderId!, message); + } + + /** + * Called when accessibility state changes. + * @param state - The new accessibility state + */ + accessibilityStateChange(state: Boolean): void { + Log.i(DefaultHandler.TAG, "handler accessibilityStateChange"); + } +} + +/** + * Accessibility actions that can be performed on semantic nodes. + */ +export enum Action { + TAP = 1 << 0, + LONG_PRESS = 1 << 1, + SCROLL_LEFT = 1 << 2, + SCROLL_RIGHT = 1 << 3, + SCROLL_UP = 1 << 4, + SCROLL_DOWN = 1 << 5, + INCREASE = 1 << 6, + DECREASE = 1 << 7, + SHOW_ON_SCREEN = 1 << 8, + MOVE_CURSOR_FORWARD_BY_CHARACTER = 1 << 9, + MOVE_CURSOR_BACKWARD_BY_CHARACTER = 1 << 10, + SET_SELECTION = 1 << 11, + COPY = 1 << 12, + CUT = 1 << 13, + PASTE = 1 << 14, + DID_GAIN_ACCESSIBILITY_FOCUS = 1 << 15, + DID_LOSE_ACCESSIBILITY_FOCUS = 1 << 16, + CUSTOM_ACTION = 1 << 17, + DISMISS = 1 << 18, + MOVE_CURSOR_FORWARD_BY_WORD = 1 << 19, + MOVE_CURSOR_BACKWARD_BY_WORD = 1 << 20, + SET_NEXT = 1 << 21, +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/DisplayMetricsChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/DisplayMetricsChannel.ets new file mode 100644 index 0000000..fd44a8a --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/DisplayMetricsChannel.ets @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import StandardMethodCodec from '../../../plugin/common/StandardMethodCodec'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; +import { common } from '@kit.AbilityKit'; + +const TAG: string = 'DisplayMetricsChannel'; + +export default class DisplayMetricsChannel implements MethodCallHandler { + public channel: MethodChannel; + public context: common.Context; + + onMethodCall(call: MethodCall, result: MethodResult): void { + let method: string = call.method; + Log.i(TAG, "Received '" + method + "' message."); + try { + // More methods are expected to be added here, hence the switch. + switch (method) { + case "updateDpiScale": + let dpiScaleFactor: number = call.argument('dpiScale'); + Log.i(TAG, "Received dpiScaleFactor '" + dpiScaleFactor + "' message."); + this.context.eventHub.emit('changeDevicePixelRatio', dpiScaleFactor) + result.success(true); + break; + default: + result.notImplemented(); + break; + } + } catch (error) { + result.error("error", "UnHandled error: " + JSON.stringify(error), null) + } + } + + constructor(dartExecutor: DartExecutor, context: common.Context) { + this.channel = new MethodChannel(dartExecutor, "flutter/displaymetrics", StandardMethodCodec.INSTANCE); + this.channel.setMethodCallHandler(this); + this.context = context; + } +} + diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/KeyEventChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/KeyEventChannel.ets new file mode 100644 index 0000000..de5ff4f --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/KeyEventChannel.ets @@ -0,0 +1,259 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on KeyEventChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import BasicMessageChannel from '../../../plugin/common/BasicMessageChannel'; +import { BinaryMessenger } from '../../../plugin/common/BinaryMessenger'; +import Log from '../../../util/Log'; +import JSONMessageCodec from '../../../plugin/common/JSONMessageCodec'; +import { KeyCode } from '@kit.InputKit'; +import { ModifierKeyMetaInfo } from '../../ohos/KeyboardMap' +import Any from '../../../plugin/common/Any'; + +/** + * Channel for handling keyboard key events between OpenHarmony and Flutter. + * This channel manages communication of key events, including both hardware keyboard events + * and simulated key events for soft-keyboard text-editing. It uses a BasicMessageChannel + * with JSON encoding to send key event data to the Flutter framework. + */ +export default class KeyEventChannel { + private static TAG = "KeyEventChannel"; + private static CHANNEL_NAME = "flutter/keyevent"; + private channel: BasicMessageChannel; + + /** + * Constructs a new KeyEventChannel instance. + * @param binaryMessenger - The BinaryMessenger for sending messages to Dart + */ + constructor(binaryMessenger: BinaryMessenger) { + this.channel = new BasicMessageChannel(binaryMessenger, KeyEventChannel.CHANNEL_NAME, + JSONMessageCodec.INSTANCE); + } + + /** + * Sends a hardware key event to the Flutter framework. + * The event is encoded and sent asynchronously. The response handler will be called + * when Flutter responds, indicating whether the event was handled. + * @param keyEvent - The FlutterKeyEvent containing the OpenHarmony key event data + * @param isKeyUp - Whether this is a key up event (true) or key down event (false) + * @param responseHandler - Handler to receive the response from Flutter indicating if the event was handled + */ + sendFlutterKeyEvent(keyEvent: FlutterKeyEvent, + isKeyUp: boolean, + responseHandler: EventResponseHandler): void { + this.channel.send(this.encodeKeyEvent(keyEvent, isKeyUp), + (message: Object) => { + let isEventHandled = false; + try { + if (message != null) { + const tmp: Record = message as Record; + isEventHandled = tmp["handled"] || false; + } + } catch (e) { + Log.e(KeyEventChannel.TAG, "Unable to unpack JSON message: " + e); + } + responseHandler.onFrameworkResponse(isEventHandled); + } + ); + } + + private encodeKeyEvent(keyEvent: FlutterKeyEvent, isKeyUp: boolean): Map { + let message: Map = new Map(); + message.set("type", isKeyUp ? "keyup" : "keydown"); + message.set("keymap", "ohos"); + message.set("keyCode", keyEvent.event.keyCode); + message.set("deviceId", keyEvent.event.deviceId); + message.set("flags", keyEvent.event.keyText); + // the keyEvent of ohos do not support the getMetaState feature, + // so the flutter-ohos side adapts the getMetaState() method + message.set("metaState", keyEvent.getMetaState()); + message.set("source", keyEvent.event.keySource); + message.set("intentionCode", keyEvent.event.intentionCode); + return message; + } + + /** + * Sends a simulated key event for soft-keyboard text-editing in the input method. + * This method is used for virtual keyboard events (e.g., arrow keys, selection keys) + * that are generated programmatically rather than from hardware input. + * @param keyEvent - The SimulateKeyEvent containing the simulated key data + * @param isKeyUp - Whether this is a key up event (true) or key down event (false) + * @param responseHandler - Handler to receive the response from Flutter indicating if the event was handled + */ + simulateSendFlutterKeyEvent(keyEvent: SimulateKeyEvent, + isKeyUp: boolean, + responseHandler: EventResponseHandler): void { + this.channel.send(this.encodeSimulatedKeyEvent(keyEvent, isKeyUp), + (message: Object) => { + let isEventHandled = false; + try { + if (message !== null) { + const tmp: Record = message as Record; + isEventHandled = tmp["handled"] || false; + } + } catch (e) { + Log.e(KeyEventChannel.TAG, "Unable to unpack JSON message: " + e); + } + responseHandler.onFrameworkResponse(isEventHandled); + } + ); + } + + private encodeSimulatedKeyEvent(keyEvent: SimulateKeyEvent, isKeyUp: boolean): Map { + let message: Map = new Map(); + message.set("type", isKeyUp ? "keyup" : "keydown"); + message.set("keymap", "ohos"); + message.set("keyCode", keyEvent.keyCode); + message.set("flags", keyEvent.keyText); + return message; + } +} + +/** + * Interface for handling event responses from the Flutter framework. + * Implementations of this interface receive callbacks when Flutter processes + * key events and indicates whether the event was handled. + */ +export interface EventResponseHandler { + /** + * Called when Flutter responds to a key event. + * This callback is invoked asynchronously after Flutter processes the key event. + * @param isEventHandled - Whether Flutter handled the event (true) or not (false) + */ + onFrameworkResponse: (isEventHandled: boolean) => void; +} + +/** + * Wrapper for OpenHarmony KeyEvent that provides Flutter-compatible meta state information. + * OpenHarmony KeyEvent does not natively support meta state tracking for modifier keys + * (Ctrl, Alt, Shift) in the same way Flutter expects. This class supplements the missing + * functionality by tracking modifier key states and providing a getMetaState() method + * that returns a bitmask compatible with Flutter's key event handling. + */ +export class FlutterKeyEvent { + /** The underlying OpenHarmony KeyEvent instance. */ + event: KeyEvent; + private mMetaState: number; + private isCtrlPressed: boolean | undefined = false; + private isAltPressed: boolean | undefined = false; + private isShiftPressed: boolean | undefined = false; + + /** + * Constructs a new FlutterKeyEvent instance. + * @param ohosKeyEvent - The OpenHarmony KeyEvent to wrap + */ + constructor(ohosKeyEvent: KeyEvent) { + this.event = ohosKeyEvent; + this.mMetaState = ModifierKeyMetaInfo.NONE; + this.isCtrlPressed = ohosKeyEvent.getModifierKeyState && ohosKeyEvent.getModifierKeyState(['Ctrl']); + this.isAltPressed = ohosKeyEvent.getModifierKeyState && ohosKeyEvent.getModifierKeyState(['Alt']); + this.isShiftPressed = ohosKeyEvent.getModifierKeyState && ohosKeyEvent.getModifierKeyState(['Shift']); + this.didUpdateOhosMetaState(); + } + + /** + * Gets the meta state value as a bitmask. + * This supplements the missing metaState feature of OpenHarmony when pressing + * modifier keys (Ctrl, Alt, Shift) to perform combination key operations. + * The meta state is a bitmask that indicates which modifier keys are currently pressed. + * Currently only supports metaState tracking for Ctrl, Alt, and Shift keys. + * @returns The meta state value as a bitmask indicating pressed modifier keys + */ + getMetaState(): number { + return this.mMetaState; + } + + private didUpdateOhosMetaState(): void { + // The ohos platform only supports whether the Ctrl/Alt/Shift key or combination of them is pressed or not, + // and cannot distinguish the left or right directions. + // Here, the left direction key of ctrl/alt/shift is used by default + if (this.isCtrlPressed != undefined) { + this.updateMetaStateIfNeeded(KeyCode.KEYCODE_CTRL_LEFT, this.isCtrlPressed); + } + if (this.isAltPressed != undefined) { + this.updateMetaStateIfNeeded(KeyCode.KEYCODE_ALT_LEFT, this.isAltPressed); + } + if (this.isShiftPressed != undefined) { + this.updateMetaStateIfNeeded(KeyCode.KEYCODE_SHIFT_LEFT, this.isShiftPressed); + } + } + + private updateMetaStateIfNeeded(keyCode: number, isPressed: boolean): void { + const oldMetaState: number = this.mMetaState; + const newMetaState: number = this.updateMetaState(keyCode, isPressed, oldMetaState); + const isMetaStateChanged: number = oldMetaState ^ newMetaState; + if (isMetaStateChanged) { + this.mMetaState = newMetaState; + } + } + + private updateMetaState(keyCode: number, isPressed: boolean, oldMetaState: number): number { + switch (keyCode) { + case KeyCode.KEYCODE_ALT_LEFT: + return this.setMetaState(ModifierKeyMetaInfo.ALT_LEFT, isPressed, oldMetaState); + case KeyCode.KEYCODE_ALT_RIGHT: + return this.setMetaState(ModifierKeyMetaInfo.ALT_RIGHT, isPressed, oldMetaState); + case KeyCode.KEYCODE_SHIFT_LEFT: + return this.setMetaState(ModifierKeyMetaInfo.SHIFT_LEFT, isPressed, oldMetaState); + case KeyCode.KEYCODE_SHIFT_RIGHT: + return this.setMetaState(ModifierKeyMetaInfo.SHIFT_RIGHT, isPressed, oldMetaState); + case KeyCode.KEYCODE_CTRL_LEFT: + return this.setMetaState(ModifierKeyMetaInfo.CTRL_LEFT, isPressed, oldMetaState); + case KeyCode.KEYCODE_CTRL_RIGHT: + return this.setMetaState(ModifierKeyMetaInfo.CTRL_RIGHT, isPressed, oldMetaState); + default: + return oldMetaState; + } + } + + private setMetaState(mask: number, isPressed: boolean, oldMetaState: number): number { + let newMetaState: number; + if (isPressed) { // set the metaState of the pressed modifier key + newMetaState = oldMetaState | mask; + } else { // reset/clear the metaState of all modifier keys (ctrl|alt|shift|win/cmd) + newMetaState = oldMetaState & + ~(mask | ModifierKeyMetaInfo.CTRL | ModifierKeyMetaInfo.ALT | + ModifierKeyMetaInfo.SHIFT | ModifierKeyMetaInfo.META); + } + // Update the non-sided modifier key metaState to match the content of the sided ones. + if (newMetaState & (ModifierKeyMetaInfo.ALT_LEFT | ModifierKeyMetaInfo.ALT_RIGHT)) { + newMetaState |= ModifierKeyMetaInfo.ALT; + } + if (newMetaState & (ModifierKeyMetaInfo.SHIFT_LEFT | ModifierKeyMetaInfo.SHIFT_RIGHT)) { + newMetaState |= ModifierKeyMetaInfo.SHIFT; + } + if (newMetaState & (ModifierKeyMetaInfo.CTRL_LEFT | ModifierKeyMetaInfo.CTRL_RIGHT)) { + newMetaState |= ModifierKeyMetaInfo.CTRL; + } + return newMetaState; + } +} + +/** + * Simulated key event used in soft-keyboard text-editing. + * This class represents a programmatically generated key event, typically used + * for virtual keyboard interactions such as arrow keys, selection keys, or + * other input method editor (IME) operations. Unlike FlutterKeyEvent, this + * does not wrap a hardware KeyEvent and contains only the essential key information. + */ +export class SimulateKeyEvent { + /** The key code value identifying which key was pressed. */ + keyCode: number; + /** The text representation of the key (e.g., "Arrow Up", "Shift"). */ + keyText: string; + /** + * Constructs a new SimulateKeyEvent instance. + * @param keyCode - The key code value (e.g., KeyCode.KEYCODE_DPAD_UP) + * @param keyText - The text representation of the key (e.g., "Arrow Up") + */ + constructor(keyCode: number, keyText: string) { + this.keyCode = keyCode; + this.keyText = keyText; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/KeyboardChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/KeyboardChannel.ets new file mode 100644 index 0000000..9902e27 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/KeyboardChannel.ets @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021-2025 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + + +import DartExecutor from '../dart/DartExecutor'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import MethodCall from '../../../plugin/common/MethodCall'; +import StandardMethodCodec from '../../../plugin/common/StandardMethodCodec'; +import Log from '../../../util/Log'; + +/** + * Channel for handling keyboard-related communication between Flutter and OpenHarmony. + * This channel manages keyboard state queries and keyboard method calls. + */ +export default class KeyboardChannel implements MethodCallHandler { + private static TAG = "KeyboardChannel"; + private static CHANNEL_NAME = "flutter/keyboard"; + private channel: MethodChannel; + private handler: KeyboardMethodHandler | null = null; + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult): void { + if (this.handler == null) { + Log.i(KeyboardChannel.TAG, "KeyboardMethodHandler is null"); + return; + } + + let method: string = call.method; + switch (method) { + case "getKeyboardState": { + Log.i(KeyboardChannel.TAG, "getKeyboardState enter"); + result.success(this.handler?.getKeyboardState()); + break; + } + default: { + result.notImplemented(); + break; + } + } + } + + /** + * Constructs a new KeyboardChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new MethodChannel(dartExecutor, KeyboardChannel.CHANNEL_NAME, StandardMethodCodec.INSTANCE); + this.channel.setMethodCallHandler(this); + } + + /** + * Sets the keyboard method handler for processing keyboard-related requests. + * @param keyboardMessageHandler - The KeyboardMethodHandler instance, or null to remove + */ + public setKeyboardMethodHandler(keyboardMessageHandler: KeyboardMethodHandler | null): void { + this.handler = keyboardMessageHandler; + } +} + +/** + * Interface for handling keyboard method calls. + */ +export interface KeyboardMethodHandler { + /** + * Gets the current keyboard state. + * @returns A map containing keyboard state information + */ + getKeyboardState(): Map; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/LifecycleChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/LifecycleChannel.ets new file mode 100644 index 0000000..98b75ff --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/LifecycleChannel.ets @@ -0,0 +1,119 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on LifecycleChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import Log from '../../../util/Log'; +import StringCodec from '../../../plugin/common/StringCodec'; +import DartExecutor from '../dart/DartExecutor'; +import BasicMessageChannel from '../../../plugin/common/BasicMessageChannel'; + +/** + * Channel for handling application lifecycle events. + * This channel manages communication of lifecycle state changes between OpenHarmony and Flutter, + * including resumed, inactive, paused, and detached states. + */ +export default class LifecycleChannel { + private static TAG = "LifecycleChannel"; + private static CHANNEL_NAME = "flutter/lifecycle"; + // These should stay in sync with the AppLifecycleState enum in the framework. + private static RESUMED = "AppLifecycleState.resumed"; + private static INACTIVE = "AppLifecycleState.inactive"; + private static PAUSED = "AppLifecycleState.paused"; + private static DETACHED = "AppLifecycleState.detached"; + private lastOhosState = ""; + private lastFlutterState = ""; + private lastFocus = true; + private channel: BasicMessageChannel; + + /** + * Constructs a new LifecycleChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new BasicMessageChannel(dartExecutor, LifecycleChannel.CHANNEL_NAME, StringCodec.INSTANCE) + } + + /** + * Called when at least one window in the app has focus. + */ + aWindowIsFocused(): void { + this.sendState(this.lastOhosState, true); + } + + /** + * Called when no windows in the app have focus. + */ + noWindowsAreFocused(): void { + this.sendState(this.lastOhosState, false); + } + + /** + * Called when the app is resumed. + */ + appIsResumed(): void { + this.sendState(LifecycleChannel.RESUMED, this.lastFocus); + } + + /** + * Called when the app is inactive. + */ + appIsInactive(): void { + this.sendState(LifecycleChannel.INACTIVE, this.lastFocus); + } + + /** + * Called when the app is paused. + */ + appIsPaused(): void { + this.sendState(LifecycleChannel.PAUSED, this.lastFocus); + } + + /** + * Called when the app is detached. + */ + appIsDetached(): void { + this.sendState(LifecycleChannel.DETACHED, this.lastFocus); + } + + // Here's the state table this implements: + // + // | UIAbility State | Window focused | Flutter state | + // |-----------------|----------------|---------------| + // | onCreate | true | resumed | + // | onCreate | false | inactive | + // | onForeground | true | resumed | + // | onForeground | false | inactive | + // | onBackground | true | paused | + // | onBackground | false | paused | + // | onDestroy | true | detached | + // | onDestroy | false | detached | + + private sendState(state: string, hasFocus: boolean): void { + if (this.lastOhosState == state && hasFocus == this.lastFocus) { + // No inputs changed, so Flutter state could not have changed. + return; + } + let newState: string; + if (state == LifecycleChannel.RESUMED) { + newState = hasFocus ? LifecycleChannel.RESUMED : LifecycleChannel.INACTIVE; + } else { + newState = state; + } + // Keep the last reported values for future updates. + this.lastOhosState = state; + this.lastFocus = hasFocus; + if (newState == this.lastFlutterState) { + // No change in the resulting Flutter state, so don't report anything. + return; + } + Log.i(LifecycleChannel.TAG, "Sending " + newState + " message."); + this.channel.send(newState); + this.lastFlutterState = newState; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/LocalizationChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/LocalizationChannel.ets new file mode 100644 index 0000000..a8ce802 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/LocalizationChannel.ets @@ -0,0 +1,104 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on LocalizationChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import DartExecutor from '../dart/DartExecutor'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import MethodCall from '../../../plugin/common/MethodCall'; +import List from '@ohos.util.List'; +import JSONMethodCodec from '../../../plugin/common/JSONMethodCodec'; +import intl from '@ohos.intl'; +import Log from '../../../util/Log'; + +const TAG = "LocalizationChannel"; + +/** + * Channel for handling localization-related communication between Flutter and OpenHarmony. + * This channel manages locale information and string resource retrieval. + */ +export default class LocalizationChannel implements MethodCallHandler { + private static TAG = "LocalizationChannel"; + private static CHANNEL_NAME = "flutter/localization"; + private channel: MethodChannel; + private localizationMessageHandler: LocalizationMessageHandler | null = null; + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult): void { + if (this.localizationMessageHandler == null) { + Log.e(TAG, "localizationMessageHandler is null"); + return; + } + let method: string = call.method; + switch (method) { + case "Localization.getStringResource": { + Log.i(TAG, "Localization.getStringResource enter"); + let key: string = call.argument("key"); + let localeString: string = ""; + if (call.hasArgument("locale")) { + localeString = call.argument("locale"); + } + result.success(this.localizationMessageHandler?.getStringResource(key, localeString)); + break; + } + default: { + result.notImplemented(); + break; + } + } + } + + /** + * Constructs a new LocalizationChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new MethodChannel(dartExecutor, LocalizationChannel.CHANNEL_NAME, JSONMethodCodec.INSTANCE); + this.channel.setMethodCallHandler(this); + } + + /** + * Sets the localization message handler for processing localization requests. + * @param localizationMessageHandler - The LocalizationMessageHandler instance + */ + setLocalizationMessageHandler(localizationMessageHandler: LocalizationMessageHandler): void { + this.localizationMessageHandler = localizationMessageHandler; + } + + /** + * Sends locale information to Flutter. + * @param locales - Array of locale strings to send + */ + sendLocales(locales: string[]): void { + let data: string[] = []; + for (let i = 0; i < locales.length; i++) { + let locale = new intl.Locale(locales[i]); + data.push(locale.language); + data.push(locale.region); + data.push(locale.script); + data.push(''); // locale.getVariant locale的一种变体 + } + this.channel.invokeMethod("setLocale", data); + } +} + +/** + * Interface for handling localization message requests. + */ +export interface LocalizationMessageHandler { + /** + * Gets a string resource by key and locale. + * @param key - The resource key + * @param local - The locale string + */ + getStringResource(key: string, local: string): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/MouseCursorChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/MouseCursorChannel.ets new file mode 100644 index 0000000..9bba2eb --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/MouseCursorChannel.ets @@ -0,0 +1,104 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MouseCursorChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import HashMap from '@ohos.util.HashMap'; +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import StandardMethodCodec from '../../../plugin/common/StandardMethodCodec'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; + +const TAG: string = 'MouseCursorChannel'; + +/** + * Channel for handling mouse cursor changes. + * This channel manages communication between Flutter and OpenHarmony for cursor appearance changes. + */ +export default class MouseCursorChannel implements MethodCallHandler { + /** The MethodChannel for mouse cursor communication with Flutter. */ + public channel: MethodChannel; + private mouseCursorMethodHandler: MouseCursorMethodHandler | null = null; + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult): void { + if (this.mouseCursorMethodHandler === null) { + // if no explicit mouseCursorMethodHandler has been registered then we don't + // need to formed this call to an API. Return + Log.e(TAG, "mouseCursorMethodHandler is null") + return; + } + + let method: string = call.method; + Log.i(TAG, "Received '" + method + "' message."); + try { + // More methods are expected to be added here, hence the switch. + switch (method) { + case "activateSystemCursor": + let argument: HashMap = call.args; + let kind: string = argument.get("kind"); + try { + this.mouseCursorMethodHandler.activateSystemCursor(kind); + } catch (err) { + result.error("error", "Error when setting cursors: " + JSON.stringify(err), null); + break; + } + result.success(true); + break; + default: + break; + } + } catch (error) { + result.error("error", "UnHandled error: " + JSON.stringify(error), null) + } + } + + /** + * Constructs a new MouseCursorChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new MethodChannel(dartExecutor, "flutter/mousecursor", StandardMethodCodec.INSTANCE); + this.channel.setMethodCallHandler(this); + } + + /** + * Sets the MouseCursorMethodHandler which receives all events and requests that are + * parsed from the underlying platform channel. + * @param mouseCursorMethodHandler - The MouseCursorMethodHandler instance, or null to remove + */ + public setMethodHandler(mouseCursorMethodHandler: MouseCursorMethodHandler | null): void { + this.mouseCursorMethodHandler = mouseCursorMethodHandler; + } + + /** + * Synthesizes a method call for testing purposes. + * @param call - The method call to synthesize + * @param result - The result callback to send a response + */ + public synthesizeMethodCall(call: MethodCall, result: MethodResult): void { + this.onMethodCall(call, result); + } +} + +/** + * Interface for handling mouse cursor method calls. + */ +export interface MouseCursorMethodHandler { + /** + * Called when the pointer should start displaying a system mouse cursor + * specified by the kind parameter. + * @param kind - The cursor kind/type to activate + */ + activateSystemCursor(kind: String): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/NativeVsyncChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/NativeVsyncChannel.ets new file mode 100644 index 0000000..b962bde --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/NativeVsyncChannel.ets @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2024 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; +import HashMap from '@ohos.util.HashMap'; +import StringUtils from '../../../util/StringUtils'; +import Any from '../../../plugin/common/Any'; +import FlutterNapi from '../FlutterNapi'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import MethodCall from '../../../plugin/common/MethodCall'; +import StandardMethodCodec from '../../../plugin/common/StandardMethodCodec'; + +/** + * Types of animation voting. + */ +enum AnimationVotingType { + TRANSLATE = 0, + SCALE, + RATATION, +} + +/** + * Channel for handling native VSync functionality. + * This channel manages VSync switching, animation velocity voting, and LTPO switch state checking. + */ +export default class NativeVsyncChannel implements MethodCallHandler { + private static TAG = "NativeVsyncChannel"; + private static CHANNEL_NAME = "flutter/nativevsync"; + /** The MethodChannel for native VSync communication with Flutter. */ + public channel: MethodChannel; + private flutterNapi: FlutterNapi; + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult): void { + let method: string = call.method; + try { + switch (method) { + case "isEnable": + // Whether to enable DVsync + let isEnable: boolean = call.argument('isEnable'); + this.flutterNapi.SetDVsyncSwitch(isEnable); + break; + case "sendVelocity": + // Send animation velocity + let type: string = call.argument('type'); + let velocity: number = call.argument('velocity'); + if (type == "translate") { + FlutterNapi.animationVoting(AnimationVotingType.TRANSLATE, velocity); + } + break; + case "checkLTPOSwitchStatus": + // Check LTPO switch state + result.success(FlutterNapi.checkLTPOSwitchState()); + break; + default: + result.notImplemented(); + break; + } + } catch (error) { + result.error("error", "UnHandled error: " + JSON.stringify(error), null) + } + } + + /** + * Constructs a new NativeVsyncChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + * @param flutterNapi - The FlutterNapi instance for native communication + */ + constructor(dartExecutor: DartExecutor, flutterNapi: FlutterNapi) { + this.channel = new MethodChannel(dartExecutor, NativeVsyncChannel.CHANNEL_NAME, StandardMethodCodec.INSTANCE); + this.channel.setMethodCallHandler(this); + this.flutterNapi = flutterNapi; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/NavigationChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/NavigationChannel.ets new file mode 100644 index 0000000..027f79d --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/NavigationChannel.ets @@ -0,0 +1,241 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on NavigationChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import { common } from '@kit.AbilityKit'; +import hiTraceMeter from '@ohos.hiTraceMeter'; + +import JSONMethodCodec from '../../../plugin/common/JSONMethodCodec'; +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; +import FlutterManager from '../../ohos/FlutterManager'; +import Any from '../../../plugin/common/Any'; + +/** + * Navigator activity types. + */ +export enum NavigatorActivity { + PUSH = "push", + POP = "pop", +} + +/** + * Navigator status types. + */ +export enum NavigatorStatus { + START = "start", + FINISH = "finish", +} + +/** + * Channel for handling navigation-related communication between Flutter and OpenHarmony. + * This channel manages route navigation, including setting initial routes, pushing routes, + * and popping routes. + */ +export default class NavigationChannel { + private static TAG = "NavigationChannel"; + private channel: MethodChannel; + + /** + * Constructs a new NavigationChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + * @param context - The application context + */ + constructor(dartExecutor: DartExecutor, context?: common.Context) { + this.channel = new MethodChannel(dartExecutor, "flutter/navigation", JSONMethodCodec.INSTANCE); + // Provide a default handler that returns an empty response to any messages + // on this channel. + this.channel.setMethodCallHandler(new NavigationCallback(dartExecutor, context)); + } + + /** + * Sets the initial route for navigation. + * @param initialRoute - The initial route string + */ + setInitialRoute(initialRoute: string): void { + Log.i(NavigationChannel.TAG, "Sending message to set initial route to '" + initialRoute + "'"); + this.channel.invokeMethod("setInitialRoute", initialRoute); + } + + /** + * Pushes a new route onto the navigation stack. + * @param route - The route string to push + */ + pushRoute(route: string): void { + Log.i(NavigationChannel.TAG, "Sending message to push route '" + route + "'"); + this.channel.invokeMethod("pushRoute", route); + } + + /** + * Pushes route information onto the navigation stack. + * @param route - The route information string to push + */ + pushRouteInformation(route: string): void { + Log.i(NavigationChannel.TAG, "Sending message to push route information '" + route + "'"); + this.channel.invokeMethod("pushRouteInformation", new Map().set("location", route)); + } + + /** + * Pops the current route from the navigation stack. + */ + popRoute(): void { + Log.i(NavigationChannel.TAG, "Sending message to pop route."); + this.channel.invokeMethod("popRoute", null); + } + + /** + * Sets a custom method call handler for navigation events. + * @param handler - The MethodCallHandler to handle navigation method calls + */ + setMethodCallHandler(handler: MethodCallHandler) { + this.channel.setMethodCallHandler(handler); + } +} + +/** + * Default callback handler for navigation channel that returns empty responses. + */ +class NavigationCallback implements MethodCallHandler { + private static TAG = "NavigationChannel"; + private dartExecutor: DartExecutor; + private context?: common.Context; + + constructor(dartExecutor: DartExecutor, context?: common.Context) { + this.dartExecutor = dartExecutor; + this.context = context; + } + + onMethodCall(call: MethodCall, result: MethodResult) { + let method: string = call.method; + let args: Any = call.args; + Log.i(NavigationCallback.TAG, "method = " + method); + switch (method) { + case "reportNavigatorActivity": { + let activity: string = args.get("activity"); + if (activity === undefined) { + Log.e(NavigationCallback.TAG, "reportNavigatorActivity, incorrect parameter activity"); + break; + } + + let status: string = args.get("status"); + if (status === undefined) { + Log.e(NavigationCallback.TAG, "reportNavigatorActivity, incorrect parameter status"); + break; + } + let navigatorStatus: NavigatorStatus = this.getNavigatorStatusFromValue(status); + + let navigatorActivity: NavigatorActivity = this.getNavigatorActivityFromValue(activity); + switch(navigatorActivity) { + case NavigatorActivity.PUSH: + this.reportNavigatorPush(navigatorStatus); + break; + case NavigatorActivity.POP: + this.reportNavigatorPop(navigatorStatus); + break; + } + break; + } + default: { + this.notifyPageChanged(call); + result.success(null); + break; + } + } + } + + private notifyPageChanged(call: MethodCall) { + if (this.dartExecutor == null || this.dartExecutor.flutterNapi == null) { + Log.e(NavigationCallback.TAG, "dartExecutor or flutterNapi is null, cancel OHOS page change notification"); + return; + } + + // Skip notification if context is not provided + if (this.context == null) { + Log.e(NavigationCallback.TAG, "context is not provided, skip OHOS page change notification"); + return; + } + + const argsMap = call.args as Map; + const currentUri: string = argsMap.get('uri') ?? ''; + const currentUriLen: number = currentUri.length; + + // Dynamically get windowId + const windowId: number = FlutterManager.getInstance().getWindowId(this.context); + if (windowId == 0) { + Log.e(NavigationCallback.TAG, "Failed to get windowId, skip OHOS page change notification, uri: " + currentUri); + return; + } + + const notifyResult = this.dartExecutor.flutterNapi.NotifyPageChanged(currentUri, currentUriLen, windowId); + // Return value: 0 means success, non-zero means error + if (notifyResult == 0) { + Log.i(NavigationCallback.TAG, "NotifyPageChanged success, uri: " + currentUri + ", windowId: " + windowId); + } else { + Log.e(NavigationCallback.TAG, "NotifyPageChanged failed, uri: " + currentUri + ", windowId: " + windowId + ", result: " + notifyResult); + } + } + + /** + * Gets a NavigatorActivity from an encoded activity string. + * @param activity - The encoded activity string + * @returns The NavigatorActivity + * @throws Error if the activity string is not recognized + */ + private getNavigatorActivityFromValue(activity: string): NavigatorActivity { + let activityTypes: string[] = [ + NavigatorActivity.PUSH, + NavigatorActivity.POP + ]; + if (activityTypes.includes(activity as NavigatorActivity)) { + return activity as NavigatorActivity; + } + throw new Error("No such NavigatorActivity: " + activity); + } + + /** + * Gets a NavigatorStatus from an encoded status string. + * @param status - The encoded status string + * @returns The NavigatorStatus + * @throws Error if the status string is not recognized + */ + private getNavigatorStatusFromValue(status: string): NavigatorStatus { + let statusTypes: string[] = [ + NavigatorStatus.START, + NavigatorStatus.FINISH + ]; + if (statusTypes.includes(status as NavigatorStatus)) { + return status as NavigatorStatus; + } + throw new Error("No such NavigatorStatus: " + status); + } + + private reportNavigatorPush(navigatorStatus: NavigatorStatus) { + switch(navigatorStatus) { + case NavigatorStatus.START: + hiTraceMeter.startTrace("flutter::NAVIGATOR_PUSH", 0); + break; + case NavigatorStatus.FINISH: + hiTraceMeter.finishTrace("flutter::NAVIGATOR_PUSH", 0); + break; + } + } + + private reportNavigatorPop(navigatorStatus: NavigatorStatus) { + switch(navigatorStatus) { + case NavigatorStatus.START: + hiTraceMeter.startTrace("flutter::NAVIGATOR_POP", 0); + break; + case NavigatorStatus.FINISH: + hiTraceMeter.finishTrace("flutter::NAVIGATOR_POP", 0); + break; + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/PlatformChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/PlatformChannel.ets new file mode 100644 index 0000000..79be8ac --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/PlatformChannel.ets @@ -0,0 +1,674 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import hiTraceMeter from '@ohos.hiTraceMeter'; +import JSONMethodCodec from '../../../plugin/common/JSONMethodCodec'; +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; +import pasteboard from '@ohos.pasteboard'; +import bundleManager from '@ohos.bundle.bundleManager'; +import window from '@ohos.window'; +import Any from '../../../plugin/common/Any'; +import { BusinessError } from '@kit.BasicServicesKit'; +import FlutterNapi from '../FlutterNapi'; +import flutter from 'libflutter.so'; + +/** + * Channel for handling platform-level communication between Flutter and OpenHarmony. + * This channel manages system UI, clipboard, haptic feedback, orientation, and other platform services. + */ +export default class PlatformChannel { + private static TAG = "PlatformChannel"; + private static CHANNEL_NAME = "flutter/platform"; + flutterNapi: FlutterNapi; + /** The MethodChannel for platform-level communication with Flutter. */ + channel: MethodChannel; + /** The platform message handler for processing platform requests, or null if not set. */ + platformMessageHandler: PlatformMessageHandler | null = null; + + /** + * Constructs a new PlatformChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor, flutterNapi: FlutterNapi) { + this.channel = new MethodChannel(dartExecutor, PlatformChannel.CHANNEL_NAME, JSONMethodCodec.INSTANCE); + let callback = new PlatformMethodCallback(this); + this.channel.setMethodCallHandler(callback); + this.flutterNapi = flutterNapi; + } + + /** + * Sets the platform message handler for processing platform requests. + * @param platformMessageHandler - The PlatformMessageHandler instance, or null to remove + */ + setPlatformMessageHandler(platformMessageHandler: PlatformMessageHandler | null): void { + this.platformMessageHandler = platformMessageHandler; + } + + /** + * Notifies Flutter that system chrome overlays have changed. + * @param areOverlaysVisible - Whether system overlays are visible + */ + systemChromeChanged(areOverlaysVisible: boolean): void { + Log.d(PlatformChannel.TAG, "Sending 'systemUIChange' message."); + this.channel.invokeMethod("SystemChrome.systemUIChange", [areOverlaysVisible]); + } + + /** + * Decodes orientation strings into OpenHarmony orientation values. + * @param encodedOrientations - Array of encoded orientation strings + * @returns The decoded orientation value + */ + decodeOrientations(encodedOrientations: string[]): number { + let requestedOrientation = 0x00; + let firstRequestedOrientation = 0x00; + for (let index = 0; index < encodedOrientations.length; index += 1) { + let encodedOrientation = encodedOrientations[index]; + Log.d(PlatformChannel.TAG, "encodedOrientation[" + index + "]: " + encodedOrientation); + let orientation = this.getDeviceOrientationFromValue(encodedOrientation); + switch (orientation) { + case DeviceOrientation.PORTRAIT_UP: + requestedOrientation |= 0x01; + break; + case DeviceOrientation.PORTRAIT_DOWN: + requestedOrientation |= 0x04; + break; + case DeviceOrientation.LANDSCAPE_LEFT: + requestedOrientation |= 0x08; + break; + case DeviceOrientation.LANDSCAPE_RIGHT: + requestedOrientation |= 0x02; + break; + } + if (firstRequestedOrientation == 0x00) { + firstRequestedOrientation = requestedOrientation; + } + } + + switch (requestedOrientation) { + case 0x00: + return window.Orientation.UNSPECIFIED; + case 0x01: + return window.Orientation.PORTRAIT; + case 0x02: + return window.Orientation.LANDSCAPE_INVERTED; + case 0x03: + case 0x04: + return window.Orientation.PORTRAIT_INVERTED; + case 0x05: + return window.Orientation.AUTO_ROTATION_PORTRAIT; + case 0x06: + case 0x07: + case 0x08: + return window.Orientation.LANDSCAPE; + case 0x09: + case 0x0a: + return window.Orientation.AUTO_ROTATION_LANDSCAPE; + case 0x0b: + return window.Orientation.LOCKED; + case 0x0c: + case 0x0d: + case 0x0e: + switch (firstRequestedOrientation) { + case 0x01: + return bundleManager.DisplayOrientation.PORTRAIT; + case 0x02: + return bundleManager.DisplayOrientation.LANDSCAPE_INVERTED; + case 0x04: + return bundleManager.DisplayOrientation.PORTRAIT_INVERTED; + case 0x08: + return bundleManager.DisplayOrientation.LANDSCAPE; + } + case 0x0f: + return window.Orientation.AUTO_ROTATION_RESTRICTED; + } + return bundleManager.DisplayOrientation.PORTRAIT; + } + + /** + * Gets a HapticFeedbackType from an encoded name. + * @param encodedName - The encoded feedback type name + * @returns The HapticFeedbackType, or STANDARD if not found + */ + getFeedbackTypeFromValue(encodedName: string): HapticFeedbackType { + if (encodedName == null) { + return HapticFeedbackType.STANDARD; + } + let feedbackTypes: string[] = [ + HapticFeedbackType.STANDARD, + HapticFeedbackType.LIGHT_IMPACT, + HapticFeedbackType.MEDIUM_IMPACT, + HapticFeedbackType.HEAVY_IMPACT, + HapticFeedbackType.SELECTION_CLICK + ]; + if (feedbackTypes.includes(encodedName as HapticFeedbackType)) { + return encodedName as HapticFeedbackType; + } else { + Log.e(PlatformChannel.TAG, "No such HapticFeedbackType:" + encodedName); + return HapticFeedbackType.STANDARD; + } + } + + /** + * Gets a ClipboardContentFormat from an encoded name. + * @param encodedName - The encoded format name + * @returns The ClipboardContentFormat, or PLAIN_TEXT if not found + */ + getClipboardContentFormatFromValue(encodedName: string): ClipboardContentFormat { + let clipboardFormats: string[] = [ClipboardContentFormat.PLAIN_TEXT]; + if (clipboardFormats.includes(encodedName as ClipboardContentFormat)) { + return encodedName as ClipboardContentFormat; + } + return ClipboardContentFormat.PLAIN_TEXT; + } + + /** + * Gets a SystemUiOverlay from an encoded name. + * @param encodedName - The encoded overlay name + * @returns The SystemUiOverlay + * @throws Error if the overlay name is not recognized + */ + getSystemUiOverlayFromValue(encodedName: string): SystemUiOverlay { + let systemUiOverlays: string[] = [SystemUiOverlay.TOP_OVERLAYS, SystemUiOverlay.BOTTOM_OVERLAYS]; + if (systemUiOverlays.includes(encodedName as SystemUiOverlay)) { + return encodedName as SystemUiOverlay; + } + throw new Error("No such SystemUiOverlay: " + encodedName); + } + + /** + * Gets a SystemUiMode from an encoded name. + * @param encodedName - The encoded mode name + * @returns The SystemUiMode + * @throws Error if the mode name is not recognized + */ + getSystemUiModeFromValue(encodedName: string): SystemUiMode { + let systemUiModes: string[] = [ + SystemUiMode.LEAN_BACK, SystemUiMode.IMMERSIVE, + SystemUiMode.IMMERSIVE_STICKY, SystemUiMode.EDGE_TO_EDGE + ]; + if (systemUiModes.includes(encodedName as SystemUiMode)) { + return encodedName as SystemUiMode; + } + throw new Error("No such SystemUiOverlay: " + encodedName); + } + + /** + * Gets a Brightness from an encoded name. + * @param encodedName - The encoded brightness name + * @returns The Brightness + * @throws Error if the brightness name is not recognized + */ + getBrightnessFromValue(encodedName: string): Brightness { + let brightnesses: string[] = [Brightness.LIGHT, Brightness.DARK]; + if (brightnesses.includes(encodedName as Brightness)) { + return encodedName as Brightness; + } + throw new Error("No such Brightness: " + encodedName); + } + + /** + * Gets a DeviceOrientation from an encoded name. + * @param encodedName - The encoded orientation name + * @returns The DeviceOrientation + * @throws Error if the orientation name is not recognized + */ + getDeviceOrientationFromValue(encodedName: string): DeviceOrientation { + let deviceOrientations: DeviceOrientation[] = [ + DeviceOrientation.PORTRAIT_UP, DeviceOrientation.PORTRAIT_DOWN, + DeviceOrientation.LANDSCAPE_LEFT, DeviceOrientation.LANDSCAPE_RIGHT + ]; + if (deviceOrientations.includes(encodedName as DeviceOrientation)) { + return encodedName as DeviceOrientation; + } + throw new Error("No such DeviceOrientation: " + encodedName); + } + + /** + * Gets a ScrollActivity from an encoded activity string. + * @param activity - The encoded activity string + * @returns The ScrollActivity + * @throws Error if the activity string is not recognized + */ + getScrollActivityFromValue(activity: string): ScrollActivity { + let activityTypes: string[] = [ + ScrollActivity.START, + ScrollActivity.END + ]; + if (activityTypes.includes(activity as ScrollActivity)) { + return activity as ScrollActivity; + } + throw new Error("No such ScrollActivity: " + activity); + } + +} + +/** + * Types of haptic feedback. + */ +export enum HapticFeedbackType { + STANDARD = "STANDARD", + LIGHT_IMPACT = "HapticFeedbackType.lightImpact", + MEDIUM_IMPACT = "HapticFeedbackType.mediumImpact", + HEAVY_IMPACT = "HapticFeedbackType.heavyImpact", + SELECTION_CLICK = "HapticFeedbackType.selectionClick" +} + +/** + * Interface for handling platform message requests from Flutter. + */ +export interface PlatformMessageHandler { + playSystemSound(soundType: SoundType): void; + + vibrateHapticFeedback(feedbackType: HapticFeedbackType): Promise; + + setPreferredOrientations(ohosOrientation: number, result: MethodResult): void; + + setApplicationSwitcherDescription(description: AppSwitcherDescription): void; + + showSystemOverlays(overlays: SystemUiOverlay[]): void; + + showSystemUiMode(mode: SystemUiMode): void; + + setSystemUiChangeListener(): void; + + restoreSystemUiOverlays(): void; + + setSystemUiOverlayStyle(systemUiOverlayStyle: SystemChromeStyle): void; + + popSystemNavigator(): void; + + getClipboardData(result: MethodResult): void; + + setClipboardData(text: string, result: MethodResult): void; + + clipboardHasStrings(): boolean; +} + +/** + * Clipboard content formats. + */ +export enum ClipboardContentFormat { + PLAIN_TEXT = "text/plain", +} + +/** + * System sound types. + */ +export enum SoundType { + CLICK = "SystemSoundType.click", + ALERT = "SystemSoundType.alert", +} + +/** + * Description for the app switcher. + */ +export class AppSwitcherDescription { + /** The color value for the app switcher. */ + public readonly color: number; + /** The label text for the app switcher. */ + public readonly label: string; + + /** + * Constructs a new AppSwitcherDescription instance. + * @param color - The color value + * @param label - The label text + */ + constructor(color: number, label: string) { + this.color = color; + this.label = label; + } +} + +/** + * System UI overlay positions. + */ +export enum SystemUiOverlay { + TOP_OVERLAYS = "SystemUiOverlay.top", + BOTTOM_OVERLAYS = "SystemUiOverlay.bottom", +} + +/** + * System UI display modes. + */ +export enum SystemUiMode { + LEAN_BACK = "SystemUiMode.leanBack", + IMMERSIVE = "SystemUiMode.immersive", + IMMERSIVE_STICKY = "SystemUiMode.immersiveSticky", + EDGE_TO_EDGE = "SystemUiMode.edgeToEdge", +} + +export enum Brightness { + LIGHT = "Brightness.light", + DARK = "Brightness.dark", +} + +/** + * Style configuration for system chrome (status bar and navigation bar). + */ +export class SystemChromeStyle { + /** The status bar color, or null if not set. */ + public readonly statusBarColor: number | null; + /** The status bar icon brightness, or null if not set. */ + public readonly statusBarIconBrightness: Brightness | null; + /** Whether status bar contrast is enforced, or null if not set. */ + public readonly systemStatusBarContrastEnforced: boolean | null; + /** The navigation bar color, or null if not set. */ + public readonly systemNavigationBarColor: number | null; + /** The navigation bar icon brightness, or null if not set. */ + public readonly systemNavigationBarIconBrightness: Brightness | null; + /** The navigation bar divider color, or null if not set. */ + public readonly systemNavigationBarDividerColor: number | null; + /** Whether navigation bar contrast is enforced, or null if not set. */ + public readonly systemNavigationBarContrastEnforced: boolean | null; + + /** + * Constructs a new SystemChromeStyle instance. + * @param statusBarColor - The status bar color + * @param statusBarIconBrightness - The status bar icon brightness + * @param systemStatusBarContrastEnforced - Whether status bar contrast is enforced + * @param systemNavigationBarColor - The navigation bar color + * @param systemNavigationBarIconBrightness - The navigation bar icon brightness + * @param systemNavigationBarDividerColor - The navigation bar divider color + * @param systemNavigationBarContrastEnforced - Whether navigation bar contrast is enforced + */ + constructor(statusBarColor: number | null, + statusBarIconBrightness: Brightness | null, + systemStatusBarContrastEnforced: boolean | null, + systemNavigationBarColor: number | null, + systemNavigationBarIconBrightness: Brightness | null, + systemNavigationBarDividerColor: number | null, + systemNavigationBarContrastEnforced: boolean | null) { + this.statusBarColor = statusBarColor; + this.statusBarIconBrightness = statusBarIconBrightness; + this.systemStatusBarContrastEnforced = systemStatusBarContrastEnforced; + this.systemNavigationBarColor = systemNavigationBarColor; + this.systemNavigationBarIconBrightness = systemNavigationBarIconBrightness; + this.systemNavigationBarDividerColor = systemNavigationBarDividerColor; + this.systemNavigationBarContrastEnforced = systemNavigationBarContrastEnforced; + } +} + +/** + * Device orientation types. + */ +export enum DeviceOrientation { + PORTRAIT_UP = "DeviceOrientation.portraitUp", + PORTRAIT_DOWN = "DeviceOrientation.portraitDown", + LANDSCAPE_LEFT = "DeviceOrientation.landscapeLeft", + LANDSCAPE_RIGHT = "DeviceOrientation.landscapeRight", +} + +/** + * Scroll activity types. + */ +export enum ScrollActivity { + START = "start", + END = "end", +} + +enum AnimationStatus { + SCROLL_START = 0, + SCROLL_END = 1, +} + +/** + * Method call handler for platform channel requests. + */ +class PlatformMethodCallback implements MethodCallHandler { + private static TAG = "PlatformMethodCallback" + platform: PlatformChannel; + private scrollType: string | null = null; + + /** + * Constructs a new PlatformMethodCallback instance. + * @param platform - The PlatformChannel instance + */ + constructor(platform: PlatformChannel) { + this.platform = platform; + } + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult) { + if (this.platform.platformMessageHandler == null) { + Log.w(PlatformMethodCallback.TAG, "platformMessageHandler is null"); + return; + } + + let method: string = call.method; + let args: Any = call.args; + Log.d(PlatformMethodCallback.TAG, "Received '" + method + "' message."); + try { + switch (method) { + case "SystemSound.play": + break; + case "HapticFeedback.vibrate": + try { + Log.d(PlatformMethodCallback.TAG, "HapticFeedback: " + args as string); + let feedbackType = this.platform.getFeedbackTypeFromValue(args as string); + this.platform.platformMessageHandler.vibrateHapticFeedback(feedbackType) + .then(() => { + result.success(null); + }) + .catch((e: BusinessError) => { + Log.e(PlatformMethodCallback.TAG, `HapticFeedback.vibrate error: ${e.code} - ${e.message}`); + }); + } catch (e) { + Log.e(PlatformMethodCallback.TAG, "HapticFeedback.vibrate error:" + JSON.stringify(e)); + } + break; + case "SystemChrome.setPreferredOrientations": + Log.d(PlatformMethodCallback.TAG, "setPreferredOrientations: " + JSON.stringify(args)); + let ohosOrientation = this.platform.decodeOrientations(args as string[]); + this.platform.platformMessageHandler.setPreferredOrientations(ohosOrientation, result); + break; + case "SystemChrome.setApplicationSwitcherDescription": + Log.d(PlatformMethodCallback.TAG, "setApplicationSwitcherDescription: " + JSON.stringify(args)); + try { + let description: AppSwitcherDescription = this.decodeAppSwitcherDescription(args); + this.platform.platformMessageHandler.setApplicationSwitcherDescription(description); + result.success(null); + } catch (err) { + Log.e(PlatformMethodCallback.TAG, "setApplicationSwitcherDescription err:" + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + } + break; + case "SystemChrome.setEnabledSystemUIOverlays": + try { + let overlays: SystemUiOverlay[] = this.decodeSystemUiOverlays(args); + Log.d(PlatformMethodCallback.TAG, "overlays: " + overlays); + this.platform.platformMessageHandler.showSystemOverlays(overlays); + result.success(null); + } catch (err) { + Log.e(PlatformMethodCallback.TAG, "setEnabledSystemUIOverlays err:" + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + } + break; + case "SystemChrome.setEnabledSystemUIMode": + try { + Log.d(PlatformMethodCallback.TAG, "setEnabledSystemUIMode args:" + args as string); + let mode: SystemUiMode = this.decodeSystemUiMode(args as string) + this.platform.platformMessageHandler.showSystemUiMode(mode); + } catch (err) { + Log.e(PlatformMethodCallback.TAG, "setEnabledSystemUIMode err:" + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + } + break; + case "SystemChrome.setSystemUIChangeListener": + this.platform.platformMessageHandler.setSystemUiChangeListener(); + result.success(null); + break; + case "SystemChrome.restoreSystemUIOverlays": + this.platform.platformMessageHandler.restoreSystemUiOverlays(); + result.success(null); + break; + case "SystemChrome.setSystemUIOverlayStyle": + try { + Log.d(PlatformMethodCallback.TAG, "setSystemUIOverlayStyle asrgs: " + JSON.stringify(args)); + let systemChromeStyle: SystemChromeStyle = this.decodeSystemChromeStyle(args); + this.platform.platformMessageHandler.setSystemUiOverlayStyle(systemChromeStyle); + result.success(null); + } catch (err) { + Log.e(PlatformMethodCallback.TAG, "setSystemUIOverlayStyle err:" + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + } + break; + case "SystemNavigator.pop": + this.platform.platformMessageHandler.popSystemNavigator(); + result.success(null); + break; + case "Clipboard.getData": + this.platform.platformMessageHandler.getClipboardData(result); + break; + case "Clipboard.setData": + let clipboardContent: string = args.get('text'); + this.platform.platformMessageHandler.setClipboardData(clipboardContent, result); + break; + case "Clipboard.hasStrings": + let response: Any = new Map().set("value", false); + let systemPasteboard = pasteboard.getSystemPasteboard(); + systemPasteboard.hasData().then((hasData) => { + response.set("value", hasData); + result.success(response); + }).catch((err: Any) => { + Log.e(PlatformMethodCallback.TAG, "systemPasteboard.hasData err: " + JSON.stringify(err)); + }) + break; + case "Scroll.Activity": + /// Report the behavior of scrolling components. + /// The optional values for Scroll.Activity include [start] and [end]. + this.recordScrollActivity(args as string); + break; + case "Scroll.type": + /// Report the type of the scrolling component. + /// Track scrollable widget names to identify [_PagePosition] instances. + let type: string = args.get("type"); + this.recordTabSwitch(type); + break; + default: + result.notImplemented(); + break; + } + } catch (e) { + result.error("error", JSON.stringify(e), null); + } + } + + private decodeAppSwitcherDescription(encodedDescription: Map): AppSwitcherDescription { + let color: number = encodedDescription.get('color') as number; + let label: string = encodedDescription.get('label') as string; + return new AppSwitcherDescription(color, label); + } + + private decodeSystemUiOverlays(encodedSystemUiOverlay: string[]): SystemUiOverlay[] { + let overlays: SystemUiOverlay[] = []; + for (let i = 0; i < encodedSystemUiOverlay.length; i++) { + const encodedOverlay = encodedSystemUiOverlay[i]; + const overlay = this.platform.getSystemUiOverlayFromValue(encodedOverlay); + switch (overlay) { + case SystemUiOverlay.TOP_OVERLAYS: + overlays.push(SystemUiOverlay.TOP_OVERLAYS); + break; + case SystemUiOverlay.BOTTOM_OVERLAYS: + overlays.push(SystemUiOverlay.BOTTOM_OVERLAYS); + break; + } + } + return overlays; + } + + private decodeSystemUiMode(encodedSystemUiMode: string): SystemUiMode { + let mode: SystemUiMode = this.platform.getSystemUiModeFromValue(encodedSystemUiMode); + switch (mode) { + case SystemUiMode.LEAN_BACK: + return SystemUiMode.LEAN_BACK; + case SystemUiMode.IMMERSIVE: + return SystemUiMode.IMMERSIVE; + case SystemUiMode.IMMERSIVE_STICKY: + return SystemUiMode.IMMERSIVE_STICKY; + case SystemUiMode.EDGE_TO_EDGE: + default: + return SystemUiMode.EDGE_TO_EDGE; + } + } + + private decodeSystemChromeStyle(encodedStyle: Map | null): SystemChromeStyle { + let statusBarColor: number | null = null; + let statusBarIconBrightness: Brightness | null = null; + let systemStatusBarContrastEnforced: boolean | null = null; + let systemNavigationBarColor: number | null = null; + let systemNavigationBarIconBrightness: Brightness | null = null; + let systemNavigationBarDividerColor: number | null = null; + let systemNavigationBarContrastEnforced: boolean | null = null; + if (encodedStyle?.get('statusBarColor') != null) { + statusBarColor = encodedStyle.get('statusBarColor') as number; + } + if (encodedStyle?.get('statusBarIconBrightness') != null) { + statusBarIconBrightness = + this.platform.getBrightnessFromValue(encodedStyle.get('statusBarIconBrightness') as string); + } + if (encodedStyle?.get('systemStatusBarContrastEnforced') != null) { + systemStatusBarContrastEnforced = encodedStyle.get('systemStatusBarContrastEnforced') as boolean; + } + if (encodedStyle?.get('systemNavigationBarColor') != null) { + systemNavigationBarColor = encodedStyle.get('systemNavigationBarColor') as number; + } + if (encodedStyle?.get('systemNavigationBarIconBrightness') != null) { + systemNavigationBarIconBrightness = + this.platform.getBrightnessFromValue(encodedStyle.get('systemNavigationBarIconBrightness') as string); + } + if (encodedStyle?.get('systemNavigationBarDividerColor') != null) { + systemNavigationBarDividerColor = encodedStyle.get('systemNavigationBarDividerColor') as number; + } + if (encodedStyle?.get('systemNavigationBarContrastEnforced') != null) { + systemNavigationBarContrastEnforced = encodedStyle.get('systemNavigationBarContrastEnforced') as boolean; + } + return new SystemChromeStyle( + statusBarColor, + statusBarIconBrightness, + systemStatusBarContrastEnforced, + systemNavigationBarColor, + systemNavigationBarIconBrightness, + systemNavigationBarDividerColor, + systemNavigationBarContrastEnforced + ); + } + + private recordScrollActivity(scrollActivity: string) { + let activityType = this.platform.getScrollActivityFromValue(scrollActivity); + switch(activityType) { + case ScrollActivity.START: + hiTraceMeter.startTrace('flutter::APP_LIST_FLING', 0); + this.platform.flutterNapi.SetAnimationStatus(AnimationStatus.SCROLL_START); + break; + case ScrollActivity.END: + hiTraceMeter.finishTrace('flutter::APP_LIST_FLING', 0); + if (this.scrollType !== null) { + this.scrollType = null; + hiTraceMeter.finishTrace('flutter::TABVIEW_SWITCH', 0); + } + this.platform.flutterNapi.SetAnimationStatus(AnimationStatus.SCROLL_END); + break; + } + } + + private recordTabSwitch(type: string) { + if (type == '_PagePosition') { + hiTraceMeter.startTrace('flutter::TABVIEW_SWITCH', 0); + this.scrollType = type; + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/PlatformViewsChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/PlatformViewsChannel.ets new file mode 100644 index 0000000..2f761a8 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/PlatformViewsChannel.ets @@ -0,0 +1,679 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformViewsChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import Any from '../../../plugin/common/Any'; + +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import StandardMethodCodec from '../../../plugin/common/StandardMethodCodec'; +import { ByteBuffer } from '../../../util/ByteBuffer'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; + +const TAG = "PlatformViewsChannel"; +const NON_TEXTURE_FALLBACK = -2; + +/** + * System channel that sends 2-way communication between Flutter and OpenHarmony to facilitate + * embedding of OpenHarmony Views within a Flutter application. + * + * Implement PlatformViewsHandler and register it via setPlatformViewsHandler() to implement + * the OpenHarmony side of this channel. + */ +export default class PlatformViewsChannel { + private channel: MethodChannel; + private handler: PlatformViewsHandler | null = null; + private parsingHandler = new ParsingCallback(); + + /** + * Constructs a PlatformViewsChannel that connects OpenHarmony to the Dart + * code running in dartExecutor. + * + * The given dartExecutor is permitted to be idle or executing code. + * + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new MethodChannel(dartExecutor, "flutter/platform_views", StandardMethodCodec.INSTANCE); + this.parsingHandler.platformChannel = this; + this.channel.setMethodCallHandler(this.parsingHandler); + } + + /** + * Sets the PlatformViewsHandler which receives all events and requests that are parsed + * from the underlying platform views channel. + * @param handler - The PlatformViewsHandler instance, or null to remove + */ + public setPlatformViewsHandler(handler: PlatformViewsHandler | null): void { + this.handler = handler; + this.parsingHandler.handler = handler; + } + + /** + * Notifies Flutter that a platform view has gained focus. + * @param viewId - The ID of the platform view that gained focus + */ + public invokeViewFocused(viewId: number): void { + if (this.channel == null) { + return; + } + this.channel.invokeMethod("viewFocused", viewId); + } + + /** + * Handles platform view creation requests. + * @param call - The method call containing creation parameters + * @param result - The result callback to send a response + */ + create(call: MethodCall, result: MethodResult): void { + const createArgs: Map = call.args; + const usesPlatformViewLayer: boolean = createArgs.has("hybrid") && createArgs.get("hybrid") as boolean; + const additionalParams: ByteBuffer = createArgs.has("params") ? createArgs.get("params") : null; + + let direction: Direction = Direction.Ltr; + if (createArgs.get("direction") == 0) { + direction = Direction.Ltr; + } else if (createArgs.get("direction") == 1) { + direction = Direction.Rtl; + } + + try { + if (usesPlatformViewLayer) { + const request: PlatformViewCreationRequest = new PlatformViewCreationRequest( + createArgs.get("id"), + createArgs.get("viewType"), + 0, + 0, + 0, + 0, + direction, + additionalParams, + RequestedDisplayMode.HYBRID_ONLY + ); + this.handler?.createForPlatformViewLayer(request); + result.success(null); + } else { + const hybridFallback: boolean = createArgs.has("hybridFallback") && createArgs.get("hybridFallback"); + const displayMode: RequestedDisplayMode = + hybridFallback ? RequestedDisplayMode.TEXTURE_WITH_HYBRID_FALLBACK + : RequestedDisplayMode.TEXTURE_WITH_VIRTUAL_FALLBACK; + const request: PlatformViewCreationRequest = new PlatformViewCreationRequest( + createArgs.get("id"), + createArgs.get("viewType"), + createArgs.has("top") ? createArgs.get("top") : 0.0, + createArgs.has("left") ? createArgs.get("left") : 0.0, + createArgs.get("width"), + createArgs.get("height"), + direction, + additionalParams, + displayMode + ); + + Log.i(TAG, `Create texture param id:${request.viewId}, + type:${request.viewType}, + w:${request.logicalWidth}, + h:${request.logicalHeight}, + l:${request.logicalLeft}, + t:${request.logicalTop}, + d:${request.direction}`); + + const textureId = this.handler?.createForTextureLayer(request); + if (textureId == NON_TEXTURE_FALLBACK) { + if (!hybridFallback) { + throw new Error( + "Platform view attempted to fall back to hybrid mode when not requested."); + } + + // A fallback to hybrid mode is indicated with a null texture ID. + result.success(null); + } else { + result.success(textureId); + } + } + } catch (err) { + Log.e(TAG, "create failed" + err); + result.error("error", err, null); + } + } + + /** + * Handles platform view disposal requests. + * @param call - The method call containing the view ID to dispose + * @param result - The result callback to send a response + */ + dispose(call: MethodCall, result: MethodResult): void { + const disposeArgs: Map = call.args; + const viewId: number = disposeArgs.get("id"); + try { + this.handler?.dispose(viewId); + result.success(null); + } catch (err) { + Log.e(TAG, "dispose failed", err); + result.error("error", err, null); + } + } + + /** + * Handles platform view resize requests. + * @param call - The method call containing resize parameters + * @param result - The result callback to send a response + */ + resize(call: MethodCall, result: MethodResult): void { + const resizeArgs: Map = call.args; + const resizeRequest: PlatformViewResizeRequest = new PlatformViewResizeRequest( + resizeArgs.get("id"), + resizeArgs.get("width"), + resizeArgs.get("height") + ); + try { + let resizeCallback = new ResizeCallback(); + resizeCallback.result = result; + this.handler?.resize(resizeRequest, resizeCallback); + } catch (err) { + Log.e(TAG, "resize failed", err); + result.error("error", err, null); + } + } + + /** + * Handles platform view offset change requests. + * @param call - The method call containing offset parameters + * @param result - The result callback to send a response + */ + offset(call: MethodCall, result: MethodResult): void { + const offsetArgs: Map = call.args; + try { + this.handler?.offset( + offsetArgs.get("id"), + offsetArgs.get("top"), + offsetArgs.get("left")); + result.success(null); + } catch (err) { + Log.e(TAG, "offset failed", err); + result.error("error", err, null); + } + } + + /** + * Handles touch events on platform views. + * @param call - The method call containing touch event data + * @param result - The result callback to send a response + */ + touch(call: MethodCall, result: MethodResult): void { + const args: Array = call.args; + let index = 0; + const touch: PlatformViewTouch = new PlatformViewTouch( + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index++], + args[index] + ); + + try { + this.handler?.onTouch(touch); + result.success(null); + } catch (err) { + Log.e(TAG, "offset failed", err); + result.error("error", err, null); + } + } + + /** + * Handles platform view direction change requests. + * @param call - The method call containing direction parameters + * @param result - The result callback to send a response + */ + setDirection(call: MethodCall, result: MethodResult): void { + const setDirectionArgs: Map = call.args; + const newDirectionViewId: number = setDirectionArgs.get("id"); + const direction: number = setDirectionArgs.get("direction"); + + try { + this.handler?.setDirection(newDirectionViewId, direction); + result.success(null); + } catch (err) { + Log.e(TAG, "setDirection failed", err); + result.error("error", err, null); + } + } + + /** + * Handles platform view focus clearing requests. + * @param call - The method call containing the view ID + * @param result - The result callback to send a response + */ + clearFocus(call: MethodCall, result: MethodResult): void { + const viewId: number = call.args; + try { + this.handler?.clearFocus(viewId); + result.success(null); + } catch (err) { + Log.e(TAG, "clearFocus failed", err); + result.error("error", err, null); + } + } + + /** + * Handles requests to synchronize to native view hierarchy. + * @param call - The method call containing synchronization parameters + * @param result - The result callback to send a response + */ + synchronizeToNativeViewHierarchy(call: MethodCall, result: MethodResult): void { + const yes: boolean = call.args; + try { + this.handler?.synchronizeToNativeViewHierarchy(yes); + result.success(null); + } catch (err) { + Log.e(TAG, "synchronizeToNativeViewHierarchy failed", err); + result.error("error", err, null); + } + } + + /** + * Handles hover events on platform views. + * @param call - The method call containing the view ID + * @param result - The result callback to send a response + */ + hover(call: MethodCall, result: MethodResult) { + const viewId: number = call.args; + try { + this.handler?.hover(viewId); + result.success(null); + } catch (err) { + Log.e(TAG, "hover failed", err); + result.error("error", err, null); + } + } +} + +/** + * Handler that receives platform view messages sent from Flutter to OpenHarmony through a given + * PlatformViewsChannel. + * + * To register a PlatformViewsHandler with a PlatformViewsChannel, + * see PlatformViewsChannel.setPlatformViewsHandler. + */ +export interface PlatformViewsHandler { + /** + * The Flutter application would like to display a new OpenHarmony View, i.e., platform view. + * + * The OpenHarmony View is added to the view hierarchy. This view is rendered in the Flutter + * framework by a PlatformViewLayer. + * + * @param request - The metadata sent from the framework + */ + createForPlatformViewLayer(request: PlatformViewCreationRequest): void; + + /** + * The Flutter application would like to display a new OpenHarmony View, i.e., platform view. + * + * The OpenHarmony View is added to the view hierarchy. This view is rendered in the Flutter + * framework by a TextureLayer. + * + * The ID returned by createForTextureLayer to indicate that the requested texture mode + * was not available and the view creation fell back to PlatformViewLayer mode. + * This can only be returned if the PlatformViewCreationRequest sets + * TEXTURE_WITH_HYBRID_FALLBACK as the requested display mode. + * + * @param request - The metadata sent from the framework + * @returns The texture ID, or NON_TEXTURE_FALLBACK if falling back to hybrid mode + */ + createForTextureLayer(request: PlatformViewCreationRequest): number; + + /** + * The Flutter application would like to dispose of an existing OpenHarmony View. + * @param viewId - The ID of the platform view to dispose + */ + dispose(viewId: number): void; + + /** + * The Flutter application would like to resize an existing OpenHarmony View. + * + * @param request - The request to resize the platform view + * @param onComplete - Once the resize is completed, this is the handler to notify the size of the + * platform view buffer + */ + resize(request: PlatformViewResizeRequest, onComplete: PlatformViewBufferResized): void; + + /** + * The Flutter application would like to change the offset of an existing OpenHarmony View. + * @param viewId - The ID of the platform view + * @param top - The new top offset + * @param left - The new left offset + */ + offset(viewId: number, top: number, left: number): void; + + /** + * The user touched a platform view within Flutter. + * + * Touch data is reported in the touch parameter. + * @param touch - The touch event data + */ + onTouch(touch: PlatformViewTouch): void; + + /** + * The Flutter application would like to change the layout direction of an existing OpenHarmony + * View, i.e., platform view. + * @param viewId - The ID of the platform view + * @param direction - The new layout direction + */ + setDirection(viewId: number, direction: Direction): void; + + /** + * Clears the focus from the platform view with a given id if it is currently focused. + * @param viewId - The ID of the platform view + */ + clearFocus(viewId: number): void; + + /** + * Whether the render surface of FlutterView should be converted to a + * FlutterImageView when a PlatformView is added. + * + * This is done to synchronize the rendering of the PlatformView and the FlutterView. Defaults + * to true. + * @param yes - Whether to synchronize to native view hierarchy + */ + synchronizeToNativeViewHierarchy(yes: boolean): void; + + /** + * Handles hover events on a platform view. + * @param viewId - The ID of the platform view + */ + hover(viewId: number): void; +} + +/** Platform view display modes that can be requested at creation time. */ +enum RequestedDisplayMode { + /** Use Texture Layer if possible, falling back to Virtual Display if not. */ + TEXTURE_WITH_VIRTUAL_FALLBACK, + /** Use Texture Layer if possible, falling back to Hybrid Composition if not. */ + TEXTURE_WITH_HYBRID_FALLBACK, + /** Use Hybrid Composition in all cases. */ + HYBRID_ONLY, +} + +/** Request sent from Flutter to create a new platform view. */ +export class PlatformViewCreationRequest { + /** The ID of the platform view as seen by the Flutter side. */ + public viewId: number; + /** The type of view to create for this platform view. */ + public viewType: string; + /** The density independent width to display the platform view. */ + public logicalWidth: number; + /** The density independent height to display the platform view. */ + public logicalHeight: number; + /** The density independent top position to display the platform view. */ + public logicalTop: number; + /** The density independent left position to display the platform view. */ + public logicalLeft: number; + /** The layout direction of the new platform view. */ + public direction: Direction; + /** The requested display mode for the platform view. */ + public displayMode: RequestedDisplayMode; + /** Custom parameters that are unique to the desired platform view. */ + public params: ByteBuffer; + + /** + * Constructs a new PlatformViewCreationRequest instance. + * @param viewId - The ID of the platform view + * @param viewType - The type of view to create + * @param logicalTop - The density-independent top position + * @param logicalLeft - The density-independent left position + * @param logicalWidth - The density-independent width + * @param logicalHeight - The density-independent height + * @param direction - The layout direction + * @param params - Custom parameters for the view + * @param displayMode - The requested display mode (optional) + */ + constructor(viewId: number, viewType: string, logicalTop: number, logicalLeft: number, logicalWidth: number, + logicalHeight: number, direction: Direction, params: ByteBuffer, displayMode?: RequestedDisplayMode) { + this.viewId = viewId; + this.viewType = viewType; + this.logicalTop = logicalTop; + this.logicalLeft = logicalLeft; + this.logicalWidth = logicalWidth; + this.logicalHeight = logicalHeight; + this.direction = direction; + this.displayMode = displayMode ? displayMode : RequestedDisplayMode.TEXTURE_WITH_VIRTUAL_FALLBACK; + this.params = params; + } +} + +/** Request sent from Flutter to resize a platform view. */ +export class PlatformViewResizeRequest { + /** The ID of the platform view as seen by the Flutter side. */ + public viewId: number; + /** The new density independent width to display the platform view. */ + public newLogicalWidth: number; + /** The new density independent height to display the platform view. */ + public newLogicalHeight: number; + + /** + * Constructs a new PlatformViewResizeRequest instance. + * @param viewId - The ID of the platform view + * @param newLogicalWidth - The new density-independent width + * @param newLogicalHeight - The new density-independent height + */ + constructor(viewId: number, newLogicalWidth: number, newLogicalHeight: number) { + this.viewId = viewId; + this.newLogicalWidth = newLogicalWidth; + this.newLogicalHeight = newLogicalHeight; + } +} + +/** The platform view buffer size. */ +export class PlatformViewBufferSize { + /** The width of the screen buffer. */ + public width: number; + /** The height of the screen buffer. */ + public height: number; + + /** + * Constructs a new PlatformViewBufferSize instance. + * @param width - The width of the buffer + * @param height - The height of the buffer + */ + constructor(width: number, height: number) { + this.width = width; + this.height = height; + } +} + +/** Allows to notify when a platform view buffer has been resized. */ +export abstract class PlatformViewBufferResized { + /** + * Called when the platform view buffer has been resized. + * @param bufferSize - The new buffer size + */ + abstract run(bufferSize: PlatformViewBufferSize): void; +} + +/** The state of a touch event in Flutter within a platform view. */ +export class PlatformViewTouch { + /** The ID of the platform view as seen by the Flutter side. */ + public viewId: number; + /** The amount of time that the touch has been pressed. */ + public downTime: number; + /** The time when the event occurred. */ + public eventTime: number; + /** The touch action type. */ + public action: number; + /** The number of pointers (e.g, fingers) involved in the touch event. */ + public pointerCount: number; + /** Properties for each pointer, encoded in a raw format. */ + public rawPointerPropertiesList: Any; + /** Coordinates for each pointer, encoded in a raw format. */ + public rawPointerCoords: Any; + /** The meta state indicating modifier keys pressed. */ + public metaState: number; + /** The button state indicating which buttons are pressed. */ + public buttonState: number; + /** Coordinate precision along the x-axis. */ + public xPrecision: number; + /** Coordinate precision along the y-axis. */ + public yPrecision: number; + /** The ID of the input device that generated the event. */ + public deviceId: number; + /** Edge flags indicating which screen edges were touched. */ + public edgeFlags: number; + /** The event source indicating the type of input device. */ + public source: number; + /** Event flags providing additional information about the event. */ + public flags: number; + /** The motion event ID for tracking the event. */ + public motionEventId: number; + + /** + * Constructs a new PlatformViewTouch instance. + * @param viewId - The ID of the platform view + * @param downTime - The time when the touch was pressed + * @param eventTime - The time of the event + * @param action - The touch action + * @param pointerCount - The number of pointers + * @param rawPointerPropertiesList - Raw pointer properties + * @param rawPointerCoords - Raw pointer coordinates + * @param metaState - The meta state + * @param buttonState - The button state + * @param xPrecision - X-axis coordinate precision + * @param yPrecision - Y-axis coordinate precision + * @param deviceId - The device ID + * @param edgeFlags - Edge flags + * @param source - The event source + * @param flags - Event flags + * @param motionEventId - The motion event ID + */ + constructor(viewId: number, + downTime: number, + eventTime: number, + action: number, + pointerCount: number, + rawPointerPropertiesList: Any, + rawPointerCoords: Any, + metaState: number, + buttonState: number, + xPrecision: number, + yPrecision: number, + deviceId: number, + edgeFlags: number, + source: number, + flags: number, + motionEventId: number) { + this.viewId = viewId; + this.downTime = downTime; + this.eventTime = eventTime; + this.action = action; + this.pointerCount = pointerCount; + this.rawPointerPropertiesList = rawPointerPropertiesList; + this.rawPointerCoords = rawPointerCoords; + this.metaState = metaState; + this.buttonState = buttonState; + this.xPrecision = xPrecision; + this.yPrecision = yPrecision; + this.deviceId = deviceId; + this.edgeFlags = edgeFlags; + this.source = source; + this.flags = flags; + this.motionEventId = motionEventId; + } +} + +/** + * Method call handler for parsing platform view requests. + */ +class ParsingCallback implements MethodCallHandler { + platformChannel: PlatformViewsChannel | null = null; + handler: PlatformViewsHandler | null = null; + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult) { + if (this.handler == null) { + return; + } + + Log.i(TAG, "Received '" + call.method + "' message."); + switch (call.method) { + case "create": { + this.platformChannel?.create(call, result); + break; + } + case "dispose": { + this.platformChannel?.dispose(call, result); + break; + } + case "resize": { + this.platformChannel?.resize(call, result); + break; + } + case "offset": { + this.platformChannel?.offset(call, result); + break; + } + case "touch": { + this.platformChannel?.touch(call, result); + break; + } + case "setDirection": { + this.platformChannel?.setDirection(call, result); + break; + } + case "clearFocus": { + this.platformChannel?.clearFocus(call, result); + break; + } + case "synchronizeToNativeViewHierarchy": { + this.platformChannel?.synchronizeToNativeViewHierarchy(call, result); + break; + } + case "hover": { + this.platformChannel?.hover(call, result); + break; + } + default: + result.notImplemented(); + } + } +} + +/** + * Callback for handling platform view resize completion. + */ +class ResizeCallback extends PlatformViewBufferResized { + result: MethodResult | null = null; + + /** + * Called when the resize is complete. + * @param bufferSize - The new buffer size + */ + run(bufferSize: PlatformViewBufferSize) { + if (bufferSize == null) { + this.result?.error("error", "Failed to resize the platform view", null); + } else { + const response: Map = new Map(); + response.set("width", bufferSize.width); + response.set("height", bufferSize.height); + this.result?.success(response); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/RestorationChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/RestorationChannel.ets new file mode 100644 index 0000000..b2978c6 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/RestorationChannel.ets @@ -0,0 +1,206 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on RestorationChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import Any from '../../../plugin/common/Any'; + +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import StandardMethodCodec from '../../../plugin/common/StandardMethodCodec'; +import Log from '../../../util/Log'; +import StringUtils from '../../../util/StringUtils'; +import DartExecutor from '../dart/DartExecutor'; + +/** + * System channel to exchange restoration data between framework and engine. + * + * The engine can obtain the current restoration data from the framework via this channel to + * store it on disk and - when the app is relaunched - provide the stored data back to the framework + * to recreate the original state of the app. + * + * The channel can be configured to delay responding to the framework's request for restoration + * data via waitForRestorationData until the engine-side has provided the data. This is + * useful when the engine is pre-warmed at a point in the application's life cycle where the + * restoration data is not available yet. For example, if the engine is pre-warmed as part of the + * Application before an Ability is created, this flag should be set to true because OpenHarmony will + * only provide the restoration data to the Ability during the onCreate callback. + * + * The current restoration data provided by the framework can be read via getRestorationData. + */ +export default class RestorationChannel { + private static TAG = "RestorationChannel"; + private static CHANNEL_NAME = "flutter/restoration"; + /** + * Whether the channel delays responding to the framework's initial request for restoration data + * until setRestorationData has been called. + * + * If the engine never calls setRestorationData this flag must be set to false. If set + * to true, the engine must call setRestorationData either with the actual restoration + * data as argument or null if it turns out that there is no restoration data. + * + * If the response to the framework's request for restoration data is not delayed until the + * data has been set via setRestorationData, the framework may intermittently initialize + * itself to default values until the restoration data has been made available. Setting this flag + * to true avoids that extra work. + */ + public waitForRestorationData: boolean = false; + /** Pending framework restoration channel request waiting for restoration data. */ + public pendingFrameworkRestorationChannelRequest: MethodResult | null = null; + /** Whether the engine has provided restoration data. */ + public engineHasProvidedData: boolean = false; + /** Whether the framework has requested restoration data. */ + public frameworkHasRequestedData: boolean = false; + private restorationData: Uint8Array; + private channel: MethodChannel | null = null; + private handler: MethodCallHandler; + + /** + * Constructs a new RestorationChannel instance. + * @param channelOrExecutor - Either a MethodChannel or DartExecutor instance + * @param waitForRestorationData - Whether to wait for restoration data before responding + */ + constructor(channelOrExecutor: MethodChannel | DartExecutor, waitForRestorationData: boolean) { + if (channelOrExecutor instanceof MethodChannel) { + this.channel = channelOrExecutor; + } else { + this.channel = + new MethodChannel(channelOrExecutor, RestorationChannel.CHANNEL_NAME, StandardMethodCodec.INSTANCE); + } + this.waitForRestorationData = waitForRestorationData; + this.restorationData = new Uint8Array(1).fill(0); + this.handler = new RestorationChannelMethodCallHandler(this); + this.channel.setMethodCallHandler(this.handler); + } + + /** + * Gets the most current restoration data that the framework has provided. + * @returns The restoration data as a Uint8Array + */ + getRestorationData(): Uint8Array { + return this.restorationData; + } + + /** + * Sets the restoration data without sending it to the framework. + * @param data - The restoration data to set + */ + setRestorationDataOnly(data: Uint8Array) { + this.restorationData = data; + } + + /** + * Sets the restoration data from which the framework will restore its state. + * @param data - The restoration data to set + */ + setRestorationData(data: Uint8Array) { + this.engineHasProvidedData = true; + if (this.pendingFrameworkRestorationChannelRequest != null) { + // If their is a pending request from the framework, answer it. + this.pendingFrameworkRestorationChannelRequest.success(RestorationChannelMethodCallHandler.packageData(data)); + this.pendingFrameworkRestorationChannelRequest = null; + this.restorationData = data; + } else if (this.frameworkHasRequestedData) { + // If the framework has previously received the engine's restoration data, push the new data + // directly to it. This case can happen when "waitForRestorationData" is false and the + // framework retrieved the restoration state before it was set via this method. + // Experimentally, this can also be used to restore a previously used engine to another state, + // e.g. when the engine is attached to a new activity. + this.channel?.invokeMethod( + "push", RestorationChannelMethodCallHandler.packageData(data), { + success: (result: Any): void => { + this.restorationData = data; + }, + + error: (errorCode: string, errorMessage: string, errorDetails: Any): void => { + Log.e( + RestorationChannel.TAG, + "Error " + errorCode + " while sending restoration data to framework: " + errorMessage + ); + }, + + notImplemented: (): void => { + // do nothing + } + }) + } else { + // Otherwise, just cache the data until the framework asks for it. + this.restorationData = data; + } + } + + /** + * Clears the current restoration data. + * + * This should be called just prior to a hot restart. Otherwise, after the hot restart the + * state prior to the hot restart will get restored. + */ + clearData() { + this.restorationData = new Uint8Array(1).fill(0); + } +} + +/** + * Method call handler for the restoration channel. + */ +class RestorationChannelMethodCallHandler implements MethodCallHandler { + private channel: RestorationChannel; + + /** + * Constructs a new RestorationChannelMethodCallHandler instance. + * @param channel - The RestorationChannel instance + */ + constructor(channel: RestorationChannel) { + this.channel = channel; + } + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult): void { + const method = call.method; + const args: Any = call.args; + switch (method) { + case "put": { + this.channel.setRestorationDataOnly(args); + result.success(null); + break; + } + case "get": { + this.channel.frameworkHasRequestedData = true; + if (this.channel.engineHasProvidedData || !this.channel.waitForRestorationData) { + result.success(RestorationChannelMethodCallHandler.packageData(this.channel.getRestorationData())); + // Do not delete the restoration data on the engine side after sending it to the + // framework. We may need to hand this data back to the operating system if the + // framework never modifies the data (and thus doesn't send us any + // data back). + } else { + this.channel.pendingFrameworkRestorationChannelRequest = result; + } + break; + } + default: { + result.notImplemented(); + break; + } + } + } + + /** + * Packages restoration data into a message format. + * @param data - The restoration data to package + * @returns A map containing the packaged restoration data + */ + static packageData(data: Uint8Array): Map { + const packaged: Map = new Map(); + packaged.set("enabled", true); + packaged.set("data", data); + return packaged; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SensitiveContentChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SensitiveContentChannel.ets new file mode 100644 index 0000000..55241d0 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SensitiveContentChannel.ets @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + +import Any from '../../../plugin/common/Any'; +import DartExecutor from '../dart/DartExecutor'; +import StandardMethodCodec from '../../../plugin/common/StandardMethodCodec'; +import Log from '../../../util/Log'; +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; + +const TAG = "SensitiveContentChannel"; + +export const SENSITIVE_CONTENT_SENSITIVITY = 1; +export const NOT_SENSITIVE_CONTENT_SENSITIVITY = 2; + +export default class SensitiveContentChannel implements MethodCallHandler { + private static CHANNEL_NAME = "flutter/sensitivecontent"; + private channel: MethodChannel; + private sensitiveContentMethodHandler: SensitiveContentMethodHandler | null = null; + + constructor(dartExecutor: DartExecutor) { + this.channel = new MethodChannel(dartExecutor, SensitiveContentChannel.CHANNEL_NAME, StandardMethodCodec.INSTANCE); + this.channel.setMethodCallHandler(this); + } + + onMethodCall(call: MethodCall, result: MethodResult): void { + if (this.sensitiveContentMethodHandler == null) { + // No SensitiveContentChannel registered, call not forwarded to sensitive content API. + return; + } + let method: string = call.method; + Log.d(TAG, "Received '" + method + "' message."); + switch (method) { + case "SensitiveContent.setContentSensitivity": + this.setContentSensitivity(call, result); + break; + case "SensitiveContent.getContentSensitivity": + this.getContentSensitivity(call, result); + break; + case "SensitiveContent.isSupported": + this.isSupported(call, result); + break; + default: + Log.d(TAG, "Method " + method + " is not implemented for the SensitiveContentChannel."); + result.notImplemented(); + break; + } + } + + private setContentSensitivity(call: MethodCall, result: MethodResult): void { + try { + const contentSensitivityLevel: number = call.args as number; + const deserializedValue = this.deserializeContentSensitivity(contentSensitivityLevel); + this.sensitiveContentMethodHandler!.setContentSensitivity(deserializedValue); + result.success(null); + } catch (error) { + result.error("error", (error as Error).message, null); + } + } + + private getContentSensitivity(call: MethodCall, result: MethodResult): void { + try { + const currentContentSensitivity: number = this.sensitiveContentMethodHandler!.getContentSensitivity(); + const serializedValue = this.serializeContentSensitivity(currentContentSensitivity); + result.success(serializedValue); + } catch (error) { + result.error("error", (error as Error).message, null); + } + } + + private isSupported(call: MethodCall, result: MethodResult): void { + const isSupported: boolean = this.sensitiveContentMethodHandler!.isSupported(); + result.success(isSupported); + } + + /** + * Deserializes Flutter content sensitivity index to native value. + */ + private deserializeContentSensitivity(contentSensitivityIndex: number): number { + switch (contentSensitivityIndex) { + case NOT_SENSITIVE_CONTENT_SENSITIVITY: + return NOT_SENSITIVE_CONTENT_SENSITIVITY; + case SENSITIVE_CONTENT_SENSITIVITY: + return SENSITIVE_CONTENT_SENSITIVITY; + default: + throw new Error( + "contentSensitivityIndex " + contentSensitivityIndex + " not known to the SensitiveContentChannel." + ); + } + } + + /** + * Serializes native content sensitivity value to Flutter index. + */ + private serializeContentSensitivity(contentSensitivityValue: number): number { + switch (contentSensitivityValue) { + case NOT_SENSITIVE_CONTENT_SENSITIVITY: + return NOT_SENSITIVE_CONTENT_SENSITIVITY; + case SENSITIVE_CONTENT_SENSITIVITY: + return SENSITIVE_CONTENT_SENSITIVITY; + default: + return NOT_SENSITIVE_CONTENT_SENSITIVITY; + } + } + + /** + * Sets the {@link SensitiveContentMethodHandler} which receives all requests to get and set a + * particular content sensitivity level sent through this channel. + */ + setSensitiveContentMethodHandler(sensitiveContentMethodHandler: SensitiveContentMethodHandler | null): void { + this.sensitiveContentMethodHandler = sensitiveContentMethodHandler; + } +} + +export interface SensitiveContentMethodHandler { + /** + * Requests that a native Flutter OpenHarmony View sets its content sensitivity level to + * {@code requestedContentSensitivity}. + */ + setContentSensitivity(requestedContentSensitivity: number): void; + + /** + * Returns the current content sensitivity level of a Flutter OpenHarmony View. + */ + getContentSensitivity(): number; + + /** + * Returns whether or not setting/getting content sensitivity via OpenHarmony APIs is supported on + * the device. + */ + isSupported(): boolean; +} + diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SettingsChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SettingsChannel.ets new file mode 100644 index 0000000..bd84d1a --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SettingsChannel.ets @@ -0,0 +1,145 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on SettingsChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import BasicMessageChannel from '../../../plugin/common/BasicMessageChannel'; +import JSONMessageCodec from '../../../plugin/common/JSONMessageCodec'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; + +/** + * Platform brightness modes. + */ +export enum PlatformBrightness { + LIGHT = "light", + DARK = "dark" +} + +const TAG = "SettingsChannel"; +const TEXT_SCALE_FACTOR = "textScaleFactor"; +const NATIVE_SPELL_CHECK_SERVICE_DEFINED = "nativeSpellCheckServiceDefined"; +const BRIEFLY_SHOW_PASSWORD = "brieflyShowPassword"; +const ALWAYS_USE_24_HOUR_FORMAT = "alwaysUse24HourFormat"; +const PLATFORM_BRIGHTNESS = "platformBrightness"; + +/** + * Channel for sending system settings to Flutter. + * This channel manages settings such as text scale factor, platform brightness, and other system preferences. + */ +export default class SettingsChannel { + private static CHANNEL_NAME = "flutter/settings"; + private channel: BasicMessageChannel; + + /** + * Constructs a new SettingsChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = + new BasicMessageChannel(dartExecutor, SettingsChannel.CHANNEL_NAME, JSONMessageCodec.INSTANCE); + } + + /** + * Starts building a settings message. + * @returns A MessageBuilder instance for constructing the settings message + */ + startMessage(): MessageBuilder { + return new MessageBuilder(this.channel); + } +} + +/** + * Builder for constructing settings messages to send to Flutter. + */ +class MessageBuilder { + private channel: BasicMessageChannel; + private settingsMessage: Map = new Map([ + [TEXT_SCALE_FACTOR, 1.0], + [NATIVE_SPELL_CHECK_SERVICE_DEFINED, false], + [BRIEFLY_SHOW_PASSWORD, false], + [ALWAYS_USE_24_HOUR_FORMAT, false], + [PLATFORM_BRIGHTNESS, PlatformBrightness.LIGHT] + ]); + + /** + * Constructs a new MessageBuilder instance. + * @param channel - The BasicMessageChannel to send messages through + */ + constructor(channel: BasicMessageChannel) { + this.channel = channel; + } + + /** + * Sets the text scale factor. + * @param textScaleFactor - The text scale factor value + * @returns This MessageBuilder instance for method chaining + */ + setTextScaleFactor(textScaleFactor: Number): MessageBuilder { + this.settingsMessage.set(TEXT_SCALE_FACTOR, textScaleFactor); + return this; + } + + /** + * Sets whether native spell check service is defined. + * @param nativeSpellCheckServiceDefined - Whether the service is defined + * @returns This MessageBuilder instance for method chaining + */ + setNativeSpellCheckServiceDefined(nativeSpellCheckServiceDefined: boolean): MessageBuilder { + this.settingsMessage.set(NATIVE_SPELL_CHECK_SERVICE_DEFINED, nativeSpellCheckServiceDefined); + return this; + } + + /** + * Sets whether to briefly show password. + * @param brieflyShowPassword - Whether to briefly show password + * @returns This MessageBuilder instance for method chaining + */ + setBrieflyShowPassword(brieflyShowPassword: boolean): MessageBuilder { + this.settingsMessage.set(BRIEFLY_SHOW_PASSWORD, brieflyShowPassword); + return this; + } + + /** + * Sets whether to always use 24-hour format. + * @param alwaysUse24HourFormat - Whether to always use 24-hour format + * @returns This MessageBuilder instance for method chaining + */ + setAlwaysUse24HourFormat(alwaysUse24HourFormat: boolean): MessageBuilder { + this.settingsMessage.set(ALWAYS_USE_24_HOUR_FORMAT, alwaysUse24HourFormat); + return this; + } + + /** + * Sets the platform brightness. + * @param platformBrightness - The platform brightness mode + * @returns This MessageBuilder instance for method chaining + */ + setPlatformBrightness(platformBrightness: PlatformBrightness): MessageBuilder { + this.settingsMessage.set(PLATFORM_BRIGHTNESS, platformBrightness); + return this; + } + + /** + * Sends the constructed settings message to Flutter. + */ + send(): void { + Log.i(TAG, "Sending message: " + + TEXT_SCALE_FACTOR + " : " + + this.settingsMessage.get(TEXT_SCALE_FACTOR) + + ", " + NATIVE_SPELL_CHECK_SERVICE_DEFINED + " : " + + this.settingsMessage.get(NATIVE_SPELL_CHECK_SERVICE_DEFINED) + + ", " + BRIEFLY_SHOW_PASSWORD + " : " + + this.settingsMessage.get(BRIEFLY_SHOW_PASSWORD) + + ", " + ALWAYS_USE_24_HOUR_FORMAT + " : " + + this.settingsMessage.get(ALWAYS_USE_24_HOUR_FORMAT) + + ", " + PLATFORM_BRIGHTNESS + " : " + + this.settingsMessage.get(PLATFORM_BRIGHTNESS)); + this.channel.send(this.settingsMessage) + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/StatusBarClickChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/StatusBarClickChannel.ets new file mode 100644 index 0000000..4cef4dc --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/StatusBarClickChannel.ets @@ -0,0 +1,21 @@ +/* +* Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_HW file. +*/ + +import DartExecutor from '../dart/DartExecutor'; +import { BinaryMessenger } from '../../../plugin/common/BinaryMessenger'; +import MethodChannel from '../../../plugin/common/MethodChannel'; + +export default class StatusBarClickChannel { + private static CHANNEL_NAME = "flutter/statusBarClick"; + private channel: MethodChannel; + + constructor(binaryMessenger: BinaryMessenger) { + this.channel = new MethodChannel(binaryMessenger, StatusBarClickChannel.CHANNEL_NAME); + } + sendClick() { + this.channel.invokeMethod('onClick', null); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SystemChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SystemChannel.ets new file mode 100644 index 0000000..c600c7d --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/SystemChannel.ets @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on SystemChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import BasicMessageChannel from '../../../plugin/common/BasicMessageChannel'; +import Any from '../../../plugin/common/Any'; +import JSONMessageCodec from '../../../plugin/common/JSONMessageCodec'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; + +const TAG: string = "SystemChannel"; + +/** + * System channel for communicating system-level events between OpenHarmony and Flutter. + * This channel handles events such as memory pressure warnings. + */ +export default class SystemChannel { + /** The BasicMessageChannel for sending system-level messages to Flutter. */ + public channel: BasicMessageChannel; + + /** + * Constructs a new SystemChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new BasicMessageChannel(dartExecutor, "flutter/system", JSONMessageCodec.INSTANCE); + } + + /** + * Sends a memory pressure warning to the Flutter framework. + */ + public sendMemoryPressureWarning(): void { + Log.i(TAG, "Sending memory pressure warning to Flutter"); + let message: Map = new Map().set("type", "memoryPressure"); + this.channel.send(message); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/TestChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/TestChannel.ets new file mode 100644 index 0000000..d9c1777 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/TestChannel.ets @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import BasicMessageChannel, { MessageHandler, Reply } from '../../../plugin/common/BasicMessageChannel'; +import JSONMessageCodec from '../../../plugin/common/JSONMessageCodec'; +import DartExecutor from '../dart/DartExecutor'; +import Log from '../../../util/Log'; + +const TAG = "TestChannel" + +/** + * Test channel for Flutter testing purposes. + * This channel provides a simple echo mechanism for testing message passing. + */ +export default class TestChannel { + private channel: BasicMessageChannel; + + /** + * Constructs a new TestChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new BasicMessageChannel(dartExecutor, "flutter/test", JSONMessageCodec.INSTANCE); + let callback = new MessageCallback(); + this.channel.setMessageHandler(callback); + } +} + +/** + * Message callback handler for the test channel. + */ +class MessageCallback implements MessageHandler { + /** + * Handles incoming messages and echoes them back. + * @param message - The message received from Dart + * @param reply - The reply callback to send a response + */ + onMessage(message: string, reply: Reply) { + Log.d(TAG, "receive msg = " + message); + reply.reply("收到消息啦:" + message); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/TextInputChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/TextInputChannel.ets new file mode 100644 index 0000000..89948f5 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/systemchannels/TextInputChannel.ets @@ -0,0 +1,810 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on TextInputChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import JSONMethodCodec from '../../../plugin/common/JSONMethodCodec'; +import MethodCall from '../../../plugin/common/MethodCall'; +import MethodChannel, { MethodCallHandler, MethodResult } from '../../../plugin/common/MethodChannel'; +import TextInputPlugin from '../../../plugin/editing/TextInputPlugin'; +import Log from '../../../util/Log'; +import DartExecutor from '../dart/DartExecutor'; +import inputMethod from '@ohos.inputMethod'; +import ArrayList from '@ohos.util.ArrayList'; +import { TextEditingDelta, TextEditingDeltaJson } from '../../../plugin/editing/TextEditingDelta'; +import Any from '../../../plugin/common/Any'; +import { display } from '@kit.ArkUI' +import { window } from '@kit.ArkUI'; +import { BusinessError, print } from '@kit.BasicServicesKit'; +import { PointerDeviceKind } from '../../ohos/OhosTouchProcessor'; + +const TAG = "TextInputChannel"; +/// 规避换行标识无法显示问题,api修改后再删除 +const NEWLINE_KEY_TYPE: number = 8; + +/** + * TextInputChannel is a platform channel between OpenHarmony and Flutter that is used to + * communicate information about the user's text input. + * + * When the user presses an action button like "done" or "next", that action is sent from + * OpenHarmony to Flutter through this TextInputChannel. + * + * When an input system in the Flutter app wants to show the keyboard, or hide it, or configure + * editing state, etc. a message is sent from Flutter to OpenHarmony through this TextInputChannel. + * + * TextInputChannel comes with a default MethodChannel.MethodCallHandler that parses incoming + * messages from Flutter. Implement TextInputMethodHandler and register it via + * setTextInputMethodHandler() to respond to standard Flutter text input messages. + */ +export default class TextInputChannel { + private static CHANNEL_NAME = "flutter/textinput"; + /** The MethodChannel for text input communication with Flutter. */ + public channel: MethodChannel; + /** The text input method handler for processing text input requests, or null if not set. */ + textInputMethodHandler: TextInputMethodHandler | null = null; + private TextInputCallback: TextInputCallback | null = null; + + /** + * Constructs a new TextInputChannel instance. + * @param dartExecutor - The DartExecutor for sending messages to Dart + */ + constructor(dartExecutor: DartExecutor) { + this.channel = new MethodChannel(dartExecutor, TextInputChannel.CHANNEL_NAME, JSONMethodCodec.INSTANCE); + } + + /** + * Sets the text input method handler. + * @param textInputMethodHandler - The TextInputMethodHandler instance, or null to remove + */ + setTextInputMethodHandler(textInputMethodHandler: TextInputMethodHandler | null): void { + this.textInputMethodHandler = textInputMethodHandler; + this.TextInputCallback = this.textInputMethodHandler == null + ? null : new TextInputCallback(this.textInputMethodHandler); + this.channel.setMethodCallHandler(this.TextInputCallback); + } + + /** + * Requests the existing input state from Flutter. + */ + requestExistingInputState(): void { + this.channel.invokeMethod("TextInputClient.requestExistingInputState", null); + } + + /** + * Creates an editing state JSON object. + * @param text - The text content + * @param selectionStart - The start of the selection + * @param selectionEnd - The end of the selection + * @param composingStart - The start of the composing region + * @param composingEnd - The end of the composing region + * @returns The editing state object + */ + createEditingStateJSON(text: string, + selectionStart: number, + selectionEnd: number, + composingStart: number, + composingEnd: number): EditingState { + let state: EditingState = { + text: text, + selectionBase: selectionStart, + selectionExtent: selectionEnd, + composingBase: composingStart, + composingExtent: composingEnd + }; + return state; + } + + /** + * Creates an editing delta JSON object from a batch of deltas. + * @param batchDeltas - Array list of text editing deltas + * @returns The editing delta object + */ + createEditingDeltaJSON(batchDeltas: ArrayList): EditingDelta { + let deltas: TextEditingDeltaJson[] = []; + batchDeltas.forEach((val, idx, array) => { + deltas.push(val.toJSON()); + }) + + let state: EditingDelta = { + deltas: deltas, + }; + return state; + } + + /** + * Instructs Flutter to update its text input editing state to reflect the given configuration. + */ + updateEditingState(inputClientId: number, + text: string, + selectionStart: number, + selectionEnd: number, + composingStart: number, + composingEnd: number): void { + Log.d(TAG, "updateEditingState:" + + "Text: " + text + " Selection start: " + selectionStart + " Selection end: " + + selectionEnd + " Composing start: " + composingStart + " Composing end: " + composingEnd); + const state: Any = this.createEditingStateJSON(text, selectionStart, selectionEnd, composingStart, composingEnd); + this.channel.invokeMethod('TextInputClient.updateEditingState', [inputClientId, state]); + } + + /** + * Updates the editing state with deltas. + * @param inputClientId - The input client ID + * @param batchDeltas - Array list of text editing deltas + */ + updateEditingStateWithDeltas(inputClientId: number, batchDeltas: ArrayList): void { + Log.d(TAG, "updateEditingStateWithDeltas:" + "batchDeltas length: " + batchDeltas.length); + if(batchDeltas.length > 0){ + const state: Any = this.createEditingDeltaJSON(batchDeltas); + this.channel.invokeMethod('TextInputClient.updateEditingStateWithDeltas', [inputClientId, state]); + } + } + + /** + * Sends a newline action to Flutter. + * @param inputClientId - The input client ID + */ + newline(inputClientId: number): void { + Log.d(TAG, "Sending 'newline' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.newline"]); + } + + /** + * Sends a go action to Flutter. + * @param inputClientId - The input client ID + */ + go(inputClientId: number): void { + Log.d(TAG, "Sending 'go' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.go"]); + } + + /** + * Sends a search action to Flutter. + * @param inputClientId - The input client ID + */ + search(inputClientId: number): void { + Log.d(TAG, "Sending 'search' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.search"]); + } + + /** + * Sends a send action to Flutter. + * @param inputClientId - The input client ID + */ + send(inputClientId: number): void { + Log.d(TAG, "Sending 'send' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.send"]); + } + + /** + * Sends a done action to Flutter. + * @param inputClientId - The input client ID + */ + done(inputClientId: number): void { + Log.d(TAG, "Sending 'done' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.done"]); + } + + /** + * Sends a next action to Flutter. + * @param inputClientId - The input client ID + */ + next(inputClientId: number): void { + Log.d(TAG, "Sending 'next' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.next"]); + } + + /** + * Sends a previous action to Flutter. + * @param inputClientId - The input client ID + */ + previous(inputClientId: number): void { + Log.d(TAG, "Sending 'previous' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.previous"]); + } + + /** + * Sends an unspecified action to Flutter. + * @param inputClientId - The input client ID + */ + unspecifiedAction(inputClientId: number): void { + Log.d(TAG, "Sending 'unspecifiedAction' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.unspecified"]); + } + + /** + * Sends a commit content action to Flutter. + * @param inputClientId - The input client ID + */ + commitContent(inputClientId: number): void { + Log.d(TAG, "Sending 'commitContent' message."); + this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.commitContent"]); + } + + /** + * Notifies Flutter that the input connection has been closed. + * @param inputClientId - The input client ID + */ + onConnectionClosed(inputClientId: number): void { + Log.d(TAG, "Sending 'onConnectionClosed' message."); + this.channel.invokeMethod("TextInputClient.onConnectionClosed", [inputClientId]); + this.textInputMethodHandler?.hide(); + } + + /** + * Performs a private command. + * @param inputClientId - The input client ID + * @param action - The action string + * @param data - The command data + */ + performPrivateCommand(inputClientId: number, action: string, data: Any) { + + } + + /** + * Sets the window position for text input. + * @param windowPosition - The window position rectangle + */ + public setWindowPosition(windowPosition: window.Rect) { + this.TextInputCallback?.setWindowPosition(windowPosition); + this.TextInputCallback?.setCursorPosition(); + } + + /** + * Sets the device pixel ratio. + * @param devicePixelRatio - The device pixel ratio value + */ + public setDevicePixelRatio(devicePixelRatio: number) { + this.TextInputCallback?.setDevicePixelRatio(devicePixelRatio); + } + + /** + * Gets the keyboard focus state. + * @returns Whether the keyboard has focus + */ + getKeyboardFocusState() { + return this.textInputMethodHandler?.getKeyboardFocusState(); + } + +} + +/** + * Editing state interface. + */ +interface EditingState { + text: string; + selectionBase: number; + selectionExtent: number; + composingBase: number; + composingExtent: number; +} + + +/** + * Editing delta interface. + */ +interface EditingDelta { + deltas: Array; +} + + +/** + * Interface for handling text input method operations. + */ +export interface TextInputMethodHandler { + show(): void; + + hide(): void; + + requestAutofill(): void; + + finishAutofillContext(shouldSave: boolean): void; + + setClient(textInputClientId: number, configuration: Configuration | null): void; + + updateConfig(configuration: Configuration | null): void; + + setPlatformViewClient(id: number, usesVirtualDisplay: boolean): void; + + setEditableSizeAndTransform(width: number, height: number, transform: number[]): void; + + setCursorSizeAndPosition(cursorInfo: inputMethod.CursorInfo): void; + + setEditingState(editingState: TextEditState): void; + + clearClient(): void; + + handleChangeFocus(focusState: boolean): void; + + getKeyboardFocusState(): boolean; + +} + +/** + * A text editing configuration. + */ +export class Configuration { + /** Whether text should be obscured (e.g., for password fields). */ + obscureText: boolean = false; + /** Whether autocorrect is enabled. */ + autocorrect: boolean = false; + /** Whether autofill is enabled. */ + autofill: boolean = false; + /** Whether suggestions are enabled. */ + enableSuggestions: boolean = false; + /** Whether IME personalized learning is enabled. */ + enableIMEPersonalizedLearning: boolean = false; + /** Whether delta model is enabled for text editing. */ + enableDeltaModel: boolean = false; + /** The input type for the text field, or null if not set. */ + inputType: InputType | null = null; + /** The input action to perform when the user submits. */ + inputAction: Number = 0; + /** The label for the action button. */ + actionLabel: String = ""; + /** MIME types for content commit operations. */ + contentCommitMimeTypes: String[] = []; + /** The kind of pointer device being used. */ + deviceKind: PointerDeviceKind = PointerDeviceKind.UNKNOWN; + /** Array of field configurations for autofill. */ + fields: Configuration[] = []; + + /** + * Constructs a new Configuration instance. + * @param obscureText - Whether text should be obscured + * @param autocorrect - Whether autocorrect is enabled + * @param enableSuggestions - Whether suggestions are enabled + * @param enableIMEPersonalizedLearning - Whether IME personalized learning is enabled + * @param enableDeltaModel - Whether delta model is enabled + * @param inputType - The input type + * @param inputAction - The input action + * @param actionLabel - The action label + * @param autofill - Whether autofill is enabled + * @param contentListString - Content commit MIME types + * @param deviceKind - The pointer device kind + * @param fields - Array of field configurations + */ + constructor(obscureText: boolean, + autocorrect: boolean, + enableSuggestions: boolean, + enableIMEPersonalizedLearning: boolean, + enableDeltaModel: boolean, + inputType: InputType, + inputAction: Number, + actionLabel: String, + autofill: boolean, + contentListString: [], + deviceKind: PointerDeviceKind, + fields: Configuration[] + ) { + this.obscureText = obscureText; + this.autocorrect = autocorrect; + this.enableSuggestions = enableSuggestions; + this.enableIMEPersonalizedLearning = enableIMEPersonalizedLearning; + this.enableDeltaModel = enableDeltaModel; + this.inputType = inputType; + this.inputAction = inputAction; + this.actionLabel = actionLabel; + this.autofill = autofill; + this.contentCommitMimeTypes = contentListString; + this.fields = fields + this.deviceKind = deviceKind + } + + private static inputActionFromTextInputAction(inputActionName: string): number { + switch (inputActionName) { + case "TextInputAction.previous": + return inputMethod.EnterKeyType.PREVIOUS + case "TextInputAction.unspecified": + return inputMethod.EnterKeyType.UNSPECIFIED + case "TextInputAction.none": + return inputMethod.EnterKeyType.NONE + case "TextInputAction.go": + return inputMethod.EnterKeyType.GO + case "TextInputAction.search": + return inputMethod.EnterKeyType.SEARCH + case "TextInputAction.send": + return inputMethod.EnterKeyType.SEND + case "TextInputAction.next": + return inputMethod.EnterKeyType.NEXT + case "TextInputAction.newline": + return NEWLINE_KEY_TYPE + case "TextInputAction.done": + return inputMethod.EnterKeyType.DONE + default: + // Present default key if bad input type is given. + return inputMethod.EnterKeyType.UNSPECIFIED + } + } + + /** + * Creates a Configuration instance from JSON. + * @param json - The JSON object + * @returns A new Configuration instance + */ + static fromJson(json: Any) { + const inputActionName: string = json.inputAction; + if (!inputActionName) { + throw new Error("Configuration JSON missing 'inputAction' property."); + } + + let fields: Array = new Array(); + if (json.fields !== null && json.fields !== undefined) { + fields = json.fields.map((field: Any): Any => Configuration.fromJson(field)); + } + + const inputAction: number = Configuration.inputActionFromTextInputAction(inputActionName); + + // Build list of content commit mime types from the data in the JSON list. + const contentList: Array = []; + if (json.contentCommitMimeTypes !== null && json.contentCommitMimeTypes !== undefined) { + json.contentCommitMimeTypes.forEach((type: Any) => { + contentList.push(type); + }); + } + return new Configuration( + json.obscureText ?? false, + json.autocorrect ?? true, + json.enableSuggestions ?? false, + json.enableIMEPersonalizedLearning ?? false, + json.enableDeltaModel ?? false, + InputType.fromJson(json.inputType), + inputAction, + json.actionLabel ?? null, + json.autofill ?? null, + contentList as Any, + json.deviceKind ?? PointerDeviceKind.UNKNOWN, + fields + ); + } + + /** + * Creates a Configuration instance from a map. + * @param map - The map containing configuration data + * @returns A new Configuration instance + */ + static fromMap(map: Map) { + let inputTypeSrc: Any = map.get('inputType'); + let type = TextInputType.get(inputTypeSrc.name) ?? inputMethod.TextInputType.TEXT; + let inputType = new InputType(type, inputTypeSrc.decimal, inputTypeSrc.signed); + let inputAction = Configuration.inputActionFromTextInputAction(map.get('inputAction')); + + let fields: Array = new Array(); + if (map.get('fields')) { + fields = map.get('fields').map((field: Any): Any => Configuration.fromJson(field)); + } + + // Build list of content commit mime types from the data in the JSON list. + const contentList: Array = []; + if (map.get('contentCommitMimeTypes')) { + map.get('contentCommitMimeTypes').forEach((type: Any) => { + contentList.push(type); + }); + } + return new Configuration( + map.get('obscureText') ?? false, + map.get('autocorrect') ?? true, + map.get('enableSuggestions') ?? false, + map.get('enableIMEPersonalizedLearning') ?? false, + map.get('enableDeltaModel') ?? false, + inputType, + inputAction, + map.get('actionLabel') ?? null, + map.get('autofill') ?? null, + contentList as Any, + map.get('deviceKind') ?? PointerDeviceKind.UNKNOWN, + fields + ); + } +} + +/* +/// All possible enum values from flutter. +static const List values = [ + text, multiline, number, phone, datetime, emailAddress, url, visiblePassword, name, streetAddress, none, +]; + +// Corresponding string name for each of the [values]. +static const List _names = [ + 'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url', 'visiblePassword', 'name', 'address', 'none', +]; + +// Because TextInputType.name and TextInputType.streetAddress do not exist on ohos, +// these two types will be mapped to the default keyboard. +*/ +const TextInputType: Map = new Map([ + ["TextInputType.text", inputMethod.TextInputType.TEXT], + ["TextInputType.multiline", inputMethod.TextInputType.MULTILINE], + ["TextInputType.number", inputMethod.TextInputType.NUMBER], + ["TextInputType.phone", inputMethod.TextInputType.PHONE], + ["TextInputType.datetime", inputMethod.TextInputType.DATETIME], + ["TextInputType.emailAddress", inputMethod.TextInputType.EMAIL_ADDRESS], + ["TextInputType.url", inputMethod.TextInputType.URL], + ["TextInputType.visiblePassword", inputMethod.TextInputType.VISIBLE_PASSWORD], + ["TextInputType.name", inputMethod.TextInputType.TEXT], + ["TextInputType.address", inputMethod.TextInputType.TEXT], + ["TextInputType.none", inputMethod.TextInputType.NONE], +]); + +/** + * A text input type. + */ +export class InputType { + /** The text input type. */ + type: inputMethod.TextInputType; + /** Whether the input accepts signed numbers. */ + isSigned: boolean; + /** Whether the input accepts decimal numbers. */ + isDecimal: boolean; + + /** + * Constructs a new InputType instance. + * @param type - The text input type + * @param isSigned - Whether the input is signed + * @param isDecimal - Whether the input is decimal + */ + constructor(type: inputMethod.TextInputType, isSigned: boolean, isDecimal: boolean) { + this.type = type; + this.isSigned = isSigned; + this.isDecimal = isDecimal; + } + + /** + * Creates an InputType instance from JSON. + * @param json - The JSON object + * @returns A new InputType instance + * @throws Error if the input type is not recognized + */ + static fromJson(json: Any): InputType { + if (TextInputType.has(json.name as string)) { + return new InputType(TextInputType.get(json.name as string) as inputMethod.TextInputType, + json.signed as boolean, json.decimal as boolean) + } + throw new Error("No such TextInputType: " + json.name as string); + } +} + +/** + * State of an on-going text editing session.. + */ +export class TextEditState { + private static TAG = "TextEditState"; + /** The text content. */ + text: string; + /** The start position of the text selection. */ + selectionStart: number; + /** The end position of the text selection. */ + selectionEnd: number; + /** The start position of the composing region. */ + composingStart: number; + /** The end position of the composing region. */ + composingEnd: number; + + /** + * Constructs a new TextEditState instance. + * @param text - The text content + * @param selectionStart - The start of the selection + * @param selectionEnd - The end of the selection + * @param composingStart - The start of the composing region + * @param composingEnd - The end of the composing region + */ + constructor(text: string, + selectionStart: number, + selectionEnd: number, + composingStart: number, + composingEnd: number) { + if ((selectionStart != -1 || selectionEnd != -1) + && (selectionStart < 0 || selectionEnd < 0)) { + throw new Error("invalid selection: (" + selectionStart + ", " + selectionEnd + ")"); + } + + if ((composingStart != -1 || composingEnd != -1) + && (composingStart < 0 || composingStart > composingEnd)) { + throw new Error("invalid composing range: (" + composingStart + ", " + composingEnd + ")"); + } + + if (composingEnd > text.length) { + throw new Error("invalid composing start: " + composingStart); + } + + if (selectionStart > text.length) { + throw new Error("invalid selection start: " + selectionStart); + } + + if (selectionEnd > text.length) { + throw new Error("invalid selection end: " + selectionEnd); + } + + this.text = text; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; + this.composingStart = composingStart; + this.composingEnd = composingEnd; + } + + hasSelection(): boolean { + // When selectionStart == -1, it's guaranteed that selectionEnd will also + // be -1. + return this.selectionStart >= 0; + } + + /** + * Checks if there is an active composing region. + * @returns True if there is an active composing region, false otherwise + */ + hasComposing(): boolean { + return this.composingStart >= 0 && this.composingEnd > this.composingStart; + } + + /** + * Creates a TextEditState instance from JSON. + * @param textEditState - The JSON object or map + * @returns A new TextEditState instance + */ + static fromJson(textEditState: Any): TextEditState { + if (textEditState.text != null && textEditState.text != undefined && textEditState.text != "") { + return new TextEditState( + textEditState.text, + textEditState.selectionBase, + textEditState.selectionExtent, + textEditState.composingBase, + textEditState.composingExtent + ) + } else { + return new TextEditState( + textEditState.get('text'), + textEditState.get('selectionBase'), + textEditState.get('selectionExtent'), + textEditState.get('composingBase'), + textEditState.get('composingExtent') + ) + } + } +} + +/** + * Method call handler for text input channel requests. + */ +class TextInputCallback implements MethodCallHandler { + /** The text input method handler for processing text input requests. */ + textInputMethodHandler: TextInputMethodHandler; + /** The window position rectangle, or null if not set. */ + windowPosition: window.Rect | null = null; + /** The cursor position rectangle. */ + cursorPosition: window.Rect = { + left: 0, + top: 0, + width: 0, + height: 0, + } + /** The device pixel ratio for converting logical to physical pixels. */ + devicePixelRatio = display.getDefaultDisplaySync()?.densityPixels as number; + /** The input position rectangle. */ + inputPosition: window.Rect = { + left: 0, + top: 0, + width: 0, + height: 0, + } + + /** + * Constructs a new TextInputCallback instance. + * @param handler - The TextInputMethodHandler instance + */ + constructor(handler: TextInputMethodHandler) { + this.textInputMethodHandler = handler; + } + + /** + * Sets the window position. + * @param windowPosition - The window position rectangle + */ + setWindowPosition(windowPosition: window.Rect) { + this.windowPosition = windowPosition; + } + + /** + * Sets the device pixel ratio. + * @param devicePixelRatio - The device pixel ratio value + */ + setDevicePixelRatio(devicePixelRatio: number) { + this.devicePixelRatio = devicePixelRatio; + } + + /** + * Sets the cursor position based on window and input positions. + */ + setCursorPosition() { + const left = (this.windowPosition?.left ?? 0 as number) + (this.cursorPosition.left + this.inputPosition.left) * this.devicePixelRatio; + const top = (this.windowPosition?.top ?? 0 as number) + (this.cursorPosition.top + this.inputPosition.top) * this.devicePixelRatio; + this.textInputMethodHandler.setCursorSizeAndPosition({ + left: left, + top: top, + width: 100, + height: 50, + }) + } + + + /** + * Handles method calls from Dart. + * @param call - The method call from Dart + * @param result - The result callback to send a response + */ + onMethodCall(call: MethodCall, result: MethodResult) { + if (this.textInputMethodHandler == null) { + return; + } + let method: string = call.method; + let args: Any = call.args; + Log.d(TAG, "Received '" + method + "' message."); + switch (method) { + case "TextInput.show": + this.textInputMethodHandler.show(); + Log.d(TAG, "textInputMethodHandler.show()"); + result.success(null); + break; + case "TextInput.hide": + this.textInputMethodHandler.hide(); + result.success(null); + break; + case "TextInput.setClient": + const textInputClientId: number = args[0] as number; + const jsonConfiguration: string = args[1]; + const config: Configuration | null = Configuration.fromJson(jsonConfiguration); + + this.textInputMethodHandler.setClient(textInputClientId, config); + result.success(null); + break; + case 'TextInput.updateConfig': + const newConfig: Configuration | null = Configuration.fromMap(args as Map); + this.textInputMethodHandler.updateConfig(newConfig); + result.success(null); + break; + case "TextInput.requestAutofill": + //TODO: requestAutofill + result.notImplemented(); + break; + case "TextInput.setPlatformViewClient": + //TODO: + result.notImplemented(); + break; + case "TextInput.setEditingState": + this.textInputMethodHandler.setEditingState(TextEditState.fromJson(args)); + result.success(null); + break; + case "TextInput.setCaretRect": + this.cursorPosition.top = args.get('y'); + this.cursorPosition.left = args.get('x'); + this.cursorPosition.width = args.get('width'); + this.cursorPosition.height = args.get('height'); + this.setCursorPosition(); + break; + case "TextInput.setEditableSizeAndTransform": + this.inputPosition.left = args.get('transform')[12]; + this.inputPosition.top = args.get('transform')[13]; + this.setCursorPosition(); + break; + case "TextInput.clearClient": + this.textInputMethodHandler.clearClient(); + result.success(null); + break; + case "TextInput.sendAppPrivateCommand": + //TODO: + result.notImplemented(); + break; + case "TextInput.finishAutofillContext": + //TODO: + result.notImplemented(); + break; + default: + result.notImplemented(); + break; + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/workers/PlatformChannelWorker.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/workers/PlatformChannelWorker.ets new file mode 100644 index 0000000..7219844 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/engine/workers/PlatformChannelWorker.ets @@ -0,0 +1,72 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; + +import Log from '../../../util/Log'; +import SendableBinaryMessageHandler from '../../../plugin/common/SendableBinaryMessageHandler'; +import { TaskState } from '../dart/DartMessenger'; + + +const TAG: string = 'PlatformChannelWorker'; +const workerPort: ThreadWorkerGlobalScope = worker.workerPort; + +/** + * Defines the event handler to be called when the worker thread receives a message sent by the host thread. + * The event handler is executed in the worker thread. + * + * @param e - The message event data + */ +workerPort.onmessage = async (e: MessageEvents) => { + let data: TaskState = e.data; + let result: ArrayBuffer | null = await handleMessage(data.handler, data.message, data.args); + workerPort.postMessage(result, [result]); +} + +/** + * Defines the event handler to be called when the worker receives a message that cannot be deserialized. + * The event handler is executed in the worker thread. + * + * @param e - The message event data + */ +workerPort.onmessageerror = (e: MessageEvents) => { + Log.e(TAG, '#onmessageerror = ' + e.data); +} + +/** + * Defines the event handler to be called when an exception occurs during worker execution. + * The event handler is executed in the worker thread. + * + * @param e - The error event + */ +workerPort.onerror = (e: ErrorEvent) => { + Log.e(TAG, '#onerror = ' + e.message); +} + +/** + * Handles a message in the worker thread. + * @param handler - The message handler to execute + * @param message - The message data as an ArrayBuffer + * @param args - Additional arguments to pass to the handler + * @returns A promise that resolves to the reply ArrayBuffer, or null if no reply + */ +async function handleMessage(handler: SendableBinaryMessageHandler, + message: ArrayBuffer, + args: Object[]): Promise { + const result = await new Promise((resolve, reject) => { + try { + handler.onMessage(message, { + reply: (reply: ArrayBuffer | null): void => { + resolve(reply); + } + }, ...args); + } catch (e) { + reject(null); + Log.e(TAG, "Oops! Failed to handle message in the background: ", e); + } + }); + return result; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/EmbeddingNodeController.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/EmbeddingNodeController.ets new file mode 100644 index 0000000..bc53121 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/EmbeddingNodeController.ets @@ -0,0 +1,266 @@ +/* +* Copyright (c) 2024 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +*/ +import { BuilderNode, FrameNode, NodeController, NodeRenderType } from '@kit.ArkUI'; +import Any from '../../plugin/common/Any'; +import PlatformView, { Params, PlatformViewVisibleAreaEventOptions } from '../../plugin/platform/PlatformView'; +import Log from '../../util/Log'; +import { DVModel, DVModelChildren, DynamicView } from '../../view/DynamicView/dynamicView'; + + +declare class nodeControllerParams { + surfaceId: string + type: string + renderType: NodeRenderType + embedId: string + width: number + height: number +} + +const TAG = 'EmbeddingNodeController' + +/** + * Node controller for embedding platform views in Flutter. + * This class manages the lifecycle and rendering of platform views using BuilderNode. + */ +export class EmbeddingNodeController extends NodeController { + private builderNode: BuilderNode<[Params]> | undefined | null = null; + private wrappedBuilder: WrappedBuilder<[Params]> | null = null; + private platformView: PlatformView | undefined = undefined; + private embedId: string = ""; + private surfaceId: string = ""; + private renderType: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY; + private direction: Direction = Direction.Auto; + private isDestroy: boolean = false; + private platformViewVisibleAreaEventOptions: PlatformViewVisibleAreaEventOptions | null = null; + + /** + * Sets the render options for the platform view. + * @param platformView - The platform view instance + * @param surfaceId - The surface ID + * @param renderType - The render type + * @param direction - The layout direction + */ + setRenderOption(platformView: PlatformView, surfaceId: string, renderType: NodeRenderType, direction: Direction) { + if (platformView == undefined) { + Log.e(TAG, "platformView undefined"); + } else { + this.wrappedBuilder = platformView.getView(); + } + this.platformView = platformView; + this.surfaceId = surfaceId; + this.renderType = renderType; + this.direction = direction; + } + + /** + * Notify the PlatformView that it has entered an invisible state, and animations on it need to be inactive. + */ + notifyPlatformViewInvisible(): void { + if (!this.platformView || !this.platformViewVisibleAreaEventOptions || + this.platformViewVisibleAreaEventOptions.enable === false) { + return; + } + + this.platformView.onInactive(); + } + + /** + * Set up the callback for monitoring changes in the visible area of the external texture. + */ + setPlatformViewVisibleAreaEventCallback(): void { + if (!this.platformView) { + return; + } + + this.platformViewVisibleAreaEventOptions = + this.platformView.getPlatformViewVisibleAreaEventOptions(); + if (!this.platformViewVisibleAreaEventOptions) { + return; + } + + const options = this.platformViewVisibleAreaEventOptions!; + + Log.i(TAG, + "setPlatformViewVisibleAreaEventCallback surfaceId:" + this.surfaceId + + ", enable:" + options.enable + + ", ratios:" + options.ratios + + ", expectedUpdateInterval:" + options.expectedUpdateInterval + + ", onInactiveThreshold:" + options.onInactiveThreshold + + ", onActiveThreshold:" + options.onActiveThreshold); + if (options.enable === false) { + return; + } + + let node: FrameNode | null | undefined = this.builderNode?.getFrameNode(); + if (!node) { + return; + } + + node?.commonEvent.setOnVisibleAreaApproximateChange( + { ratios: options.ratios, + expectedUpdateInterval: options.expectedUpdateInterval }, + + (isExpanding: boolean, currentRatio: number) => + { + if (!this.platformView) { + return; + } + + Log.i(TAG, + "PlatformViewVisibleAreaEventCallback surfaceId:" + this.surfaceId + + ", isExpanding:" + isExpanding + + ", currentRatio:" + currentRatio); + if (!isExpanding && + currentRatio <= options.onInactiveThreshold) { + // Pause the operation of continuous production of textures + this.platformView.onInactive(); + } else if (isExpanding && + currentRatio >= options.onActiveThreshold) { + // Resume the operation of continuous production of textures + this.platformView.onActive(); + } + } + ) + } + + /** + * Creates a FrameNode for the platform view. + * @param uiContext - The UI context + * @returns The created FrameNode, or null if creation fails + */ + makeNode(uiContext: UIContext): FrameNode | null { + this.builderNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId, type: this.renderType }); + + if (this.platformView) { + this.builderNode.build(this.wrappedBuilder, { direction: this.direction, platformView: this.platformView }); + this.setPlatformViewVisibleAreaEventCallback(); + } + return this.builderNode.getFrameNode(); + } + + /** + * Sets the builder node. + * @param builderNode - The BuilderNode instance, or null + */ + setBuilderNode(builderNode: BuilderNode | null): void { + this.builderNode = builderNode; + } + + /** + * Gets the builder node. + * @returns The BuilderNode instance, or null if not set + */ + getBuilderNode(): BuilderNode<[Params]> | undefined | null { + return this.builderNode; + } + + /** + * Updates the node with new arguments. + * @param arg - The update arguments + */ + updateNode(arg: Object): void { + this.builderNode?.update(arg); + } + + /** + * Gets the embed ID. + * @returns The embed ID string + */ + getEmbedId(): string { + return this.embedId; + } + + /** + * Sets the destroy state and disposes the builder node if needed. + * @param isDestroy - Whether the controller is being destroyed + */ + setDestroy(isDestroy: boolean): void { + this.isDestroy = isDestroy; + if (this.isDestroy) { + this.builderNode?.dispose(); + } + } + + /** + * Disposes the frame node and cleans up resources. + */ + disposeFrameNode() { + this.builderNode?.getFrameNode()?.getRenderNode()?.dispose(); + this.builderNode?.dispose(); + + this.builderNode = null; + this.wrappedBuilder = null; + } + + /** + * Posts a mouse event to the builder node. + * Note: postInputEvent is an API20 interface, so we check for its existence to avoid compilation errors. + * @param event - The mouse event to post + */ + postMouseEvent(event: MouseEvent) { + // Avoid compilation errors: postInputEvent is an API20 interface + if (typeof (this.builderNode as ESObject)?.postInputEvent == 'function') { + (this.builderNode as ESObject)?.postInputEvent(event); + } + } + + /** + * Posts an axis event to the builder node. + * Note: postInputEvent is an API20 interface, so we check for its existence to avoid compilation errors. + * @param event - The axis event to post + */ + postAxisEvent(event: AxisEvent) { + // Avoid compilation errors: postInputEvent is an API20 interface + if (typeof (this.builderNode as ESObject)?.postInputEvent == 'function') { + (this.builderNode as ESObject)?.postInputEvent(event); + } + } + + /** + * Posts a touch event to the builder node. + * @param event - The touch event to post, or undefined + * @param isPx - Whether the coordinates are already in pixels (default: false, will convert from vp to px) + * @returns Whether the event was successfully posted + */ + postEvent(event: TouchEvent | undefined, isPx: boolean = false): boolean { + if (event == undefined) { + return false; + } + + // change vp to px + if (!isPx) { + let changedTouchLen = event.changedTouches.length; + for (let i = 0; i < changedTouchLen; i++) { + event.changedTouches[i].displayX = vp2px(event.changedTouches[i].displayX); + event.changedTouches[i].displayY = vp2px(event.changedTouches[i].displayY); + event.changedTouches[i].windowX = vp2px(event.changedTouches[i].windowX); + event.changedTouches[i].windowY = vp2px(event.changedTouches[i].windowY); + event.changedTouches[i].screenX = vp2px(event.changedTouches[i].screenX); + event.changedTouches[i].screenY = vp2px(event.changedTouches[i].screenY); + event.changedTouches[i].x = vp2px(event.changedTouches[i].x); + event.changedTouches[i].y = vp2px(event.changedTouches[i].y); + Log.d(TAG, "changedTouches[" + i + "] displayX:" + event.changedTouches[i].displayX + " displayY:" + + event.changedTouches[i].displayY + " x:" + event.changedTouches[i].x + " y:" + event.changedTouches[i].y); + } + let touchesLen = event.touches.length; + for (let i = 0; i< touchesLen; i++) { + event.touches[i].displayX = vp2px(event.touches[i].displayX); + event.touches[i].displayY = vp2px(event.touches[i].displayY); + event.touches[i].windowX = vp2px(event.touches[i].windowX); + event.touches[i].windowY = vp2px(event.touches[i].windowY); + event.touches[i].screenX = vp2px(event.touches[i].screenX); + event.touches[i].screenY = vp2px(event.touches[i].screenY); + event.touches[i].x = vp2px(event.touches[i].x); + event.touches[i].y = vp2px(event.touches[i].y); + Log.d(TAG, "touches[" + i + "] displayX:" + event.touches[i].displayX + " displayY:" + + event.touches[i].displayY + " x:" + event.touches[i].x + " y:" + event.touches[i].y); + } + } + + return this.builderNode?.postTouchEvent(event) as boolean + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/ExclusiveAppComponent.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/ExclusiveAppComponent.ets new file mode 100644 index 0000000..125d035 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/ExclusiveAppComponent.ets @@ -0,0 +1,31 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on ExclusiveAppComponent.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * Interface for app components that exclusively attach to a FlutterEngine. + * @template T - The type of the underlying app component + */ +export default interface ExclusiveAppComponent { + /** + * Called when another App Component is about to become attached to the + * {@link FlutterEngine} this App Component is currently attached to. + * + * This App Component's connections to the {@link FlutterEngine} + * are still valid at the moment of this call. + */ + detachFromFlutterEngine(): void; + + /** + * Retrieves the App Component behind this exclusive App Component. + * + * @returns The app component + */ + getAppComponent(): T; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbility.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbility.ets new file mode 100644 index 0000000..2b799f7 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbility.ets @@ -0,0 +1,546 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import UIAbility from '@ohos.app.ability.UIAbility'; +import window from '@ohos.window'; +import { FlutterAbilityAndEntryDelegate, Host } from './FlutterAbilityAndEntryDelegate'; +import Log from '../../util/Log'; +import FlutterEngine from '../engine/FlutterEngine'; +import PlatformPlugin from '../../plugin/PlatformPlugin'; +import SensitiveContentPlugin from '../../plugin/view/SensitiveContentPlugin'; +import FlutterShellArgs from '../engine/FlutterShellArgs'; +import FlutterAbilityLaunchConfigs from './FlutterAbilityLaunchConfigs'; +import common from '@ohos.app.ability.common'; +import Want from '@ohos.app.ability.Want'; +import { FlutterPlugin } from '../engine/plugins/FlutterPlugin'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import I18n from '@ohos.i18n' +import { PlatformBrightness } from '../engine/systemchannels/SettingsChannel'; +import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant'; +import { Configuration } from '@ohos.app.ability.Configuration'; +import { deviceInfo } from '@kit.BasicServicesKit'; +import ExclusiveAppComponent from './ExclusiveAppComponent'; +import errorManager from '@ohos.app.ability.errorManager'; +import appRecovery from '@ohos.app.ability.appRecovery'; +import FlutterManager from './FlutterManager'; +import { FlutterView } from '../../view/FlutterView'; +import ApplicationInfoLoader from '../engine/loader/ApplicationInfoLoader'; +import { accessibility } from '@kit.AccessibilityKit'; + +const TAG = "FlutterAbility"; + +/** + * Base Flutter Ability for OpenHarmony. + * Main responsibilities: + * 1. Holds and initializes FlutterAbilityDelegate + * 2. Forwards lifecycle events + * + * Main abilities should inherit from this class. + */ +export class FlutterAbility extends UIAbility implements Host { + private delegate?: FlutterAbilityAndEntryDelegate | null; + private flutterView: FlutterView | null = null; + private mainWindow?: window.Window | null; + private errorManagerId: number = 0; + + /** + * Gets the FlutterView instance. + * @returns The FlutterView instance, or null if not available + */ + getFlutterView(): FlutterView | null { + return this.flutterView; + } + + /** + * Gets the page path for loading content. + * @returns The page path string + */ + pagePath(): string { + return "pages/Index" + } + + /** + * Determines whether FlutterAbility should be full screen by default. + * Can be overridden to customize full screen behavior. + * Default value: based on device type, determines if full screen is needed. + * @returns True if full screen by default, false otherwise + */ + isDefaultFullScreen(): boolean { + return deviceInfo.deviceType != '2in1'; + } + + /** + * Called when the ability is created. + * 1. Creates and attaches delegate + * 2. Configures windows (transparency not needed) + * 3. Handles lifecycle.onCreate + * 4. setContentView() not needed + * @param want - The Want object + * @param launchParam - The launch parameters + */ + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { + // On cold start, get the current system font size from the context and store it + AppStorage.setOrCreate('fontSizeScale', this.context.config.fontSizeScale); + Log.i(TAG, "this.context.config.fontSizeScale = " + this.context.config.fontSizeScale); + + Log.i(TAG, "bundleCodeDir=" + this.context.bundleCodeDir); + FlutterManager.getInstance().pushUIAbility(this) + + this.delegate = new FlutterAbilityAndEntryDelegate(this); + this?.delegate?.onAttach(this.context); + Log.i(TAG, 'onAttach end'); + this?.delegate?.platformPlugin?.setUIAbilityContext(this.context); + this?.delegate?.onRestoreInstanceState(want); + + if (this.stillAttachedForEvent("onWindowStageCreate")) { + this?.delegate?.onWindowStageCreate(); + } + + Log.i(TAG, 'MyAbility onCreate'); + + let observer: errorManager.ErrorObserver = { + onUnhandledException(errorMsg) { + Log.e(TAG, "onUnhandledException, errorMsg:", errorMsg); + appRecovery.saveAppState(); + appRecovery.restartApp(); + } + } + this.errorManagerId = errorManager.on('error', observer); + + let flutterApplicationInfo = ApplicationInfoLoader.load(this.context); + + if (flutterApplicationInfo.isDebugMode) { + this.delegate?.initWindow(); + } + } + + /** + * Called when the ability is destroyed. + * Cleans up resources and removes the ability from FlutterManager. + */ + onDestroy() { + FlutterManager.getInstance().popUIAbility(this); + + errorManager.off('error', this.errorManagerId); + + if (this.flutterView != null) { + this.flutterView.onDestroy() + this.flutterView = null; + } + + if (this.stillAttachedForEvent("onDestroy")) { + this?.delegate?.onDetach(); + } + + this.release() + } + + /** + * Called to save the ability state. + * @param reason - The reason for saving state + * @param wantParam - The parameters to save state to + * @returns The save result + */ + onSaveState(reason: AbilityConstant.StateType, wantParam: Record): AbilityConstant.OnSaveResult { + return this?.delegate?.onSaveState(reason, wantParam) ?? AbilityConstant.OnSaveResult.ALL_REJECT; + } + + protected windowStageEventCallback = (data: window.WindowStageEventType) => { + this.delegate?.onWindowStageChanged(data) + } + + /** + * Called when the window stage is created. + * @param windowStage - The WindowStage instance + */ + onWindowStageCreate(windowStage: window.WindowStage) { + FlutterManager.getInstance().pushWindowStage(this, windowStage); + this.delegate?.initWindow(); + this.mainWindow = windowStage.getMainWindowSync(); + try { + windowStage.on('windowStageEvent', this.windowStageEventCallback); + this.flutterView = this.delegate!!.createView(this.context) + Log.i(TAG, 'onWindowStageCreate:' + this.flutterView!!.getId()); + let storage: LocalStorage = new LocalStorage(); + storage.setOrCreate("viewId", this.flutterView!!.getId()) + windowStage.loadContent(this.pagePath(), storage, (err, data) => { + if (err.code) { + Log.e(TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + this.flutterView?.onWindowCreated(); + + Log.i(TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); + }); + if (this.isDefaultFullScreen()) { + FlutterManager.getInstance().setUseFullScreen(true, this.context); + } + } catch (exception) { + Log.e(TAG, 'Failed to enable the listener for window stage event changes. Cause:' + JSON.stringify(exception)); + } + } + + /** + * Called when a new Want is received. + * @param want - The new Want object + * @param launchParams - The launch parameters + */ + onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void { + this?.delegate?.onNewWant(want, launchParams) + } + + /** + * Called when the window stage is destroyed. + */ + onWindowStageDestroy() { + FlutterManager.getInstance().popWindowStage(this); + if (this.stillAttachedForEvent("onWindowStageDestroy")) { + this?.delegate?.onWindowStageDestroy(); + } + } + + /** + * Called when the ability comes to foreground. + */ + onForeground() { + if (this.stillAttachedForEvent("onForeground")) { + this?.delegate?.onShow(); + } + } + + /** + * Called when the ability goes to background. + */ + onBackground() { + if (this.stillAttachedForEvent("onBackground")) { + this?.delegate?.onHide(); + } + } + + /** + * Called when the window stage is about to be destroyed. + * @param windowStage - The WindowStage instance + */ + onWindowStageWillDestroy(windowStage: window.WindowStage) { + try { + windowStage.off('windowStageEvent', this.windowStageEventCallback); + } catch (err) { + Log.e(TAG, "windowStage off failed"); + } + } + + /** + * Releases all held objects. + */ + release() { + if (this?.delegate != null) { + this?.delegate?.release(); + this.delegate = null; + } + } + + /** + * Gets the UIAbility instance. + * @returns This UIAbility instance + */ + getAbility(): UIAbility { + return this; + } + + /** + * Gets the FlutterAbilityAndEntryDelegate instance. + * @returns The delegate instance, or null if not available + */ + getFlutterAbilityAndEntryDelegate(): FlutterAbilityAndEntryDelegate | null { + return this.delegate ?? null; + } + + /** + * Determines whether to dispatch app lifecycle state changes. + * @returns True to dispatch lifecycle state, false otherwise + */ + shouldDispatchAppLifecycleState(): boolean { + return true; + } + + /** + * Provides a FlutterEngine instance. + * @param context - The context + * @returns A FlutterEngine instance, or null if not provided + */ + provideFlutterEngine(context: common.Context): FlutterEngine | null { + return null; + } + + /** + * Provides a PlatformPlugin instance. + * @param flutterEngine - The FlutterEngine instance + * @returns A PlatformPlugin instance + */ + providePlatformPlugin(flutterEngine: FlutterEngine): PlatformPlugin | undefined { + return new PlatformPlugin(flutterEngine.getPlatformChannel()!, this.context, this); + } + + /** + * Provides a SensitiveContentPlugin instance. + * @param flutterEngine - The FlutterEngine instance + * @returns A SensitiveContentPlugin instance + */ + provideSensitiveContentPlugin(flutterEngine: FlutterEngine): SensitiveContentPlugin | undefined { + return new SensitiveContentPlugin(flutterEngine.getSensitiveContentChannel()!); + } + + /** + * Configures the Flutter engine. + * @param flutterEngine - The FlutterEngine to configure + */ + configureFlutterEngine(flutterEngine: FlutterEngine) { + + } + + /** + * Cleans up the Flutter engine. + * @param flutterEngine - The FlutterEngine to clean up + */ + cleanUpFlutterEngine(flutterEngine: FlutterEngine) { + + } + + /** + * Gets Flutter shell arguments from the Want. + * @returns A FlutterShellArgs instance + */ + getFlutterShellArgs(): FlutterShellArgs { + return FlutterShellArgs.fromWant(this.getWant()); + } + + /** + * Gets Dart entrypoint arguments from launch parameters. + * @returns Array of entrypoint arguments + */ + getDartEntrypointArgs(): Array { + if (this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS]) { + return this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS] as Array; + } + return new Array() + } + + /** + * Detaches from the Flutter engine. + */ + detachFromFlutterEngine() { + if (this?.delegate != null) { + this?.delegate?.onDetach(); + } + } + + /** + * Determines whether to pop the system navigator. + * @returns False by default + */ + popSystemNavigator(): boolean { + return false; + } + + /** + * Determines whether to attach the engine to the ability. + * @returns True to attach the engine, false otherwise + */ + shouldAttachEngineToAbility(): boolean { + return true; + } + + /** + * Gets the Dart entrypoint library URI. + * @returns The library URI string + */ + getDartEntrypointLibraryUri(): string { + return ""; + } + + /** + * Gets the app bundle path. + * @returns The bundle path string + */ + getAppBundlePath(): string { + return ""; + } + + /** + * Gets the Dart entrypoint function name from launch parameters. + * @returns The entrypoint function name, or default if not set + */ + getDartEntrypointFunctionName(): string { + if (this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT]) { + return this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT] as string; + } + return FlutterAbilityLaunchConfigs.DEFAULT_DART_ENTRYPOINT + } + + /** + * Gets the initial route from launch parameters. + * @returns The initial route string, or empty string if not set + */ + getInitialRoute(): string { + if (this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_INITIAL_ROUTE]) { + return this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_INITIAL_ROUTE] as string; + } + return "" + } + + /** + * Gets the Want object. + * @returns The launch Want instance + */ + getWant(): Want { + return this.launchWant; + } + + /** + * Determines whether to destroy the engine when the host is destroyed. + * @returns True to destroy the engine, false otherwise + */ + shouldDestroyEngineWithHost(): boolean { + if ((this.getCachedEngineId() != null && this.getCachedEngineId().length > 0) || + this.delegate!!.isFlutterEngineFromHost()) { + // Only destroy a cached engine if explicitly requested by app developer. + return false; + } + return true; + } + + /** + * Determines whether to automatically attach to the engine. + * @returns True to attach automatically, false otherwise + */ + attachToEngineAutomatically(): boolean { + return true; + } + + /** + * Determines whether to restore and save state. + * @returns True to restore and save state, false otherwise + */ + shouldRestoreAndSaveState(): boolean { + if (this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_ID] != undefined) { + return this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_ID] as boolean; + } + if (this.getCachedEngineId() != null && this.getCachedEngineId().length > 0) { + // Prevent overwriting the existing state in a cached engine with restoration state. + return false; + } + return true; + } + + /** + * Gets the exclusive app component. + * @returns The ExclusiveAppComponent instance, or null + */ + getExclusiveAppComponent(): ExclusiveAppComponent | null { + return this.delegate ? this.delegate : null + } + + /** + * Gets the cached engine ID from launch parameters. + * @returns The cached engine ID string + */ + getCachedEngineId(): string { + return this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_ID] as string + } + + /** + * Gets the cached engine group ID from launch parameters. + * @returns The cached engine group ID string, or null if not set + */ + getCachedEngineGroupId(): string | null { + return this.launchWant.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_GROUP_ID] as string + } + + /** + * Checks if the delegate is still attached for an event. + * @param event - The event name + * @returns True if attached, false otherwise + */ + private stillAttachedForEvent(event: string) { + Log.i(TAG, 'Ability ' + event); + if (this?.delegate == null) { + Log.w(TAG, "FlutterAbility " + event + " call after release."); + return false; + } + if (!this?.delegate?.isAttached) { + Log.w(TAG, "FlutterAbility " + event + " call after detach."); + return false; + } + return true; + } + + /** + * Adds a Flutter plugin. + * @param plugin - The FlutterPlugin to add + */ + addPlugin(plugin: FlutterPlugin): void { + if (this?.delegate != null) { + this?.delegate?.addPlugin(plugin) + } + } + + /** + * Removes a Flutter plugin. + * @param plugin - The FlutterPlugin to remove + */ + removePlugin(plugin: FlutterPlugin): void { + if (this?.delegate != null) { + this?.delegate?.removePlugin(plugin) + } + } + + /** + * Called when memory level changes. + * @param level - The memory level + */ + onMemoryLevel(level: AbilityConstant.MemoryLevel): void { + Log.i(TAG, 'onMemoryLevel: ' + level); + if (level === AbilityConstant.MemoryLevel.MEMORY_LEVEL_CRITICAL) { + this?.delegate?.onLowMemory(); + } + this.delegate?.getFlutterNapi()?.SetQosOnLowMemory(level as number); + } + + /** + * Called when configuration is updated. + * @param config - The new configuration + */ + onConfigurationUpdate(config: Configuration) { + Log.i(TAG, 'onConfigurationUpdate config:' + JSON.stringify(config)); + this?.delegate?.flutterEngine?.getSettingsChannel()?.startMessage() + .setNativeSpellCheckServiceDefined(false) + .setBrieflyShowPassword(false) + .setAlwaysUse24HourFormat(I18n.System.is24HourClock()) + .setPlatformBrightness(config.colorMode != ConfigurationConstant.ColorMode.COLOR_MODE_DARK + ? PlatformBrightness.LIGHT : PlatformBrightness.DARK) + .setTextScaleFactor(config.fontSizeScale == undefined ? 1.0 : config.fontSizeScale) + .send(); //热启动生命周期内,实时监听系统设置环境改变并实时发送相应信息 + + //实时获取系统字体加粗系数 + this.delegate?.getFlutterNapi()?.setFontWeightScale(config.fontWeightScale == undefined ? 0 : + config.fontWeightScale); + Log.i(TAG, 'fontWeightScale: ' + JSON.stringify(config.fontWeightScale)); + + if (config.language != '') { + this.getFlutterEngine()?.getLocalizationPlugin()?.sendLocaleToFlutter(); + } + this?.delegate?.onCheckAndReloadFont(); + this.delegate?.changeColorMode(config.colorMode); + } + + /** + * Gets the FlutterEngine instance. + * @returns The FlutterEngine instance, or null if not available + */ + getFlutterEngine(): FlutterEngine | null { + return this.delegate?.flutterEngine || null; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbilityAndEntryDelegate.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbilityAndEntryDelegate.ets new file mode 100644 index 0000000..30bd7fc --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbilityAndEntryDelegate.ets @@ -0,0 +1,705 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import common from '@ohos.app.ability.common'; +import FlutterEngineConfigurator from './FlutterEngineConfigurator'; +import FlutterEngineProvider from './FlutterEngineProvider'; +import FlutterEngine from '../engine/FlutterEngine'; +import PlatformPlugin, { PlatformPluginDelegate } from '../../plugin/PlatformPlugin'; +import SensitiveContentPlugin from '../../plugin/view/SensitiveContentPlugin'; +import Want from '@ohos.app.ability.Want'; +import FlutterShellArgs from '../engine/FlutterShellArgs'; +import DartExecutor, { DartEntrypoint } from '../engine/dart/DartExecutor'; +import FlutterAbilityLaunchConfigs from './FlutterAbilityLaunchConfigs'; +import Log from '../../util/Log'; +import FlutterInjector from '../../FlutterInjector'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import ExclusiveAppComponent from './ExclusiveAppComponent'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import { FlutterPlugin } from '../engine/plugins/FlutterPlugin'; +import FlutterEngineCache from '../engine/FlutterEngineCache'; +import FlutterEngineGroupCache from '../engine/FlutterEngineGroupCache'; +import FlutterEngineGroup, { Options } from '../engine/FlutterEngineGroup'; +import FlutterNapi from '../engine/FlutterNapi'; +import { FlutterView } from '../../view/FlutterView'; +import FlutterManager from './FlutterManager'; +import Any from '../../plugin/common/Any'; +import inputMethod from '@ohos.inputMethod'; +import window from '@ohos.window'; +import { ConfigurationConstant } from '@kit.AbilityKit'; +import { resourceManager } from '@kit.LocalizationKit'; +import { EmbeddingNodeController } from './EmbeddingNodeController'; + +const TAG = "FlutterAbilityDelegate"; +const PLUGINS_RESTORATION_BUNDLE_KEY = "plugins"; +const FRAMEWORK_RESTORATION_BUNDLE_KEY = "framework"; + +/** + * Delegate for managing FlutterAbility and FlutterEntry lifecycle. + * Main responsibilities: + * 1. Initializes the Flutter engine + * 2. Handles ability lifecycle callbacks + */ +class FlutterAbilityAndEntryDelegate implements ExclusiveAppComponent { + protected host?: Host | null; + /** The FlutterEngine instance, or null if not created. */ + flutterEngine?: FlutterEngine | null; + /** The PlatformPlugin instance, or undefined if not set. */ + platformPlugin?: PlatformPlugin; + sensitiveContentPlugin?:SensitiveContentPlugin; + protected context?: common.Context; + protected isFlutterEngineFromHostOrCache: boolean = false; + private engineGroup?: FlutterEngineGroup; + private isHost: boolean = false; + private flutterView?: FlutterView; + private inputMethodController: inputMethod.InputMethodController = inputMethod.getController(); + private isPageShow: boolean = false; + private currentColorMode?: ConfigurationConstant.ColorMode; + + /** + * Constructs a new FlutterAbilityAndEntryDelegate instance. + * @param host - The Host instance, optional + */ + constructor(host?: Host) { + this.host = host; + if (this.host) { + this.isHost = true; + } + } + + /** + * Whether the delegate is still attached to the ability. + */ + isAttached = false; + + /** + * Called when the delegate is attached to a context. + * @param context - The application context + */ + onAttach(context: common.Context) { + this.context = context; + this.ensureAlive(); + if (this.flutterEngine == null) { + this.setupFlutterEngine(); + } + + if (this.host?.shouldAttachEngineToAbility()) { + // Notify any plugins that are currently attached to our FlutterEngine that they + // are now attached to an Ability. + Log.d(TAG, "Attaching FlutterEngine to the Ability that owns this delegate."); + this.flutterEngine?.getAbilityControlSurface()?.attachToAbility(this); + } + + this.platformPlugin = this.host?.providePlatformPlugin(this.flutterEngine!) + + this.sensitiveContentPlugin = this.host?.provideSensitiveContentPlugin(this.flutterEngine!) + + this.isAttached = true; + if (this.flutterEngine) { + this.flutterEngine.getSystemLanguages(); + const config = this.context.resourceManager.getConfigurationSync(); + this.currentColorMode = + config.colorMode === resourceManager.ColorMode.DARK ? ConfigurationConstant.ColorMode.COLOR_MODE_DARK : + ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT; + } + if (this.flutterEngine && this.flutterView && this.host?.attachToEngineAutomatically()) { + this.flutterView.attachToFlutterEngine(this.flutterEngine!!); + } + this.host?.configureFlutterEngine(this.flutterEngine!!); + if (this.flutterEngine) { + this.flutterEngine.processPendingMessages(); + } + } + + private doInitialFlutterViewRun(): void { + let initialRoute = this.host?.getInitialRoute(); + if (initialRoute == null && this.host != null) { + initialRoute = this.maybeGetInitialRouteFromIntent(this.host.getWant()); + + } + if (initialRoute == null) { + initialRoute = FlutterAbilityLaunchConfigs.DEFAULT_INITIAL_ROUTE; + } + const libraryUri = this.host?.getDartEntrypointLibraryUri(); + Log.d(TAG, + "Executing Dart entrypoint: " + this.host?.getDartEntrypointFunctionName() + ", library uri: " + libraryUri == + null ? "\"\"" : libraryUri + ", and sending initial route: " + initialRoute); + + // The engine needs to receive the Flutter app's initial route before executing any + // Dart code to ensure that the initial route arrives in time to be applied. + this.flutterEngine?.getNavigationChannel()?.setInitialRoute(initialRoute ?? ''); + + let appBundlePathOverride = this.host?.getAppBundlePath(); + if (appBundlePathOverride == null || appBundlePathOverride == '') { + appBundlePathOverride = FlutterInjector.getInstance().getFlutterLoader().findAppBundlePath(); + } + + const dartEntrypoint: DartEntrypoint = new DartEntrypoint( + appBundlePathOverride, + this.host?.getDartEntrypointLibraryUri() ?? '', + this.host?.getDartEntrypointFunctionName() ?? '' + ); + this.flutterEngine?.dartExecutor.executeDartEntrypoint(dartEntrypoint, this.host?.getDartEntrypointArgs()); + } + + private maybeGetInitialRouteFromIntent(want: Want): string { + return ''; + } + + /** + * Configures the FlutterEngine through parameters. + * @param want - The Want object containing restoration data + */ + onRestoreInstanceState(want: Want) { + let frameworkState: Uint8Array = this.getRestorationData(want.parameters as Record); + if (this.host?.shouldRestoreAndSaveState()) { + this.flutterEngine?.getRestorationChannel()?.setRestorationData(frameworkState); + } + } + + private getRestorationData(wantParam: Record): Uint8Array { + let result: Uint8Array = new Uint8Array(1).fill(0); + if (wantParam == null) { + return result; + } + if (wantParam[FRAMEWORK_RESTORATION_BUNDLE_KEY] == undefined) { + return result + } + if (typeof wantParam[FRAMEWORK_RESTORATION_BUNDLE_KEY] == 'object') { + let data: Record = wantParam[FRAMEWORK_RESTORATION_BUNDLE_KEY] as Record; + let byteArray: Array = new Array; + Object.keys(data).forEach( + key => { + byteArray.push(data[key]); + } + ); + result = Uint8Array.from(byteArray); + } + return result; + } + + /** + * Initializes the FlutterEngine. + * Checks for cached engines, engine groups, or creates a new engine. + */ + setupFlutterEngine() { + // First, check if the host wants to use a cached FlutterEngine. + const cachedEngineId = this.host?.getCachedEngineId(); + Log.d(TAG, "cachedEngineId=" + cachedEngineId); + if (cachedEngineId && cachedEngineId.length > 0) { + this.flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId); + this.isFlutterEngineFromHostOrCache = true; + if (this.flutterEngine == null) { + throw new Error( + "The requested cached FlutterEngine did not exist in the FlutterEngineCache: '" + + cachedEngineId + + "'"); + } + return; + } + + // Second, defer to subclasses for a custom FlutterEngine. + if (this.host && this.context) { + this.flutterEngine = this.host.provideFlutterEngine(this.context); + } + if (this.flutterEngine != null) { + this.isFlutterEngineFromHostOrCache = true; + return; + } + + // Third, check if the host wants to use a cached FlutterEngineGroup + // and create new FlutterEngine using FlutterEngineGroup#createAndRunEngine + const cachedEngineGroupId = this.host?.getCachedEngineGroupId(); + Log.d(TAG, "cachedEngineGroupId=" + cachedEngineGroupId); + if (cachedEngineGroupId != null) { + const flutterEngineGroup = FlutterEngineGroupCache.instance.get(cachedEngineGroupId); + if (flutterEngineGroup == null) { + throw new Error( + "The requested cached FlutterEngineGroup did not exist in the FlutterEngineGroupCache: '" + + cachedEngineGroupId + + "'"); + } + + if (this.context != null) { + this.flutterEngine = flutterEngineGroup.createAndRunEngineByOptions( + this.addEntrypointOptions(new Options(this.context))); + } + this.isFlutterEngineFromHostOrCache = false; + return; + } + + // Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our + // FlutterView. + Log.d( + TAG, + "No preferred FlutterEngine was provided. Creating a new FlutterEngine for this FlutterAbility."); + + let group = this.engineGroup; + if (group == null && this.context != null) { + group = new FlutterEngineGroup(); + const flutterShellArgs = this.host ? this.host.getFlutterShellArgs() : new FlutterShellArgs(); + group.checkLoader(this.context, flutterShellArgs.toArray() ?? []); + this.engineGroup = group; + } + if (this.context) { + this.flutterEngine = group?.createAndRunEngineByOptions(this.addEntrypointOptions(new Options(this.context) + .setWaitForRestorationData(this.host?.shouldRestoreAndSaveState() || false))); + } + this.isFlutterEngineFromHostOrCache = false; + } + + /** + * Adds entrypoint options to the engine options. + * @param options - The Options instance to configure + * @returns The configured Options instance + */ + addEntrypointOptions(options: Options): Options { + let appBundlePathOverride = this.host?.getAppBundlePath(); + if (appBundlePathOverride == null || appBundlePathOverride.length == 0) { + appBundlePathOverride = FlutterInjector.getInstance().getFlutterLoader().findAppBundlePath(); + } + + const dartEntrypoint = new DartEntrypoint(appBundlePathOverride ?? '', + '', + this.host?.getDartEntrypointFunctionName() ?? ''); + let initialRoute = this.host?.getInitialRoute(); + if (initialRoute == null && this.host != null) { + initialRoute = this.maybeGetInitialRouteFromIntent(this.host.getWant()); + } + if (initialRoute == null) { + initialRoute = FlutterAbilityLaunchConfigs.DEFAULT_INITIAL_ROUTE; + } + return options + .setDartEntrypoint(dartEntrypoint) + .setInitialRoute(initialRoute) + .setDartEntrypointArgs(this.host?.getDartEntrypointArgs() ?? []); + } + + /** + * Creates a FlutterView instance. + * @param context - The context for creating the view + * @returns A new FlutterView instance + */ + createView(context: Context): FlutterView { + this.flutterView = FlutterManager.getInstance().createFlutterView(context) + if (this.flutterEngine && this.host?.attachToEngineAutomatically()) { + this.flutterView.attachToFlutterEngine(this.flutterEngine!!); + } + this.platformPlugin?.setFlutterView(this.flutterView); + return this.flutterView + } + + /** + * Releases all held objects. + */ + release() { + this.host = null; + this.flutterEngine = null; + this.platformPlugin = undefined; + } + + /** + * Called when the delegate is detached from the ability. + * Cleans up resources and optionally destroys the engine. + */ + onDetach() { + if (this.host?.shouldAttachEngineToAbility()) { + // Notify plugins that they are no longer attached to an Ability. + Log.d(TAG, "Detaching FlutterEngine from the Ability"); + this.flutterEngine?.getAbilityControlSurface()?.detachFromAbility(); + } + this.flutterView?.detachFromFlutterEngine(); + this.host?.cleanUpFlutterEngine(this.flutterEngine!!); + + if (this.host?.shouldDispatchAppLifecycleState() && this.flutterEngine != null) { + this.flutterEngine?.getLifecycleChannel()?.appIsDetached(); + } + + if (this.platformPlugin) { + this.platformPlugin.destroy(); + } + + if (this.sensitiveContentPlugin) { + this.sensitiveContentPlugin.destroy(); + } + + // Destroy our FlutterEngine if we're not set to retain it. + if (this.host?.shouldDestroyEngineWithHost()) { + this.flutterEngine?.destroy(); + if (this.host.getCachedEngineId() != null && this.host.getCachedEngineId().length > 0) { + FlutterEngineCache.getInstance().remove(this.host.getCachedEngineId()); + } + this.flutterEngine = null; + } + + this.isAttached = false; + } + + /** + * Called when the system is running low on memory. + * Notifies Flutter about the memory pressure. + */ + onLowMemory(): void { + this.getFlutterNapi()?.notifyLowMemoryWarning(); + this.flutterEngine?.getSystemChannel()?.sendMemoryPressureWarning(); + } + + /** + * Called when the window stage is created. + * Runs the initial Flutter view. + */ + onWindowStageCreate() { + this.ensureAlive(); + this.doInitialFlutterViewRun(); + } + + /** + * Called when the window stage is destroyed. + */ + onWindowStageDestroy() { + + } + + /** + * Called when the window stage state changes. + * @param stageEventType - The window stage event type + */ + onWindowStageChanged(stageEventType: window.WindowStageEventType) { + switch (stageEventType) { + case window.WindowStageEventType.SHOWN: + Log.i(TAG, 'windowStage shown.'); + break; + case window.WindowStageEventType.ACTIVE: // 获焦状态 + Log.i(TAG, 'windowStage active.'); + this.getFlutterEngine()?.getTextInputChannel()?.textInputMethodHandler?.handleChangeFocus(true); + this.onWindowFocusChanged(true); + break; + case window.WindowStageEventType.INACTIVE: // 失焦状态 + Log.i(TAG, 'windowStage inactive.'); + this.onWindowFocusChanged(false); + break; + case window.WindowStageEventType.PAUSED: + Log.i(TAG, 'windowStage paused.'); + this.onPaused(); + break; + case window.WindowStageEventType.RESUMED: + Log.i(TAG, 'windowStage resumed.'); + this.onResumed(); + break; + case window.WindowStageEventType.HIDDEN: + Log.i(TAG, 'windowStage hidden.'); + break; + } + } + + /** + * Called when window focus changes. + * @param hasFocus - Whether the window has focus + */ + onWindowFocusChanged(hasFocus: boolean): void { + if (this.shouldDispatchAppLifecycleState()) { + this.flutterEngine?.getAbilityControlSurface()?.onWindowFocusChanged(hasFocus); + if (hasFocus) { + this.flutterEngine?.getLifecycleChannel()?.aWindowIsFocused(); + } else { + this.flutterEngine?.getLifecycleChannel()?.noWindowsAreFocused(); + } + } + } + + /** + * Called when the ability is shown. + * Notifies Flutter that the app is resumed. + */ + onShow() { + this.ensureAlive(); + this.isPageShow = true; + this.flutterView?.setActive(true); + if (this.shouldDispatchAppLifecycleState()) { + this.flutterEngine?.getLifecycleChannel()?.appIsResumed(); + } + } + + /** + * Called when the ability is paused. + * Notifies Flutter that the app is inactive. + */ + onPaused() { + if (this.shouldDispatchAppLifecycleState()) { + this.flutterEngine?.getLifecycleChannel()?.appIsInactive(); + } + } + + /** + * Called when the ability is resumed. + * Notifies Flutter that the app is resumed. + */ + onResumed() { + if (this.shouldDispatchAppLifecycleState()) { + this.flutterEngine?.getLifecycleChannel()?.appIsResumed(); + } + } + + /** + * Called when the ability is hidden. + * Notifies Flutter that the app is paused. + */ + onHide() { + if (this.shouldDispatchAppLifecycleState()) { + this.isPageShow = false; + this.flutterView?.setActive(false); + this.flutterEngine?.getLifecycleChannel()?.appIsPaused(); + } + } + + /** + * Checks and reloads fonts if needed. + */ + onCheckAndReloadFont() { + this.getFlutterNapi()?.checkAndReloadFont(); + } + + /** + * Determines whether to dispatch app lifecycle state changes. + * @returns True to dispatch lifecycle state, false otherwise + */ + shouldDispatchAppLifecycleState(): boolean { + if (!this.isHost) { + return this.isAttached; + } + if (this.host == null) { + return false; + } + if (!this.isPageShow) { + return false; + } + return this.host.shouldDispatchAppLifecycleState() && this.isAttached; + } + + /** + * Ensures the delegate is still alive. + * @throws Error if the delegate has been destroyed + */ + ensureAlive() { + if (this.isHost && this.host == null) { + throw new Error("Cannot execute method on a destroyed FlutterAbilityDelegate."); + } + } + + /** + * Gets the FlutterNapi instance. + * @returns The FlutterNapi instance, or null if not available + */ + getFlutterNapi(): FlutterNapi | null { + return this.flutterEngine?.getFlutterNapi() ?? null + } + + /** + * Gets the FlutterEngine instance. + * @returns The FlutterEngine instance, or null if not available + */ + getFlutterEngine(): FlutterEngine | null { + return this.flutterEngine ?? null; + } + + /** + * Detaches from the Flutter engine. + * @throws Error if the engine should not be detached + */ + detachFromFlutterEngine() { + if (this.host?.shouldDestroyEngineWithHost()) { + // The host owns the engine and should never have its engine taken by another exclusive + // ability. + throw new Error( + "The internal FlutterEngine created by " + + this.host + + " has been attached to by another Ability. To persist a FlutterEngine beyond the " + + "ownership of this ability, explicitly create a FlutterEngine"); + } + + // Default, but customizable, behavior is for the host to call {@link #onDetach} + // deterministically as to not mix more events during the lifecycle of the next exclusive + // ability. + this.host?.detachFromFlutterEngine(); + } + + /** + * Gets the app component (UIAbility). + * @returns The UIAbility instance + * @throws Error if the ability is null + */ + getAppComponent(): UIAbility { + const ability = this.host?.getAbility(); + if (ability == null) { + throw new Error( + "FlutterAbilityAndFragmentDelegate's getAppComponent should only " + + "be queried after onAttach, when the host's ability should always be non-null"); + } + return ability; + } + + /** + * Called when a new Want is received. + * @param want - The new Want object + * @param launchParams - The launch parameters + */ + onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void { + this.ensureAlive() + if (this.flutterEngine != null) { + Log.i(TAG, "Forwarding onNewWant() to FlutterEngine and sending pushRouteInformation message."); + this.flutterEngine?.getAbilityControlSurface()?.onNewWant(want, launchParams); + const initialRoute = this.maybeGetInitialRouteFromIntent(want); + if (initialRoute && initialRoute.length > 0) { + this.flutterEngine?.getNavigationChannel()?.pushRouteInformation(initialRoute); + } + } else { + Log.w(TAG, "onNewIntent() invoked before FlutterFragment was attached to an Ability."); + } + } + + /** + * Called to save the ability state. + * @param reason - The reason for saving state + * @param wantParam - The parameters to save state to + * @returns The save result + */ + onSaveState(reason: AbilityConstant.StateType, wantParam: Record): AbilityConstant.OnSaveResult { + Log.i(TAG, "onSaveInstanceState. Giving framework and plugins an opportunity to save state."); + this.ensureAlive(); + if (this.host?.shouldRestoreAndSaveState()) { + wantParam[FRAMEWORK_RESTORATION_BUNDLE_KEY] = this.flutterEngine!.getRestorationChannel()!.getRestorationData(); + } + if (this.host?.shouldAttachEngineToAbility()) { + const plugins: Record = {} + const result = this.flutterEngine?.getAbilityControlSurface()?.onSaveState(reason, plugins); + wantParam[PLUGINS_RESTORATION_BUNDLE_KEY] = plugins; + return result ?? AbilityConstant.OnSaveResult.ALL_REJECT + } + return AbilityConstant.OnSaveResult.ALL_REJECT + } + + /** + * Adds a Flutter plugin. + * @param plugin - The FlutterPlugin to add + */ + addPlugin(plugin: FlutterPlugin): void { + this.flutterEngine?.getPlugins()?.add(plugin) + } + + /** + * Removes a Flutter plugin. + * @param plugin - The FlutterPlugin to remove + */ + removePlugin(plugin: FlutterPlugin): void { + this.flutterEngine?.getPlugins()?.remove(plugin.getUniqueClassName()) + } + + /** + * Checks if the FlutterEngine was provided by the host or cache. + * @returns True if from host or cache, false if created by delegate + */ + isFlutterEngineFromHost(): boolean { + return this.isFlutterEngineFromHostOrCache; + } + + /** + * Initializes the window. + */ + initWindow() { + if (this.flutterEngine && this.isAttached) { + this.platformPlugin?.initWindow() + } + } + + changeColorMode(colorMode?: ConfigurationConstant.ColorMode) { + if (colorMode !== undefined && this.currentColorMode !== colorMode) { + this.currentColorMode = colorMode; + let length = this.flutterView?.getDVModel()?.children.length ?? 0; + for (let index = length - 1; index >= 0; index--) { + const dvModel = this.flutterView?.getDVModel()?.children[index]; + const params = dvModel?.getLayoutParams() as Record; + const nodeController = params['nodeController'] as EmbeddingNodeController; + if (nodeController && nodeController.rebuild) { + nodeController.rebuild(); + } + } + } + } +} + +/** + * Interface for FlutterAbility host. + * This interface extends FlutterEngineProvider, FlutterEngineConfigurator, and PlatformPluginDelegate. + */ +interface Host extends FlutterEngineProvider, FlutterEngineConfigurator, PlatformPluginDelegate { + + getAbility(): UIAbility; + + shouldDispatchAppLifecycleState(): boolean; + + detachFromFlutterEngine(): void; + + shouldAttachEngineToAbility(): boolean; + + getCachedEngineId(): string; + + getCachedEngineGroupId(): string | null; + + /** + * Returns true if the {@link io.flutter.embedding.engine.FlutterEngine} used in this delegate + * should be destroyed when the host/delegate are destroyed. + */ + shouldDestroyEngineWithHost(): boolean; + + /** Returns the {@link FlutterShellArgs} that should be used when initializing Flutter. */ + getFlutterShellArgs(): FlutterShellArgs; + + /** Returns arguments that passed as a list of string to Dart's entrypoint function. */ + getDartEntrypointArgs(): Array; + + /** + * Returns the URI of the Dart library which contains the entrypoint method (example + * "package:foo_package/main.dart"). If null, this will default to the same library as the + * `main()` function in the Dart program. + */ + getDartEntrypointLibraryUri(): string; + + /** Returns the path to the app bundle where the Dart code exists. */ + getAppBundlePath(): string; + + /** + * Returns the Dart entrypoint that should run when a new {@link io.flutter.embedding.engine.FlutterEngine} is created. + */ + getDartEntrypointFunctionName(): string; + + /** Returns the initial route that Flutter renders. */ + getInitialRoute(): string; + + getWant(): Want; + + shouldRestoreAndSaveState(): boolean; + + getExclusiveAppComponent(): ExclusiveAppComponent | null + + providePlatformPlugin(flutterEngine: FlutterEngine): PlatformPlugin | undefined + + provideSensitiveContentPlugin(flutterEngine: FlutterEngine): SensitiveContentPlugin | undefined + + /** + * Whether to automatically attach the {@link FlutterView} to the engine. + * + * In the add-to-app scenario where multiple {@link FlutterView} share the same {@link FlutterEngine}, + * the host application desires to determine the timing of attaching the {@link FlutterView} + * to the engine, for example, during the {@code onResume} instead of the {@code onCreateView}. + + * + * Defaults to {@code true}. + */ + attachToEngineAutomatically(): boolean; +} + +export { Host, FlutterAbilityAndEntryDelegate } \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbilityLaunchConfigs.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbilityLaunchConfigs.ets new file mode 100644 index 0000000..d413496 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterAbilityLaunchConfigs.ets @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +/** + * The mode of the background of a Flutter Ability, either opaque or transparent. + */ +enum BackgroundMode { + /** Indicates a Flutter Ability with an opaque background. This is the default. */ + opaque, + /** Indicates a Flutter Ability with a transparent background. */ + transparent +} + +/** + * Configuration constants for Flutter Ability launch parameters. + * This class contains metadata keys, Want extras, and default values for launching Flutter abilities. + */ +export default class FlutterAbilityLaunchConfigs { + static DART_ENTRYPOINT_META_DATA_KEY = "io.flutter.Entrypoint"; + static DART_ENTRYPOINT_URI_META_DATA_KEY = "io.flutter.EntrypointUri"; + static INITIAL_ROUTE_META_DATA_KEY = "io.flutter.InitialRoute"; + static SPLASH_SCREEN_META_DATA_KEY = "io.flutter.embedding.android.SplashScreenDrawable"; + static NORMAL_THEME_META_DATA_KEY = "io.flutter.embedding.android.NormalTheme"; + static HANDLE_DEEPLINKING_META_DATA_KEY = "flutter_deeplinking_enabled"; + // Want extra arguments. + static EXTRA_DART_ENTRYPOINT = "dart_entrypoint"; + static EXTRA_DART_ENTRYPOINT_LIBRARY_URI = "dart_entrypoint_library_uri"; + static EXTRA_INITIAL_ROUTE = "route"; + static EXTRA_BACKGROUND_MODE = "background_mode"; + static EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; + static EXTRA_DART_ENTRYPOINT_ARGS = "dart_entrypoint_args"; + static EXTRA_CACHED_ENGINE_GROUP_ID = "cached_engine_group_id"; + static EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity"; + static EXTRA_ENABLE_STATE_RESTORATION = "enable_state_restoration"; + // Default configuration. + static DEFAULT_DART_ENTRYPOINT = "main"; + static DEFAULT_INITIAL_ROUTE = "/"; + static DEFAULT_BACKGROUND_MODE = BackgroundMode.opaque; + // Preload configuration. + static PRELOAD_VIEWPORT_METRICS_KEY = "viewport_metrics"; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEngineConfigurator.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEngineConfigurator.ets new file mode 100644 index 0000000..3db0dda --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEngineConfigurator.ets @@ -0,0 +1,29 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterEngineConfigurator.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import FlutterEngine from '../engine/FlutterEngine'; + +/** + * Interface for configuring FlutterEngine instances. + * Implementations can customize engine setup and cleanup. + */ +export default interface FlutterEngineConfigurator { + /** + * Configures a FlutterEngine instance. + * @param flutterEngine - The FlutterEngine to configure + */ + configureFlutterEngine: (flutterEngine: FlutterEngine) => void; + + /** + * Cleans up a FlutterEngine instance. + * @param flutterEngine - The FlutterEngine to clean up + */ + cleanUpFlutterEngine: (flutterEngine: FlutterEngine) => void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEngineProvider.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEngineProvider.ets new file mode 100644 index 0000000..59a6bc9 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEngineProvider.ets @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterEngineProvider.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import FlutterEngine from '../engine/FlutterEngine'; +import common from '@ohos.app.ability.common'; + +/** + * Interface for providing FlutterEngine instances. + * Implementations of this interface can provide custom FlutterEngine instances. + */ +export default interface FlutterEngineProvider { + /** + * Provides a FlutterEngine instance for the given context. + * @param context - The application context + * @returns A FlutterEngine instance, or null if no engine should be provided + */ + provideFlutterEngine(context: common.Context): FlutterEngine | null; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEntry.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEntry.ets new file mode 100644 index 0000000..6b449a3 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterEntry.ets @@ -0,0 +1,468 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import FlutterEngine from '../engine/FlutterEngine'; +import PlatformPlugin from '../../plugin/PlatformPlugin'; +import Want from '@ohos.app.ability.Want'; +import FlutterShellArgs from '../engine/FlutterShellArgs'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import ExclusiveAppComponent from './ExclusiveAppComponent'; +import { FlutterAbilityAndEntryDelegate, Host } from './FlutterAbilityAndEntryDelegate'; +import FlutterAbilityLaunchConfigs from './FlutterAbilityLaunchConfigs'; +import SensitiveContentPlugin from '../../plugin/view/SensitiveContentPlugin'; +import Log from '../../util/Log'; +import { FlutterView } from '../../view/FlutterView'; +import FlutterManager from './FlutterManager'; +import window from '@ohos.window'; +import FlutterEngineConfigurator from './FlutterEngineConfigurator'; +import { FlutterPlugin } from '../engine/plugins/FlutterPlugin'; +import { BusinessError } from '@ohos.base'; +import { PlatformBrightness } from '../engine/systemchannels/SettingsChannel'; +import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant'; +import I18n from '@ohos.i18n' +import { AbilityConstant, EnvironmentCallback } from '@kit.AbilityKit'; + +const TAG = "FlutterEntry"; + +/** + * Entry point for Flutter content in a page component. + * This class manages the lifecycle of FlutterView and FlutterEngine within a page context. + */ +export default class FlutterEntry implements Host { + private static ARG_SHOULD_ATTACH_ENGINE_TO_ABILITY: string = "should_attach_engine_to_ability"; + protected uiAbility: UIAbility | null = null + protected delegate: FlutterAbilityAndEntryDelegate | null = null + protected flutterView: FlutterView | null = null + protected context: Context; + protected windowStage: window.WindowStage | null = null + private parameters: Record = {}; + protected engineConfigurator: FlutterEngineConfigurator | null = null + protected hasInit: boolean = false; + protected callbackId: number | undefined = undefined; + + /** + * Constructs a new FlutterEntry instance. + * @param context - The page context + * @param params - Optional parameters for configuration + */ + constructor(context: Context, params: Record = {}) { + this.context = context; + this.uiAbility = FlutterManager.getInstance().getUIAbility(context); + this.parameters = params; + this.windowStage = FlutterManager.getInstance().getWindowStage(this.uiAbility); + this.hasInit = false; + } + + /** + * Callback for window stage events. + * @param data - The window stage event type + */ + protected windowStageEventCallback = (data: window.WindowStageEventType) => { + this.delegate?.onWindowStageChanged(data) + } + + /** + * Called when the page is about to appear. + * Initializes the Flutter delegate and view. + */ + aboutToAppear() { + Log.i(TAG, 'aboutToAppear'); + if (this.hasInit == false) { + this.delegate = new FlutterAbilityAndEntryDelegate(this); + this.flutterView = this.delegate?.createView(this.context); + this.flutterView?.onWindowCreated(); + this?.delegate?.onAttach(this.context); + //this.flutterView?.preDraw(); + //Log.d(TAG, "XComponent aboutToAppear predraw"); + Log.i(TAG, 'onAttach end'); + this?.delegate?.platformPlugin?.setUIAbilityContext(this.uiAbility!!.context); + this.delegate?.onWindowStageCreate() + this.windowStage?.on('windowStageEvent', this.windowStageEventCallback); + this.hasInit = true; + this.delegate?.initWindow(); + this.registerEnvironmentCallback(); + } + } + + /** + * Registers an environment callback to listen for configuration changes. + */ + registerEnvironmentCallback() { + let environmentCallback: EnvironmentCallback = { + onConfigurationUpdated: (config) => { + Log.i(TAG, 'onConfigurationUpdate config: ' + JSON.stringify(config)); + this?.delegate?.flutterEngine?.getSettingsChannel()?.startMessage() + .setNativeSpellCheckServiceDefined(false) + .setBrieflyShowPassword(false) + .setAlwaysUse24HourFormat(I18n.System.is24HourClock()) + .setPlatformBrightness(config.colorMode != ConfigurationConstant.ColorMode.COLOR_MODE_DARK + ? PlatformBrightness.LIGHT : PlatformBrightness.DARK) + .setTextScaleFactor(config.fontSizeScale == undefined ? 1.0 : config.fontSizeScale) + .send(); //热启动生命周期内,实时监听系统设置环境改变并实时发送相应信息 + + //实时获取系统字体加粗系数 + this.delegate?.getFlutterNapi()?.setFontWeightScale(config.fontWeightScale == undefined ? 0 : + config.fontWeightScale); + Log.i(TAG, 'fontWeightScale: ' + JSON.stringify(config.fontWeightScale)); + + if (config.language != '') { + this.delegate?.flutterEngine?.getLocalizationPlugin()?.sendLocaleToFlutter(); + } + this?.delegate?.onCheckAndReloadFont(); + this.delegate?.changeColorMode(config.colorMode); + }, + onMemoryLevel: (level: AbilityConstant.MemoryLevel) => { + this.delegate?.getFlutterNapi()?.SetQosOnLowMemory(level as number); + } + }; + let applicationContext = this.uiAbility?.context.getApplicationContext(); + try { + this.callbackId = applicationContext?.on('environment', environmentCallback); + } catch (paramError) { + Log.e(TAG, 'registerEnvironmentCallback error: ' + (paramError as BusinessError).code + ' message: ' + + (paramError as BusinessError).message); + } + } + + /** + * Unregisters the environment callback. + */ + unregisterEnvironmentCallback() { + let applicationContext = this.uiAbility?.context.getApplicationContext(); + try { + applicationContext?.off('environment', this.callbackId, (error, data) => { + if (error && error.code !== 0) { + Log.e(TAG, 'unregisterEnvironmentCallback fail, error: ' + JSON.stringify(error)); + } + }); + } catch (paramError) { + Log.e(TAG, 'error: ' + (paramError as BusinessError).code + ' message: ' + + (paramError as BusinessError).message); + } + } + + /** + * Sets the Flutter engine configurator. + * @param configurator - The FlutterEngineConfigurator instance + */ + setFlutterEngineConfigurator(configurator: FlutterEngineConfigurator) { + this.engineConfigurator = configurator; + } + + /** + * Gets the FlutterView instance. + * @returns The FlutterView instance + */ + getFlutterView(): FlutterView { + return this.flutterView!! + } + + /** + * Gets the FlutterEngine instance. + * @returns The FlutterEngine instance, or null if not available + */ + getFlutterEngine(): FlutterEngine | null { + return this.delegate?.flutterEngine! + } + + /** + * Called when the page is about to disappear. + * Cleans up resources and detaches from the Flutter engine. + */ + aboutToDisappear() { + Log.d(TAG, "FlutterEntry aboutToDisappear"); + this.unregisterEnvironmentCallback(); + try { + this.windowStage?.off('windowStageEvent', this.windowStageEventCallback); + } catch (err) { + Log.e(TAG, "windowStage off failed"); + } + if (this.flutterView != null) { + this.flutterView.onDestroy(); + this.flutterView = null; + } + if (this.delegate != null) { + this.delegate?.onDetach(); + this.delegate?.release() + } + } + + /** + * Called when the page is shown. + * Notifies the delegate that the page is visible. + */ + onPageShow() { //生命周期 + Log.d(TAG, "FlutterEntry onPageShow"); + this?.delegate?.onShow(); + } + + /** + * Called when the page is hidden. + * Notifies the delegate that the page is hidden. + */ + onPageHide() { //生命周期 + Log.d(TAG, "FlutterEntry onPageHide"); + this?.delegate?.onHide(); + } + + /** + * Called when the back button is pressed. + * Pops the current route in Flutter navigation. + */ + onBackPress() { + Log.d(TAG, "FlutterEntry onBackPress"); + this?.delegate?.flutterEngine?.getNavigationChannel()?.popRoute(); + } + + /** + * Determines whether to dispatch app lifecycle state changes. + * @returns True to dispatch lifecycle state, false otherwise + */ + shouldDispatchAppLifecycleState(): boolean { + return true; + } + + /** + * Detaches from the Flutter engine. + */ + detachFromFlutterEngine() { + if (this?.delegate != null) { + this?.delegate?.onDetach(); + } + } + + /** + * Gets the associated UIAbility. + * @returns The UIAbility instance + */ + getAbility(): UIAbility { + return this.uiAbility!! + } + + /** + * Loads the page content. + * This method is called by the framework. + */ + loadContent() { + + } + + /** + * Determines whether to attach the engine to the ability. + * @returns True to attach the engine, false otherwise + */ + shouldAttachEngineToAbility(): boolean { + let param = this.parameters![FlutterEntry.ARG_SHOULD_ATTACH_ENGINE_TO_ABILITY]; + if (!param) { + return true; + } + return param as boolean + } + + /** + * Gets the cached engine ID from parameters. + * @returns The cached engine ID, or empty string if not set + */ + getCachedEngineId(): string { + let param = this.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_ID]; + if (!param) { + return ""; + } + return param as string + } + + /** + * Gets the cached engine group ID from parameters. + * @returns The cached engine group ID, or null if not set + */ + getCachedEngineGroupId(): string | null { + let param = this.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_GROUP_ID]; + if (!param) { + return null; + } + return param as string + } + + /** + * Determines whether to destroy the engine when the host is destroyed. + * @returns True to destroy the engine, false otherwise + */ + shouldDestroyEngineWithHost(): boolean { + if ((this.getCachedEngineId() != null && this.getCachedEngineId().length > 0) || + this.delegate!!.isFlutterEngineFromHost()) { + // Only destroy a cached engine if explicitly requested by app developer. + return false; + } + return true; + } + + /** + * Determines whether to automatically attach to the engine. + * @returns True to attach automatically, false otherwise + */ + attachToEngineAutomatically(): boolean { + return true; + } + + /** + * Gets Flutter shell arguments. + * @returns A FlutterShellArgs instance + */ + getFlutterShellArgs(): FlutterShellArgs { + return new FlutterShellArgs(); + } + + /** + * Gets Dart entrypoint arguments from parameters. + * @returns Array of entrypoint arguments + */ + getDartEntrypointArgs(): string[] { + if (this.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS]) { + return this.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT_ARGS] as Array; + } + return new Array() + } + + /** + * Gets the Dart entrypoint library URI. + * @returns The library URI string + */ + getDartEntrypointLibraryUri(): string { + return ""; + } + + /** + * Gets the app bundle path. + * @returns The bundle path string + */ + getAppBundlePath(): string { + return ""; + } + + /** + * Gets the Dart entrypoint function name from parameters. + * @returns The entrypoint function name, or default if not set + */ + getDartEntrypointFunctionName(): string { + if (this.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT]) { + return this.parameters![FlutterAbilityLaunchConfigs.EXTRA_DART_ENTRYPOINT] as string; + } + return FlutterAbilityLaunchConfigs.DEFAULT_DART_ENTRYPOINT + } + + /** + * Gets the initial route from parameters. + * @returns The initial route string, or empty string if not set + */ + getInitialRoute(): string { + if (this.parameters![FlutterAbilityLaunchConfigs.EXTRA_INITIAL_ROUTE]) { + return this.parameters![FlutterAbilityLaunchConfigs.EXTRA_INITIAL_ROUTE] as string + } + return ""; + } + + /** + * Gets the Want object. + * @returns A new Want instance + */ + getWant(): Want { + return new Want(); + } + + /** + * Determines whether to restore and save state. + * @returns True to restore and save state, false otherwise + */ + shouldRestoreAndSaveState(): boolean { + if (this.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_ID] != undefined) { + return this.parameters![FlutterAbilityLaunchConfigs.EXTRA_CACHED_ENGINE_ID] as boolean; + } + if (this.getCachedEngineId() != null && this.getCachedEngineId().length > 0) { + // Prevent overwriting the existing state in a cached engine with restoration state. + return false; + } + return true; + } + + /** + * Gets the exclusive app component. + * @returns The ExclusiveAppComponent instance, or null + */ + getExclusiveAppComponent(): ExclusiveAppComponent | null { + return this.delegate ? this.delegate : null + } + + /** + * Provides a FlutterEngine instance. + * @param context - The context + * @returns A FlutterEngine instance, or null if not provided + */ + provideFlutterEngine(context: Context): FlutterEngine | null { + return null; + } + + /** + * Provides a PlatformPlugin instance. + * @param flutterEngine - The FlutterEngine instance + * @returns A PlatformPlugin instance + */ + providePlatformPlugin(flutterEngine: FlutterEngine): PlatformPlugin | undefined { + return new PlatformPlugin(flutterEngine.getPlatformChannel()!, this.context, this); + } + + /** + * Provides a SensitiveContentPlugin instance. + * @param flutterEngine - The FlutterEngine instance + * @returns A SensitiveContentPlugin instance + */ + provideSensitiveContentPlugin(flutterEngine: FlutterEngine): SensitiveContentPlugin | undefined { + return new SensitiveContentPlugin(flutterEngine.getSensitiveContentChannel()!); + } + + /** + * Configures the Flutter engine. + * @param flutterEngine - The FlutterEngine to configure + */ + configureFlutterEngine(flutterEngine: FlutterEngine) { + if (this.engineConfigurator) { + this.engineConfigurator.configureFlutterEngine(flutterEngine) + } + } + + /** + * Cleans up the Flutter engine. + * @param flutterEngine - The FlutterEngine to clean up + */ + cleanUpFlutterEngine(flutterEngine: FlutterEngine) { + if (this.engineConfigurator) { + this.engineConfigurator.cleanUpFlutterEngine(flutterEngine) + } + } + + /** + * Determines whether to pop the system navigator. + * @returns False by default + */ + popSystemNavigator(): boolean { + return false; + } + + /** + * Adds a Flutter plugin. + * @param plugin - The FlutterPlugin to add + */ + addPlugin(plugin: FlutterPlugin): void { + this.delegate?.addPlugin(plugin) + } + + /** + * Removes a Flutter plugin. + * @param plugin - The FlutterPlugin to remove + */ + removePlugin(plugin: FlutterPlugin): void { + this.delegate?.removePlugin(plugin) + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterManager.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterManager.ets new file mode 100644 index 0000000..81a2780 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterManager.ets @@ -0,0 +1,575 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + + +import { FlutterView } from '../../view/FlutterView'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import window from '@ohos.window'; +import Log from '../../util/Log'; +import HashMap from '@ohos.util.HashMap'; +import List from '@ohos.util.List'; +import { deviceInfo } from '@kit.BasicServicesKit'; +import { common } from '@kit.AbilityKit'; + +const TAG = "FlutterManager" + +/** + * Singleton manager for Flutter views and UI abilities. + * This class manages the lifecycle of FlutterView instances and their association with UI abilities. + */ +export default class FlutterManager { + private static instance: FlutterManager; + + /** + * Gets the singleton instance of FlutterManager. + * @returns The singleton FlutterManager instance + */ + static getInstance(): FlutterManager { + if (FlutterManager.instance == null) { + FlutterManager.instance = new FlutterManager(); + } + return FlutterManager.instance; + } + + private flutterViewList = new Map(); + private flutterViewIndex = 1; + private uiAbilityList = new Array(); + private windowStageList = new Map(); + private mFullScreenListener: FullScreenListener = new DefaultFullScreenListener(); + + private dragEnterCbId: number = 1; + private dragMoveCbId: number = 1; + private dragLeaveCbId: number = 1; + private dropCbId: number = 1; + + private dragEnterCbs: HashMap = new HashMap(); + private dragMoveCbs: HashMap = new HashMap(); + private dragLeaveCbs: HashMap = new HashMap(); + private dropCbs: HashMap = new HashMap(); + + private getValuesFromMap(map: HashMap): List { + let list: List = new List(); + map.forEach((value, key) => { + list.add(value); + }); + return list; + } + + /** + * Gets all drag enter callbacks. + * @returns A list of drag enter callbacks + */ + getDragEnterCbs(): List { + return this.getValuesFromMap(this.dragEnterCbs); + } + + /** + * Gets all drag move callbacks. + * @returns A list of drag move callbacks + */ + getDragMoveCbs(): List { + return this.getValuesFromMap(this.dragMoveCbs); + } + + /** + * Gets all drag leave callbacks. + * @returns A list of drag leave callbacks + */ + getDragLeaveCbs(): List { + return this.getValuesFromMap(this.dragLeaveCbs); + } + + /** + * Gets all drop callbacks. + * @returns A list of drop callbacks + */ + getDropCbs(): List { + return this.getValuesFromMap(this.dropCbs); + } + + /** + * Adds a drag enter callback. + * @param callback - The drag enter callback + * @returns The callback ID + */ + addDragEnterCb(callback: DragDropCallback): number { + this.dragEnterCbs.set(this.dragEnterCbId, callback); + return this.dragEnterCbId++; + } + + /** + * Adds a drag move callback. + * @param callback - The drag move callback + * @returns The callback ID + */ + addDragMoveCb(callback: DragDropCallback): number { + this.dragMoveCbs.set(this.dragMoveCbId, callback); + return this.dragMoveCbId++; + } + + /** + * Adds a drag leave callback. + * @param callback - The drag leave callback + * @returns The callback ID + */ + addDragLeaveCb(callback: DragDropCallback): number { + this.dragLeaveCbs.set(this.dragLeaveCbId, callback); + return this.dragLeaveCbId++; + } + + /** + * Adds a drop callback. + * @param callback - The drop callback + * @returns The callback ID + */ + addDropCb(callback: DragDropCallback): number { + this.dropCbs.set(this.dropCbId, callback); + return this.dropCbId++; + } + + /** + * Removes a drag enter callback. + * @param id - The callback ID to remove + */ + removeDragEnterCb(id: number) { + this.dragEnterCbs.remove(id); + } + + /** + * Removes a drag move callback. + * @param id - The callback ID to remove + */ + removeDragMoveCb(id: number) { + this.dragMoveCbs.remove(id); + } + + /** + * Removes a drag leave callback. + * @param id - The callback ID to remove + */ + removeDragLeaveCb(id: number) { + this.dragLeaveCbs.remove(id); + } + + /** + * Removes a drop callback. + * @param id - The callback ID to remove + */ + removeDropCb(id: number) { + this.dropCbs.remove(id); + } + + /** + * Pushes a UIAbility to the list. + * @param uiAbility - The UIAbility to add + */ + pushUIAbility(uiAbility: UIAbility) { + this.uiAbilityList.push(uiAbility); + } + + /** + * Removes a UIAbility from the list. + * @param uiAbility - The UIAbility to remove + */ + popUIAbility(uiAbility: UIAbility) { + let index = this.uiAbilityList.findIndex((item: UIAbility) => item == uiAbility) + if (index >= 0) { + this.uiAbilityList.splice(index, 1) + } + } + + /** + * Associates a WindowStage with a UIAbility. + * @param uiAbility - The UIAbility + * @param windowStage - The WindowStage to associate + */ + pushWindowStage(uiAbility: UIAbility, windowStage: window.WindowStage) { + this.windowStageList.set(uiAbility, windowStage) + } + + /** + * Removes the WindowStage association for a UIAbility. + * @param uiAbility - The UIAbility + */ + popWindowStage(uiAbility: UIAbility) { + this.windowStageList.delete(uiAbility) + } + + /** + * Gets the WindowStage for a UIAbility. + * @param uiAbility - The UIAbility + * @returns The associated WindowStage + */ + getWindowStage(uiAbility: UIAbility): window.WindowStage { + return this.windowStageList.get(uiAbility)!! + } + + /** + * Gets a UIAbility by context, or returns the first one if no context is provided. + * @param context - Optional context to search for + * @returns The UIAbility instance + */ + getUIAbility(context?: Context): UIAbility { + if (!context && this.uiAbilityList.length > 0) { + return this.uiAbilityList[0]; + } + return this.uiAbilityList.find((item: UIAbility) => item.context == context)!! + } + + /** + * Checks if a FlutterView exists with the given ID. + * @param viewId - The view ID to check + * @returns True if the view exists, false otherwise + */ + hasFlutterView(viewId: string): boolean { + return this.flutterViewList.has(viewId); + } + + /** + * Gets a FlutterView by ID. + * @param viewId - The view ID + * @returns The FlutterView instance, or null if not found + */ + getFlutterView(viewId: string): FlutterView | null { + return this.flutterViewList.get(viewId) ?? null; + } + + /** + * Gets all FlutterView instances. + * @returns A map of all FlutterView instances by ID + */ + getFlutterViewList(): Map { + return this.flutterViewList; + } + + /** + * Stores or removes a FlutterView in the list. + * @param viewId - The view ID + * @param flutterView - The FlutterView instance, or undefined to remove + */ + private putFlutterView(viewId: string, flutterView?: FlutterView): void { + if (flutterView != null) { + this.flutterViewList.set(viewId, flutterView); + } else { + this.flutterViewList.delete(viewId); + } + } + + /** + * Creates a new FlutterView instance. + * It's suggested to keep 'oh_flutter_' as the prefix for xcomponent_id. + * Otherwise it might affect the performance. + * @param context - The context for creating the view + * @returns A new FlutterView instance + */ + createFlutterView(context: Context): FlutterView { + let flutterView = new FlutterView(`oh_flutter_${this.flutterViewIndex++}`, context); + this.putFlutterView(flutterView.getId(), flutterView); + return flutterView; + } + + /** + * Gets the next FlutterView ID that will be used. + * @param idOffset - Optional offset to add to the index + * @returns The next FlutterView ID string + */ + getNextFlutterViewId(idOffset: number = 0): string { + return `oh_flutter_${this.flutterViewIndex + idOffset}`; + } + + /** + * Clears all FlutterView instances. + */ + clear(): void { + this.flutterViewList.clear(); + } + + /** + * Sets the full screen listener. + * @param listener - The FullScreenListener instance + */ + setFullScreenListener(listener: FullScreenListener) { + this.mFullScreenListener = listener + } + + /** + * Gets the full screen listener. + * @returns The FullScreenListener instance + */ + getFullScreenListener(): FullScreenListener { + return this.mFullScreenListener; + } + + /** + * Sets whether to use full screen mode. + * @param use - Whether to use full screen + * @param context - Optional context + */ + setUseFullScreen(use: boolean, context?: Context | null | undefined) { + this.mFullScreenListener.setUseFullScreen(use, context); + } + + /** + * Checks if full screen mode is enabled. + * @returns True if full screen is enabled, false otherwise + */ + useFullScreen(): boolean { + return this.mFullScreenListener.useFullScreen(); + } + + /** + * Deletes a FlutterView from the list. + * @param viewId - The view ID + * @param flutterView - Optional FlutterView instance to verify + */ + deleteFlutterView(viewId: string, flutterView?: FlutterView): void { + if (flutterView != null) { + this.flutterViewList.delete(viewId); + } + } + + /** + * Get window ID for notifyPageChanged. + * @param context The context to get UIAbility + * @returns The window ID, or 0 if failed + */ + getWindowId(context: common.Context): number { + try { + const uiAbility = this.getUIAbility(context); + if (uiAbility == null) { + Log.e(TAG, "getWindowId: uiAbility is null"); + return 0; + } + const windowStage = this.getWindowStage(uiAbility); + if (windowStage == null) { + Log.e(TAG, "getWindowId: windowStage is null"); + return 0; + } + const mainWindow = windowStage.getMainWindowSync(); + if (mainWindow == null) { + Log.e(TAG, "getWindowId: mainWindow is null"); + return 0; + } + // get windowID for notifyPageChanged. + const windowId = mainWindow.getWindowProperties()?.id ?? 0; + return windowId; + } catch (error) { + Log.e(TAG, "getWindowId failed: " + JSON.stringify(error)); + } + return 0; + } + + /** + * Sets the visibility of a specific system bar and applies safe area padding. + * When hiding a system bar, automatically applies padding to avoid content being covered. + * @param flutterViewId - The ID of the FlutterView to apply padding + * @param specificSystemBar - System bar type: 'status' for status bar, 'navigation' for three-key navigation bar, 'navigationIndicator' for gesture navigation bar + * @param isVisible - True to show, false to hide + * @param enableAnimation - Whether to enable animation effect, optional + * @param context - Optional context, uses the first UIAbility if not provided + */ + setSpecificSystemBarEnabled(flutterViewId: string, specificSystemBar: 'status' | 'navigation' | 'navigationIndicator', isVisible: boolean, enableAnimation?: boolean, context?: Context): void { + const flutterView: FlutterView | null = this.getFlutterView(flutterViewId); + if (!flutterView) { + Log.e(TAG, `setSpecificSystemBarEnabled failed: flutterView not found for id ${flutterViewId}`); + return; + } + + try { + const windowStage = this.getWindowStage(this.getUIAbility(context)); + if (!windowStage) { + Log.e(TAG, 'setSpecificSystemBarEnabled failed: windowStage is null'); + return; + } + + const mainWindow = windowStage.getMainWindowSync(); + if (!mainWindow) { + Log.e(TAG, 'setSpecificSystemBarEnabled failed: mainWindow is null'); + return; + } + mainWindow.setSpecificSystemBarEnabled(specificSystemBar, isVisible, enableAnimation); + + switch (specificSystemBar) { + case 'status': + // Apply top padding when hiding status bar to avoid content being covered + const statusBarHeight = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height; + if (statusBarHeight > 0) { + flutterView.setPaddingTop(statusBarHeight); + } + break; + case 'navigation': + case 'navigationIndicator': + // Apply bottom padding when hiding navigation bar to avoid content being covered + const navHeight = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect.height; + if (navHeight > 0) { + flutterView.setPaddingBottom(navHeight); + } + break; + } + + Log.i(TAG, `setSpecificSystemBarEnabled: ${specificSystemBar} = ${isVisible}, animation = ${enableAnimation} success`); + } catch (error) { + Log.e(TAG, `setSpecificSystemBarEnabled error: ${JSON.stringify(error)}`); + } + } + + /** + * Adjusts FlutterView top padding to avoid window decoration overlap. + * @param flutterViewId - The FlutterView ID + * @param context - Optional context, defaults to first UIAbility + */ + handleWindowDecorSafeArea(flutterViewId: string, context?: Context | null | undefined): void { + if (!this.isWindowDecorSafeAreaAvoidSupported(context)) { + return; + } + const flutterView = this.getFlutterView(flutterViewId); + if (!flutterView) { + return; + } + const mainWindow = this.getWindowStage(this.getUIAbility(context ?? undefined))?.getMainWindowSync(); + if (!mainWindow) { + return; + } + // Clear padding if window decoration is visible + if (mainWindow.getWindowDecorVisible() ) { + flutterView.setPaddingTop(0); + return; + } + // Clear padding if title buttons are hidden + const titleButtonRect = mainWindow.getTitleButtonRect(); + if (!titleButtonRect || titleButtonRect.height <= 0) { + flutterView.setPaddingTop(0); + return; + } + // Set padding to match title button height + flutterView.setPaddingTop(vp2px(titleButtonRect.height)); + } + + /** + * Checks if window decoration safe area avoidance is supported on the current device. + * Returns true only when the device is in freeform multi-window mode and supports window decoration. + * @param context - Optional context, defaults to first UIAbility + * @returns True if window decoration safe area avoidance is supported, false otherwise + */ + isWindowDecorSafeAreaAvoidSupported(context?: Context | null | undefined): boolean { + // API 18+ required for window decoration APIs + if (deviceInfo.sdkApiVersion < 18) { + return false; + } + const mainWindow = this.getWindowStage(this.getUIAbility(context ?? undefined))?.getMainWindowSync(); + if (!mainWindow) { + return false; + } + try { + // Avoid compilation errors: getWindowDecorVisible is an API18 interface + // Check if getWindowDecorVisible API is available + const result = typeof (mainWindow as ESObject)?.getWindowDecorVisible(); + return result == 'boolean'; + } catch (exception) { + Log.i(TAG, `Failed to check window decor visibility support. Cause code: ${exception.code}, message: ${exception.message}`); + return false; + } + } +} + +/** + * Interface for drag and drop callbacks. + */ +export interface DragDropCallback { + /** + * Handles a drag and drop event. + * @param event - The drag event + * @param extraParams - Additional parameters + */ + do(event: DragEvent, extraParams: string): void; +} + +/** + * Interface for full screen state management. + */ +export interface FullScreenListener { + /** + * Checks if full screen mode is enabled. + * @returns True if full screen is enabled, false otherwise + */ + useFullScreen(): boolean; + + /** + * Sets whether to use full screen mode. + * @param useFullScreen - Whether to use full screen + * @param context - Optional context + */ + setUseFullScreen(useFullScreen: boolean, context?: Context | null | undefined): void; + + /** + * Called when the screen state changes. + * @param data - The window status type + */ + onScreenStateChanged(data: window.WindowStatusType): void; +} + +/** + * Default implementation of FullScreenListener. + */ +export class DefaultFullScreenListener implements FullScreenListener { + private fullScreen: boolean = true; + private skipCheck: boolean = false; + + /** + * Checks if full screen mode is enabled. + * @returns True if full screen is enabled, false otherwise + */ + useFullScreen(): boolean { + return this.fullScreen; + } + + /** + * Sets whether to use full screen mode. + * @param useFullScreen - Whether to use full screen + * @param context - Optional context + */ + setUseFullScreen(useFullScreen: boolean, context?: Context | null | undefined): void { + this.fullScreen = useFullScreen; + this.skipCheck = true; + + context = context??getContext(this); + const currentWindow = FlutterManager.getInstance() + .getWindowStage(FlutterManager.getInstance().getUIAbility(context)); + + currentWindow.getMainWindowSync().setWindowLayoutFullScreen(useFullScreen); + + if (deviceInfo.deviceType === '2in1' && deviceInfo.sdkApiVersion >= 14 && useFullScreen) { + currentWindow.getMainWindowSync().maximize(window.MaximizePresentation.ENTER_IMMERSIVE); + } + + Log.i(TAG, "WindowLayoutFullScreen is on") + } + + /** + * Called when the screen state changes. + * @param data - The window status type + */ + onScreenStateChanged(data: window.WindowStatusType): void { + if (this.skipCheck) { + Log.i(TAG, "onScreenStateChanged: skipCheck is on, WindowStatusType = " + data) + return; + } + switch (data) { + case window.WindowStatusType.FULL_SCREEN: + + case window.WindowStatusType.SPLIT_SCREEN: + case window.WindowStatusType.FLOATING: + case window.WindowStatusType.MAXIMIZE: + this.fullScreen = true; + Log.i(TAG, "onScreenStateChanged: fullScreen = true") + break; + default: + this.fullScreen = false; + Log.i(TAG, "onScreenStateChanged: fullScreen = false") + break; + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterPage.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterPage.ets new file mode 100644 index 0000000..ed4c4f7 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/FlutterPage.ets @@ -0,0 +1,354 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import Log from '../../util/Log'; +import { FlutterView } from '../../view/FlutterView'; +import FlutterManager from './FlutterManager'; +import { DVModel, DVModelChildren, DynamicView } from '../../view/DynamicView/dynamicView'; +import Any from '../../plugin/common/Any'; +import deviceInfo from '@ohos.deviceInfo'; +import flutter from 'libflutter.so'; +const TAG = "FlutterPage"; + + +/** + * Basic page component that hosts XComponent for Flutter rendering. + * This component handles the display of Flutter content within an OpenHarmony page. + */ +@Component +export struct FlutterPage { + /** Safe area edges to expand, or undefined for default. */ + @Prop safeAreaEdges: SafeAreaEdge[] | undefined = []; + /** Safe area types to expand, or undefined for default. */ + @Prop safeAreaTypes: SafeAreaType[] | undefined = []; + /** The unique identifier for the XComponent view. */ + @Prop viewId: string = "" + /** The XComponent type for rendering. */ + @Prop xComponentType: XComponentType = XComponentType.SURFACE + /** + * renderFit under XComponent has a default setting of RESIZE_FILL. + * If the size of XComponent may change, this property needs to be passed in and set to a size-preserving property, + * such as TOP_LEFT. + */ + @Prop xComponentRenderFit: RenderFit = RenderFit.RESIZE_FILL; + + /** + * A switch for enabling the frame cache. + * When it is true, one frame of response latency will be increased in exchange for higher smoothness, + * and occasional timeouts in rendering frame submissions will not result in frame dropping. + */ + @Prop enableFrameCacheForSmooth: boolean = true; + + /** + * Empty builder function used as default. + */ + @Builder + doNothingBuilder() { + } + + defaultFocusOnTouch = false; + + @BuilderParam splashScreenView: () => void = this.doNothingBuilder; + + /** + * Default page builder that displays Flutter content with XComponent. + */ + @Builder + defaultPage() { + Stack() { + ForEach(this.rootDvModel!!, (child: ESObject) => { + DynamicView({ + model: child as DVModel, + params: child.params, + events: child.events, + children: child.children, + customBuilder: child.builder + }) + }, (child: ESObject) => `${child.id_}`) + + Text("").id("unfocus-xcomponent-node").focusable(true) + + XComponent({ id: this.viewId, type: this.xComponentType, libraryname: 'flutter' }) + .id(this.viewId) + .focusable(true) + .focusOnTouch(this.defaultFocusOnTouch) + .onLoad((context) => { + this.flutterView?.onSurfaceCreated(); + // Callback is triggered when the xcomponent window is partially visible or completely hidden. + if (deviceInfo.sdkApiVersion < 15) { + this.getUIContext()?.getAttachedFrameNodeById(this.viewId)?.commonEvent.setOnVisibleAreaApproximateChange( + { ratios: [0.0, 1.0], expectedUpdateInterval: 0 }, + (isExpanding: boolean, currentRatio: number) => { + if (isExpanding) { + Log.i(TAG, "setOnVisibleAreaApproximateChange -> xcomponentId: " + this.viewId + + " isExpanding: " + isExpanding + " ratio: " + currentRatio); + flutter.nativeUpdateCurrentXComponentId(this.viewId); + } + } + ) + } + Log.d(TAG, "XComponent onLoad "); + }) + .onDestroy(() => { + Log.d(TAG, "XComponent onDestroy "); + this.flutterView?.onSurfaceDestroyed() + }) + .renderFit(this.xComponentRenderFit) + .backgroundColor(this.firstFrameDisplayed && this.xComponentRenderFit == RenderFit.RESIZE_FILL ? + this.xComponentColor : Color.Transparent) + .expandSafeArea(this.safeAreaTypes, this.safeAreaEdges) + .onAreaChange((oldValue: Area, newValue: Area) => { + // Only handle when Y position changes (window decoration state changed) + if (oldValue.globalPosition.y != newValue.globalPosition.y) { + FlutterManager.getInstance().handleWindowDecorSafeArea(this.viewId, this.getUIContext().getHostContext()); + } + }) + if (this.showSplashScreen) { + this.splashScreenView(); + } + } + .defaultFocus(true) + .onKeyPreIme((event: KeyEvent) => { + return this.flutterView?.onKeyPreIme(event) ?? false; + }) + .onKeyEvent((event: KeyEvent) => { + return this.flutterView?.onKeyEvent(event) ?? false; + }) + .onDragEnter((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDragEnterCbs().forEach(dragEnterCb => { + dragEnterCb.do(event, extraParams); + }); + Log.d(TAG, "onDragEnter"); + }) + .onDragMove((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDragMoveCbs().forEach(dragMoveCb => { + dragMoveCb.do(event, extraParams); + }); + Log.d(TAG, "onDragMove"); + }) + .onDragLeave((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDragLeaveCbs().forEach(dragLeaveCb => { + dragLeaveCb.do(event, extraParams); + }); + Log.d(TAG, "onDragLeave"); + }) + .onDrop((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDropCbs().forEach(dropCb => { + dropCb.do(event, extraParams); + }); + Log.d(TAG, "onDrop"); + }) + } + + /** + * Page builder that supports mouse wheel gestures. + */ + @Builder + mouseWheelPage() { + Stack() { + ForEach(this.rootDvModel!!, (child: Any) => { + DynamicView({ + model: child as DVModel, + params: child.params, + events: child.events, + children: child.children, + customBuilder: child.builder + }) + }, (child: ESObject) => `${child.id_}`) + + Text("").id("unfocus-xcomponent-node").focusable(true) + + XComponent({ id: this.viewId, type: this.xComponentType, libraryname: 'flutter' }) + .id(this.viewId) + .focusable(true) + .focusOnTouch(this.defaultFocusOnTouch) + .onLoad((context) => { + this.flutterView?.onSurfaceCreated(); + // Callback is triggered when the xcomponent window is partially visible or completely hidden. + if (deviceInfo.sdkApiVersion < 15) { + this.getUIContext()?.getAttachedFrameNodeById(this.viewId)?.commonEvent.setOnVisibleAreaApproximateChange( + { ratios: [0.0, 1.0], expectedUpdateInterval: 0 }, + (isExpanding: boolean, currentRatio: number) => { + if (isExpanding) { + Log.i(TAG, "setOnVisibleAreaApproximateChange -> xcomponentId: " + this.viewId + + " isExpanding: " + isExpanding + " ratio: " + currentRatio); + flutter.nativeUpdateCurrentXComponentId(this.viewId); + } + } + ) + } + Log.d(TAG, "XComponent onLoad "); + }) + .onDestroy(() => { + Log.d(TAG, "XComponent onDestroy "); + this.flutterView?.onSurfaceDestroyed() + }) + .renderFit(this.xComponentRenderFit) + .backgroundColor(this.firstFrameDisplayed && this.xComponentRenderFit == RenderFit.RESIZE_FILL ? + this.xComponentColor : Color.Transparent) + .expandSafeArea(this.safeAreaTypes, this.safeAreaEdges) + .onAreaChange((oldValue: Area, newValue: Area) => { + // Only handle when Y position changes (window decoration state changed) + if (oldValue.globalPosition.y != newValue.globalPosition.y) { + FlutterManager.getInstance().handleWindowDecorSafeArea(this.viewId, this.getUIContext().getHostContext()); + } + }) + + if (this.showSplashScreen) { + this.splashScreenView(); + } + } + .defaultFocus(true) + .onKeyPreIme((event: KeyEvent) => { + return this.flutterView?.onKeyPreIme(event) ?? false; + }) + .onKeyEvent((event: KeyEvent) => { + this.flutterView?.onKeyEvent(event) + }) + .onDragEnter((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDragEnterCbs().forEach(dragEnterCb => { + dragEnterCb.do(event, extraParams); + }); + Log.d(TAG, "onDragEnter"); + }) + .onDragMove((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDragMoveCbs().forEach(dragMoveCb => { + dragMoveCb.do(event, extraParams); + }); + Log.d(TAG, "onDragMove"); + }) + .onDragLeave((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDragLeaveCbs().forEach(dragLeaveCb => { + dragLeaveCb.do(event, extraParams); + }); + Log.d(TAG, "onDragLeave"); + }) + .onDrop((event: DragEvent, extraParams: string) => { + FlutterManager.getInstance().getDropCbs().forEach(dropCb => { + dropCb.do(event, extraParams); + }); + Log.d(TAG, "onDrop"); + }) + .gesture( + PanGesture(this.panOption) + .onActionStart((event: GestureEvent) => { + this.flutterView?.onMouseWheel("actionStart", event); + }) + .onActionUpdate((event: GestureEvent) => { + this.flutterView?.onMouseWheel("actionUpdate", event); + }) + .onActionEnd((event: GestureEvent) => { + this.flutterView?.onMouseWheel("actionEnd", event); + }) + ) + } + + /** Whether to show the splash screen. */ + @State showSplashScreen: boolean = true; + /** + * To address the black(or other color set by usr) flashing frame when switching between ArkUI and Flutter pages, + * the background color should be kept transparent until the onFirstFrame is called. + * When the window size changes, modifying the renderFit property in the relevant callback does not take effect immediately. + * The first frame will use the old renderFit property and the old background color, resulting in visual artifacts (such as stretching or a black screen). + * Therefore, we cannot automatically change the relevant properties through state variables at this time. + */ + @State firstFrameDisplayed: boolean = false; + /** Background color for the XComponent. */ + @State xComponentColor: Color = Color.Black + + /** Whether to check for full screen mode. */ + @State checkFullScreen: boolean = true; + /** Whether to check for keyboard visibility. */ + @State checkKeyboard: boolean = true; + /** Whether to check for gesture navigation. */ + @State checkGesture: boolean = true; + /** Whether to enable mouse wheel gesture support. */ + @State checkMouseWheel: boolean = true; + /** Whether to check for AI bar. */ + @State checkAiBar: boolean = true; + /** Top padding value, or undefined if not set. */ + @Prop @Watch("onPaddingChange")paddingTop?: number = undefined; + /** Storage link for node width. */ + @StorageLink('nodeWidth') storageLinkWidth: number = 0; + /** Storage link for node height. */ + @StorageLink('nodeHeight') storageLinkHeight: number = 0; + + /** Root dynamic view model children, or undefined if not set. */ + @State rootDvModel: DVModelChildren | undefined = undefined + + private flutterView?: FlutterView | null + private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Up | PanDirection.Down }); + + /** + * Called when the page is about to appear. + * Initializes the FlutterView and sets up listeners and callbacks. + */ + aboutToAppear() { + this.flutterView = FlutterManager.getInstance().getFlutterView(this.viewId); + this.flutterView?.addFirstFrameListener(this) + this.flutterView?.addFirstPreloadFrameListener(this) + + // api18开始支持getDistance()接口 + if (deviceInfo.sdkApiVersion >= 18) { + this.flutterView?.setTouchSlopCallbackValue(() => { + return this.panOption.getDistance() + }) + } + + this.flutterView?.setCheckFullScreen(this.checkFullScreen) + this.flutterView?.setCheckKeyboard(this.checkKeyboard) + this.flutterView?.setCheckGesture(this.checkGesture) + this.flutterView?.setPaddingTop(this.paddingTop) + this.flutterView?.setCheckAiBar(this.checkAiBar) + this.flutterView?.enableFrameCache(this.enableFrameCacheForSmooth); + + this.rootDvModel = this.flutterView!!.getDVModel().children + } + + /** + * Called when the page is about to disappear. + * Removes frame listeners to clean up resources. + */ + aboutToDisappear() { + this.flutterView?.removeFirstFrameListener(this); + this.flutterView?.removeFirstPreloadFrameListener(this) + } + + /** + * Called when the first frame is displayed. + * Hides the splash screen and marks the first frame as displayed. + */ + onFirstFrame() { + this.showSplashScreen = false; + this.firstFrameDisplayed = true; + } + + /** + * Called when the first preload frame is displayed. + */ + onFirstPreloadFrame() { + } + + /** + * Called when padding changes. + * Updates the FlutterView's padding. + */ + onPaddingChange() { + this.flutterView?.setPaddingTop(this.paddingTop); + } + + /** + * Builds the page UI. + * Selects between mouse wheel page and default page based on configuration. + */ + build() { + if (this.checkMouseWheel) { + this.mouseWheelPage(); + } else { + this.defaultPage(); + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyData.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyData.ets new file mode 100644 index 0000000..8c43af1 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyData.ets @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2021-2025 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + +import util from '@ohos.util' + +/** + * Represents key event data for communication between OpenHarmony and Flutter. + * This class can serialize and deserialize key data to/from binary format. + */ +export default class KeyData { + private static TAG = "KeyData"; + public static CHANNEL = "flutter/keydata"; + // If this value changes, update the code in the following files: + // + // * key_data.h (kKeyDataFieldCount) + // * platform_dispatcher.dart (_kKeyDataFieldCount) + private static FIELD_COUNT: number = 6; + private static BYTES_PER_FIELD: number = 8; + /** Timestamp of the key event. */ + public timestamp: number = 0; + /** Type of the key event (KDOWN, KUP, or KREPEAT). */ + public type: Type = Type.KDOWN; + /** Physical key code. */ + public physicalKey: number = 0; + /** Logical key code. */ + public logicalKey: number = 0; + /** Whether this key event was synthesized. */ + public isSynthesized: boolean = false; + /** Type of the input device. */ + public deviceType: DeviceType = DeviceType.KKEYBOARD; + /** Character representation of the key, or null if not applicable. */ + public character: string | null = null; + + /** + * Constructs a new KeyData instance. + * @param buffer - Optional ArrayBuffer to deserialize key data from + */ + constructor(buffer?: ArrayBuffer) { + if (buffer !== undefined) { + const view = new DataView(buffer); + let offset = 0; + + const decoder = new util.TextDecoder("utf-8"); + const charSize = Number(view.getBigInt64(offset, true)); + offset += 8; + + this.timestamp = Number(view.getBigInt64(offset, true)); + offset += 8; + + this.type = Number(view.getBigInt64(offset, true)) as Type; + offset += 8; + + this.physicalKey = Number(view.getBigInt64(offset, true)); + offset += 8; + + this.logicalKey = Number(view.getBigInt64(offset, true)); + offset += 8; + + this.isSynthesized = view.getBigInt64(offset, true) === BigInt(1); + offset += 8; + + this.deviceType = Number(view.getBigInt64(offset, true)) as DeviceType; + offset += 8; + + if (offset + charSize !== buffer.byteLength) { + throw new Error("KeyData corruption: String length does not match remaining bytes in buffer"); + } + + if (charSize != 0) { + const strBytes = new Uint8Array(buffer, offset, charSize); + this.character = decoder.decode(strBytes); + } + } + } + + /** + * Serializes this KeyData instance to a binary ArrayBuffer. + * @returns The serialized key data as an ArrayBuffer + */ + public toBytes(): ArrayBuffer { + const encoder = new util.TextEncoder("utf-8"); + const encodedCharBytes = this.character == null ? null : encoder.encode(this.character); + const charSize = this.character == null ? 0 : this.character.length; + + const totalBytes = (KeyData.FIELD_COUNT + 1) * KeyData.BYTES_PER_FIELD + charSize; + const buffer = new ArrayBuffer(totalBytes); + const view = new DataView(buffer); + let offset = 0; + + view.setBigInt64(offset, BigInt(charSize), true); + offset += 8; + + view.setBigInt64(offset, BigInt(this.timestamp), true); + offset += 8; + + view.setBigInt64(offset, BigInt(this.type), true); + offset += 8; + + view.setBigInt64(offset, BigInt(this.physicalKey), true); + offset += 8; + + view.setBigInt64(offset, BigInt(this.logicalKey), true); + offset += 8; + + view.setBigInt64(offset, this.isSynthesized ? BigInt(1) : BigInt(0), true); + offset += 8; + + view.setBigInt64(offset, BigInt(this.deviceType), true); + offset += 8; + + if (encodedCharBytes != null) { + new Uint8Array(buffer, offset, charSize).set(encodedCharBytes); + } + + return buffer; + } +} + +/** + * Key event types. + */ +export enum Type { + KDOWN = 0, + KUP, + KREPEAT +} + +/** + * Types of input devices. + */ +export enum DeviceType { + KKEYBOARD = 0, + KDIRECTIONALPAD, + KGAMEPAD, + KJOYSTICK, + KHDMI +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyEmbedderResponder.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyEmbedderResponder.ets new file mode 100644 index 0000000..2d0440a --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyEmbedderResponder.ets @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2021-2025 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + +import { BinaryMessenger, BinaryReply } from '../../plugin/common/BinaryMessenger'; +import KeyData, { Type, DeviceType } from './KeyData'; +import { Responder } from './KeyboardManager'; +import KeyboardMap, { KeyPair, ModifierGoal } from './KeyboardMap'; +import Log from '../../util/Log'; + +/** + * Task runner for executing event tasks asynchronously. + */ +class EventTaskRunner { + private tasks: Array<() => void> = []; + + /** + * Constructs a new EventTaskRunner instance. + */ + constructor() { + } + + /** + * Adds a task to be executed later. + * @param task - The task function to add + */ + public addTask(task: () => void): void { + this.tasks.push(task); + } + + /** + * Runs all queued tasks. + */ + public runTasks(): void { + this.tasks.forEach(task => task()); + } +} + +/** + * Responder for handling key events using the embedder API. + * This class converts OpenHarmony key events to Flutter key data format and sends them to Flutter. + */ +export default class KeyEmbedderResponder implements Responder { + private static TAG = "KeyEmbedderResponder"; + private messenger: BinaryMessenger; + private pressingRecords: Map = new Map(); + + /** + * Constructs a new KeyEmbedderResponder instance. + * @param binaryMessenger - The BinaryMessenger for sending key events to Flutter + */ + constructor(binaryMessenger: BinaryMessenger) { + this.messenger = binaryMessenger; + } + + private keyOfPlane(key: number, plane: number): number { + return plane | (key & KeyboardMap.kValueMask); + } + + private getEventType(event: KeyEvent): Type { + let physicalKey: number = this.getPhysicalKey(event); + let isPressed: boolean = this.pressingRecords.has(physicalKey); + switch (event.type) { + case KeyType.Down: + return isPressed ? Type.KREPEAT : Type.KDOWN; + break; + case KeyType.Up: + return Type.KUP; + break; + default: + throw new Error("getEventType: Unexpected event type"); + } + } + + private getLogicalKey(event: KeyEvent): number { + let keyCode: number = event.keyCode; + let logicalKey: number | undefined = KeyboardMap.toLogicalKey.get(keyCode); + if (logicalKey !== undefined) { + return logicalKey; + } + return this.keyOfPlane(keyCode, KeyboardMap.kOhosPlane); + } + + private getPhysicalKey(event: KeyEvent): number { + let keyCode: number = event.keyCode; + let physicalKey: number | undefined = KeyboardMap.toPhysicalKey.get(keyCode); + if (physicalKey !== undefined) { + return physicalKey; + } + return this.keyOfPlane(keyCode, KeyboardMap.kOhosPlane); + } + + /** + * Updates the pressing keys record. + * @param physicalKey - The physical key code + * @param logicalKey - The logical key code, or null if the key is released + */ + updatePressingKeys(physicalKey: number, logicalKey: number | null): void { + if (logicalKey != null) { // press + if (this.pressingRecords.has(physicalKey)) { + Log.e(KeyEmbedderResponder.TAG, "updatePressingKeys adding nonempty key"); + } + this.pressingRecords.set(physicalKey, logicalKey); + } else { // release + if (!this.pressingRecords.has(physicalKey)) { + Log.e(KeyEmbedderResponder.TAG, "updatePressingKeys deleting empty key"); + } + this.pressingRecords.delete(physicalKey); + } + } + + /** + * Synchronizes modifier key states to match expected states. + * @param goal - The modifier goal to synchronize + * @param truePressed - Whether the modifier key is actually pressed + * @param logicalKey - The logical key code + * @param physicalKey - The physical key code + * @param event - The key event + * @param postSyncEvents - Task runner for post-synchronization events + */ + synchronizeModifierKey(goal: ModifierGoal, + truePressed: boolean, + logicalKey: number, + physicalKey: number, + event: KeyEvent, + postSyncEvents: EventTaskRunner) { + let nowStates: boolean[] = new Array(goal.keys.length); + let expectedPreStates: boolean[] = new Array(goal.keys.length); + let postAnyPressed: boolean = false; + + for (let keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + let key: KeyPair = goal.keys[keyIdx]; + nowStates[keyIdx] = this.pressingRecords.has(key.physicalKey); + if (key.logicalKey == logicalKey) { + switch (this.getEventType(event)) { + case Type.KDOWN: + expectedPreStates[keyIdx] = false; + postAnyPressed = true; + if (!truePressed) { + postSyncEvents.addTask(() => { + this.synthesizeEvent(false, event.timestamp, logicalKey, physicalKey); + }); + } + break; + case Type.KUP: + expectedPreStates[keyIdx] = nowStates[keyIdx]; + break; + case Type.KREPEAT: + expectedPreStates[keyIdx] = nowStates[keyIdx]; + postAnyPressed = true; + if (!truePressed) { + postSyncEvents.addTask(() => { + this.synthesizeEvent(false, event.timestamp, logicalKey, physicalKey); + }); + } + break; + } + } else { + postAnyPressed = postAnyPressed || nowStates[keyIdx]; + } + } + + if (truePressed) { + for (let keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (expectedPreStates[keyIdx] !== undefined) { + continue; + } + if (postAnyPressed) { + expectedPreStates[keyIdx] = nowStates[keyIdx]; + } else { + expectedPreStates[keyIdx] = true; + postAnyPressed = true; + } + } + if (!postAnyPressed) { + expectedPreStates[0] = true; + } + } else { + for (let keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (expectedPreStates[keyIdx] !== undefined) { + continue; + } + expectedPreStates[keyIdx] = false; + } + } + + for (let keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (expectedPreStates[keyIdx] != nowStates[keyIdx]) { + let key: KeyPair = goal.keys[keyIdx]; + this.synthesizeEvent(expectedPreStates[keyIdx], event.timestamp, + key.logicalKey, key.physicalKey); + } + } + } + + /** + * Synthesizes a key event. + * @param isDown - Whether the key is down + * @param timestamp - The event timestamp + * @param logicalKey - The logical key code + * @param physicalKey - The physical key code + */ + synthesizeEvent(isDown: boolean, timestamp: number, + logicalKey: number, physicalKey: number) { + const data: KeyData = new KeyData(); + data.timestamp = timestamp; + data.type = isDown ? Type.KDOWN : Type.KUP; + data.logicalKey = logicalKey; + data.physicalKey = physicalKey; + data.character = null; + data.isSynthesized = true; + data.deviceType = DeviceType.KKEYBOARD; + if (physicalKey != 0 && logicalKey != 0) { + this.updatePressingKeys(physicalKey, isDown ? logicalKey : null); + } + + this.sendKeyEvent(data); + } + + /** + * Sends a key event to Flutter. + * @param data - The KeyData to send + */ + sendKeyEvent(data: KeyData) { + this.messenger.send(KeyData.CHANNEL, data.toBytes()); + } + + /** + * Handles a key event from OpenHarmony. + * @param event - The key event to handle + * @returns Whether the event was handled + */ + handleKeyEvent(event: KeyEvent): boolean { + if (event.keyCode == 0) { + return false; + } + + let physicalKey: number = this.getPhysicalKey(event); + let logicalKey: number = this.getLogicalKey(event); + + let postSyncEvents: EventTaskRunner = new EventTaskRunner(); + + for (let goalIdx = 0; goalIdx < KeyboardMap.modifierGoals.length; goalIdx += 1) { + let goal: ModifierGoal = KeyboardMap.modifierGoals[goalIdx]; + if (event.getModifierKeyState != undefined) { + this.synchronizeModifierKey( + goal, + event.getModifierKeyState([goal.name]), + logicalKey, + physicalKey, + event, + postSyncEvents + ); + } + } + + let isDownEvent: boolean; + switch (event.type) { + case KeyType.Down: + isDownEvent = true; + break; + case KeyType.Up: + isDownEvent = false; + break; + default: + isDownEvent = false; + } + + let type: Type; + let lastLogicalKey: number | undefined = this.pressingRecords.get(physicalKey); + if (isDownEvent) { + if (lastLogicalKey === undefined) { + type = Type.KDOWN; + } else { + /* Nothing about repeat found in KeyEvent, so if isDownEvent and the key is + * currently pressed, take this event as a KREPEAT one. + */ + type = Type.KREPEAT; + } + } else { + if (lastLogicalKey === undefined) { + /* Ignore abrupt up events */ + return false; + } else { + type = Type.KUP; + } + } + + if (type != Type.KREPEAT) { + this.updatePressingKeys(physicalKey, isDownEvent ? logicalKey : null); + } + + const data: KeyData = new KeyData(); + data.timestamp = event.timestamp; + data.type = type; + data.physicalKey = physicalKey; + data.logicalKey = logicalKey; + data.character = null; + data.isSynthesized = false; + // no deviceType found in KeyEvent + data.deviceType = DeviceType.KKEYBOARD; + this.sendKeyEvent(data); + + postSyncEvents.runTasks(); + return true; + } + + /** + * Gets the currently pressed keys. + * @returns A map of physical key codes to logical key codes + */ + public getPressedKeys(): Map { + return new Map(this.pressingRecords); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyEventHandler.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyEventHandler.ets new file mode 100644 index 0000000..329a7b8 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyEventHandler.ets @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2021-2024 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + +import { HashMap } from '@kit.ArkTS'; +import deviceInfo from '@ohos.deviceInfo'; +import TextInputPlugin from '../../plugin/editing/TextInputPlugin'; +import Log from '../../util/Log'; +import { KeyCode } from '@kit.InputKit'; +import { ListenableEditingState } from '../../plugin/editing/ListenableEditingState'; + +const TAG = "KeyEventHandler"; + +/** + * Represents text for a key in normal and shift cases. + */ +class KeyText { + /** The text in normal case. */ + public normalCase: string; + /** The text in shift case. */ + public shiftCase: string; + + /** + * Constructs a new KeyText instance. + * @param normalCase - The text in normal case + * @param shiftCase - The text in shift case + */ + constructor(normalCase: string, shiftCase: string) { + this.normalCase = normalCase; + this.shiftCase = shiftCase; + } +} +; + +/** + * Handler for key events in emulator/hdc tool input scenarios. + * + * In the emulator/hdc tool input scenario, all keyevents will be passed to the onKeyEvent callback, + * so we need to insert the text to textInputPlugin ourselves. In other scenarios like phone/pc/ets, + * the input method will be responsible for inserting the text to textInputPlugin, consuming 'down' events + * and letting 'up' events go which will be captured by onKeyEvent. + * + * There is no need to process the status of the capslock button. Because in the scenario of the emulator/hdc tool, + * if capslock is pressed, os will send a 'shift' keyevent before the keyevent of the input key and we will insert + * uppercase characters correctly. + */ +export class KeyEventHandler { + + private static keyTextMap: Map = new Map([ + [KeyCode.KEYCODE_0, new KeyText('0', ')')], + [KeyCode.KEYCODE_1, new KeyText('1', '!')], + [KeyCode.KEYCODE_2, new KeyText('2', '@')], + [KeyCode.KEYCODE_3, new KeyText('3', '#')], + [KeyCode.KEYCODE_4, new KeyText('4', '$')], + [KeyCode.KEYCODE_5, new KeyText('5', '%')], + [KeyCode.KEYCODE_6, new KeyText('6', '^')], + [KeyCode.KEYCODE_7, new KeyText('7', '&')], + [KeyCode.KEYCODE_8, new KeyText('8', '*')], + [KeyCode.KEYCODE_9, new KeyText('9', '(')], + [KeyCode.KEYCODE_A, new KeyText('a', 'A')], + [KeyCode.KEYCODE_B, new KeyText('b', 'B')], + [KeyCode.KEYCODE_C, new KeyText('c', 'C')], + [KeyCode.KEYCODE_D, new KeyText('d', 'D')], + [KeyCode.KEYCODE_E, new KeyText('e', 'E')], + [KeyCode.KEYCODE_F, new KeyText('f', 'F')], + [KeyCode.KEYCODE_G, new KeyText('g', 'G')], + [KeyCode.KEYCODE_H, new KeyText('h', 'H')], + [KeyCode.KEYCODE_I, new KeyText('i', 'I')], + [KeyCode.KEYCODE_J, new KeyText('j', 'J')], + [KeyCode.KEYCODE_K, new KeyText('k', 'K')], + [KeyCode.KEYCODE_L, new KeyText('l', 'L')], + [KeyCode.KEYCODE_M, new KeyText('m', 'M')], + [KeyCode.KEYCODE_N, new KeyText('n', 'N')], + [KeyCode.KEYCODE_O, new KeyText('o', 'O')], + [KeyCode.KEYCODE_P, new KeyText('p', 'P')], + [KeyCode.KEYCODE_Q, new KeyText('q', 'Q')], + [KeyCode.KEYCODE_R, new KeyText('r', 'R')], + [KeyCode.KEYCODE_S, new KeyText('s', 'S')], + [KeyCode.KEYCODE_T, new KeyText('t', 'T')], + [KeyCode.KEYCODE_U, new KeyText('u', 'U')], + [KeyCode.KEYCODE_V, new KeyText('v', 'V')], + [KeyCode.KEYCODE_W, new KeyText('w', 'W')], + [KeyCode.KEYCODE_X, new KeyText('x', 'X')], + [KeyCode.KEYCODE_Y, new KeyText('y', 'Y')], + [KeyCode.KEYCODE_Z, new KeyText('z', 'Z')], + + [KeyCode.KEYCODE_GRAVE, new KeyText('`', '~')], + [KeyCode.KEYCODE_MINUS, new KeyText('-', '_')], + [KeyCode.KEYCODE_EQUALS, new KeyText('=', '+')], + [KeyCode.KEYCODE_LEFT_BRACKET, new KeyText('[', '{')], + [KeyCode.KEYCODE_RIGHT_BRACKET, new KeyText(']', '}')], + [KeyCode.KEYCODE_BACKSLASH, new KeyText('\\', '|')], + [KeyCode.KEYCODE_SEMICOLON, new KeyText(';', ':')], + [KeyCode.KEYCODE_APOSTROPHE, new KeyText('\'', '"')], + [KeyCode.KEYCODE_COMMA, new KeyText(',', '<')], + [KeyCode.KEYCODE_PERIOD, new KeyText('.', '>')], + [KeyCode.KEYCODE_SLASH, new KeyText('/', '?')], + [KeyCode.KEYCODE_SPACE, new KeyText(' ', ' ')], + + [KeyCode.KEYCODE_NUMPAD_0, new KeyText('0', '')], + [KeyCode.KEYCODE_NUMPAD_1, new KeyText('1', '')], + [KeyCode.KEYCODE_NUMPAD_2, new KeyText('2', '')], + [KeyCode.KEYCODE_NUMPAD_3, new KeyText('3', '')], + [KeyCode.KEYCODE_NUMPAD_4, new KeyText('4', '')], + [KeyCode.KEYCODE_NUMPAD_5, new KeyText('5', '')], + [KeyCode.KEYCODE_NUMPAD_6, new KeyText('6', '')], + [KeyCode.KEYCODE_NUMPAD_7, new KeyText('7', '')], + [KeyCode.KEYCODE_NUMPAD_8, new KeyText('8', '')], + [KeyCode.KEYCODE_NUMPAD_9, new KeyText('9', '')], + [KeyCode.KEYCODE_NUMPAD_DOT, new KeyText('.', '')], + [KeyCode.KEYCODE_NUMPAD_ADD, new KeyText('+', '')], + [KeyCode.KEYCODE_NUMPAD_SUBTRACT, new KeyText('-', '')], + [KeyCode.KEYCODE_NUMPAD_MULTIPLY, new KeyText('*', '')], + [KeyCode.KEYCODE_NUMPAD_DIVIDE, new KeyText('/', '')], + [KeyCode.KEYCODE_NUMPAD_EQUALS, new KeyText('=', '')], + ]); + private textInputPlugin?: TextInputPlugin; + + /** + * Constructs a new KeyEventHandler instance. + * @param textInputPlugin - The TextInputPlugin instance, optional + */ + constructor(textInputPlugin?: TextInputPlugin) { + this.textInputPlugin = textInputPlugin; + } + + /** + * Gets the text representation of a key event. + * @param event - The key event + * @returns The text string for the key, considering shift state + */ + getKeyText(event: KeyEvent) : string { + let keyText = KeyEventHandler.keyTextMap.get(event.keyCode); + if (keyText !== undefined) { + // Check if it's a letter key (A-Z) + const isLetter = event.keyCode >= KeyCode.KEYCODE_A && event.keyCode <= KeyCode.KEYCODE_Z; + // Use event.isCapsLockOn to check CapsLock state + const isCapsLockOn = event.isCapsLockOn !== undefined ? event.isCapsLockOn : false; + // Use getModifierKeyState to check Shift state + const isShiftPressed = this.getModifierKeyStateSafe(event, ['Shift']); + // If CapsLock is enabled, reverse the case for letter keys + if (isLetter && isCapsLockOn) { + // Shift + CapsLock = lowercase (reverses CapsLock effect) + return isShiftPressed ? keyText.normalCase : keyText.shiftCase; + } else { + // Normal case: Shift determines case + return isShiftPressed ? keyText.shiftCase : keyText.normalCase; + } + } + return ''; + } + + /** + * Starts a deletion operation. + * @param code - The key code for deletion + */ + startDeleting(code: number) { + this.textInputPlugin?.getEditingState().startDeleting(code); + } + + /** + * Ends a deletion operation. + * @param code - The key code for deletion + */ + endDeletion(code: number) { + this.textInputPlugin?.getEditingState().endDeletion(code); + } + + /** + * Helper method to safely execute an action on the editing state. + * @param action - The action to execute with the editing state + * @returns True if the action was executed, false otherwise + */ + private withEditingState(action: (editingState: ListenableEditingState) => void): boolean { + const editingState = this.textInputPlugin?.getEditingState(); + if (editingState) { + action(editingState); + return true; + } + return false; + } + + /** + * Safely gets the modifier key state from a key event. + * @param event - The key event + * @param modifierKeys - Array of modifier key names (e.g., ['Shift'], ['Ctrl'], ['Alt']) + * @returns True if the modifier key is pressed, false otherwise + */ + private getModifierKeyStateSafe(event: KeyEvent, modifierKeys: string[]): boolean { + if (!event.getModifierKeyState) { + return false; + } + try { + return event.getModifierKeyState(modifierKeys) || false; + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e); + Log.e(TAG, `Failed to get modifier key state for ${modifierKeys.join(', ')}: ${errorMsg}`); + return false; + } + } + + /** + * Handles a key event and inserts text if appropriate. + * @param event - The key event to handle + */ + handleKeyEvent(event: KeyEvent) { + Log.i(TAG, JSON.stringify({ + "name": "handleKeyEvent", + "event": event + })); + if (event.type === KeyType.Down) { + switch (event.keyCode) { + case KeyCode.KEYCODE_ENTER: + case KeyCode.KEYCODE_NUMPAD_ENTER: { + // Handle Enter key (both regular and numpad), insert newline character + this.withEditingState((editingState) => { + editingState.handleInsertTextEvent('\n'); + Log.i(TAG, `ENTER: inserted newline`); + }); + break; + } + default: { + // Check if it's a numpad number key (0-9) + const isNumpadNumberKey = event.keyCode >= KeyCode.KEYCODE_NUMPAD_0 && event.keyCode <= KeyCode.KEYCODE_NUMPAD_9; + // Check if it's NUMPAD_DOT key + const isNumpadDotKey = event.keyCode === KeyCode.KEYCODE_NUMPAD_DOT; + + // Use event.isNumLockOn to check NumLock state + const isNumLockOn = event.isNumLockOn !== undefined ? event.isNumLockOn : true; // Default to true if not available + + // When NumLock is on, block numpad number keys (0-9) and NUMPAD_DOT, allow other special keys to input symbols + if ((isNumpadNumberKey || isNumpadDotKey) && !isNumLockOn) { + Log.i(TAG, `Numpad key ${event.keyCode} blocked: NumLock is off`); + return; + } + // Other special keys (NUMPAD_ADD, NUMPAD_SUBTRACT, etc.) can input symbols even when NumLock is off + + // Use getModifierKeyState to check Ctrl and Alt state + // Returns true if either Ctrl or Alt (or both) is pressed + const isCtrlPressed = this.getModifierKeyStateSafe(event, ['Ctrl']); + const isAltPressed = this.getModifierKeyStateSafe(event, ['Alt']); + const isCombinationMode = isCtrlPressed || isAltPressed; + + // Don't input characters (letters/numbers/symbols) when Ctrl/Alt keys are pressed or data is empty + if (!isCombinationMode && this.getKeyText(event)) { + this.withEditingState((editingState) => { + // Insert character + editingState.handleInsertTextEvent(this.getKeyText(event)); + }); + } + break; + } + } + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyboardManager.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyboardManager.ets new file mode 100644 index 0000000..eb8213f --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyboardManager.ets @@ -0,0 +1,87 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on KeyboardManager.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import TextInputPlugin from '../../plugin/editing/TextInputPlugin'; +import FlutterEngine from '../engine/FlutterEngine'; +import KeyEventChannel, { FlutterKeyEvent } from '../engine/systemchannels/KeyEventChannel'; +import KeyboardChannel from '../engine/systemchannels/KeyboardChannel'; +import KeyEmbedderResponder from './KeyEmbedderResponder'; +import { BinaryMessenger } from '../../plugin/common/BinaryMessenger'; +import { KeyEventHandler } from './KeyEventHandler'; +import HashSet from '@ohos.util.HashSet'; +import { KeyCode } from '@kit.InputKit'; + +/** + * Manages keyboard events and state for Flutter. + * This class coordinates between key event channels, keyboard channels, and text input handling. + */ +export default class KeyboardManager { + private keyEventChannel: KeyEventChannel | null = null; + private keyboardChannel: KeyboardChannel | null = null; + /** The key embedder responder for handling key events. */ + protected keyEmbedderResponder: KeyEmbedderResponder; + private keyEventHandler: KeyEventHandler; + + /** + * Constructs a new KeyboardManager instance. + * @param engine - The FlutterEngine instance + * @param textInputPlugin - The TextInputPlugin instance + */ + constructor(engine: FlutterEngine, textInputPlugin: TextInputPlugin) { + this.keyEventChannel = new KeyEventChannel(engine.dartExecutor); + this.keyboardChannel = new KeyboardChannel(engine.dartExecutor); + this.keyboardChannel.setKeyboardMethodHandler(this); + this.keyEmbedderResponder = new KeyEmbedderResponder(engine.dartExecutor); + this.keyEventHandler = new KeyEventHandler(textInputPlugin); + } + + /** + * Handles key events before they are processed by the input method editor. + * @param event - The key event + * @returns Whether the event was handled + */ + onKeyPreIme(event: KeyEvent) : boolean { + return false; + } + + /** + * Handles key events. + * @param event - The key event + * @returns Whether the event was handled + */ + onKeyEvent(event: KeyEvent) : boolean { + this.keyEmbedderResponder.handleKeyEvent(event); + + this.keyEventChannel?.sendFlutterKeyEvent(new FlutterKeyEvent(event), event.type == KeyType.Up, { + onFrameworkResponse: (isEventHandled: boolean): void => { + } + }) + this.keyEventHandler.handleKeyEvent(event); + return false; + } + + /** + * Gets the current keyboard state (pressed keys). + * @returns A map of pressed key codes + */ + public getKeyboardState(): Map { + return this.keyEmbedderResponder.getPressedKeys(); + } +} + +/** + * Interface for handling key events. + */ +export interface Responder { + /** + * Handles a key event. + * @param keyEvent - The key event to handle + */ + handleKeyEvent(keyEvent: KeyEvent): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyboardMap.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyboardMap.ets new file mode 100644 index 0000000..52b86e4 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/KeyboardMap.ets @@ -0,0 +1,652 @@ +/* + * Copyright (c) 2021-2025 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + +/** + * Represents a pair of physical and logical key codes. + */ +export class KeyPair { + /** The physical key code. */ + public physicalKey: number; + /** The logical key code. */ + public logicalKey: number; + + /** + * Constructs a new KeyPair instance. + * @param physicalKey - The physical key code + * @param logicalKey - The logical key code + */ + constructor(physicalKey: number, logicalKey: number) { + this.physicalKey = physicalKey; + this.logicalKey = logicalKey; + } +} + +/** + * Represents a modifier key goal with associated key pairs. + */ +export class ModifierGoal { + /** The modifier name (e.g., "Ctrl", "Shift", "Alt"). */ + public name: string; + /** Array of KeyPair instances for this modifier. */ + public keys: KeyPair[]; + + /** + * Constructs a new ModifierGoal instance. + * @param name - The modifier name (e.g., "Ctrl", "Shift", "Alt") + * @param keys - Array of KeyPair instances for this modifier + */ + constructor(name: string, keys: KeyPair[]) { + this.name = name; + this.keys = keys; + } +} + +/** + * Maps OpenHarmony KeyCodes to Flutter LogicalKeys and PhysicalKeys. + * This class provides static mappings for keyboard key translation between OpenHarmony and Flutter. + */ +export default class KeyboardMap { + /** Map from OpenHarmony KeyCode to Flutter LogicalKey. */ + public static toLogicalKey: Map = + new Map([ + [0x000000007D0, 0x00000000030], // digit0 + [0x000000007D1, 0x00000000031], // digit1 + [0x000000007D2, 0x00000000032], // digit2 + [0x000000007D3, 0x00000000033], // digit3 + [0x000000007D4, 0x00000000034], // digit4 + [0x000000007D5, 0x00000000035], // digit5 + [0x000000007D6, 0x00000000036], // digit6 + [0x000000007D7, 0x00000000037], // digit7 + [0x000000007D8, 0x00000000038], // digit8 + [0x000000007D9, 0x00000000039], // digit9 + [0x000000007DA, 0x0000000002A], // asterisk + [0x000000007DB, 0x00000000023], // numberSign + [0x000000007DC, 0x00100000304], // arrowUp + [0x000000007DD, 0x00100000301], // arrowDown + [0x000000007DE, 0x00100000302], // arrowLeft + [0x000000007DF, 0x00100000303], // arrowRight + [0x000000007E1, 0x00000000061], // keyA + [0x000000007E2, 0x00000000062], // keyB + [0x000000007E3, 0x00000000063], // keyC + [0x000000007E4, 0x00000000064], // keyD + [0x000000007E5, 0x00000000065], // keyE + [0x000000007E6, 0x00000000066], // keyF + [0x000000007E7, 0x00000000067], // keyG + [0x000000007E8, 0x00000000068], // keyH + [0x000000007E9, 0x00000000069], // keyI + [0x000000007EA, 0x0000000006A], // keyJ + [0x000000007EB, 0x0000000006B], // keyK + [0x000000007EC, 0x0000000006C], // keyL + [0x000000007ED, 0x0000000006D], // keyM + [0x000000007EE, 0x0000000006E], // keyN + [0x000000007EF, 0x0000000006F], // keyO + [0x000000007F0, 0x00000000070], // keyP + [0x000000007F1, 0x00000000071], // keyQ + [0x000000007F2, 0x00000000072], // keyR + [0x000000007F3, 0x00000000073], // keyS + [0x000000007F4, 0x00000000074], // keyT + [0x000000007F5, 0x00000000075], // keyU + [0x000000007F6, 0x00000000076], // keyV + [0x000000007F7, 0x00000000077], // keyW + [0x000000007F8, 0x00000000078], // keyX + [0x000000007F9, 0x00000000079], // keyY + [0x000000007FA, 0x0000000007A], // keyZ + [0x000000007FB, 0x0000000002C], // comma + [0x000000007FC, 0x0000000002E], // period + [0x000000007FD, 0x00200000104], // altLeft + [0x000000007FE, 0x00200000105], // altRight + [0x000000007FF, 0x00200000102], // shiftLeft + [0x00000000800, 0x00200000103], // shiftRight + [0x00000000801, 0x00100000009], // tab + [0x00000000802, 0x00000000020], // space + [0x00000000804, 0x00100000B09], // launchWebBrowser + [0x00000000805, 0x00100000B03], // launchMail + [0x00000000806, 0x0010000000D], // enter + [0x00000000807, 0x00100000008], // backspace + [0x00000000808, 0x00000000060], // backquote + [0x00000000809, 0x0000000002D], // minus + [0x0000000080A, 0x0000000003D], // equal + [0x0000000080B, 0x0000000005B], // bracketLeft + [0x0000000080C, 0x0000000005D], // bracketRight + [0x0000000080D, 0x0000000005C], // backslash + [0x0000000080E, 0x0000000003B], // semicolon + [0x0000000080F, 0x00000000022], // apostrophe/quote + [0x00000000810, 0x0000000002F], // slash + [0x00000000813, 0x00100000505], // contextMenu + [0x000000009A2, 0x00100000704], // compose + [0x00000000814, 0x00100000308], // pageUp + [0x00000000815, 0x00100000307], // pageDown + [0x00000000816, 0x0010000001B], // escape + [0x00000000817, 0x0010000007F], // delete + [0x00000000818, 0x00200000100], // controlLeft + [0x00000000819, 0x00200000101], // controlRight + [0x0000000081A, 0x00100000104], // capsLock + [0x0000000081B, 0x0010000010C], // scrollLock + [0x0000000081C, 0x00200000106], // metaLeft + [0x0000000081D, 0x00200000107], // metaRight + [0x0000000081E, 0x00100000106], // fn + [0x0000000081F, 0x00100000608], // printScreen + [0x00000000820, 0x00100000509], // pause + [0x00000000821, 0x00100000306], // home + [0x00000000822, 0x00100000305], // end + [0x00000000823, 0x00100000407], // insert + [0x00000000824, 0x00100000C03], // browserForward + [0x00000000825, 0x00100000D2F], // mediaPlay + [0x00000000A53, 0x0010000050A], // play + [0x00000000826, 0x00100000D2E], // mediaPause + [0x00000000827, 0x00100000D5B], // mediaClose + [0x00000000828, 0x00100000604], // eject + [0x00000000829, 0x00100000D30], // mediaRecord + [0x0000000082A, 0x00100000801], // f1 + [0x0000000082B, 0x00100000802], // f2 + [0x0000000082C, 0x00100000803], // f3 + [0x0000000082D, 0x00100000804], // f4 + [0x0000000082E, 0x00100000805], // f5 + [0x0000000082F, 0x00100000806], // f6 + [0x00000000830, 0x00100000807], // f7 + [0x00000000831, 0x00100000808], // f8 + [0x00000000832, 0x00100000809], // f9 + [0x00000000833, 0x0010000080A], // f10 + [0x00000000834, 0x0010000080B], // f11 + [0x00000000835, 0x0010000080C], // f12 + [0x00000000836, 0x0010000010A], // numLock + [0x00000000837, 0x00200000230], // numpad0 + [0x00000000838, 0x00200000231], // numpad1 + [0x00000000839, 0x00200000232], // numpad2 + [0x0000000083A, 0x00200000233], // numpad3 + [0x0000000083B, 0x00200000234], // numpad4 + [0x0000000083C, 0x00200000235], // numpad5 + [0x0000000083D, 0x00200000236], // numpad6 + [0x0000000083E, 0x00200000237], // numpad7 + [0x0000000083F, 0x00200000238], // numpad8 + [0x00000000840, 0x00200000239], // numpad9 + [0x00000000841, 0x0020000022F], // numpadDivide + [0x00000000842, 0x0020000022A], // numpadMultiply + [0x00000000843, 0x0020000022D], // numpadSubtract + [0x00000000844, 0x0020000022B], // numpadAdd + [0x00000000845, 0x0020000022E], // numpadDecimal + [0x00000000846, 0x0020000022C], // numpadComma + [0x00000000847, 0x0020000020D], // numpadEnter + [0x00000000848, 0x0020000023D], // numpadEqual + [0x00000000849, 0x00200000228], // numpadParenLeft + [0x0000000084A, 0x00200000229], // numpadParenRight + [0x00000000010, 0x00100000A10], // audioVolumeUp + [0x00000000011, 0x00100000A0F], // audioVolumeDown + [0x00000000012, 0x00100000606], // power + [0x00000000016, 0x00100000E09], // microphoneVolumeMute + [0x00000000001, 0x00100000306], // home + [0x00000000002, 0x00100001005], // goBack + [0x00000000013, 0x00100000603], // camera + [0x00000000028, 0x00100000602], // brightnessUp + [0x00000000029, 0x00100000601], // brightnessDown + [0x00000000005, 0x00100000401], // clear + [0x0000000000A, 0x00100000A05], // mediaPlayPause + [0x0000000000B, 0x00100000A07], // mediaStop + [0x0000000000C, 0x00100000A08], // mediaTrackNext + [0x0000000000D, 0x00100000A09], // mediaTrackPrevious + [0x0000000000E, 0x00100000D31], // mediaRewind + [0x0000000000F, 0x00100000D2C], // mediaFastForward + [0x00000000A28, 0x00200000002], // sleep + [0x00000000A29, 0x0010000071D], // zenkakuHankaku + [0x00000000A2C, 0x0010000071A], // katakana + [0x00000000A2D, 0x00100000716], // hiragana + [0x00000000A2E, 0x00100000705], // convert + [0x00000000A2F, 0x00100000717], // hiraganaKatakana + [0x00000000A30, 0x0010000070D], // nonConvert + [0x00000000A37, 0x00200000022], // intlYen + [0x00000000A39, 0x00100000502], // again + [0x00000000A3A, 0x0010000050B], // props + [0x00000000A3B, 0x0010000040A], // undo + [0x00000000A3C, 0x00100000402], // copy + [0x00000000A3D, 0x00100000A0B], // open + [0x00000000A3E, 0x00100000408], // paste + [0x00000000A3F, 0x00100000507], // find + [0x00000000A40, 0x00100000404], // cut + [0x00000000A41, 0x00100000508], // help + [0x00000000A44, 0x00100000C02], // browserFavorites + [0x00000000A46, 0x00100000A05], // mediaPlayPause + [0x00000000A48, 0x00100000A01], // close + [0x00000000003, 0x00100001002], // call + [0x00000000A4B, 0x00100000C05], // browserRefresh + [0x00000000A4C, 0x00100000D15], // exit + [0x00000000A51, 0x00100000409], // redo + [0x00000000A52, 0x00100000A01], // close + [0x00000000A55, 0x00100000A0C], // print + [0x00000000A58, 0x00100000504], // cancel + [0x00000000A5F, 0x00100000A0D], // save + [0x00000000A68, 0x00100000D25], // info + [0x00000000A6B, 0x00100000D47], // subtitle + [0x00000000A70, 0x00100000D49], // tv + [0x00000000A7E, 0x00100000D0C], // colorF0Red + [0x00000000A7F, 0x00100000D0D], // colorF1Green + [0x00000000A80, 0x00100000D0E], // colorF2Yellow + [0x00000000A81, 0x00100000D0F], // colorF3Blue + [0x00000000A82, 0x00100000D0B], // channelUp + [0x00000000A83, 0x00100000D0A], // channelDown + [0x00000000A8A, 0x0010000050D], // zoomIn + [0x00000000A8B, 0x0010000050E], // zoomOut + [0x00000000A98, 0x00100000A0E], // spellCheck + [0x00000000AF2, 0x0010000060B], // wakeUp + [0x00000000AFD, 0x00100000604], // eject + [0x00000000B00, 0x0010000080D], // f13 + [0x00000000B01, 0x0010000080E], // f14 + [0x00000000B02, 0x0010000080F], // f15 + [0x00000000B03, 0x00100000810], // f16 + [0x00000000B04, 0x00100000811], // f17 + [0x00000000B05, 0x00100000812], // f18 + [0x00000000B06, 0x00100000813], // f19 + [0x00000000B07, 0x00100000814], // f20 + [0x00000000B08, 0x00100000815], // f21 + [0x00000000B09, 0x00100000816], // f22 + [0x00000000B0A, 0x00100000817], // f23 + [0x00000000B0B, 0x00100000818], // f24 + [0x00000000B0F, 0x00200000000], // suspend + [0x00000000B12, 0x0000000003F], // question + [0x00000000811, 0x00000000040], // at + [0x00000000006, 0x00100001007], // headsetHook + [0x00000000017, 0x00100000A11], // audioVolumeMute + [0x00000000004, 0x00100001004] // endCall + ]); + /** Map OH KeyCode to Flutter PhysicalKey. + * Should map OH ScanCode to Flutter PhysicalKey, but we use KeyCode here + * instead since there is no ScanCode in OH KeyEvent yet. There may be some + * mistakes and should correct the map as soon as we can access ScanCode. + */ + public static toPhysicalKey: Map = + new Map([ + [0x000000007D0, 0x00000070027], // digit0 + [0x000000007D1, 0x0000007001E], // digit1 + [0x000000007D2, 0x0000007001F], // digit2 + [0x000000007D3, 0x00000070020], // digit3 + [0x000000007D4, 0x00000070021], // digit4 + [0x000000007D5, 0x00000070022], // digit5 + [0x000000007D6, 0x00000070023], // digit6 + [0x000000007D7, 0x00000070024], // digit7 + [0x000000007D8, 0x00000070025], // digit8 + [0x000000007D9, 0x00000070026], // digit9 + [0x000000007DC, 0x00000070052], // arrowUp + [0x000000007DD, 0x00000070051], // arrowDown + [0x000000007DE, 0x00000070050], // arrowLeft + [0x000000007DF, 0x0000007004F], // arrowRight + [0x000000007E1, 0x00000070004], // keyA + [0x000000007E2, 0x00000070005], // keyB + [0x000000007E3, 0x00000070006], // keyC + [0x000000007E4, 0x00000070007], // keyD + [0x000000007E5, 0x00000070008], // keyE + [0x000000007E6, 0x00000070009], // keyF + [0x000000007E7, 0x0000007000A], // keyG + [0x000000007E8, 0x0000007000B], // keyH + [0x000000007E9, 0x0000007000C], // keyI + [0x000000007EA, 0x0000007000D], // keyJ + [0x000000007EB, 0x0000007000E], // keyK + [0x000000007EC, 0x0000007000F], // keyL + [0x000000007ED, 0x00000070010], // keyM + [0x000000007EE, 0x00000070011], // keyN + [0x000000007EF, 0x00000070012], // keyO + [0x000000007F0, 0x00000070013], // keyP + [0x000000007F1, 0x00000070014], // keyQ + [0x000000007F2, 0x00000070015], // keyR + [0x000000007F3, 0x00000070016], // keyS + [0x000000007F4, 0x00000070017], // keyT + [0x000000007F5, 0x00000070018], // keyU + [0x000000007F6, 0x00000070019], // keyV + [0x000000007F7, 0x0000007001A], // keyW + [0x000000007F8, 0x0000007001B], // keyX + [0x000000007F9, 0x0000007001C], // keyY + [0x000000007FA, 0x0000007001D], // keyZ + [0x000000007FB, 0x00000070036], // comma + [0x000000007FC, 0x00000070037], // period + [0x000000007FD, 0x000000700E2], // altLeft + [0x000000007FE, 0x000000700E6], // altRight + [0x000000007FF, 0x000000700E1], // shiftLeft + [0x00000000800, 0x000000700E5], // shiftRight + [0x00000000801, 0x0000007002B], // tab + [0x00000000802, 0x0000007002C], // space + [0x00000000805, 0x000000C018A], // launchMail + [0x00000000806, 0x00000070028], // enter + [0x00000000807, 0x0000007002A], // backspace + [0x00000000808, 0x00000070035], // backquote + [0x00000000809, 0x0000007002D], // minus + [0x0000000080A, 0x0000007002E], // equal + [0x0000000080B, 0x0000007002F], // bracketLeft + [0x0000000080C, 0x00000070030], // bracketRight + [0x0000000080D, 0x00000070031], // backslash + [0x0000000080E, 0x00000070033], // semicolon + [0x0000000080F, 0x00000070034], // apostrophe/quote + [0x00000000810, 0x00000070038], // slash + [0x00000000813, 0x00000070065], // contextMenu + [0x00000000814, 0x0000007004B], // pageUp + [0x00000000815, 0x0000007004E], // pageDown + [0x00000000816, 0x00000070029], // escape + [0x00000000817, 0x0000007004C], // delete + [0x00000000818, 0x000000700E0], // controlLeft + [0x00000000819, 0x000000700E4], // controlRight + [0x0000000081A, 0x00000070039], // capsLock + [0x0000000081B, 0x00000070047], // scrollLock + [0x0000000081C, 0x000000700E3], // metaLeft + [0x0000000081D, 0x000000700E7], // metaRight + [0x0000000081E, 0x00000000012], // fn + [0x0000000081F, 0x00000070046], // printScreen + [0x00000000820, 0x00000070048], // pause + [0x00000000821, 0x0000007004A], // home + [0x00000000822, 0x0000007004D], // end + [0x00000000823, 0x00000070049], // insert + [0x00000000824, 0x000000C0225], // browserForward + [0x00000000825, 0x000000C00B0], // mediaPlay + [0x00000000826, 0x000000C00B1], // mediaPause + [0x00000000828, 0x000000C00B8], // eject + [0x00000000829, 0x000000C00B2], // mediaRecord + [0x0000000082A, 0x0000007003A], // f1 + [0x0000000082B, 0x0000007003B], // f2 + [0x0000000082C, 0x0000007003C], // f3 + [0x0000000082D, 0x0000007003D], // f4 + [0x0000000082E, 0x0000007003E], // f5 + [0x0000000082F, 0x0000007003F], // f6 + [0x00000000830, 0x00000070040], // f7 + [0x00000000831, 0x00000070041], // f8 + [0x00000000832, 0x00000070042], // f9 + [0x00000000833, 0x00000070043], // f10 + [0x00000000834, 0x00000070044], // f11 + [0x00000000835, 0x00000070045], // f12 + [0x00000000836, 0x00000070053], // numLock + [0x00000000837, 0x00000070062], // numpad0 + [0x00000000838, 0x00000070059], // numpad1 + [0x00000000839, 0x0000007005A], // numpad2 + [0x0000000083A, 0x0000007005B], // numpad3 + [0x0000000083B, 0x0000007005C], // numpad4 + [0x0000000083C, 0x0000007005D], // numpad5 + [0x0000000083D, 0x0000007005E], // numpad6 + [0x0000000083E, 0x0000007005F], // numpad7 + [0x0000000083F, 0x00000070060], // numpad8 + [0x00000000840, 0x00000070061], // numpad9 + [0x00000000841, 0x00000070054], // numpadDivide + [0x00000000842, 0x00000070055], // numpadMultiply + [0x00000000843, 0x00000070056], // numpadSubtract + [0x00000000844, 0x00000070057], // numpadAdd + [0x00000000845, 0x00000070063], // numpadDecimal + [0x00000000846, 0x00000070085], // numpadComma + [0x00000000847, 0x00000070058], // numpadEnter + [0x00000000848, 0x00000070067], // numpadEqual + [0x00000000849, 0x000000700B6], // numpadParenLeft + [0x0000000084A, 0x000000700B7], // numpadParenRight + [0x00000000010, 0x00000070080], // audioVolumeUp + [0x00000000011, 0x00000070081], // audioVolumeDown + [0x00000000012, 0x00000070066], // power + [0x00000000001, 0x0000007004A], // home + [0x00000000028, 0x000000C006F], // brightnessUp + [0x00000000029, 0x000000C0070], // brightnessDown + [0x0000000000A, 0x000000C00CD], // mediaPlayPause + [0x0000000000B, 0x000000C00B7], // mediaStop + [0x0000000000C, 0x000000C00B5], // mediaTrackNext + [0x0000000000D, 0x000000C00B6], // mediaTrackPrevious + [0x0000000000E, 0x000000C00B4], // mediaRewind + [0x0000000000F, 0x000000C00B3], // mediaFastForward + [0x00000000A28, 0x00000010082], // sleep + [0x00000000A2E, 0x0000007008A], // convert + [0x00000000A30, 0x0000007008B], // nonConvert + [0x00000000A37, 0x00000070089], // intlYen + [0x00000000A39, 0x00000070079], // again + [0x00000000A3A, 0x000000700A3], // props + [0x00000000A3B, 0x0000007007A], // undo + [0x00000000A3C, 0x0000007007C], // copy + [0x00000000A3D, 0x00000070074], // open + [0x00000000A3E, 0x0000007007D], // paste + [0x00000000A3F, 0x0000007007E], // find + [0x00000000A40, 0x0000007007B], // cut + [0x00000000A41, 0x00000070075], // help + [0x00000000A44, 0x000000C022A], // browserFavorites + [0x00000000A46, 0x000000C00CD], // mediaPlayPause + [0x00000000A48, 0x000000C0203], // close + [0x00000000A4B, 0x000000C0227], // browserRefresh + [0x00000000A4C, 0x000000C0094], // exit + [0x00000000A51, 0x000000C0279], // redo + [0x00000000A52, 0x000000C0203], // close + [0x00000000A55, 0x000000C0208], // print + [0x00000000A5F, 0x000000C0207], // save + [0x00000000A68, 0x000000C0060], // info + [0x00000000A82, 0x000000C009C], // channelUp + [0x00000000A83, 0x000000C009D], // channelDown + [0x00000000A8A, 0x000000C022D], // zoomIn + [0x00000000A8B, 0x000000C022E], // zoomOut + [0x00000000A98, 0x000000C01AB], // spellCheck + [0x00000000AF2, 0x00000010083], // wakeUp + [0x00000000AFD, 0x000000C00B8], // eject + [0x00000000B00, 0x00000070068], // f13 + [0x00000000B01, 0x00000070069], // f14 + [0x00000000B02, 0x0000007006A], // f15 + [0x00000000B03, 0x0000007006B], // f16 + [0x00000000B04, 0x0000007006C], // f17 + [0x00000000B05, 0x0000007006D], // f18 + [0x00000000B06, 0x0000007006E], // f19 + [0x00000000B07, 0x0000007006F], // f20 + [0x00000000B08, 0x00000070070], // f21 + [0x00000000B09, 0x00000070071], // f22 + [0x00000000B0A, 0x00000070072], // f23 + [0x00000000B0B, 0x00000070073], // f24 + [0x00000000B0F, 0x00000000014], // suspend + [0x00000000017, 0x0000007007F] // audioVolumeMute + ]); + /** Map OpenHarmony KeyCode to ScanCode since cannot access ScanCode directly from KeyEvent. */ + public static ohKeyToScanCode: Map = + new Map([ + [0x000000007D0, 0x0000000000B], // digit0 + [0x000000007D1, 0x00000000002], // digit1 + [0x000000007D2, 0x00000000003], // digit2 + [0x000000007D3, 0x00000000004], // digit3 + [0x000000007D4, 0x00000000005], // digit4 + [0x000000007D5, 0x00000000006], // digit5 + [0x000000007D6, 0x00000000007], // digit6 + [0x000000007D7, 0x00000000008], // digit7 + [0x000000007D8, 0x00000000009], // digit8 + [0x000000007D9, 0x0000000000A], // digit9 + [0x000000007DC, 0x00000000067], // arrowUp + [0x000000007DD, 0x0000000006C], // arrowDown + [0x000000007DE, 0x00000000069], // arrowLeft + [0x000000007DF, 0x0000000006A], // arrowRight + [0x000000007E1, 0x0000000001E], // keyA + [0x000000007E2, 0x00000000030], // keyB + [0x000000007E3, 0x0000000002E], // keyC + [0x000000007E4, 0x00000000020], // keyD + [0x000000007E5, 0x00000000012], // keyE + [0x000000007E6, 0x00000000021], // keyF + [0x000000007E7, 0x00000000022], // keyG + [0x000000007E8, 0x00000000023], // keyH + [0x000000007E9, 0x00000000017], // keyI + [0x000000007EA, 0x00000000024], // keyJ + [0x000000007EB, 0x00000000025], // keyK + [0x000000007EC, 0x00000000026], // keyL + [0x000000007ED, 0x00000000032], // keyM + [0x000000007EE, 0x00000000031], // keyN + [0x000000007EF, 0x00000000018], // keyO + [0x000000007F0, 0x00000000019], // keyP + [0x000000007F1, 0x00000000010], // keyQ + [0x000000007F2, 0x00000000013], // keyR + [0x000000007F3, 0x0000000001F], // keyS + [0x000000007F4, 0x00000000014], // keyT + [0x000000007F5, 0x00000000016], // keyU + [0x000000007F6, 0x0000000002F], // keyV + [0x000000007F7, 0x00000000011], // keyW + [0x000000007F8, 0x0000000002D], // keyX + [0x000000007F9, 0x00000000015], // keyY + [0x000000007FA, 0x0000000002C], // keyZ + [0x000000007FB, 0x00000000033], // comma + [0x000000007FC, 0x00000000034], // period + [0x000000007FD, 0x00000000038], // altLeft + [0x000000007FE, 0x00000000064], // altRight + [0x000000007FF, 0x0000000002A], // shiftLeft + [0x00000000800, 0x00000000036], // shiftRight + [0x00000000801, 0x0000000000F], // tab + [0x00000000802, 0x00000000039], // space + [0x00000000805, 0x000000000D7], // launchMail + [0x00000000806, 0x0000000001C], // enter + [0x00000000807, 0x0000000000E], // backspace + [0x00000000808, 0x00000000029], // backquote + [0x00000000809, 0x0000000000C], // minus + [0x0000000080A, 0x0000000000D], // equal + [0x0000000080B, 0x0000000001A], // bracketLeft + [0x0000000080C, 0x0000000001B], // bracketRight + [0x0000000080D, 0x00000000056], // backslash + [0x0000000080E, 0x00000000027], // semicolon + [0x0000000080F, 0x00000000028], // apostrophe/quote + [0x00000000810, 0x00000000035], // slash + [0x00000000813, 0x0000000008B], // contextMenu + [0x00000000814, 0x000000000B1], // pageUp + [0x00000000815, 0x000000000B2], // pageDown + [0x00000000816, 0x00000000001], // escape + [0x00000000817, 0x0000000006F], // delete + [0x00000000818, 0x0000000001D], // controlLeft + [0x00000000819, 0x00000000061], // controlRight + [0x0000000081A, 0x0000000003A], // capsLock + [0x0000000081B, 0x00000000046], // scrollLock + [0x0000000081C, 0x0000000007D], // metaLeft + [0x0000000081D, 0x0000000007E], // metaRight + [0x0000000081E, 0x000000001D0], // fn + [0x0000000081F, 0x00000000063], // printScreen + [0x00000000820, 0x0000000019B], // pause + [0x00000000821, 0x00000000066], // home + [0x00000000822, 0x0000000006B], // end + [0x00000000823, 0x0000000006E], // insert + [0x00000000824, 0x0000000009F], // browserForward + [0x00000000825, 0x000000000CF], // mediaPlay + [0x00000000826, 0x000000000C9], // mediaPause + [0x00000000828, 0x000000000A2], // eject + [0x00000000829, 0x000000000A7], // mediaRecord + [0x0000000082A, 0x0000000003B], // f1 + [0x0000000082B, 0x0000000003C], // f2 + [0x0000000082C, 0x0000000003D], // f3 + [0x0000000082D, 0x0000000003E], // f4 + [0x0000000082E, 0x0000000003F], // f5 + [0x0000000082F, 0x00000000040], // f6 + [0x00000000830, 0x00000000041], // f7 + [0x00000000831, 0x00000000042], // f8 + [0x00000000832, 0x00000000043], // f9 + [0x00000000833, 0x00000000044], // f10 + [0x00000000834, 0x00000000057], // f11 + [0x00000000835, 0x00000000058], // f12 + [0x00000000836, 0x00000000045], // numLock + [0x00000000837, 0x00000000052], // numpad0 + [0x00000000838, 0x0000000004F], // numpad1 + [0x00000000839, 0x00000000050], // numpad2 + [0x0000000083A, 0x00000000051], // numpad3 + [0x0000000083B, 0x0000000004B], // numpad4 + [0x0000000083C, 0x0000000004C], // numpad5 + [0x0000000083D, 0x0000000004D], // numpad6 + [0x0000000083E, 0x00000000047], // numpad7 + [0x0000000083F, 0x00000000048], // numpad8 + [0x00000000840, 0x00000000049], // numpad9 + [0x00000000841, 0x00000000062], // numpadDivide + [0x00000000842, 0x00000000037], // numpadMultiply + [0x00000000843, 0x0000000004A], // numpadSubtract + [0x00000000844, 0x0000000004E], // numpadAdd + [0x00000000845, 0x00000000053], // numpadDecimal + [0x00000000846, 0x00000000079], // numpadComma + [0x00000000847, 0x00000000060], // numpadEnter + [0x00000000848, 0x00000000075], // numpadEqual + [0x00000000849, 0x000000000B3], // numpadParenLeft + [0x0000000084A, 0x000000000B4], // numpadParenRight + [0x00000000010, 0x00000000073], // audioVolumeUp + [0x00000000011, 0x00000000072], // audioVolumeDown + [0x00000000012, 0x00000000098], // power + [0x00000000001, 0x00000000066], // home + [0x00000000028, 0x000000000E1], // brightnessUp + [0x00000000029, 0x000000000E0], // brightnessDown + [0x0000000000A, 0x000000000A4], // mediaPlayPause + [0x0000000000B, 0x000000000A6], // mediaStop + [0x0000000000C, 0x000000000A3], // mediaTrackNext + [0x0000000000D, 0x000000000A5], // mediaTrackPrevious + [0x0000000000E, 0x000000000A8], // mediaRewind + [0x0000000000F, 0x000000000D0], // mediaFastForward + [0x00000000A28, 0x0000000008E], // sleep + [0x00000000A2E, 0x0000000005C], // convert + [0x00000000A30, 0x0000000005E], // nonConvert + [0x00000000A37, 0x0000000007C], // intlYen + [0x00000000A39, 0x00000000081], // again + [0x00000000A3A, 0x00000000082], // props + [0x00000000A3B, 0x00000000083], // undo + [0x00000000A3C, 0x00000000085], // copy + [0x00000000A3D, 0x00000000086], // open + [0x00000000A3E, 0x00000000087], // paste + [0x00000000A3F, 0x00000000088], // find + [0x00000000A40, 0x00000000089], // cut + [0x00000000A41, 0x0000000008A], // help + [0x00000000A44, 0x0000000009C], // browserFavorites + [0x00000000A46, 0x000000000A4], // mediaPlayPause + [0x00000000A48, 0x000000000CE], // close + [0x00000000A4C, 0x000000000AE], // exit + [0x00000000A51, 0x000000000B6], // redo + [0x00000000A52, 0x000000000CE], // close + [0x00000000A55, 0x000000000D2], // print + [0x00000000A68, 0x00000000166], // info + [0x00000000A82, 0x00000000192], // channelUp + [0x00000000A83, 0x00000000193], // channelDown + [0x00000000AF2, 0x0000000008F], // wakeUp + [0x00000000AFD, 0x000000000A2], // eject + [0x00000000B00, 0x000000000B7], // f13 + [0x00000000B01, 0x000000000B8], // f14 + [0x00000000B02, 0x000000000B9], // f15 + [0x00000000B03, 0x000000000BA], // f16 + [0x00000000B04, 0x000000000BB], // f17 + [0x00000000B05, 0x000000000BC], // f18 + [0x00000000B06, 0x000000000BD], // f19 + [0x00000000B07, 0x000000000BE], // f20 + [0x00000000B08, 0x000000000BF], // f21 + [0x00000000B09, 0x000000000C0], // f22 + [0x00000000B0A, 0x000000000C1], // f23 + [0x00000000B0B, 0x000000000C2], // f24 + [0x00000000B0F, 0x000000000CD], // suspend + [0x00000000017, 0x00000000071], // audioVolumeMute + ]); + /** Bitmask for extracting key values. */ + public static kValueMask: number = 0x000FFFFFFFF; + /** Unicode plane identifier. */ + public static kUnicodePlane: number = 0x00000000000; + /** OpenHarmony plane identifier. */ + public static kOhosPlane: number = 0x01900000000; + /** Array of modifier key goals for synchronization. */ + public static modifierGoals: ModifierGoal[] = [ + new ModifierGoal( + "Ctrl", + [ + new KeyPair(0x000700e0, 0x0200000100), // CtrlLeft + new KeyPair(0x000700e4, 0x0200000101) // CtrlRight + ] + ), + new ModifierGoal( + "Shift", + [ + new KeyPair(0x000700e1, 0x0200000102), // ShiftLeft + new KeyPair(0x000700e5, 0x0200000103) // SHIftRight + ] + ), + new ModifierGoal( + "Alt", + [ + new KeyPair(0x000700e2, 0x0200000104), // AltLeft + new KeyPair(0x000700e6, 0x0200000105) // AltRight + ] + ) + ]; +} + +// Due to the lack of ability of metaState in Ohos, and to maintain consistency +// in metaState with other platforms. Hence, flutter-ohos defines these modifier +// key meta values to check whether the following keys are pressed +export class ModifierKeyMetaInfo { + private constructor() {} + static readonly NONE: number = 0; // no modifier keys are pressed + static readonly ALT: number = 0x02; + static readonly ALT_LEFT: number = 0x10; + static readonly ALT_RIGHT: number = 0x20; + static readonly SHIFT: number = 0x01; + static readonly SHIFT_LEFT: number = 0x40; + static readonly SHIFT_RIGHT: number = 0x80; + static readonly CTRL: number = 0x1000; + static readonly CTRL_LEFT: number = 0x2000; + static readonly CTRL_RIGHT: number = 0x4000; + static readonly META: number = 0x10000; + static readonly META_LEFT: number = 0x20000; + static readonly META_RIGHT: number = 0x40000; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/OhosTouchProcessor.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/OhosTouchProcessor.ets new file mode 100644 index 0000000..5150568 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/OhosTouchProcessor.ets @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import { TouchEvent } from '@ohos.multimodalInput.touchEvent'; +import Any from '../../plugin/common/Any'; + +/** + * Processor for handling touch events from OpenHarmony. + * This class processes touch events and converts them for Flutter. + */ +export default class OhosTouchProcessor { + private static POINTER_DATA_FIELD_COUNT: number = 35; + /** Number of bytes per field in pointer data. */ + static BYTES_PER_FIELD: number = 8; + private static POINTER_DATA_FLAG_BATCHED: number = 1; + + /** + * Processes a touch event. + * @param event - The touch event from OpenHarmony + * @param transformMatrix - The transformation matrix to apply + */ + public onTouchEvent(event: TouchEvent, transformMatrix: Any): void { + + } +} + +/** + * Types of pointer state changes. + */ +export enum PointerChange { + CANCEL = 0, + ADD = 1, + REMOVE = 2, + HOVER = 3, + DOWN = 4, + MOVE = 5, + UP = 6, + PAN_ZOOM_START = 7, + PAN_ZOOM_UPDATE = 8, + PAN_ZOOM_END = 9 +} + +/** + * Types of pointer input devices. + */ +export enum PointerDeviceKind { + TOUCH = 0, + MOUSE = 1, + STYLUS = 2, + INVERTED_STYLUS = 3, + TRACKPAD = 4, + UNKNOWN = 5 +} + +/** + * Types of pointer signal events. + */ +export enum PointerSignalKind { + NONE = 0, + SCROLL = 1, + SCROLL_INERTIA_CANCEL = 2, + SCALE = 3, + UNKNOWN = 4 +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/PlatformViewInfo.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/PlatformViewInfo.ets new file mode 100644 index 0000000..0aa90fa --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/PlatformViewInfo.ets @@ -0,0 +1,49 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + + +import PlatformView from '../../plugin/platform/PlatformView'; + +/** + * Information about a platform view embedded in Flutter. + */ +export class PlatformViewInfo { + /** The platform view instance. */ + public platformView: PlatformView; + /** The surface ID for rendering. */ + public surfaceId: string; + /** The width of the platform view. */ + public width: number; + /** The height of the platform view. */ + public height: number; + /** The top position of the platform view. */ + public top: number; + /** The left position of the platform view. */ + public left: number; + /** The layout direction for the platform view. */ + public direction: Direction; + + /** + * Constructs a new PlatformViewInfo instance. + * @param platformView - The platform view instance + * @param surfaceId - The surface ID + * @param width - The width of the view + * @param height - The height of the view + * @param top - The top position + * @param left - The left position + * @param direction - The layout direction + */ + constructor(platformView: PlatformView, surfaceId: string, width: number, height: number, top: number, left: number, + direction: Direction) { + this.platformView = platformView; + this.surfaceId = surfaceId; + this.width = width; + this.height = height; + this.top = top; + this.left = left; + this.direction = direction; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/Settings.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/Settings.ets new file mode 100644 index 0000000..3633492 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/Settings.ets @@ -0,0 +1,76 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import SettingsChannel, { PlatformBrightness } from '../engine/systemchannels/SettingsChannel' +import I18n from '@ohos.i18n' +import Log from '../../util/Log'; +import { MediaQuery } from '@ohos.arkui.UIContext'; + + +const TAG = "Settings"; + +/** + * Manages and sends system settings to Flutter. + * This class handles theme mode, text scale factor, and other system preferences. + */ +export default class Settings { + /** The SettingsChannel for sending settings to Flutter, or null if not set. */ + settingsChannel: SettingsChannel | null; + + /** + * Constructs a new Settings instance. + * @param settingsChannel - The SettingsChannel instance, or null + */ + constructor(settingsChannel: SettingsChannel | null) { + this.settingsChannel = settingsChannel; + } + + /** + * Sends current system settings to Flutter. + * @param mediaQuery - The MediaQuery instance for detecting theme mode + */ + sendSettings(mediaQuery: MediaQuery): void { + this.settingsChannel?.startMessage() + .setAlwaysUse24HourFormat(I18n.System.is24HourClock()) + .setNativeSpellCheckServiceDefined(false) + .setBrieflyShowPassword(false) + .setPlatformBrightness(this.getThemeMode(mediaQuery)) + .setTextScaleFactor(this.getTextScaleFactor()) + .send(); + } + + /** + * Gets the current theme mode (light or dark). + * @param mediaQuery - The MediaQuery instance for detecting dark mode + * @returns The platform brightness mode + */ + getThemeMode(mediaQuery: MediaQuery): PlatformBrightness { + + let listener = mediaQuery.matchMediaSync('(dark-mode: true)'); + if (listener.matches) { + Log.i(TAG, "return dark"); + return PlatformBrightness.DARK; + } else { + Log.i(TAG, "return light"); + return PlatformBrightness.LIGHT; + } + } + + /** + * Gets the text scale factor from system settings. + * @returns The text scale factor value + */ + getTextScaleFactor() : number { + let sysTextScaleFactor = AppStorage.get('fontSizeScale'); + if(sysTextScaleFactor == undefined) { + sysTextScaleFactor = 1.0; + Log.e(TAG, 'get textScaleFactor error, it is assigned to ' + JSON.stringify(sysTextScaleFactor)); + } + Log.i(TAG, "return textScaleFactor = " + JSON.stringify(sysTextScaleFactor)) + return sysTextScaleFactor; + } + +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/TouchEventProcessor.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/TouchEventProcessor.ets new file mode 100644 index 0000000..e33ab28 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/TouchEventProcessor.ets @@ -0,0 +1,539 @@ +/* +* Copyright (c) 2024 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +/** Handle the motion events received by the FlutterNapi. */ +// import PlainArray from '@ohos.util.PlainArray'; +// import { TouchEvent } from '@ohos.multimodalInput.touchEvent'; +// import Queue from '@ohos.util.Queue'; + +import { CustomTouchEvent, CustomTouchObject } from '../../plugin/platform/CustomTouchEvent'; +import display from '@ohos.display'; +import FlutterManager from './FlutterManager'; +import { EmbeddingNodeController } from './EmbeddingNodeController'; +import Any from '../../plugin/common/Any'; + + +const OH_NATIVEXCOMPONENT_UNKNOWN = 4; +const OH_NATIVEXCOMPONENT_TOOL_TYPE_UNKNOWN = 0; + +/** + * Represents a touch point in a native XComponent touch event. + */ +class OH_NativeXComponent_TouchPoint { + id: number = 0; + screenX: number = 0.0; + screenY: number = 0.0; + x: number = 0.0; + y: number = 0.0; + type: number = OH_NATIVEXCOMPONENT_UNKNOWN; + size: number = 0; + force: number = 0; + timeStamp: number = 0; + isPressed: boolean = false; + + /** + * Constructs a new OH_NativeXComponent_TouchPoint instance. + * @param id - The touch point ID + * @param screenX - The screen X coordinate + * @param screenY - The screen Y coordinate + * @param x - The local X coordinate + * @param y - The local Y coordinate + * @param type - The touch type + * @param size - The touch size + * @param force - The touch force + * @param timeStamp - The timestamp + * @param isPressed - Whether the touch is pressed + */ + constructor(id: number, + screenX: number, + screenY: number, + x: number, + y: number, + type: number, + size: number, + force: number, + timeStamp: number, + isPressed: boolean) { + this.id = id; + this.screenX = screenX; + this.screenY = screenY; + this.x = x; + this.y = y; + this.type = type; + this.size = size; + this.force = force; + this.timeStamp = timeStamp; + this.isPressed = isPressed; + } +} + +/** + * Represents a native XComponent touch event. + */ +class OH_NativeXComponent_TouchEvent { + id: number = 0; + screenX: number = 0.0; + screenY: number = 0.0; + x: number = 0.0; + y: number = 0.0; + type: number = OH_NATIVEXCOMPONENT_UNKNOWN; + size: number = 0; + force: number = 0; + deviceId: number = 0; + timeStamp: number = 0; + touchPoints: OH_NativeXComponent_TouchPoint[] = []; + numPoints: number = 0; + + /** + * Constructs a new OH_NativeXComponent_TouchEvent instance. + * @param id - The event ID + * @param screenX - The screen X coordinate + * @param screenY - The screen Y coordinate + * @param x - The local X coordinate + * @param y - The local Y coordinate + * @param type - The touch type + * @param size - The touch size + * @param force - The touch force + * @param deviceId - The device ID + * @param timeStamp - The timestamp + * @param touchPoints - Array of touch points + * @param numPoints - Number of touch points + */ + constructor(id: number, + screenX: number, + screenY: number, + x: number, + y: number, + type: number, + size: number, + force: number, + deviceId: number, + timeStamp: number, + touchPoints: OH_NativeXComponent_TouchPoint[], + numPoints: number) { + this.id = id; + this.screenX = screenX; + this.screenY = screenY; + this.x = x; + this.y = y; + this.type = type; + this.size = size; + this.force = force; + this.deviceId = deviceId; + this.timeStamp = timeStamp; + this.touchPoints = touchPoints; + this.numPoints = numPoints; + } +} + +/** + * Packet containing touch event data and tool information. + */ +class TouchPacket { + touchEvent: OH_NativeXComponent_TouchEvent; + toolType: number = OH_NATIVEXCOMPONENT_TOOL_TYPE_UNKNOWN; + tiltX: number = 0; + tiltY: number = 0; + + /** + * Constructs a new TouchPacket instance. + * @param touchEvent - The touch event + * @param toolType - The tool type + * @param tiltX - The X-axis tilt + * @param tiltY - The Y-axis tilt + */ + constructor(touchEvent: OH_NativeXComponent_TouchEvent, + toolType: number, + tiltX: number, + tiltY: number) { + this.touchEvent = touchEvent; + this.toolType = toolType; + this.tiltX = tiltX; + this.tiltY = tiltY; + } +} + +/** + * Packet containing mouse event data. + */ +class MousePacket { + offset: number; + x: number; + y: number; + screenX: number; + screenY: number; + timestamp: number; + action: number; + button: number; + + /** + * Constructs a new MousePacket instance. + * @param offset - The offset value + * @param x - The local X coordinate + * @param y - The local Y coordinate + * @param screenX - The screen X coordinate + * @param screenY - The screen Y coordinate + * @param timestamp - The timestamp + * @param action - The mouse action + * @param button - The mouse button + */ + constructor( offset: number, + x: number, + y: number, + screenX: number, + screenY: number, + timestamp: number, + action: number, + button: number) { + this.offset = offset; + this.x = x; + this.y = y; + this.screenX = screenX; + this.screenY = screenY; + this.timestamp = timestamp; + this.action = action; + this.button = button; + } +} + +/** + * Processor for handling touch events received by FlutterNapi. + * This class processes and converts touch events from native format to Flutter format. + */ +export default class TouchEventProcessor { + private static instance: TouchEventProcessor; + + /** + * Gets the singleton instance of TouchEventProcessor. + * @returns The singleton TouchEventProcessor instance + */ + static getInstance(): TouchEventProcessor { + if (TouchEventProcessor.instance == null) { + TouchEventProcessor.instance = new TouchEventProcessor(); + } + return TouchEventProcessor.instance; + } + + private decodeTouchPacket(strings: Array, densityPixels: number, top: number, left: number): TouchPacket { + let offset: number = 0; + let numPoint: number = parseInt(strings[offset++]); + let changesId: number = parseInt(strings[offset++]); + let changesscreenX: number = (parseFloat(strings[offset++]) / densityPixels); + let changesscreenY: number = (parseFloat(strings[offset++]) / densityPixels); + let changesX: number = ((parseFloat(strings[offset++]) / densityPixels) - left); + let changesY: number = ((parseFloat(strings[offset++]) / densityPixels) - top); + let changesType: number = parseInt(strings[offset++]); + let changesSize: number = parseFloat(strings[offset++]); + let changesForce: number = parseFloat(strings[offset++]); + let changesDeviceId: number = parseInt(strings[offset++]); + let changesTimeStamp: number = parseInt(strings[offset++]); + + const touchPoints: OH_NativeXComponent_TouchPoint[] = []; + for (let i = 0; i < numPoint; i++) { + const touchPoint: OH_NativeXComponent_TouchPoint = new OH_NativeXComponent_TouchPoint( + parseInt(strings[offset++]), + (parseFloat(strings[offset++]) / densityPixels), + (parseFloat(strings[offset++]) / densityPixels), + ((parseFloat(strings[offset++]) / densityPixels) - left), + ((parseFloat(strings[offset++]) / densityPixels) - top), + parseInt(strings[offset++]), + parseFloat(strings[offset++]), + parseFloat(strings[offset++]), + parseInt(strings[offset++]), + parseInt(strings[offset++]) === 1 ? true : false + ); + touchPoints.push(touchPoint); + } + + const touchEventInput: OH_NativeXComponent_TouchEvent = new OH_NativeXComponent_TouchEvent( + changesId, + changesscreenX, + changesscreenY, + changesX, + changesY, + changesType, + changesSize, + changesForce, + changesDeviceId, + changesTimeStamp, + touchPoints, + numPoint + ); + + let toolTypeInput: number = parseInt(strings[offset++]); + let tiltXTouch: number = parseInt(strings[offset++]); + let tiltYTouch: number = parseInt(strings[offset++]); + + const touchPointEventPacket: TouchPacket = new TouchPacket( + touchEventInput, + toolTypeInput, + tiltXTouch, + tiltYTouch + ); + return touchPointEventPacket; + } + + private constructCustomTouchEventImpl(touchPacket: TouchPacket): CustomTouchEvent { + let changes1: CustomTouchObject = new CustomTouchObject( + touchPacket.touchEvent.type, + touchPacket.touchEvent.id, + touchPacket.touchEvent.screenX, + touchPacket.touchEvent.screenY, + touchPacket.touchEvent.screenX, + touchPacket.touchEvent.screenY, + touchPacket.touchEvent.screenX, + touchPacket.touchEvent.screenY, + touchPacket.touchEvent.x, + touchPacket.touchEvent.y + ); + + let touches: CustomTouchObject[] = []; + let touchPointer: number = touchPacket.touchEvent.numPoints; + for (let i = 0; i < touchPointer; i++) { + let touchesItem: CustomTouchObject = new CustomTouchObject( + touchPacket.touchEvent.touchPoints[i].type, + touchPacket.touchEvent.touchPoints[i].id, + touchPacket.touchEvent.touchPoints[i].screenX, + touchPacket.touchEvent.touchPoints[i].screenY, + touchPacket.touchEvent.touchPoints[i].screenX, + touchPacket.touchEvent.touchPoints[i].screenY, + touchPacket.touchEvent.touchPoints[i].screenX, + touchPacket.touchEvent.touchPoints[i].screenY, + touchPacket.touchEvent.touchPoints[i].x, + touchPacket.touchEvent.touchPoints[i].y + ); + touches.push(touchesItem); + } + + let customTouchEvent1: CustomTouchEvent = new CustomTouchEvent( + touchPacket.touchEvent.type, + touches, + [changes1], + touchPacket.touchEvent.timeStamp, + SourceType.TouchScreen, + touchPacket.touchEvent.force, + touchPacket.tiltX, + touchPacket.tiltY, + touchPacket.toolType + ); + + return customTouchEvent1; + } + + /** + * Constructs a CustomTouchEvent from string array data. + * @param strings - Array of string values representing touch data + * @param top - The top offset + * @param left - The left offset + * @returns A CustomTouchEvent instance + */ + public constructCustomTouchEvent(strings: Array, top: number, left: number): CustomTouchEvent { + let densityPixels: number = display.getDefaultDisplaySync().densityPixels; + + let touchPacket: TouchPacket = this.decodeTouchPacket(strings, densityPixels, top, left); + let customTouchEvent: CustomTouchEvent = this.constructCustomTouchEventImpl(touchPacket); + return customTouchEvent; + } + + /** + * Posts an axis/scroll event to platform views. + * Axis/wheel coordinates are passed to nodes through this method. + * @param strings - Array of string values representing axis event data + */ + public postAxisEvent(strings: Array) { + FlutterManager.getInstance().getFlutterViewList().forEach((value) => { + if (!value.getActive()) { + return + } + let length = value.getDVModel().children.length + for (let index = length - 1; index >= 0; index--) { + const dvModel = value.getDVModel().children[index] + const params = dvModel.getLayoutParams() as Record; + if (!params["hover"]) { + continue; + } + const left = params['left'] as number ?? 0; + const top = params['top'] as number ?? 0; + const densityPixels: number = display.getDefaultDisplaySync().densityPixels; + + let offset: number = 0; + const action: number = parseFloat(strings[offset++]); + const x: number = vp2px((parseInt(strings[offset++])) / densityPixels - left); + const y: number = vp2px((parseInt(strings[offset++])) / densityPixels - top); + const windowX: number = vp2px(parseFloat(strings[offset++]) / densityPixels); + const windowY: number = vp2px(parseFloat(strings[offset++]) / densityPixels); + const displayX: number = vp2px(parseFloat(strings[offset++]) / densityPixels); + const displayY: number = vp2px(parseFloat(strings[offset++]) / densityPixels); + const scroll: number = parseFloat(strings[offset++]); + + // 构造AxisEvent数据 + const axisEvent: AxisEvent = { + action: action, + x: x, + y: y, + windowX: x, + windowY: y, + displayX: displayX, + displayY: displayY, + scrollStep: scroll, + axisVertical: scroll + } as AxisEvent; + + let nodeController = params['nodeController'] as EmbeddingNodeController; + nodeController.postAxisEvent(axisEvent) + } + }); + } + + /** + * Posts a touch event to platform views. + * @param strings - Array of string values representing touch event data + */ + public postTouchEvent(strings: Array) { + FlutterManager.getInstance().getFlutterViewList().forEach((value) => { + if (!value.getActive()) { + return + } + let length = value.getDVModel().children.length + for (let index = length - 1; index >= 0; index--) { + let dvModel = value.getDVModel().children[index] + let params = dvModel.getLayoutParams() as Record; + let left = params['left'] as number ?? 0; + let top = params['top'] as number ?? 0; + let down = params['down'] as boolean ?? false; + if (down) { + //如果flutter端判断当前platformView是可点击的,则将事件分发出去 + let touchEvent: CustomTouchEvent = TouchEventProcessor.getInstance().constructCustomTouchEvent(strings, top, left); + let nodeController = params['nodeController'] as EmbeddingNodeController; + nodeController.postEvent(touchEvent) + } else { + //如果触摸事件为OH_NATIVEXCOMPONENT_DOWN=0,且只有一个手指,说明是下一次点击了,这时候需要清空上一次的数据 + if (strings[6] == '0' && strings[0] == '1') { + params['touchEvent'] = undefined + } + //如果触摸事件为OH_NATIVEXCOMPONENT_DOWN=0类型,且在flutter端还没判断当前view是否处于点击区域内,则 + //将点击事件存储在list列表中。 + let touchEvent: CustomTouchEvent = TouchEventProcessor.getInstance().constructCustomTouchEvent(strings, top, left); + let array: Array | undefined = params['touchEvent'] as Array + if (array == undefined) { + array = [] + params['touchEvent'] = array + } + array.push(touchEvent) + } + } + }); + } + + private decodeMousePacket(strings: Array, densityPixels: number, top: number, left: number): MousePacket { + let offset: number = 0; + let x: number = vp2px((parseInt(strings[offset++])) / densityPixels - left); + let y: number = vp2px((parseInt(strings[offset++])) / densityPixels - top); + let screenX: number = vp2px(parseFloat(strings[offset++]) / densityPixels); + let screenY: number = vp2px(parseFloat(strings[offset++]) / densityPixels); + let timestamp: number = parseFloat(strings[offset++]); + let action: number = parseFloat(strings[offset++]); + let button: number = parseFloat(strings[offset++]); + + const mouseEventPacket: MousePacket = new MousePacket( + offset, + x, + y, + screenX, + screenY, + timestamp, + action, + button + ); + return mouseEventPacket; + } + + private constructMouseEventImpl(mousePacket: MousePacket): MouseEvent { + // 构造MouseEvent数据 + const mouseEvent: MouseEvent = { + button: mousePacket.button, + action: mousePacket.action, + displayX: mousePacket.x, + displayY: mousePacket.y, + windowX: mousePacket.x, + windowY: mousePacket.y, + screenX: mousePacket.screenX, + screenY: mousePacket.screenY, + x: mousePacket.x, + y: mousePacket.y, + stopPropagation: () => {}, + timestamp: mousePacket.timestamp, + pressure: 0, + tiltX: mousePacket.x, + tiltY: mousePacket.y, + } as MouseEvent; + + return mouseEvent; + } + + /** Construct the MouseEvent and return. */ + public constructMouseEvent(strings: Array, top: number, left: number): MouseEvent { + let densityPixels: number = display.getDefaultDisplaySync().densityPixels; + + let mousePacket: MousePacket = this.decodeMousePacket(strings, densityPixels, top, left); + let mouseEvent: MouseEvent = this.constructMouseEventImpl(mousePacket); + return mouseEvent; + } + + // 鼠标坐标通过postInputEvent传入到节点 + /** + * Posts a mouse event to platform views. + * @param strings - Array of string values representing mouse event data + */ + public postMouseEvent(strings: Array) { + FlutterManager.getInstance().getFlutterViewList().forEach((value) => { + if (!value.getActive()) { + return + } + let length = value.getDVModel().children.length + for (let index = length - 1; index >= 0; index--) { + const dvModel = value.getDVModel().children[index] + const params = dvModel.getLayoutParams() as Record; + const left = params['left'] as number ?? 0; + const top = params['top'] as number ?? 0; + let down = params['down'] as boolean ?? false; + let mouseEvent: MouseEvent = TouchEventProcessor.getInstance().constructMouseEvent(strings, top, left); + + if (mouseEvent.action != MouseAction.Press && mouseEvent.action != MouseAction.Release && params["hover"]) { + // 如果鼠标事件不是OH_NATIVEXCOMPONENT_MOUSE_PRESS类型或OH_NATIVEXCOMPONENT_MOUSE_RELEASE类型,则 + // 直接分发出去 + let nodeController = params['nodeController'] as EmbeddingNodeController; + nodeController.postMouseEvent(mouseEvent) + } else if (down) { + // 如果flutter端判断当前platformView是可点击的,则将事件分发出去,并结束分发 + let nodeController = params['nodeController'] as EmbeddingNodeController; + nodeController.postMouseEvent(mouseEvent) + break; + } else { + // 如果鼠标事件为OH_NATIVEXCOMPONENT_MOUSE_PRESS,说明是下一次点击了,这时候需要清空上一次的数据 + if (strings[5] == '1') { + params['mouseEvent'] = undefined + } + // 如果鼠标事件为OH_NATIVEXCOMPONENT_MOUSE_PRESS类型,且在flutter端还没判断当前view是否处于点击区域内,则将点击事件存储在list列表中。 + let array: Array | undefined = params['mouseEvent'] as Array + if (array == undefined) { + array = [] + params['mouseEvent'] = array + } + array.push(mouseEvent) + } + } + }); + } + + public checkHitPlatformView(left: number, top: number, width: number, height: number, x: number, y: number): boolean { + if (x >= left && x <= (left + width) && y >= top && y <= (top + height)) { + return true; + } else { + return false; + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/TouchEventTracker.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/TouchEventTracker.ets new file mode 100644 index 0000000..dfc30b0 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/TouchEventTracker.ets @@ -0,0 +1,113 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +/** Tracks the motion events received by the FlutterView. */ +import PlainArray from '@ohos.util.PlainArray'; +import { TouchEvent } from '@ohos.multimodalInput.touchEvent'; +import Queue from '@ohos.util.Queue'; + +/** + * Tracks touch events and provides unique identifiers for them. + * This class maintains a singleton instance for managing touch event tracking. + */ +export class TouchEventTracker { + private eventById: PlainArray; + private unusedEvents: Queue; + private static INSTANCE: TouchEventTracker; + + /** + * Gets the singleton instance of TouchEventTracker. + * @returns The singleton TouchEventTracker instance + */ + public static getInstance(): TouchEventTracker { + if (TouchEventTracker.INSTANCE == null) { + TouchEventTracker.INSTANCE = new TouchEventTracker(); + } + return TouchEventTracker.INSTANCE; + } + + /** + * Constructs a new TouchEventTracker instance. + */ + constructor() { + this.eventById = new PlainArray(); + this.unusedEvents = new Queue(); + } + + /** + * Tracks the event and returns a unique TouchEventId identifying the event. + * @param event - The touch event to track + * @returns A unique TouchEventId for the event + */ + public track(event: TouchEvent): TouchEventId { + const eventId: TouchEventId = TouchEventId.createUnique(); + this.eventById.add(eventId.getId(), event); + this.unusedEvents.add(eventId.getId()); + return eventId; + } + + /** + * Returns the TouchEvent corresponding to the eventId while discarding all the motion events + * that occurred prior to the event represented by the eventId. + * @param eventId - The TouchEventId to retrieve + * @returns The TouchEvent corresponding to the eventId + */ + public pop(eventId: TouchEventId): TouchEvent { + // remove all the older events. + while (this.unusedEvents.length != 0 && this.unusedEvents.getFirst() < eventId.getId()) { + this.eventById.remove(this.unusedEvents.pop()); + } + + // remove the current event from the heap if it exists. + if (this.unusedEvents.length != 0 && this.unusedEvents.getFirst() == eventId.getId()) { + this.unusedEvents.pop(); + } + + const event: TouchEvent = this.eventById.get(eventId.getId()); + this.eventById.remove(eventId.getId()); + return event; + } +} + +/** + * Represents a unique identifier corresponding to a touch event. + */ +export class TouchEventId { + private static ID_COUNTER: number = 0; + private id: number; + + /** + * Constructs a new TouchEventId instance. + * @param id - The ID value + */ + constructor(id: number) { + this.id = id; + } + + /** + * Creates a TouchEventId from a given ID. + * @param id - The ID value + * @returns A new TouchEventId instance + */ + public static from(id: number): TouchEventId { + return new TouchEventId(id); + } + + /** + * Creates a unique TouchEventId with an auto-incremented ID. + * @returns A new unique TouchEventId instance + */ + public static createUnique(): TouchEventId { + return new TouchEventId(TouchEventId.ID_COUNTER++); + } + + /** + * Gets the ID value. + * @returns The ID value + */ + public getId(): number { + return this.id; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/WindowInfoRepositoryCallbackAdapterWrapper.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/WindowInfoRepositoryCallbackAdapterWrapper.ets new file mode 100644 index 0000000..d262cef --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/embedding/ohos/WindowInfoRepositoryCallbackAdapterWrapper.ets @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import UIAbility from '@ohos.app.ability.UIAbility'; + +/** + * Wrapper adapter for window information repository callbacks. + */ +export default class WindowInfoRepositoryCallbackAdapterWrapper { + /** + * Constructs a new WindowInfoRepositoryCallbackAdapterWrapper instance. + */ + constructor() { + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/PlatformPlugin.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/PlatformPlugin.ets new file mode 100644 index 0000000..39904c8 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/PlatformPlugin.ets @@ -0,0 +1,551 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformPlugin.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; +import { BusinessError } from '@kit.BasicServicesKit'; +import PlatformChannel, { + AppSwitcherDescription, + Brightness, + ClipboardContentFormat, + HapticFeedbackType, + PlatformMessageHandler, + SoundType, + SystemChromeStyle, + SystemUiMode, + SystemUiOverlay +} from '../embedding/engine/systemchannels/PlatformChannel'; +import FlutterManager from '../embedding/ohos/FlutterManager'; +import pasteboard from '@ohos.pasteboard'; +import Log from '../util/Log'; +import vibrator from '@ohos.vibrator'; +import window from '@ohos.window'; +import common from '@ohos.app.ability.common'; +import { MethodResult } from './common/MethodChannel'; +import Any from './common/Any'; +import router from '@ohos.router'; +import { PasteboardUtils } from '../util/PasteboardUtils'; +import { FlutterView } from '../view/FlutterView'; + +/** + * Plugin for handling platform-specific functionality in Flutter applications. + * This class manages system UI, clipboard, haptic feedback, and other platform services. + */ +export default class PlatformPlugin { + private static TAG = "PlatformPlugin"; + /** The callback handler for platform messages. */ + callback = new PlatformPluginCallback(); + + /** + * Constructs a new PlatformPlugin instance. + * @param platformChannel - The PlatformChannel for communication with Flutter. + * @param context - The application context. + * @param platformPluginDelegate - Optional delegate for platform-specific behavior. + */ + constructor(platformChannel: PlatformChannel, context: common.Context, + platformPluginDelegate?: PlatformPluginDelegate) { + this.callback.platformChannel = platformChannel; + this.callback.context = context; + this.callback.applicationContext = context?.getApplicationContext(); + this.callback.platform = this; + this.callback.platformPluginDelegate = platformPluginDelegate ?? null; + this.callback.platformChannel?.setPlatformMessageHandler(this.callback); + } + + /** + * Initializes the window references for system UI management. + */ + initWindow() { + try { + let context = this.callback.context!! + window.getLastWindow(context, (err, data) => { + if (err.code) { + Log.e(PlatformPlugin.TAG, "Failed to obtain the top window. Cause: " + JSON.stringify(err)); + return; + } + this.callback.lastWindow = data; + }); + const uiAbility = FlutterManager.getInstance().getUIAbility(context); + const windowStage = FlutterManager.getInstance().getWindowStage(uiAbility); + this.callback.mainWindow = windowStage.getMainWindowSync(); + } catch (err) { + Log.e(PlatformPlugin.TAG, "Failed to obtain the top window. Cause: " + JSON.stringify(err)); + } + } + + + /** + * Updates the system UI overlays (status bar and navigation bar) visibility. + */ + updateSystemUiOverlays(): void { + this.callback.mainWindow?.setWindowSystemBarEnable(this.callback.showBarOrNavigation); + + if (FlutterManager.getInstance().isWindowDecorSafeAreaAvoidSupported(this.callback.context)) { + const shouldHideWindowDecor = this.callback.shouldHideWindowDecor(); + this.callback.mainWindow?.setWindowDecorVisible(!shouldHideWindowDecor); + + // Adjust padding based on window decoration visibility + if (shouldHideWindowDecor) { + const titleButtonRect = this.callback.mainWindow?.getTitleButtonRect(); + const paddingHeight = (titleButtonRect && titleButtonRect.height > 0) ? titleButtonRect.height : 0; + this.callback.flutterView?.setPaddingTop(vp2px(paddingHeight)); + } else { + this.callback.flutterView?.setPaddingTop(0); + } + } + + if (this.callback.currentTheme != null) { + this.callback.setSystemChromeSystemUIOverlayStyle(this.callback.currentTheme); + } + } + + /** + * Sets the UIAbility context for platform operations. + * @param context - The UIAbility context + */ + setUIAbilityContext(context: common.UIAbilityContext): void { + this.callback.uiAbilityContext = context; + } + + /** + * Sets up a listener for system configuration changes. + */ + setSystemChromeChangeListener(): void { + if (this.callback.callbackId == null && this.callback.applicationContext != null) { + let that = this; + this.callback.callbackId = this.callback.applicationContext?.on('environment', { + onConfigurationUpdated(config) { + Log.d(PlatformPlugin.TAG, "onConfigurationUpdated: " + that.callback.showBarOrNavigation); + that.callback.platformChannel?.systemChromeChanged(that.callback.showBarOrNavigation.includes('status')); + }, + onMemoryLevel(level) { + } + }) + } + } + + /** + * Binds a FlutterView to this plugin. + * @param flutterView - The FlutterView instance + */ + setFlutterView(flutterView: FlutterView): void { + this.callback.flutterView = flutterView; + } + + /** + * Destroys the platform plugin and cleans up resources. + */ + public destroy() { + this.callback.platformChannel?.setPlatformMessageHandler(null); + } +} + +/** + * Delegate interface for platform-specific behavior. + */ +export interface PlatformPluginDelegate { + /** + * Called when the system navigator should be popped. + * @returns True if the delegate handled the pop, false otherwise. + */ + popSystemNavigator(): boolean; +} + +/** + * Callback implementation for handling platform messages from Flutter. + */ +export class PlatformPluginCallback implements PlatformMessageHandler { + private static TAG = "PlatformPluginCallback"; + /** The PlatformPlugin instance this callback is associated with. */ + platform: PlatformPlugin | null = null; + /** The main window for system UI operations. */ + mainWindow: window.Window | null = null; + /** The last window reference. */ + lastWindow: window.Window | null = null; + /** The PlatformChannel for communication with Flutter. */ + platformChannel: PlatformChannel | null = null; + /** The delegate for platform-specific behavior. */ + platformPluginDelegate: PlatformPluginDelegate | null = null; + /** The application context. */ + context: common.Context | null = null; + /** Array indicating which system bars should be shown ('status' and/or 'navigation'). */ + showBarOrNavigation: ('status' | 'navigation')[] = ['status', 'navigation']; + /** The UIAbility context for platform operations. */ + uiAbilityContext: common.UIAbilityContext | null = null; + /** The callback ID for system configuration changes. */ + callbackId: number | null = null; + /** The application context. */ + applicationContext: common.ApplicationContext | null = null; + /** The current system UI theme style. */ + currentTheme: SystemChromeStyle | null = null; + /** The FlutterView instance for padding control. */ + flutterView: FlutterView | null = null; + + /** + * Plays a system sound. + * @param soundType - The type of sound to play. + */ + playSystemSound(soundType: SoundType) { + } + + /** + * Triggers haptic feedback vibration. + * @param feedbackType - The type of haptic feedback + */ + async vibrateHapticFeedback(feedbackType: HapticFeedbackType) { + switch (feedbackType) { + case HapticFeedbackType.STANDARD: + await vibrator.startVibration({ type: 'time', duration: 75 }, + { id: 0, usage: 'touch' }); + break; + case HapticFeedbackType.LIGHT_IMPACT: + await vibrator.startVibration({ type: 'time', duration: 25 }, + { id: 0, usage: 'touch' }); + break; + case HapticFeedbackType.MEDIUM_IMPACT: + await vibrator.startVibration({ type: 'time', duration: 150 }, + { id: 0, usage: 'touch' }); + break; + case HapticFeedbackType.HEAVY_IMPACT: + await vibrator.startVibration({ type: 'time', duration: 300 }, + { id: 0, usage: 'touch' }); + break; + case HapticFeedbackType.SELECTION_CLICK: + await vibrator.startVibration({ type: 'time', duration: 100 }, + { id: 0, usage: 'touch' }); + break; + } + } + + /** + * Sets the preferred screen orientation. + * @param ohosOrientation - The orientation value. + * @param result - The method result callback. + */ + setPreferredOrientations(ohosOrientation: number, result: MethodResult) { + try { + Log.d(PlatformPluginCallback.TAG, "ohosOrientation: " + ohosOrientation); + this.mainWindow!.setPreferredOrientation(ohosOrientation, (err: BusinessError) => { + const errCode: number = err.code; + if (errCode) { + Log.e(PlatformPluginCallback.TAG, "Failed to set window orientation:" + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + return; + } + result.success(null); + }); + } catch (exception) { + Log.e(PlatformPluginCallback.TAG, "Failed to set window orientation:" + JSON.stringify(exception)); + result.error("error", JSON.stringify(exception), null); + } + } + + /** + * Sets the application switcher description (mission label). + * @param description - The app switcher description. + */ + setApplicationSwitcherDescription(description: AppSwitcherDescription) { + Log.d(PlatformPluginCallback.TAG, "setApplicationSwitcherDescription: " + JSON.stringify(description)); + try { + let label: string = description?.label; + this.uiAbilityContext?.setMissionLabel(label).then(() => { + Log.d(PlatformPluginCallback.TAG, "Succeeded in seting mission label"); + }) + } catch (err) { + Log.d(PlatformPluginCallback.TAG, "Failed to set mission label: " + JSON.stringify(err)); + } + } + + /** + * Shows the specified system UI overlays. + * @param overlays - Array of overlays to show. + */ + showSystemOverlays(overlays: SystemUiOverlay[]) { + this.setSystemChromeEnabledSystemUIOverlays(overlays); + } + + /** + * Sets the system UI mode. + * @param mode - The system UI mode to set. + */ + showSystemUiMode(mode: SystemUiMode) { + this.setSystemChromeEnabledSystemUIMode(mode); + } + + /** + * Sets up a listener for system UI changes. + */ + setSystemUiChangeListener() { + this.platform?.setSystemChromeChangeListener(); + } + + /** + * Restores the system UI overlays to their previous state. + */ + restoreSystemUiOverlays() { + this.platform?.updateSystemUiOverlays(); + } + + /** + * Sets the system UI overlay style (colors, brightness, etc.). + * @param systemUiOverlayStyle - The style to apply. + */ + setSystemUiOverlayStyle(systemUiOverlayStyle: SystemChromeStyle) { + Log.d(PlatformPluginCallback.TAG, "systemUiOverlayStyle:" + JSON.stringify(systemUiOverlayStyle)); + this.setSystemChromeSystemUIOverlayStyle(systemUiOverlayStyle); + } + + /** + * Pops the system navigator (goes back in navigation stack). + */ + popSystemNavigator() { + if (this.platformPluginDelegate != null && this.platformPluginDelegate?.popSystemNavigator()) { + return; + } + router.back(); + } + + /** + * Gets data from the system clipboard. + * @param result - The method result callback. + */ + getClipboardData(result: MethodResult): void { + let atManager = abilityAccessCtrl.createAtManager(); + atManager.requestPermissionsFromUser(this.uiAbilityContext, ['ohos.permission.READ_PASTEBOARD']).then((data) => { + // https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-permissionrequestresult-V5 + // Permission request result codes: + // -1: Not authorized, permission is configured but requires user to modify in Settings + // 0: Granted + // 2: Not authorized, request is invalid, possible reasons: + // - Target permission not declared in configuration file + // - Invalid permission name + // - Special conditions for certain permissions not met + enum AuthResultStatus { + NOT_CONFIGURED = -1, + GRANTED = 0, + INVALID_REQ = 2 + } + + let message: string = 'Failed to request permissions from user.'; + let authResult: number = data.authResults[0]; + switch (authResult) { + case AuthResultStatus.GRANTED: { + let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); + systemPasteboard.getData().then(async (pasteData: pasteboard.PasteData) => { + let pasteText: string = ''; + const recordCount: number = pasteData.getRecordCount(); + for (let i = 0; i < recordCount; i++) { + const record = pasteData.getRecord(i); + let text: string = ''; + if (typeof record.getValidTypes === 'function') { + // For api14 and above, click here. More formats are supported + text = await PasteboardUtils.getTargetTypesData(record); + } else if (record.mimeType === pasteboard.MIMETYPE_TEXT_HTML) { + const htmlText: StyledString = await StyledString.fromHtml(record.htmlText); + text = htmlText.getString(); + } else if (record.mimeType === pasteboard.MIMETYPE_TEXT_PLAIN) { + text = record.plainText; + } + pasteText += text; + } + let response: Any = new Map().set("text", pasteText); + result.success(response); + }).catch((err: BusinessError) => { + Log.e(PlatformPluginCallback.TAG, "Failed to get PasteData. Cause: " + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + }); + break; + } + case AuthResultStatus.NOT_CONFIGURED: { + message += 'Cause: Not configured in Settings'; + Log.i(PlatformPluginCallback.TAG, message); + result.success(null); + break; + } + case AuthResultStatus.INVALID_REQ: { + message += 'Cause: Invalid request'; + Log.i(PlatformPluginCallback.TAG, message); + result.success(null); + break; + } + default: { + message += `Unknown error: authResult=${authResult}`; + result.error("error", message, null); + break; + } + } + }).catch((err: BusinessError) => { + Log.e(PlatformPluginCallback.TAG, "Failed to request permissions from user. Cause: " + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + }) + } + + /** + * Sets data to the system clipboard. + * @param text - The text to set + * @param result - The method result callback + */ + setClipboardData(text: string, result: MethodResult) { + let pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); + let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); + try { + systemPasteboard.setDataSync(pasteData); + result.success(null); + } catch (err) { + Log.d(PlatformPluginCallback.TAG, "Failed to set PasteData. Cause: " + JSON.stringify(err)); + result.error("error", JSON.stringify(err), null); + } + } + + /** + * Checks if the clipboard contains string data. + * @returns True if clipboard has strings, false otherwise + */ + clipboardHasStrings(): boolean { + return false; + } + + /** + * Sets the system UI mode (fullscreen, immersive, etc.). + * @param mode - The system UI mode to set + */ + setSystemChromeEnabledSystemUIMode(mode: SystemUiMode): void { + Log.d(PlatformPluginCallback.TAG, "mode: " + mode); + let uiConfig: ('status' | 'navigation')[] = []; + if (mode == SystemUiMode.LEAN_BACK) { + // Full screen mode, status and navigation bars can be shown by tapping anywhere on the display + FlutterManager.getInstance().setUseFullScreen(true, null); + } else if (mode == SystemUiMode.IMMERSIVE) { + // Full screen mode, status and navigation bars can be shown by swiping from display edges, gesture not received by app + FlutterManager.getInstance().setUseFullScreen(true, null); + } else if (mode == SystemUiMode.IMMERSIVE_STICKY) { + // Full screen mode, status and navigation bars can be shown by swiping from display edges, gesture received by app + FlutterManager.getInstance().setUseFullScreen(true, null); + } else if (mode == SystemUiMode.EDGE_TO_EDGE) { + uiConfig = ['status', 'navigation']; + } else { + return; + } + this.showBarOrNavigation = uiConfig; + this.platform?.updateSystemUiOverlays(); + } + + /** + * Sets the system UI overlay style with colors and brightness. + * @param systemChromeStyle - The style configuration + */ + setSystemChromeSystemUIOverlayStyle(systemChromeStyle: SystemChromeStyle): void { + let isStatusBarLightIconValue: boolean = false; + let statusBarContentColorValue: string | undefined = undefined; + let statusBarColorValue: string | undefined = undefined; + let navigationBarColorValue: string | undefined = undefined; + let isNavigationBarLightIconValue: boolean = false; + + const currentProps = this.mainWindow?.getWindowSystemBarProperties(); + + if (systemChromeStyle.statusBarIconBrightness != null) { + switch (systemChromeStyle.statusBarIconBrightness) { + case Brightness.DARK: + isStatusBarLightIconValue = false; + statusBarContentColorValue = '#000000'; + break; + case Brightness.LIGHT: + isStatusBarLightIconValue = true; + statusBarContentColorValue = '#FFFFFF'; + break; + } + } else { + isStatusBarLightIconValue = currentProps?.isStatusBarLightIcon ?? false + } + + if (systemChromeStyle.statusBarColor != null) { + statusBarColorValue = "#" + systemChromeStyle.statusBarColor.toString(16).padStart(8, '0'); + } else { + statusBarColorValue = currentProps?.statusBarColor + } + + if (systemChromeStyle.systemStatusBarContrastEnforced != null) { + + } + + if (systemChromeStyle.systemNavigationBarIconBrightness != null) { + switch (systemChromeStyle.systemNavigationBarIconBrightness) { + case Brightness.DARK: + isNavigationBarLightIconValue = true; + break; + case Brightness.LIGHT: + isNavigationBarLightIconValue = false; + } + } else { + isNavigationBarLightIconValue = currentProps?.isNavigationBarLightIcon ?? false + } + + if (systemChromeStyle.systemNavigationBarColor != null) { + navigationBarColorValue = "#" + systemChromeStyle.systemNavigationBarColor.toString(16).padStart(8, '0'); + } else { + navigationBarColorValue = currentProps?.navigationBarColor + } + + if (systemChromeStyle.systemNavigationBarContrastEnforced != null) { + + } + this.currentTheme = systemChromeStyle; + const systemBarProperties: window.SystemBarProperties = { + statusBarColor: statusBarColorValue, + isStatusBarLightIcon: isStatusBarLightIconValue, + statusBarContentColor: statusBarContentColorValue, + navigationBarColor: navigationBarColorValue, + isNavigationBarLightIcon: isNavigationBarLightIconValue, + navigationBarContentColor: currentProps?.navigationBarContentColor, + enableStatusBarAnimation: currentProps?.enableStatusBarAnimation, + enableNavigationBarAnimation: currentProps?.enableNavigationBarAnimation, + } + Log.d(PlatformPluginCallback.TAG, "systemBarProperties: " + JSON.stringify(systemBarProperties)); + this.mainWindow?.setWindowSystemBarProperties(systemBarProperties); + } + + /** + * Sets which system UI overlays should be enabled. + * @param overlays - Array of overlays to enable + */ + setSystemChromeEnabledSystemUIOverlays(overlays: SystemUiOverlay[]): void { + let uiConfig: ('status' | 'navigation')[] = []; + if (overlays.length == 0) { + + } + for (let index = 0; index < overlays.length; ++index) { + let overlayToShow = overlays[index]; + switch (overlayToShow) { + case SystemUiOverlay.TOP_OVERLAYS: + uiConfig.push('status'); //hide navigation + break; + case SystemUiOverlay.BOTTOM_OVERLAYS: + uiConfig.push('navigation'); //hide bar + break; + } + } + this.showBarOrNavigation = uiConfig; + this.platform?.updateSystemUiOverlays(); + } + + /** + * Determines whether to hide window decoration based on system UI overlay settings. + * @returns True to hide window decoration, false to show it + */ + shouldHideWindowDecor(): boolean { + const hasStatus = this.showBarOrNavigation.includes('status'); + const hasNavigation = this.showBarOrNavigation.includes('navigation'); + + // Fullscreen: [] → hide + // Edge-to-edge: ['status', 'navigation'] → hide + // Bottom only: ['navigation'] → hide + // Top only: ['status'] → show + + return !hasStatus || hasNavigation; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/Any.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/Any.ets new file mode 100644 index 0000000..acc6e58 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/Any.ets @@ -0,0 +1,9 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +declare type Any = ESObject; + +export default Any; \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BackgroundBasicMessageChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BackgroundBasicMessageChannel.ets new file mode 100644 index 0000000..c818e9c --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BackgroundBasicMessageChannel.ets @@ -0,0 +1,165 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import MessageChannelUtils from '../../util/MessageChannelUtils'; +import Log from '../../util/Log'; +import { BinaryReply } from './BinaryMessenger'; +import { TaskQueue } from './BinaryMessenger'; +import MessageCodec from './MessageCodec'; +import { BinaryMessenger } from './BinaryMessenger'; +import SendableBinaryMessageHandler from './SendableBinaryMessageHandler' +import SendableMessageCodec from './SendableMessageCodec'; +import SendableMessageHandler from './SendableMessageHandler'; +import StringUtils from '../../util/StringUtils'; + +/** + * A named channel for communicating with Flutter using basic, asynchronous message passing + * on background threads. This channel uses sendable codecs that can be passed across thread boundaries. + * + * Messages are encoded into binary before being sent, and binary messages received are decoded + * into objects. The {@link SendableMessageCodec} used must be compatible with the one used by the + * Flutter application. This can be achieved by creating a `BasicMessageChannel` counterpart of this channel + * on the Dart side. The type of messages sent and received + * is {@code Any}, but only values supported by the specified {@link SendableMessageCodec} can be used. + * + * The logical identity of the channel is given by its name. Identically named channels will + * interfere with each other's communication. + * @template T - The type of message being sent/received + */ +export default class BackgroundBasicMessageChannel { + /** Tag for logging. */ + public static TAG = "BackgroundBasicMessageChannel#"; + /** Channel name for buffer management. */ + public static CHANNEL_BUFFERS_CHANNEL = "dev.flutter/channel-buffers"; + private messenger: BinaryMessenger; + private name: string; + private codec: SendableMessageCodec; + private taskQueue: TaskQueue; + + /** + * Constructs a new BackgroundBasicMessageChannel instance. + * @param messenger - The BinaryMessenger to use for communication + * @param name - The channel name + * @param codec - The SendableMessageCodec to use for encoding/decoding messages + * @param taskQueue - Optional TaskQueue for background processing, defaults to a new background task queue + */ + constructor(messenger: BinaryMessenger, name: string, codec: SendableMessageCodec, taskQueue?: TaskQueue) { + this.messenger = messenger + this.name = name + this.codec = codec + this.taskQueue = taskQueue ?? messenger.makeBackgroundTaskQueue() + } + + /** + * Sends the specified message to the Flutter application, optionally expecting a reply. + * + * Any uncaught exception thrown by the reply callback will be caught and logged. + * + * @param message - The message, possibly null + * @param callback - A reply callback, possibly null + */ + send(message: T, callback?: (reply: T) => void): void { + this.messenger.send(this.name, this.codec.encodeMessage(message), + callback == null ? null : new IncomingReplyHandler(callback, this.codec)); + } + + /** + * Registers a message handler on this channel for receiving messages sent from the Flutter + * application. + * + * Overrides any existing handler registration for (the name of) this channel. + * + * If no handler has been registered, any incoming message on this channel will be handled + * silently by sending a null reply. + * + * @param handler - A {@link SendableMessageHandler}, or null to deregister + */ + setMessageHandler(handler: SendableMessageHandler | null): void { + this.messenger.setMessageHandler(this.name, + handler == null ? null : new IncomingSendableMessageHandler(handler, this.codec), this.taskQueue); + } + + /** + * Adjusts the number of messages that will get buffered when sending messages to channels that + * aren't fully set up yet. For example, the engine isn't running yet or the channel's message + * handler isn't set up on the Dart side yet. + * @param newSize - The new buffer size + */ + resizeChannelBuffer(newSize: number): void { + MessageChannelUtils.resizeChannelBuffer(this.messenger, this.name, newSize); + } +} + + +/** + * Internal handler for incoming replies from Flutter in background threads. + * @template T - The type of message being handled + */ +class IncomingReplyHandler implements BinaryReply { + private callback: (reply: T) => void; + private codec: SendableMessageCodec + + /** + * Constructs a new IncomingReplyHandler instance. + * @param callback - The callback to invoke with the decoded reply + * @param codec - The SendableMessageCodec to use for decoding + */ + constructor(callback: (reply: T) => void, codec: SendableMessageCodec) { + this.callback = callback + this.codec = codec + } + + /** + * Handles a binary reply from Flutter. + * @param reply - The binary reply, possibly null + */ + reply(reply: ArrayBuffer | null) { + try { + this.callback(this.codec.decodeMessage(reply)); + } catch (e) { + Log.e(BackgroundBasicMessageChannel.TAG, "Failed to handle message reply", e); + } + } +} + +/** + * Internal handler for incoming messages from Flutter in background threads. + * @template T - The type of message being handled + */ +@Sendable +class IncomingSendableMessageHandler implements SendableBinaryMessageHandler { + private handler: SendableMessageHandler + private codec: SendableMessageCodec + + /** + * Constructs a new IncomingSendableMessageHandler instance. + * @param handler - The SendableMessageHandler to delegate to + * @param codec - The SendableMessageCodec to use for encoding/decoding + */ + constructor(handler: SendableMessageHandler, codec: SendableMessageCodec) { + this.handler = handler; + this.codec = codec + } + + /** + * Handles a binary message from Flutter. + * @param message - The binary message + * @param callback - The BinaryReply callback to send a response + */ + onMessage(message: ArrayBuffer, callback: BinaryReply) { + try { + this.handler.onMessage( + this.codec.decodeMessage(message), + { + reply: (reply: T): void => { + callback.reply(this.codec.encodeMessage(reply)); + } + }); + } catch (e) { + Log.e('WARNNING', "Failed to handle message: ", e); + callback.reply(StringUtils.stringToArrayBuffer("")); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BackgroundMethodChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BackgroundMethodChannel.ets new file mode 100644 index 0000000..9a3913b --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BackgroundMethodChannel.ets @@ -0,0 +1,185 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import Log from '../../util/Log'; +import MessageChannelUtils from '../../util/MessageChannelUtils'; +import StringUtils from '../../util/StringUtils'; +import { BinaryMessenger, BinaryReply, TaskQueue } from './BinaryMessenger'; +import Any from './Any'; +import MethodCall from './MethodCall'; +import MethodCodec from './MethodCodec'; +import { MethodResult } from './MethodChannel' +import SendableStandardMethodCodec from './SendableStandardMethodCodec'; +import SendableMethodCallHandler from './SendableMethodCallHandler' +import SendableMethodCodec from './SendableMethodCodec' +import SendableBinaryMessageHandler from './SendableBinaryMessageHandler' + +/** + * A named channel for communicating with Flutter using asynchronous method calls on background threads. + * This channel uses sendable codecs that can be passed across thread boundaries. + * + * Incoming method calls are decoded from binary on receipt, and results are encoded into + * binary before being transmitted back to Flutter. The {@link MethodCodec} used must be compatible + * with the one used by the Flutter application. This can be achieved by creating a `MethodChannel` + * counterpart of this channel on the Dart side. The type of method call arguments and results + * is {@code Any}, but only values supported by the specified {@link MethodCodec} can be used. + * + * The logical identity of the channel is given by its name. Identically named channels will + * interfere with each other's communication. + */ +export default class BackgroundMethodChannel { + /** Tag for logging. */ + static TAG = "BackgroundMethodChannel#"; + private messenger: BinaryMessenger; + private name: string; + private codec: SendableMethodCodec; + private taskQueue: TaskQueue; + private args: Object[]; + + /** + * Constructs a new BackgroundMethodChannel instance. + * @param messenger - The BinaryMessenger to use for communication + * @param name - The channel name + * @param codec - The SendableMethodCodec to use for encoding/decoding, defaults to SendableStandardMethodCodec.INSTANCE + * @param taskQueue - Optional TaskQueue for background processing, defaults to a new background task queue + * @param args - Additional arguments to pass to message handlers + */ + constructor(messenger: BinaryMessenger, + name: string, + codec: SendableMethodCodec = SendableStandardMethodCodec.INSTANCE, + taskQueue?: TaskQueue, + ...args: Object[]) { + this.messenger = messenger + this.name = name + this.codec = codec + this.taskQueue = taskQueue ?? messenger.makeBackgroundTaskQueue() + this.args = args + } + + /** + * Invokes a method on this channel, optionally expecting a result. + * + * Any uncaught exception thrown by the result callback will be caught and logged. + * + * @param method - The name of the method + * @param args - The arguments for the invocation, possibly null + * @param callback - A {@link MethodResult} callback for the invocation result, or null + */ + invokeMethod(method: string, args: Any, callback?: MethodResult): void { + this.messenger.send(this.name, + this.codec.encodeMethodCall(new MethodCall(method, args)), + callback == null ? null : new IncomingSendableResultHandler(callback, this.codec)); + } + + /** + * Registers a method call handler on this channel. + * + * Overrides any existing handler registration for (the name of) this channel. + * + * If no handler has been registered, any incoming method call on this channel will be handled + * silently by sending a null reply. This results in a `MissingPluginException` + * on the Dart side, unless an `OptionalMethodChannel` is used. + * + * @param handler - A {@link SendableMethodCallHandler}, or null to deregister + */ + setMethodCallHandler(handler: SendableMethodCallHandler | null): void { + this.messenger.setMessageHandler(this.name, + handler == null ? null : new IncomingSendableMethodCallHandler(handler, this.codec), + this.taskQueue, ...this.args); + } + + /** + * Adjusts the number of messages that will get buffered when sending messages to channels that + * aren't fully set up yet. For example, the engine isn't running yet or the channel's message + * handler isn't set up on the Dart side yet. + * @param newSize - The new buffer size + */ + resizeChannelBuffer(newSize: number): void { + MessageChannelUtils.resizeChannelBuffer(this.messenger, this.name, newSize); + } +} + +/** + * Internal handler for incoming method call results from Flutter in background threads. + */ +export class IncomingSendableResultHandler implements BinaryReply { + private callback: MethodResult; + private codec: SendableMethodCodec; + + /** + * Constructs a new IncomingSendableResultHandler instance. + * @param callback - The MethodResult callback to invoke + * @param codec - The SendableMethodCodec to use for decoding + */ + constructor(callback: MethodResult, codec: SendableMethodCodec) { + this.callback = callback; + this.codec = codec + } + + /** + * Handles a binary reply from Flutter. + * @param reply - The binary reply, possibly null + */ + reply(reply: ArrayBuffer | null): void { + try { + if (reply == null) { + this.callback.notImplemented(); + } else { + try { + this.callback.success(this.codec.decodeEnvelope(reply)); + } catch (e) { + this.callback.error(e.code, e.getMessage(), e.details); + } + } + } catch (e) { + Log.e(BackgroundMethodChannel.TAG, "Failed to handle method call result", e); + } + } +} + +/** + * Internal handler for incoming method calls from Flutter in background threads. + */ +@Sendable +export class IncomingSendableMethodCallHandler implements SendableBinaryMessageHandler { + private handler: SendableMethodCallHandler; + private codec: SendableMethodCodec; + + /** + * Constructs a new IncomingSendableMethodCallHandler instance. + * @param handler - The SendableMethodCallHandler to delegate to + * @param codec - The SendableMethodCodec to use for encoding/decoding + */ + constructor(handler: SendableMethodCallHandler, codec: SendableMethodCodec) { + this.handler = handler; + this.codec = codec; + } + + /** + * Handles a binary method call from Flutter. + * @param message - The binary message containing the method call + * @param reply - The BinaryReply callback to send a response + * @param args - Additional arguments passed to the handler + */ + onMessage(message: ArrayBuffer, reply: BinaryReply, ...args: Object[]): void { + try { + this.handler.onMethodCall( + this.codec.decodeMethodCall(message), + { + success: (result: Any): void => { + reply.reply(this.codec.encodeSuccessEnvelope(result)); + }, + error: (errorCode: string, errorMessage: string, errorDetails: Any): void => { + reply.reply(this.codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)); + }, + notImplemented: (): void => { + reply.reply(StringUtils.stringToArrayBuffer("")); + } + }, ...args); + } catch (e) { + reply.reply(this.codec.encodeErrorEnvelopeWithStacktrace("error", e.getMessage(), null, e)); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BasicMessageChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BasicMessageChannel.ets new file mode 100644 index 0000000..ca7057b --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BasicMessageChannel.ets @@ -0,0 +1,200 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on BasicMessageChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import MessageChannelUtils from '../../util/MessageChannelUtils'; +import { BinaryMessageHandler } from './BinaryMessenger'; +import Log from '../../util/Log'; +import { BinaryReply } from './BinaryMessenger'; +import { TaskQueue } from './BinaryMessenger'; +import MessageCodec from './MessageCodec'; +import { BinaryMessenger } from './BinaryMessenger'; +import StringUtils from '../../util/StringUtils'; + +/** + * A named channel for communicating with the Flutter application using basic, asynchronous message + * passing. + * + * Messages are encoded into binary before being sent, and binary messages received are decoded + * into objects. The {@link MessageCodec} used must be compatible with the one used by the + * Flutter application. This can be achieved by creating a `BasicMessageChannel` + * counterpart of this channel on the Dart side. The static type of messages sent and received + * is `Object`, but only values supported by the specified {@link MessageCodec} can be used. + * + * The logical identity of the channel is given by its name. Identically named channels will + * interfere with each other's communication. + */ +export default class BasicMessageChannel { + /** Tag for logging. */ + public static TAG = "BasicMessageChannel#"; + /** Channel name for buffer management. */ + public static CHANNEL_BUFFERS_CHANNEL = "dev.flutter/channel-buffers"; + private messenger: BinaryMessenger; + private name: string; + private codec: MessageCodec; + + /** + * Constructs a new BasicMessageChannel instance. + * @param messenger - The BinaryMessenger to use for communication + * @param name - The channel name + * @param codec - The MessageCodec to use for encoding/decoding messages + */ + constructor(messenger: BinaryMessenger, name: string, codec: MessageCodec) { + this.messenger = messenger + this.name = name + this.codec = codec + } + + /** + * Sends the specified message to the Flutter application, optionally expecting a reply. + * + * Any uncaught exception thrown by the reply callback will be caught and logged. + * + * @param message - The message, possibly null + * @param callback - A {@link Reply} callback, possibly null + */ + send(message: T, callback?: (reply: T) => void): void { + this.messenger.send(this.name, this.codec.encodeMessage(message), + callback == null ? null : new IncomingReplyHandler(callback, this.codec)); + } + + /** + * Registers a message handler on this channel for receiving messages sent from the Flutter + * application. + * + * Overrides any existing handler registration for (the name of) this channel. + * + * If no handler has been registered, any incoming message on this channel will be handled + * silently by sending a null reply. + * + * @param handler - A {@link MessageHandler}, or null to deregister + */ + setMessageHandler(handler: MessageHandler | null): void { + this.messenger.setMessageHandler(this.name, + handler == null ? null : new IncomingMessageHandler(handler, this.codec)); + } + + /** + * Adjusts the number of messages that will get buffered when sending messages to channels that + * aren't fully set up yet. For example, the engine isn't running yet or the channel's message + * handler isn't set up on the Dart side yet. + * @param newSize - The new buffer size + */ + resizeChannelBuffer(newSize: number): void { + MessageChannelUtils.resizeChannelBuffer(this.messenger, this.name, newSize); + } +} + +/** + * Interface for handling message replies in BasicMessageChannel. + * @template T - The type of message being replied to + */ +export interface Reply { + /** + * Handles the specified message reply. + * + * @param reply - The reply, possibly null + */ + reply: (reply: T) => void; +} + +/** + * Interface for handling incoming messages in BasicMessageChannel. + * @template T - The type of message being handled + */ +export interface MessageHandler { + + /** + * Handles the specified message received from Flutter. + * + * Handler implementations must reply to all incoming messages, by submitting a single reply + * message to the given {@link Reply}. Failure to do so will result in lingering Flutter reply + * handlers. The reply may be submitted asynchronously and invoked on any thread. + * + * Any uncaught exception thrown by this method, or the preceding message decoding, will be + * caught by the channel implementation and logged, and a null reply message will be sent back + * to Flutter. + * + * Any uncaught exception thrown during encoding a reply message submitted to the {@link Reply} + * is treated similarly: the exception is logged, and a null reply is sent to Flutter. + * + * @param message - The message, possibly null + * @param reply - A {@link Reply} for sending a single message reply back to Flutter + */ + onMessage(message: T, reply: Reply): void; +} + +/** + * Internal handler for incoming replies from Flutter. + * @template T - The type of message being handled + */ +class IncomingReplyHandler implements BinaryReply { + private callback: (reply: T) => void; + private codec: MessageCodec + + /** + * Constructs a new IncomingReplyHandler instance. + * @param callback - The callback to invoke with the decoded reply + * @param codec - The MessageCodec to use for decoding + */ + constructor(callback: (reply: T) => void, codec: MessageCodec) { + this.callback = callback + this.codec = codec + } + + /** + * Handles a binary reply from Flutter. + * @param reply - The binary reply, possibly null + */ + reply(reply: ArrayBuffer | null) { + try { + this.callback(this.codec.decodeMessage(reply)); + } catch (e) { + Log.e(BasicMessageChannel.TAG, "Failed to handle message reply", e); + } + } +} + +/** + * Internal handler for incoming messages from Flutter. + * @template T - The type of message being handled + */ +class IncomingMessageHandler implements BinaryMessageHandler { + private handler: MessageHandler + private codec: MessageCodec + + /** + * Constructs a new IncomingMessageHandler instance. + * @param handler - The MessageHandler to delegate to + * @param codec - The MessageCodec to use for encoding/decoding + */ + constructor(handler: MessageHandler, codec: MessageCodec) { + this.handler = handler; + this.codec = codec + } + + /** + * Handles a binary message from Flutter. + * @param message - The binary message + * @param callback - The BinaryReply callback to send a response + */ + onMessage(message: ArrayBuffer, callback: BinaryReply) { + try { + this.handler.onMessage( + this.codec.decodeMessage(message), + { + reply: (reply: T): void => { + callback.reply(this.codec.encodeMessage(reply)); + } + }); + } catch (e) { + Log.e(BasicMessageChannel.TAG, "Failed to handle message", e); + callback.reply(StringUtils.stringToArrayBuffer("")); + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryCodec.ets new file mode 100644 index 0000000..bf64447 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryCodec.ets @@ -0,0 +1,58 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on BinaryCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import MessageCodec from './MessageCodec'; + +/** + * A {@link MessageCodec} using unencoded binary messages, represented as {@link ArrayBuffer}s. + * + * This codec is guaranteed to be compatible with the corresponding `BinaryCodec` on the Dart side. + * These parts of the Flutter SDK are evolved synchronously. + * + * On the Dart side, messages are represented using {@code ByteData}. + */ + +export default class BinaryCodec implements MessageCodec { + private returnsDirectByteBufferFromDecoding: boolean = false; + /** Direct instance that returns the direct buffer from decoding. */ + static readonly INSTANCE_DIRECT = new BinaryCodec(true); + + /** + * Constructs a new BinaryCodec instance. + * @param returnsDirectByteBufferFromDecoding - Whether to return the direct buffer from decoding + */ + constructor(returnsDirectByteBufferFromDecoding: boolean) { + this.returnsDirectByteBufferFromDecoding = returnsDirectByteBufferFromDecoding; + } + + /** + * Encodes a binary message (no-op for binary codec). + * @param message - The ArrayBuffer message to encode + * @returns The same ArrayBuffer + */ + encodeMessage(message: ArrayBuffer): ArrayBuffer { + return message + } + + /** + * Decodes a binary message. + * @param message - The ArrayBuffer message to decode, possibly null + * @returns The decoded ArrayBuffer, or a copy depending on configuration + */ + decodeMessage(message: ArrayBuffer | null): ArrayBuffer { + if (message == null) { + return new ArrayBuffer(0); + } else if (this.returnsDirectByteBufferFromDecoding) { + return message; + } else { + return message.slice(0, message.byteLength); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryMessenger.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryMessenger.ets new file mode 100644 index 0000000..ccdade6 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryMessenger.ets @@ -0,0 +1,197 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on BinaryMessenger.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * An abstraction over the threading policy used to invoke message handlers. + * + * These are generated by calling methods like {@link BinaryMessenger#makeBackgroundTaskQueue(TaskQueueOptions)} and can + * be passed into platform channels' constructors to control the threading policy for handling platform channels' messages. + */ + +import SendableBinaryMessageHandler from './SendableBinaryMessageHandler' + +/** + * An abstraction over the threading policy used to invoke message handlers. + * Task queues are used to control which thread handles platform channel messages. + */ +export interface TaskQueue {} + +/** + * The priority of task execution + * + * This priority is guaranteed to be compatible with `taskpool` + * ({@link https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-taskpool-V5#priority}). + * + */ +export enum TaskPriority { + HIGH = 0, + MEDIUM = 1, + LOW = 2, + IDLE = 3 +} + +/** + * Options that control how a TaskQueue should operate and be created. + */ +export class TaskQueueOptions { + private isSerial: boolean = true; + private isSingleThread: boolean = false; + private priority: TaskPriority = TaskPriority.MEDIUM; + + /** + * Gets whether tasks should be executed serially. + * @returns True if tasks are executed serially, false otherwise + */ + getIsSerial():boolean { + return this.isSerial; + } + + /** + * Sets whether tasks should be executed serially. + * @param isSerial - True to execute tasks serially, false for concurrent execution + * @returns This TaskQueueOptions instance for method chaining + */ + setIsSerial(isSerial: boolean): TaskQueueOptions { + this.isSerial = isSerial; + return this; + } + + /** + * Gets the task priority. + * @returns The current task priority + */ + getPriority(): TaskPriority { + return this.priority; + } + + /** + * Sets the task priority. + * @param priority - The task priority to set + * @returns This TaskQueueOptions instance for method chaining + */ + setPriority(priority: TaskPriority): TaskQueueOptions { + this.priority = priority; + return this; + } + + /** + * Checks if single thread mode is enabled. + * @returns True if single thread mode is enabled, false otherwise + */ + isSingleThreadMode(): boolean { + return this.isSingleThread; + } + + /** + * Sets single thread mode. + * @param isSingleThread - True to enable single thread mode, false otherwise + * @returns This TaskQueueOptions instance for method chaining + */ + setSingleThreadMode(isSingleThread: boolean): TaskQueueOptions { + this.isSingleThread = isSingleThread; + return this; + } +} + +/** + * Binary message reply callback. Used to submit a reply to an incoming message from Flutter. Also + * used in the dual capacity to handle a reply received from Flutter after sending a message. + */ +export interface BinaryReply { + /** + * Handles the specified reply. + * + * @param reply - The reply payload, an {@link ArrayBuffer} or null. Senders of + * outgoing replies must place the reply bytes in the buffer. + * Reply receivers can read from the buffer directly. + */ + reply: (reply: ArrayBuffer | null) => void; +} + +/** Handler for incoming binary messages from Flutter. */ +export interface BinaryMessageHandler { + /** + * Handles the specified message. + * + * Handler implementations must reply to all incoming messages, by submitting a single reply + * message to the given {@link BinaryReply}. Failure to do so will result in lingering Flutter + * reply handlers. The reply may be submitted asynchronously. + * + * Any uncaught exception thrown by this method will be caught by the messenger + * implementation and logged, and a null reply message will be sent back to Flutter. + * + * @param message - The message {@link ArrayBuffer} payload, possibly null + * @param reply - A {@link BinaryReply} used for submitting a reply back to Flutter + */ + onMessage(message: ArrayBuffer, reply: BinaryReply): void; +} + +/** + * Facility for communicating with Flutter using asynchronous message passing with binary messages. + * The Flutter Dart code should use `BinaryMessages` to participate. + * + * BinaryMessenger is expected to be utilized from a single thread throughout the + * duration of its existence. If created on the main thread, then all invocations should take place + * on the main thread. If created on a background thread, then all invocations should take place on + * that background thread. + * + * @see BasicMessageChannel , which supports message passing with Strings and semi-structured + * messages. + * @see MethodChannel , which supports communication using asynchronous method invocation. + * @see EventChannel , which supports communication using event streams. + */ + +export interface BinaryMessenger { + /** + * Creates a background task queue for handling messages on background threads. + * @param options - Optional TaskQueueOptions to configure the task queue + * @returns A TaskQueue instance + */ + makeBackgroundTaskQueue(options?: TaskQueueOptions): TaskQueue; + + /** + * Sends a binary message to the Flutter application. + * + * @param channel - The name of the logical channel used for the message + * @param message - The message payload, an {@link ArrayBuffer} or null + */ + send(channel: String, message: ArrayBuffer | null): void; + + /** + * Sends a binary message to the Flutter application, optionally expecting a reply. + * + * Any uncaught exception thrown by the reply callback will be caught and logged. + * + * @param channel - The name of the logical channel used for the message + * @param message - The message payload, an {@link ArrayBuffer} or null + * @param callback - A {@link BinaryReply} callback invoked when the Flutter application responds to + * the message, possibly null + */ + send(channel: String, message: ArrayBuffer, callback?: BinaryReply | null): void; + + /** + * Registers a handler to be invoked when the Flutter application sends a message to its host + * platform. + * + * Registration overwrites any previous registration for the same channel name. Use a null + * handler to deregister. + * + * If no handler has been registered for a particular channel, any incoming message on that + * channel will be handled silently by sending a null reply. + * + * @param channel - The name of the channel + * @param handler - A {@link BinaryMessageHandler} to be invoked on incoming messages, or null + * @param taskQueue - A {@link BinaryMessenger.TaskQueue} that specifies what thread will execute + * the handler. Specifying null means execute on the platform thread + * @param args - Additional arguments + */ + setMessageHandler(channel: String, handler: BinaryMessageHandler | SendableBinaryMessageHandler | null, + taskQueue?: TaskQueue, ...args: Object[]): void; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/EventChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/EventChannel.ets new file mode 100644 index 0000000..e736f8f --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/EventChannel.ets @@ -0,0 +1,334 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on EventChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import Log from '../../util/Log'; +import { BinaryMessageHandler, BinaryMessenger, BinaryReply, TaskQueue } from './BinaryMessenger'; +import Any from './Any'; +import MethodCodec from './MethodCodec'; +import StandardMethodCodec from './StandardMethodCodec'; + +const TAG = "EventChannel#"; + +/** + * A named channel for communicating with the Flutter application using asynchronous event streams. + * + * Incoming requests for event stream setup are decoded from binary on receipt, and + * responses and events are encoded into binary before being transmitted back to Flutter. The {@link MethodCodec} + * used must be compatible with the one used by the Flutter application. This can be achieved by creating an + * `EventChannel` counterpart of this channel on the Dart side. The type of stream configuration arguments, events, + * and error details is {@code Any}, but only values supported by the specified {@link MethodCodec} can be used. + * + * The logical identity of the channel is given by its name. Identically named channels will + * interfere with each other's communication. + */ +export default class EventChannel { + private messenger: BinaryMessenger; + private name: string; + private codec: MethodCodec; + private taskQueue: TaskQueue | null; + + /** + * Constructs a new EventChannel instance. + * @param messenger - The BinaryMessenger to use for communication + * @param name - The channel name + * @param codec - The MethodCodec to use for encoding/decoding, defaults to StandardMethodCodec.INSTANCE + * @param taskQueue - Optional TaskQueue for background processing + */ + constructor(messenger: BinaryMessenger, name: string, codec?: MethodCodec, taskQueue?: TaskQueue) { + this.messenger = messenger + this.name = name + this.codec = codec ? codec : StandardMethodCodec.INSTANCE + // TODO:(0xZOne): 实现后台处理 + // this.taskQueue = taskQueue ?? null + this.taskQueue = null + } + + + /** + * Registers a stream handler on this channel. + * + * Overrides any existing handler registration for (the name of) this channel. + * + * If no handler has been registered, any incoming stream setup requests will be handled + * silently by providing an empty stream. + * + * @param handler - A {@link StreamHandler}, or null to deregister + */ + setStreamHandler(handler: StreamHandler): void { + // We call the 2 parameter variant specifically to avoid breaking changes in + // mock verify calls. + // See https://github.com/flutter/flutter/issues/92582. + if (this.taskQueue != null) { + this.messenger.setMessageHandler( + this.name, + handler == null ? null : new IncomingStreamRequestHandler(handler, this.name, this.codec, this.messenger), + this.taskQueue); + } else { + this.messenger.setMessageHandler( + this.name, + handler == null ? null : new IncomingStreamRequestHandler(handler, this.name, this.codec, this.messenger)); + } + } +} + +/** + * Handler of stream setup and teardown requests. + * + * Implementations must be prepared to accept sequences of alternating calls to onListen and onCancel. + * Implementations should ideally consume no resources when the last such call is not onListen. + * In typical situations, this means that the implementation should register itself with + * platform-specific event sources onListen and deregister again onCancel. + */ +export interface StreamHandler { + /** + * Handles a request to set up an event stream. + * + * Any uncaught exception thrown by this method will be caught by the channel implementation + * and logged. An error result message will be sent back to Flutter. + * + * @param args - Stream configuration arguments, possibly null + * @param events - An {@link EventSink} for emitting events to the Flutter receiver + */ + onListen(args: Any, events: EventSink): void; + + /** + * Handles a request to tear down the most recently created event stream. + * + * Any uncaught exception thrown by this method will be caught by the channel implementation + * and logged. An error result message will be sent back to Flutter. + * + * The channel implementation may call this method with null arguments to separate a pair of + * two consecutive set up requests. Such request pairs may occur during Flutter hot restart. Any + * uncaught exception thrown in this situation will be logged without notifying Flutter. + * + * @param args - Stream configuration arguments, possibly null + */ + onCancel(args: Any): void; +} + +/** + * Event callback. Supports dual use: Producers of events to be sent to Flutter act as clients of + * this interface for sending events. Consumers of events sent from Flutter implement this + * interface for handling received events (the latter facility has not been implemented yet). + */ +export interface EventSink { + /** + * Consumes a successful event. + * + * @param event - The event, possibly null + */ + success(event: Any): void; + + /** + * Consumes an error event. + * + * @param errorCode - An error code string + * @param errorMessage - A human-readable error message, possibly null + * @param errorDetails - Error details, possibly null + */ + error(errorCode: string, errorMessage: string, errorDetails: Any): void; + + /** + * Consumes end of stream. Ensuing calls to success or error, if any, are ignored. + */ + endOfStream(): void; +} + +/** + * Internal handler for incoming stream requests from Flutter. + */ +class IncomingStreamRequestHandler implements BinaryMessageHandler { + private handler: StreamHandler; + private activeSink = new AtomicReference(null); + private codec: MethodCodec; + private name: string; + private messenger: BinaryMessenger; + + /** + * Constructs a new IncomingStreamRequestHandler instance. + * @param handler - The StreamHandler to delegate to + * @param name - The channel name + * @param codec - The MethodCodec to use for encoding/decoding + * @param messenger - The BinaryMessenger to use for sending events + */ + constructor(handler: StreamHandler, name: string, codec: MethodCodec, messenger: BinaryMessenger) { + this.handler = handler; + this.codec = codec; + this.name = name; + this.messenger = messenger; + } + + /** + * Handles a binary stream request from Flutter. + * @param message - The binary message containing the request + * @param reply - The BinaryReply callback to send a response + */ + onMessage(message: ArrayBuffer, reply: BinaryReply): void { + const call = this.codec.decodeMethodCall(message); + if (call.method == "listen") { + this.onListen(call.args, reply); + } else if (call.method == "cancel") { + this.onCancel(call.args, reply); + } else { + reply.reply(null); + } + } + + /** + * Handles a request to set up an event stream. + * @param args - Stream configuration arguments + * @param callback - The BinaryReply callback to send a response + */ + onListen(args: Any, callback: BinaryReply): void { + const eventSink = new EventSinkImplementation(this.activeSink, this.name, this.codec, this.messenger); + const oldSink = this.activeSink.getAndSet(eventSink); + if (oldSink != null) { + // Repeated calls to onListen may happen during hot restart. + // We separate them with a call to onCancel. + try { + this.handler.onCancel(null); + } catch (e) { + Log.e(TAG + this.name, "Failed to close existing event stream", e); + } + } + try { + this.handler.onListen(args, eventSink); + callback.reply(this.codec.encodeSuccessEnvelope(null)); + } catch (e) { + this.activeSink.set(null); + Log.e(TAG + this.name, "Failed to open event stream", e); + callback.reply(this.codec.encodeErrorEnvelope("error", e.getMessage(), null)); + } + } + + /** + * Handles a request to tear down an event stream. + * @param args - Stream configuration arguments + * @param callback - The BinaryReply callback to send a response + */ + onCancel(args: Any, callback: BinaryReply): void { + const oldSink = this.activeSink.getAndSet(null); + if (oldSink != null) { + try { + this.handler.onCancel(args); + callback.reply(this.codec.encodeSuccessEnvelope(null)); + } catch (e) { + Log.e(TAG + this.name, "Failed to close event stream", e); + callback.reply(this.codec.encodeErrorEnvelope("error", e.getMessage(), null)); + } + } else { + callback.reply(this.codec.encodeErrorEnvelope("error", "No active stream to cancel", null)); + } + } +} + +/** + * Implementation of EventSink for sending events to Flutter. + */ +class EventSinkImplementation implements EventSink { + private hasEnded = false; + private activeSink: AtomicReference; + private messenger: BinaryMessenger; + private codec: MethodCodec; + private name: string; + + /** + * Constructs a new EventSinkImplementation instance. + * @param activeSink - The AtomicReference to track the active sink + * @param name - The channel name + * @param codec - The MethodCodec to use for encoding + * @param messenger - The BinaryMessenger to use for sending events + */ + constructor(activeSink: AtomicReference, name: string, codec: MethodCodec, messenger: BinaryMessenger) { + this.activeSink = activeSink; + this.codec = codec; + this.name = name; + this.messenger = messenger; + } + + /** + * Sends a successful event to Flutter. + * @param event - The event data, possibly null + */ + success(event: Any): void { + if (this.hasEnded || this.activeSink.get() != this) { + return; + } + this.messenger.send(this.name, this.codec.encodeSuccessEnvelope(event)); + } + + /** + * Sends an error event to Flutter. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + */ + error(errorCode: string, errorMessage: string, errorDetails: Any) { + if (this.hasEnded || this.activeSink.get() != this) { + return; + } + this.messenger.send( + this.name, this.codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)); + } + + /** + * Signals the end of the event stream. + */ + endOfStream(): void { + if (this.hasEnded || this.activeSink.get() != this) { + return; + } + this.hasEnded = true; + this.messenger.send(this.name, new ArrayBuffer(0)); + } +} + +/** + * A simple atomic reference implementation for thread-safe value access. + * @template T - The type of value being referenced + */ +class AtomicReference { + private value: T | null; + + /** + * Constructs a new AtomicReference instance. + * @param value - The initial value, possibly null + */ + constructor(value: T | null) { + this.value = value + } + + /** + * Gets the current value. + * @returns The current value, possibly null + */ + get(): T | null { + return this.value; + } + + /** + * Sets a new value. + * @param newValue - The new value to set, possibly null + */ + set(newValue: T | null): void { + this.value = newValue; + } + + /** + * Atomically gets the current value and sets a new value. + * @param newValue - The new value to set, possibly null + * @returns The old value, possibly null + */ + getAndSet(newValue: T | null) { + const oldValue = this.value; + this.value = newValue; + return oldValue; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/FlutterException.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/FlutterException.ets new file mode 100644 index 0000000..31f8525 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/FlutterException.ets @@ -0,0 +1,39 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterException.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import Any from './Any'; + +/** + * Exception class for Flutter platform channel errors. + * This class represents errors that occur during communication between Flutter and the platform. + */ +export default class FlutterException implements Error { + /** Optional stack trace for the error. */ + stack?: string; + /** The error message. */ + message: string; + /** The error name. */ + name: string = ""; + /** The error code. */ + code: string; + /** Additional error details. */ + details: Any + + /** + * Constructs a new FlutterException instance. + * @param code - The error code + * @param message - The error message + * @param details - Additional error details + */ + constructor(code: string, message: string, details: Any) { + this.message = message; + this.code = code; + this.details = details; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/JSONMessageCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/JSONMessageCodec.ets new file mode 100644 index 0000000..054e1c7 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/JSONMessageCodec.ets @@ -0,0 +1,111 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on JSONMessageCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import StringUtils from '../../util/StringUtils'; + +import MessageCodec from './MessageCodec'; +import StringCodec from './StringCodec'; +import TreeMap from '@ohos.util.TreeMap'; +import HashMap from '@ohos.util.HashMap'; +import LightWeightMap from '@ohos.util.LightWeightMap'; +import PlainArray from '@ohos.util.PlainArray'; +import List from '@ohos.util.List'; +import LinkedList from '@ohos.util.LinkedList'; +import Any from './Any'; + +/** + * A {@link MessageCodec} using UTF-8 encoded JSON messages. + * + * This codec is guaranteed to be compatible with the corresponding `JSONMessageCodec` on the Dart side. + * These parts of the Flutter SDK are evolved synchronously. + * + * On the Dart side, JSON messages are handled by the JSON facilities of the `dart:convert` package. + */ +export default class JSONMessageCodec implements MessageCodec { + /** Singleton instance of JSONMessageCodec. */ + static INSTANCE = new JSONMessageCodec(); + + /** + * Encodes a message into JSON binary format. + * @param message - The message to encode, possibly null + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: Any): ArrayBuffer { + if (message == null) { + return StringUtils.stringToArrayBuffer(""); + } + return StringCodec.INSTANCE.encodeMessage(JSON.stringify(this.toBaseData(message))); + } + + /** + * Decodes a binary message from JSON format. + * @param message - The binary message to decode, possibly null + * @returns The decoded message object + * @throws Error if the JSON is invalid + */ + decodeMessage(message: ArrayBuffer | null): Any { + if (message == null) { + return StringUtils.stringToArrayBuffer(""); + } + try { + const jsonStr = StringCodec.INSTANCE.decodeMessage(message); + let jsonObj: Record = JSON.parse(jsonStr); + if (jsonObj instanceof Object) { + const list = Object.keys(jsonObj); + if (list.includes('args')) { + let args: Any = jsonObj['args']; + if (args instanceof Object && !(args instanceof Array)) { + let argsMap: Map = new Map(); + Object.keys(args).forEach(key => { + argsMap.set(key, args[key]); + }) + jsonObj['args'] = argsMap; + } + } + } + return jsonObj; + } catch (e) { + throw new Error("Invalid JSON"); + } + } + + /** + * Converts a message to base data types suitable for JSON serialization. + * @param message - The message to convert + * @returns The converted message with base data types + */ + toBaseData(message: Any): Any { + if (message == null || message == undefined) { + return null; + } else if (message instanceof List || message instanceof LinkedList) { + return this.toBaseData(message.convertToArray()); + } else if (message instanceof Map || message instanceof HashMap || message instanceof TreeMap + || message instanceof LightWeightMap || message instanceof PlainArray) { + let messageObj: Any = {}; + message.forEach((value: Any, key: Any) => { + messageObj[this.toBaseData(key)] = this.toBaseData(value); + }); + return messageObj; + } else if (message instanceof Array) { + let messageArr: Array = []; + message.forEach((value: Any) => { + messageArr.push(this.toBaseData(value)); + }) + return messageArr; + } else if (message instanceof Object) { + let messageObj: Any = {}; + Object.keys(message).forEach((key: Any) => { + messageObj[this.toBaseData(key)] = this.toBaseData(message[key]); + }) + return messageObj; + } else { + return message; + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/JSONMethodCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/JSONMethodCodec.ets new file mode 100644 index 0000000..97fb142 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/JSONMethodCodec.ets @@ -0,0 +1,132 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on JSONMethodCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import Log from '../../util/Log'; + +import ToolUtils from '../../util/ToolUtils'; +import FlutterException from './FlutterException'; +import Any from './Any'; +import JSONMessageCodec from './JSONMessageCodec'; +import MethodCall from './MethodCall'; +import MethodCodec from './MethodCodec'; + +/** + * A {@link MethodCodec} using UTF-8 encoded JSON method calls and result envelopes. + * + * This codec is guaranteed to be compatible with the corresponding `JSONMethodCodec` on + * the Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * Values supported as methods arguments and result payloads are those supported by {@link JSONMessageCodec}. + */ +export default class JSONMethodCodec implements MethodCodec { + /** Singleton instance of JSONMethodCodec. */ + static INSTANCE = new JSONMethodCodec(); + + /** + * Encodes a method call into JSON binary format. + * @param methodCall - The MethodCall to encode + * @returns The encoded method call as an ArrayBuffer + * @throws Error if encoding fails + */ + encodeMethodCall(methodCall: MethodCall): ArrayBuffer { + try { + const map: Record = { + "method": methodCall.method, "args": methodCall.args + } + + return JSONMessageCodec.INSTANCE.encodeMessage(map); + } catch (e) { + throw new Error("Invalid JSON"); + } + } + + /** + * Decodes a method call from JSON binary format. + * @param message - The binary message to decode + * @returns The decoded MethodCall + * @throws Error if decoding fails or the message is invalid + */ + decodeMethodCall(message: ArrayBuffer): MethodCall { + try { + const json: Any = JSONMessageCodec.INSTANCE.decodeMessage(message); + if (ToolUtils.isObj(json)) { + const method: string = json["method"]; + const args: Any = json["args"]; + if (typeof method == 'string') { + return new MethodCall(method, args); + } + } + throw new Error("Invalid method call: " + json); + } catch (e) { + throw new Error("Invalid JSON:" + JSON.stringify(e)); + } + } + + /** + * Encodes a successful result into a JSON envelope. + * @param result - The result value, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeSuccessEnvelope(result: Any): ArrayBuffer { + return JSONMessageCodec.INSTANCE.encodeMessage([result]); + } + + /** + * Encodes an error result into a JSON envelope. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelope(errorCode: Any, errorMessage: string, errorDetails: Any): ArrayBuffer { + return JSONMessageCodec.INSTANCE.encodeMessage([errorCode, errorMessage, errorDetails]); + } + + /** + * Encodes an error result into a JSON envelope with stacktrace. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @param errorStacktrace - The platform stacktrace, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelopeWithStacktrace(errorCode: string, errorMessage: string, errorDetails: Any, + errorStacktrace: string): ArrayBuffer { + return JSONMessageCodec.INSTANCE.encodeMessage([errorCode, errorMessage, errorDetails, errorStacktrace]) + } + + /** + * Decodes a result envelope from JSON binary format. + * @param envelope - The binary envelope to decode + * @returns The decoded result value + * @throws FlutterException if the envelope contains an error + * @throws Error if the envelope is invalid + */ + decodeEnvelope(envelope: ArrayBuffer): Any { + try { + const json: Any = JSONMessageCodec.INSTANCE.decodeMessage(envelope); + if (json instanceof Array) { + if (json.length == 1) { + return json[0]; + } + if (json.length == 3) { + const code: string = json[0]; + const message: string = json[1]; + const details: Any = json[2]; + if (typeof code == 'string' && (message == null || typeof message == 'string')) { + throw new FlutterException(code, message, details); + } + } + } + throw new Error("Invalid envelope: " + json); + } catch (e) { + throw new Error("Invalid JSON"); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MessageCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MessageCodec.ets new file mode 100644 index 0000000..cbd3690 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MessageCodec.ets @@ -0,0 +1,29 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MessageCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * A message encoding/decoding mechanism. + * @template T - The type of message being encoded/decoded + */ +export default interface MessageCodec { + /** + * Encodes the specified message into binary. + * @param message - The message to encode + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: T): ArrayBuffer; + + /** + * Decodes the specified message from binary. + * @param message - The binary message to decode, possibly null + * @returns The decoded message + */ + decodeMessage(message: ArrayBuffer | null): T; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall.ets new file mode 100644 index 0000000..de9e96e --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall.ets @@ -0,0 +1,76 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MethodCall.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import ToolUtils from '../../util/ToolUtils'; +import TreeMap from '@ohos.util.TreeMap'; +import HashMap from '@ohos.util.HashMap'; +import LightWeightMap from '@ohos.util.LightWeightMap'; +import Any from './Any'; + +/** + * Command object representing a method call on a MethodChannel. + */ +export default class MethodCall { + /** The name of the called method. */ + method: string; + /** + * Arguments for the call. + * + * Consider using the argument() method for cases where a particular run-time type is expected. + * Consider using argument(key) when that run-time type is Map or Object. + */ + args: Any; + + /** + * Constructs a new MethodCall instance. + * @param method - The name of the method to call + * @param args - The arguments for the method call + */ + constructor(method: string, args: Any) { + this.method = method; + this.args = args; + } + + /** + * Gets an argument value by key. + * @param key - The argument key + * @returns The argument value, or null if not found + * @throws Error if the args cannot be cast to a Map or Object + */ + argument(key: string): Any { + if (this.args == null) { + return null; + } else if (this.args instanceof Map) { + return (this.args as Map).get(key); + } else if (ToolUtils.isObj(this.args)) { + return this.args[key]; + } else { + throw new Error("ClassCastException"); + } + } + + /** + * Checks if an argument exists for the given key. + * @param key - The argument key to check + * @returns True if the argument exists, false otherwise + * @throws Error if the args cannot be cast to a Map or Object + */ + hasArgument(key: string): boolean { + if (this.args == null) { + return false; + } else if (this.args instanceof Map) { + return (this.args as Map).has(key); + } else if (ToolUtils.isObj(this.args)) { + return this.args.hasOwnProperty(key); + } else { + throw new Error("ClassCastException"); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel.ets new file mode 100644 index 0000000..b8fcab6 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel.ets @@ -0,0 +1,231 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MethodChannel.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import Log from '../../util/Log'; +import MessageChannelUtils from '../../util/MessageChannelUtils'; +import StringUtils from '../../util/StringUtils'; +import { BinaryMessageHandler, BinaryMessenger, BinaryReply } from './BinaryMessenger'; +import Any from './Any'; +import MethodCall from './MethodCall'; +import MethodCodec from './MethodCodec'; +import StandardMethodCodec from './StandardMethodCodec'; + +/** + * A named channel for communicating with the Flutter application using asynchronous method calls. + * + * Incoming method calls are decoded from binary on receipt, and results are encoded into + * binary before being transmitted back to Flutter. The {@link MethodCodec} used must be compatible + * with the one used by the Flutter application. This can be achieved by creating a `MethodChannel` counterpart of this + * channel on the Dart side. The type of method call arguments and results is {@code Any}, + * but only values supported by the specified {@link MethodCodec} can be used. + * + * The logical identity of the channel is given by its name. Identically named channels will + * interfere with each other's communication. + */ + +export default class MethodChannel { + /** Tag for logging. */ + static TAG = "MethodChannel#"; + private messenger: BinaryMessenger; + private name: string; + private codec: MethodCodec; + + /** + * Constructs a new MethodChannel instance. + * @param messenger - The BinaryMessenger to use for communication + * @param name - The channel name + * @param codec - The MethodCodec to use for encoding/decoding, defaults to StandardMethodCodec.INSTANCE + */ + constructor(messenger: BinaryMessenger, name: string, codec: MethodCodec = StandardMethodCodec.INSTANCE) { + this.messenger = messenger + this.name = name + this.codec = codec + } + + /** + * Invokes a method on this channel, optionally expecting a result. + * + * Any uncaught exception thrown by the result callback will be caught and logged. + * + * @param method - The name of the method + * @param args - The arguments for the invocation, possibly null + * @param callback - A {@link MethodResult} callback for the invocation result, or null + */ + invokeMethod(method: string, args: Any, callback?: MethodResult): void { + this.messenger.send(this.name, this.codec.encodeMethodCall(new MethodCall(method, args)), + callback == null ? null : new IncomingResultHandler(callback, this.codec)); + } + + /** + * Registers a method call handler on this channel. + * + * Overrides any existing handler registration for (the name of) this channel. + * + * If no handler has been registered, any incoming method call on this channel will be handled + * silently by sending a null reply. This results in a `MissingPluginException` on the Dart side, unless an + * `OptionalMethodChannel` is used. + * + * @param handler - A {@link MethodCallHandler}, or null to deregister + */ + setMethodCallHandler(handler: MethodCallHandler | null): void { + this.messenger.setMessageHandler(this.name, + handler == null ? null : new IncomingMethodCallHandler(handler, this.codec)); + } + + /** + * Adjusts the number of messages that will get buffered when sending messages to channels that + * aren't fully set up yet. For example, the engine isn't running yet or the channel's message + * handler isn't set up on the Dart side yet. + * @param newSize - The new buffer size + */ + resizeChannelBuffer(newSize: number): void { + MessageChannelUtils.resizeChannelBuffer(this.messenger, this.name, newSize); + } +} + +/** A handler of incoming method calls. */ +export interface MethodCallHandler { + /** + * Handles the specified method call received from Flutter. + * + * Handler implementations must submit a result for all incoming calls, by making a single + * call on the given {@link MethodResult} callback. Failure to do so will result in lingering Flutter + * result handlers. The result may be submitted asynchronously and on any thread. Calls to + * unknown or unimplemented methods should be handled using the notImplemented method of {@link MethodResult}. + * + * Any uncaught exception thrown by this method will be caught by the channel implementation + * and logged, and an error result will be sent back to Flutter. + * + * The handler is called on the platform thread (OpenHarmony main thread) by default, or otherwise on the thread + * specified by the {@link TaskQueue} provided to the associated {@link MethodChannel} when it was created. + * + * @param call - A {@link MethodCall} + * @param result - A {@link MethodResult} used for submitting the result of the call + */ + onMethodCall(call: MethodCall, result: MethodResult): void; +} + +/** + * Method call result callback. Supports dual use: Implementations of methods to be invoked by + * Flutter act as clients of this interface for sending results back to Flutter. Invokers of + * Flutter methods provide implementations of this interface for handling results received from + * Flutter. + * + * All methods of this class can be invoked on any thread. + */ +export interface MethodResult { + /** + * Handles a successful result. + * + * @param result - The result, possibly null. The result must be an Object type supported by the + * codec. For instance, if you are using {@link StandardMessageCodec} (default), please see + * its documentation on what types are supported. + */ + success: (result: Any) => void; + + /** + * Handles an error result. + * + * @param errorCode - An error code string + * @param errorMessage - A human-readable error message, possibly null + * @param errorDetails - Error details, possibly null. The details must be an Object type + * supported by the codec. For instance, if you are using {@link StandardMessageCodec} + * (default), please see its documentation on what types are supported. + */ + error: (errorCode: string, errorMessage: string, errorDetails: Any) => void; + + /** Handles a call to an unimplemented method. */ + notImplemented: () => void; +} + +/** + * Internal handler for incoming method call results from Flutter. + */ +export class IncomingResultHandler implements BinaryReply { + private callback: MethodResult; + private codec: MethodCodec; + + /** + * Constructs a new IncomingResultHandler instance. + * @param callback - The MethodResult callback to invoke + * @param codec - The MethodCodec to use for decoding + */ + constructor(callback: MethodResult, codec: MethodCodec) { + this.callback = callback; + this.codec = codec + } + + /** + * Handles a binary reply from Flutter. + * @param reply - The binary reply, possibly null + */ + reply(reply: ArrayBuffer | null): void { + try { + if (reply == null) { + this.callback.notImplemented(); + } else { + try { + this.callback.success(this.codec.decodeEnvelope(reply)); + } catch (e) { + this.callback.error(e.code, e.getMessage(), e.details); + } + } + } catch (e) { + Log.e(MethodChannel.TAG, "Failed to handle method call result", e); + } + } +} + +/** + * Internal handler for incoming method calls from Flutter. + */ +export class IncomingMethodCallHandler implements BinaryMessageHandler { + private handler: MethodCallHandler; + private codec: MethodCodec; + + /** + * Constructs a new IncomingMethodCallHandler instance. + * @param handler - The MethodCallHandler to delegate to + * @param codec - The MethodCodec to use for encoding/decoding + */ + constructor(handler: MethodCallHandler, codec: MethodCodec) { + this.handler = handler; + this.codec = codec + } + + /** + * Handles a binary method call from Flutter. + * @param message - The binary message containing the method call + * @param reply - The BinaryReply callback to send a response + */ + onMessage(message: ArrayBuffer, reply: BinaryReply): void { + const call = this.codec.decodeMethodCall(message); + try { + this.handler.onMethodCall( + call, { + success: (result: Any): void => { + reply.reply(this.codec.encodeSuccessEnvelope(result)); + }, + + error: (errorCode: string, errorMessage: string, errorDetails: Any): void => { + reply.reply(this.codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)); + }, + + notImplemented: (): void => { + Log.w(MethodChannel.TAG, "method not implemented"); + reply.reply(null); + } + }); + } catch (e) { + Log.e(MethodChannel.TAG, "Failed to handle method call", e); + reply.reply(this.codec.encodeErrorEnvelopeWithStacktrace("error", e.getMessage(), null, e)); + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCodec.ets new file mode 100644 index 0000000..00efea1 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCodec.ets @@ -0,0 +1,79 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MethodCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import Any from './Any'; + +import MethodCall from './MethodCall'; + +/** + * A codec for method calls and enveloped results. + * + * Method calls are encoded as binary messages with enough structure that the codec can extract a + * method name and arguments. These data items are used to populate a {@link MethodCall}. + * + * All operations throw an Error if conversion fails. + */ +export default interface MethodCodec { + /** + * Encodes a message call into binary. + * + * @param methodCall - A {@link MethodCall} + * @returns An {@link ArrayBuffer} containing the encoded method call + */ + encodeMethodCall(methodCall: MethodCall): ArrayBuffer; + + /** + * Decodes a message call from binary. + * + * @param methodCall - The binary encoding of the method call as an {@link ArrayBuffer} + * @returns A {@link MethodCall} representation of the binary data + */ + decodeMethodCall(methodCall: ArrayBuffer): MethodCall; + + /** + * Encodes a successful result into a binary envelope message. + * + * @param result - The result value, possibly null + * @returns An {@link ArrayBuffer} containing the encoded envelope + */ + encodeSuccessEnvelope(result: Any): ArrayBuffer; + + /** + * Encodes an error result into a binary envelope message. + * + * @param errorCode - An error code string + * @param errorMessage - An error message string, possibly null + * @param errorDetails - Error details, possibly null. Consider supporting {@link Error} in your + * codec. This is the most common value passed to this field. + * @returns An {@link ArrayBuffer} containing the encoded envelope + */ + encodeErrorEnvelope(errorCode: string, errorMessage: string, errorDetails: Any): ArrayBuffer; + + /** + * Encodes an error result into a binary envelope message with the native stacktrace. + * + * @param errorCode - An error code string + * @param errorMessage - An error message string, possibly null + * @param errorDetails - Error details, possibly null. Consider supporting {@link Error} in your + * codec. This is the most common value passed to this field. + * @param errorStacktrace - Platform stacktrace for the error, possibly null + * @returns An {@link ArrayBuffer} containing the encoded envelope + */ + encodeErrorEnvelopeWithStacktrace(errorCode: string, errorMessage: string, errorDetails: Any, + errorStacktrace: string): ArrayBuffer + + /** + * Decodes a result envelope from binary. + * + * @param envelope - The binary encoding of a result envelope as an {@link ArrayBuffer} + * @returns The enveloped result value + * @throws FlutterException if the envelope was an error envelope + */ + decodeEnvelope(envelope: ArrayBuffer): Any +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableBinaryCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableBinaryCodec.ets new file mode 100644 index 0000000..5a174d5 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableBinaryCodec.ets @@ -0,0 +1,63 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on BinaryCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import SendableMessageCodec from './SendableMessageCodec'; + +/** + * A {@link SendableMessageCodec} using unencoded binary messages, represented as {@link ArrayBuffer}s. + * + * This codec is guaranteed to be compatible with the corresponding `BinaryCodec` on the + * Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * On the Dart side, messages are represented using {@code ByteData}. + */ + +/** + * A sendable MessageCodec using unencoded binary messages. + * This codec can be used in background threads and worker contexts. + */ +@Sendable +export default class SendableBinaryCodec implements SendableMessageCodec { + private returnsDirectByteBufferFromDecoding: boolean = false; + /** Direct instance that returns the direct buffer from decoding. */ + static readonly INSTANCE_DIRECT: SendableBinaryCodec = new SendableBinaryCodec(true); + + /** + * Constructs a new SendableBinaryCodec instance. + * @param returnsDirectByteBufferFromDecoding - Whether to return the direct buffer from decoding + */ + constructor(returnsDirectByteBufferFromDecoding: boolean) { + this.returnsDirectByteBufferFromDecoding = returnsDirectByteBufferFromDecoding; + } + + /** + * Encodes a binary message (no-op for binary codec). + * @param message - The ArrayBuffer message to encode + * @returns The same ArrayBuffer + */ + encodeMessage(message: ArrayBuffer): ArrayBuffer { + return message + } + + /** + * Decodes a binary message. + * @param message - The ArrayBuffer message to decode, possibly null + * @returns The decoded ArrayBuffer, or a copy depending on configuration + */ + decodeMessage(message: ArrayBuffer | null): ArrayBuffer { + if (message == null) { + return new ArrayBuffer(0); + } else if (this.returnsDirectByteBufferFromDecoding) { + return message; + } else { + return message.slice(0, message.byteLength); + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableBinaryMessageHandler.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableBinaryMessageHandler.ets new file mode 100644 index 0000000..10a9c05 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableBinaryMessageHandler.ets @@ -0,0 +1,23 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import { lang } from '@kit.ArkTS'; +import { BinaryReply } from './BinaryMessenger'; + +type ISendable = lang.ISendable; + +/** + * Interface for sendable binary message handlers that can be used in background threads. + * This interface extends ISendable to allow handlers to be passed across thread boundaries. + */ +export default interface SendableBinaryMessageHandler extends ISendable { + /** + * Handles a binary message received from Flutter. + * @param message - The binary message received + * @param reply - The reply callback to send a response + * @param args - Additional arguments passed to the handler + */ + onMessage(message: ArrayBuffer, reply: BinaryReply, ...args: Object[]): void; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableJSONMessageCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableJSONMessageCodec.ets new file mode 100644 index 0000000..6870cba --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableJSONMessageCodec.ets @@ -0,0 +1,116 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on JSONMessageCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import StringUtils from '../../util/StringUtils'; + +import SendableMessageCodec from './SendableMessageCodec'; +import StringCodec from './StringCodec'; +import TreeMap from '@ohos.util.TreeMap'; +import HashMap from '@ohos.util.HashMap'; +import LightWeightMap from '@ohos.util.LightWeightMap'; +import PlainArray from '@ohos.util.PlainArray'; +import List from '@ohos.util.List'; +import LinkedList from '@ohos.util.LinkedList'; +import Any from './Any'; + +/** + * A {@link SendableMessageCodec} using UTF-8 encoded JSON messages. + * + * This codec is guaranteed to be compatible with the corresponding `JSONMessageCodec` on + * the Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * On the Dart side, JSON messages are handled by the JSON facilities of the `dart:convert` package. + */ +/** + * A sendable MessageCodec using UTF-8 encoded JSON messages. + * This codec can be used in background threads and worker contexts. + */ +@Sendable +export default class SendableJSONMessageCodec implements SendableMessageCodec { + /** Singleton instance of SendableJSONMessageCodec. */ + static INSTANCE: SendableJSONMessageCodec = new SendableJSONMessageCodec(); + + /** + * Encodes a message into JSON binary format. + * @param message - The message to encode, possibly null + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: Any): ArrayBuffer { + if (message == null) { + return StringUtils.stringToArrayBuffer(""); + } + return StringCodec.INSTANCE.encodeMessage(JSON.stringify(this.toBaseData(message))); + } + + /** + * Decodes a binary message from JSON format. + * @param message - The binary message to decode, possibly null + * @returns The decoded message object + * @throws Error if the JSON is invalid + */ + decodeMessage(message: ArrayBuffer | null): Any { + if (message == null) { + return StringUtils.stringToArrayBuffer(""); + } + try { + const jsonStr = StringCodec.INSTANCE.decodeMessage(message); + let jsonObj: Record = JSON.parse(jsonStr); + if (jsonObj instanceof Object) { + const list = Object.keys(jsonObj); + if (list.includes('args')) { + let args: Any = jsonObj['args']; + if (args instanceof Object && !(args instanceof Array)) { + let argsMap: Map = new Map(); + Object.keys(args).forEach(key => { + argsMap.set(key, args[key]); + }) + jsonObj['args'] = argsMap; + } + } + } + return jsonObj; + } catch (e) { + throw new Error("Invalid JSON"); + } + } + + /** + * Converts a message to base data types suitable for JSON serialization. + * @param message - The message to convert + * @returns The converted message with base data types + */ + toBaseData(message: Any): Any { + if (message == null || message == undefined) { + return ""; + } else if (message instanceof List || message instanceof LinkedList) { + return this.toBaseData(message.convertToArray()); + } else if (message instanceof Map || message instanceof HashMap || message instanceof TreeMap + || message instanceof LightWeightMap || message instanceof PlainArray) { + let messageObj: Any = {}; + message.forEach((value: Any, key: Any) => { + messageObj[this.toBaseData(key)] = this.toBaseData(value); + }); + return messageObj; + } else if (message instanceof Array) { + let messageArr: Array = []; + message.forEach((value: Any) => { + messageArr.push(this.toBaseData(value)); + }) + return messageArr; + } else if (message instanceof Object) { + let messageObj: Any = {}; + Object.keys(message).forEach((key: Any) => { + messageObj[this.toBaseData(key)] = this.toBaseData(message[key]); + }) + return messageObj; + } else { + return message; + } + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableJSONMethodCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableJSONMethodCodec.ets new file mode 100644 index 0000000..250b1c9 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableJSONMethodCodec.ets @@ -0,0 +1,135 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on JSONMethodCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import ToolUtils from '../../util/ToolUtils'; +import FlutterException from './FlutterException'; +import Any from './Any'; +import SendableJSONMessageCodec from './SendableJSONMessageCodec'; +import MethodCall from './MethodCall'; +import SendableMethodCodec from './SendableMethodCodec'; + +/** + * A {@link SendableMethodCodec} using UTF-8 encoded JSON method calls and result envelopes. + * + * This codec is guaranteed to be compatible with the corresponding `JSONMethodCodec` on + * the Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * Values supported as methods arguments and result payloads are those supported by {@link SendableJSONMessageCodec}. + */ +/** + * A sendable MethodCodec using UTF-8 encoded JSON method calls and result envelopes. + * This codec can be used in background threads and worker contexts. + */ +@Sendable +export default class SendableJSONMethodCodec implements SendableMethodCodec { + /** Singleton instance of SendableJSONMethodCodec. */ + static INSTANCE: SendableJSONMethodCodec = new SendableJSONMethodCodec(); + + /** + * Encodes a method call into JSON binary format. + * @param methodCall - The MethodCall to encode + * @returns The encoded method call as an ArrayBuffer + * @throws Error if encoding fails + */ + encodeMethodCall(methodCall: MethodCall): ArrayBuffer { + try { + const map: Record = { + "method": methodCall.method, "args": methodCall.args + } + + return SendableJSONMessageCodec.INSTANCE.encodeMessage(map); + } catch (e) { + throw new Error("Invalid JSON"); + } + } + + /** + * Decodes a method call from JSON binary format. + * @param message - The binary message to decode + * @returns The decoded MethodCall + * @throws Error if decoding fails or the message is invalid + */ + decodeMethodCall(message: ArrayBuffer): MethodCall { + try { + const json: Any = SendableJSONMessageCodec.INSTANCE.decodeMessage(message); + if (ToolUtils.isObj(json)) { + const method: string = json["method"]; + const args: Any = json["args"]; + if (typeof method == 'string') { + return new MethodCall(method, args); + } + } + throw new Error("Invalid method call: " + json); + } catch (e) { + throw new Error("Invalid JSON:" + JSON.stringify(e)); + } + } + + /** + * Encodes a successful result into a JSON envelope. + * @param result - The result value, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeSuccessEnvelope(result: Any): ArrayBuffer { + return SendableJSONMessageCodec.INSTANCE.encodeMessage([result]); + } + + /** + * Encodes an error result into a JSON envelope. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelope(errorCode: Any, errorMessage: string, errorDetails: Any) { + return SendableJSONMessageCodec.INSTANCE.encodeMessage([errorCode, errorMessage, errorDetails]); + } + + /** + * Encodes an error result into a JSON envelope with stacktrace. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @param errorStacktrace - The platform stacktrace, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelopeWithStacktrace(errorCode: string, errorMessage: string, errorDetails: Any, + errorStacktrace: string): ArrayBuffer { + return SendableJSONMessageCodec.INSTANCE.encodeMessage([errorCode, errorMessage, errorDetails, errorStacktrace]) + } + + /** + * Decodes a result envelope from JSON binary format. + * @param envelope - The binary envelope to decode + * @returns The decoded result value + * @throws FlutterException if the envelope contains an error + * @throws Error if the envelope is invalid + */ + decodeEnvelope(envelope: ArrayBuffer): Any { + try { + const json: Any = SendableJSONMessageCodec.INSTANCE.decodeMessage(envelope); + if (json instanceof Array) { + if (json.length == 1) { + return json[0]; + } + if (json.length == 3) { + const code: string = json[0]; + const message: string = json[1]; + const details: Any = json[2]; + if (typeof code == 'string' && (message == null || typeof message == 'string')) { + throw new FlutterException(code, message, details); + } + } + } + throw new Error("Invalid envelope: " + json); + } catch (e) { + throw new Error("Invalid JSON"); + } + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMessageCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMessageCodec.ets new file mode 100644 index 0000000..953de40 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMessageCodec.ets @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MessageCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import { lang } from '@kit.ArkTS'; + +type ISendable = lang.ISendable; + +/** + * Interface for sendable message codecs that can be used in background threads. + * This interface extends ISendable to allow codecs to be passed across thread boundaries. + * @template T - The type of message being encoded/decoded + */ +export default interface SendableMessageCodec extends ISendable { + /** + * Encodes the specified message into binary. + * @param message - The message to encode + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: T): ArrayBuffer; + + /** + * Decodes the specified message from binary. + * @param message - The binary message to decode, possibly null + * @returns The decoded message + */ + decodeMessage(message: ArrayBuffer | null): T; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMessageHandler.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMessageHandler.ets new file mode 100644 index 0000000..763067b --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMessageHandler.ets @@ -0,0 +1,23 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import { lang } from '@kit.ArkTS'; +import { Reply } from './BasicMessageChannel'; + +type ISendable = lang.ISendable; + +/** + * Interface for sendable message handlers that can be used in background threads. + * This interface extends ISendable to allow handlers to be passed across thread boundaries. + * @template T - The type of message being handled + */ +export default interface SendableMessageHandler extends ISendable { + /** + * Handles a message received from Flutter. + * @param message - The message received + * @param reply - The reply callback to send a response + */ + onMessage(message: T, reply: Reply): void; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMethodCallHandler.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMethodCallHandler.ets new file mode 100644 index 0000000..2d9be00 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMethodCallHandler.ets @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import { lang } from '@kit.ArkTS'; + +import MethodCall from './MethodCall'; +import { MethodResult } from './MethodChannel'; + +type ISendable = lang.ISendable; + +/** + * Interface for sendable method call handlers that can be used in background threads. + * This interface extends ISendable to allow handlers to be passed across thread boundaries. + */ +export default interface SendableMethodCallHandler extends ISendable { + /** + * Handles the specified method call received from Flutter. + * + * Handler implementations must submit a result for all incoming calls, by making a single + * call on the given {@link MethodResult} callback. Failure to do so will result in lingering Flutter + * result handlers. The result may be submitted asynchronously and on any thread. Calls to + * unknown or unimplemented methods should be handled using {@link MethodResult#notImplemented()}. + * + * Any uncaught exception thrown by this method will be caught by the channel implementation + * and logged, and an error result will be sent back to Flutter. + * + * The handler is called on the platform thread (OpenHarmony main thread) by default, or + * otherwise on the thread specified by the {@link BinaryMessenger.TaskQueue} provided to the + * associated {@link MethodChannel} when it was created. + * + * @param call - A {@link MethodCall} + * @param result - A {@link MethodResult} used for submitting the result of the call + * @param args - Additional arguments + */ + onMethodCall(call: MethodCall, result: MethodResult, ...args: Object[]): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMethodCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMethodCodec.ets new file mode 100644 index 0000000..d7f9e69 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableMethodCodec.ets @@ -0,0 +1,82 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MethodCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import { lang } from '@kit.ArkTS'; + +import Any from './Any'; +import MethodCall from './MethodCall'; + +/** + * A codec for method calls and enveloped results. + * + * Method calls are encoded as binary messages with enough structure that the codec can extract a + * method name and arguments. These data items are used to populate a {@link MethodCall}. + * + * All operations throw an Error if conversion fails. + */ +type ISendable = lang.ISendable; + +export default interface SendableMethodCodec extends ISendable { + /** + * Encodes a message call into binary. + * + * @param methodCall - A {@link MethodCall} + * @returns An {@link ArrayBuffer} containing the encoded method call + */ + encodeMethodCall(methodCall: MethodCall): ArrayBuffer; + + /** + * Decodes a message call from binary. + * + * @param methodCall - The binary encoding of the method call as an {@link ArrayBuffer} + * @returns A {@link MethodCall} representation of the binary data + */ + decodeMethodCall(methodCall: ArrayBuffer): MethodCall; + + /** + * Encodes a successful result into a binary envelope message. + * + * @param result - The result value, possibly null + * @returns An {@link ArrayBuffer} containing the encoded envelope + */ + encodeSuccessEnvelope(result: Any): ArrayBuffer; + + /** + * Encodes an error result into a binary envelope message. + * + * @param errorCode - An error code string + * @param errorMessage - An error message string, possibly null + * @param errorDetails - Error details, possibly null. Consider supporting {@link Error} in your + * codec. This is the most common value passed to this field. + * @returns An {@link ArrayBuffer} containing the encoded envelope + */ + encodeErrorEnvelope(errorCode: string, errorMessage: string, errorDetails: Any): ArrayBuffer; + + /** + * Encodes an error result into a binary envelope message with the native stacktrace. + * + * @param errorCode - An error code string + * @param errorMessage - An error message string, possibly null + * @param errorDetails - Error details, possibly null. Consider supporting {@link Error} in your + * codec. This is the most common value passed to this field. + * @param errorStacktrace - Platform stacktrace for the error, possibly null + * @returns An {@link ArrayBuffer} containing the encoded envelope + */ + encodeErrorEnvelopeWithStacktrace(errorCode: string, errorMessage: string, errorDetails: Any, + errorStacktrace: string): ArrayBuffer; + + /** + * Decodes a result envelope from binary. + * + * @param envelope - The binary encoding of a result envelope as an {@link ArrayBuffer} + * @returns The enveloped result value + * @throws FlutterException if the envelope was an error envelope + */ + decodeEnvelope(envelope: ArrayBuffer): Any; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStandardMessageCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStandardMessageCodec.ets new file mode 100644 index 0000000..f1e2278 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStandardMessageCodec.ets @@ -0,0 +1,413 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on StandardMessageCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import Any from './Any'; + +import { ByteBuffer } from '../../util/ByteBuffer'; +import SendableMessageCodec from './SendableMessageCodec'; +import StringUtils from '../../util/StringUtils'; +import TreeMap from '@ohos.util.TreeMap'; +import HashMap from '@ohos.util.HashMap'; +import LightWeightMap from '@ohos.util.LightWeightMap'; +import PlainArray from '@ohos.util.PlainArray'; +import List from '@ohos.util.List'; +import LinkedList from '@ohos.util.LinkedList'; + +/** + * MessageCodec using the Flutter standard binary encoding. + * + * This codec is guaranteed to be compatible with the corresponding `SendableStandardMessageCodec` + * on the Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * Supported messages are acyclic values of these forms: + * null + * Booleans + * number + * BigIntegers (see below) + * Int8Array, Int32Array, Float32Array, Float64Array + * Strings + * Array[] + * Lists of supported values + * Maps with supported keys and values + * + * On the Dart side, these values are represented as follows: + * null: null + * Boolean: bool + * Byte, Short, Integer, Long: int + * Float, Double: double + * String: String + * byte[]: Uint8List + * int[]: Int32List + * long[]: Int64List + * float[]: Float32List + * double[]: Float64List + * List: List + * Map: Map + * + * BigIntegers are represented in Dart as strings with the hexadecimal representation of the + * integer's value. + * + * To extend the codec, overwrite the writeValue and readValueOfType methods. + */ +/** + * A sendable MessageCodec using the Flutter standard binary encoding. + * This codec can be used in background threads and worker contexts. + */ +@Sendable +export default class SendableStandardMessageCodec implements SendableMessageCodec { + /** Singleton instance of SendableStandardMessageCodec. */ + static INSTANCE: SendableStandardMessageCodec = new SendableStandardMessageCodec(); + + /** + * Encodes a message into binary format using the standard encoding. + * @param message - The message to encode + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: Any): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)) + this.writeValue(stream, message); + return stream.buffer + } + + /** + * Decodes a binary message from the standard encoding. + * @param message - The binary message to decode, possibly null + * @returns The decoded message + */ + decodeMessage(message: ArrayBuffer | null): Any { + if (message == null) { + return null + } + const buffer = ByteBuffer.from(message) + return this.readValue(buffer) + } + + private static NULL: number = 0; + private static TRUE: number = 1; + private static FALSE: number = 2; + private static INT32: number = 3; + private static INT64: number = 4; + private static BIGINT: number = 5; + private static FLOAT64: number = 6; + private static STRING: number = 7; + private static UINT8_ARRAY: number = 8; + private static INT32_ARRAY: number = 9; + private static INT64_ARRAY: number = 10; + private static FLOAT64_ARRAY: number = 11; + private static LIST: number = 12; + private static MAP: number = 13; + private static FLOAT32_ARRAY: number = 14; + + /** + * Writes a value to the byte buffer using the standard encoding. + * @param stream - The ByteBuffer to write to + * @param value - The value to write + * @returns The ByteBuffer stream + */ + writeValue(stream: ByteBuffer, value: Any): Any { + if (value == null || value == undefined) { + stream.writeInt8(SendableStandardMessageCodec.NULL); + } else if (typeof value === "boolean") { + stream.writeInt8(value ? SendableStandardMessageCodec.TRUE : SendableStandardMessageCodec.FALSE) + } else if (typeof value === "number") { + if (Number.isInteger(value)) { //整型 + if (-0x7fffffff - 1 <= value && value <= 0x7fffffff) { //int32 + stream.writeInt8(SendableStandardMessageCodec.INT32); + stream.writeInt32(value, true); + } else if (Number.MIN_SAFE_INTEGER <= value && value <= Number.MAX_SAFE_INTEGER) { //int64 number整型取值范围 + stream.writeInt8(SendableStandardMessageCodec.INT64); + stream.writeInt64(value, true); + } else { //被判为整型的double型 + stream.writeInt8(SendableStandardMessageCodec.FLOAT64); + this.writeAlignment(stream, 8); + stream.writeFloat64(value, true); + } + } else { //浮点型 + stream.writeInt8(SendableStandardMessageCodec.FLOAT64); + this.writeAlignment(stream, 8); + stream.writeFloat64(value, true); + } + } else if (typeof value === "bigint") { + // + // The format is first the type byte (0x05), then the actual number + // as an ASCII string giving the hexadecimal representation of the + // integer, with the string's length as encoded by writeSize + // followed by the string bytes. + stream.writeInt8(SendableStandardMessageCodec.BIGINT); + // Convert bigint to a hexadecimal string + const hexString = value.toString(16); + // Map each character in the hexadecimal string to its ASCII code + const asciiString = hexString.split('').map(char => char.charCodeAt(0)); + this.writeBytes(stream, Uint8Array.from(asciiString)); + } else if (typeof value === "string") { + stream.writeInt8(SendableStandardMessageCodec.STRING); + let stringBuff = StringUtils.stringToArrayBuffer(value); + this.writeBytes(stream, new Uint8Array(stringBuff)); + } else if (value instanceof Uint8Array) { + stream.writeInt8(SendableStandardMessageCodec.UINT8_ARRAY); + this.writeBytes(stream, value) + } else if (value instanceof Int32Array) { + stream.writeInt8(SendableStandardMessageCodec.INT32_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 4); + value.forEach(item => stream.writeInt32(item, true)); + } else if (value instanceof BigInt64Array) { + stream.writeInt8(SendableStandardMessageCodec.INT64_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 8); + value.forEach(item => stream.writeBigInt64(item, true)); + } else if (value instanceof Float32Array) { + stream.writeInt8(SendableStandardMessageCodec.FLOAT32_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 4); + value.forEach(item => stream.writeFloat32(item, true)); + } else if (value instanceof Float64Array) { + stream.writeInt8(SendableStandardMessageCodec.FLOAT64_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 8); + value.forEach(item => stream.writeFloat64(item, true)); + } else if (value instanceof Array || value instanceof Int8Array || value instanceof Int16Array + || value instanceof Uint16Array || value instanceof Uint32Array || value instanceof List + || value instanceof LinkedList) { + stream.writeInt8(SendableStandardMessageCodec.LIST) + this.writeSize(stream, value.length); + value.forEach((item: Any): void => this.writeValue(stream, item)); + } else if (value instanceof Map) { + stream.writeInt8(SendableStandardMessageCodec.MAP); + this.writeSize(stream, value.size); + value.forEach((value: Any, key: Any) => { + this.writeValue(stream, key); + this.writeValue(stream, value); + }); + } else if (value instanceof HashMap || value instanceof TreeMap || value instanceof LightWeightMap + || value instanceof PlainArray) { + stream.writeInt8(SendableStandardMessageCodec.MAP); + this.writeSize(stream, value.length); + value.forEach((value: Any, key: Any) => { + this.writeValue(stream, key); + this.writeValue(stream, value); + }); + } else if (typeof value == 'object') { + let map: Map = new Map(); + Object.keys(value).forEach(key => { + map.set(key, value[key]); + }); + this.writeValue(stream, map); + } else { + throw new Error("Unsupported value: " + value); + stream.writeInt8(SendableStandardMessageCodec.NULL); + } + return stream; + } + + /** + * Writes alignment padding to the stream. + * @param stream - The ByteBuffer to write to + * @param alignment - The alignment requirement (e.g., 4, 8) + */ + writeAlignment(stream: ByteBuffer, alignment: number) { + let mod: number = stream.byteOffset % alignment; + if (mod != 0) { + for (let i = 0; i < alignment - mod; i++) { + stream.writeInt8(0); + } + } + } + + /** + * Writes a size value to the stream using compact encoding. + * @param stream - The ByteBuffer to write to + * @param value - The size value to write + */ + writeSize(stream: ByteBuffer, value: number) { + if (value < 254) { + stream.writeUint8(value); + } else if (value <= 0xffff) { + stream.writeUint8(254); + stream.writeUint16(value, true); + } else { + stream.writeUint8(255); + stream.writeUint32(value, true); + } + } + + /** + * Writes a byte array to the stream. + * @param stream - The ByteBuffer to write to + * @param bytes - The byte array to write + */ + writeBytes(stream: ByteBuffer, bytes: Uint8Array) { + this.writeSize(stream, bytes.length) + stream.writeUint8Array(bytes); + } + + /** + * Reads a size value from the buffer using compact encoding. + * @param buffer - The ByteBuffer to read from + * @returns The size value + */ + readSize(buffer: ByteBuffer) { + let value = buffer.readUint8() & 0xff; + if (value < 254) { + return value; + } else if (value == 254) { + return buffer.readUint16(true); + } else { + return buffer.readUint32(true); + } + } + + /** + * Reads alignment padding from the buffer. + * @param buffer - The ByteBuffer to read from + * @param alignment - The alignment requirement (e.g., 4, 8) + */ + readAlignment(buffer: ByteBuffer, alignment: number) { + let mod = buffer.byteOffset % alignment; + if (mod != 0) { + buffer.skip(alignment - mod); + } + } + + /** + * Reads a value from the buffer using the standard encoding. + * @param buffer - The ByteBuffer to read from + * @returns The decoded value + */ + readValue(buffer: ByteBuffer): Any { + let type = buffer.readUint8() + return this.readValueOfType(type, buffer); + } + + /** + * Reads a byte array from the buffer. + * @param buffer - The ByteBuffer to read from + * @returns The byte array + */ + readBytes(buffer: ByteBuffer): Uint8Array { + let length = this.readSize(buffer); + let bytesBuffer = new ArrayBuffer(length); + let bytes = new Uint8Array(bytesBuffer); + bytes.set(buffer.readUint8Array(length)); + return bytes; + } + + /** + * Reads a value of a specific type from the buffer. + * @param type - The type code to read + * @param buffer - The ByteBuffer to read from + * @returns The decoded value + * @throws Error if the type is unknown or the message is corrupted + */ + readValueOfType(type: number, buffer: ByteBuffer): Any { + let result: Any; + switch (type) { + case SendableStandardMessageCodec.NULL: + result = null; + break; + case SendableStandardMessageCodec.TRUE: + result = true; + break; + case SendableStandardMessageCodec.FALSE: + result = false; + break; + case SendableStandardMessageCodec.INT32: + result = buffer.readInt32(true); + break; + case SendableStandardMessageCodec.INT64: + result = buffer.readInt64(true); + if (Number.MIN_SAFE_INTEGER <= result && result <= Number.MAX_SAFE_INTEGER) { + result = Number(result); + } + break; + case SendableStandardMessageCodec.BIGINT: + let bytes: Uint8Array = this.readBytes(buffer); + // Convert the byte array to a UTF-8 encoded string + const hexString: string = String.fromCharCode(...bytes); + // Parse the string as a hexadecimal BigInt + result = BigInt(`0x${hexString}`); + break; + case SendableStandardMessageCodec.FLOAT64: + this.readAlignment(buffer, 8); + result = buffer.readFloat64(true) + break; + case SendableStandardMessageCodec.STRING: { + let bytes: Uint8Array = this.readBytes(buffer); + result = StringUtils.uint8ArrayToString(bytes); + break; + } + case SendableStandardMessageCodec.UINT8_ARRAY: { + result = this.readBytes(buffer); + break; + } + case SendableStandardMessageCodec.INT32_ARRAY: { + let length = this.readSize(buffer); + let array = new Int32Array(length) + this.readAlignment(buffer, 4); + for (let i = 0; i < length; i++) { + array[i] = buffer.readInt32(true) + } + result = array; + break; + } + case SendableStandardMessageCodec.INT64_ARRAY: { + let length = this.readSize(buffer); + let array = new BigInt64Array(length) + this.readAlignment(buffer, 8); + for (let i = 0; i < length; i++) { + array[i] = buffer.readBigInt64(true) + } + result = array; + break; + } + case SendableStandardMessageCodec.FLOAT64_ARRAY: { + let length = this.readSize(buffer); + let array = new Float64Array(length) + this.readAlignment(buffer, 8); + for (let i = 0; i < length; i++) { + array[i] = buffer.readFloat64(true) + } + result = array; + break; + } + case SendableStandardMessageCodec.LIST: { + let length = this.readSize(buffer); + let array: Array = new Array(length) + for (let i = 0; i < length; i++) { + array[i] = this.readValue(buffer) + } + result = array; + break; + } + case SendableStandardMessageCodec.MAP: { + let size = this.readSize(buffer); + let map: Map = new Map() + for (let i = 0; i < size; i++) { + map.set(this.readValue(buffer), this.readValue(buffer)); + } + result = map; + break; + } + case SendableStandardMessageCodec.FLOAT32_ARRAY: { + let length = this.readSize(buffer); + let array = new Float32Array(length); + this.readAlignment(buffer, 4); + for (let i = 0; i < length; i++) { + array[i] = buffer.readFloat32(true) + } + result = array; + break; + } + default: + throw new Error("Message corrupted, type=" + type); + } + return result; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStandardMethodCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStandardMethodCodec.ets new file mode 100644 index 0000000..dd31884 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStandardMethodCodec.ets @@ -0,0 +1,158 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on StandardMethodCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import { ByteBuffer } from '../../util/ByteBuffer'; +import FlutterException from './FlutterException'; +import Any from './Any'; +import MethodCall from './MethodCall'; +import SendableMethodCodec from './SendableMethodCodec'; +import SendableStandardMessageCodec from './SendableStandardMessageCodec'; + +/** + * A {@link SendableMethodCodec} using the Flutter standard binary encoding. + * + * This codec is guaranteed to be compatible with the corresponding `StandardMethodCodec` + * on the Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * Values supported as method arguments and result payloads are those supported by {@link StandardMessageCodec}. + */ +/** + * A sendable MethodCodec using the Flutter standard binary encoding. + * This codec can be used in background threads and worker contexts. + */ +@Sendable +export default class SendableStandardMethodCodec implements SendableMethodCodec { + private static TAG: string = "SendableStandardMethodCodec"; + /** Singleton instance of SendableStandardMethodCodec. */ + public static INSTANCE: SendableStandardMethodCodec = + new SendableStandardMethodCodec(SendableStandardMessageCodec.INSTANCE); + private messageCodec: SendableStandardMessageCodec; + + /** + * Creates a new method codec based on the specified message codec. + * @param messageCodec - The SendableStandardMessageCodec to use for encoding/decoding + */ + constructor(messageCodec: SendableStandardMessageCodec) { + this.messageCodec = messageCodec; + } + + /** + * Encodes a method call into binary format. + * @param methodCall - The MethodCall to encode + * @returns The encoded method call as an ArrayBuffer + */ + encodeMethodCall(methodCall: MethodCall): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + this.messageCodec.writeValue(stream, methodCall.method); + this.messageCodec.writeValue(stream, methodCall.args); + return stream.buffer; + } + + /** + * Decodes a method call from binary format. + * @param methodCall - The binary message to decode + * @returns The decoded MethodCall + * @throws Error if the method call is corrupted + */ + decodeMethodCall(methodCall: ArrayBuffer): MethodCall { + const buffer = ByteBuffer.from(methodCall); + const method: Any = this.messageCodec.readValue(buffer); + const args: Any = this.messageCodec.readValue(buffer); + if (typeof method == 'string' && !buffer.hasRemaining()) { + return new MethodCall(method, args); + } + throw new Error("Method call corrupted"); + } + + /** + * Encodes a successful result into a binary envelope. + * @param result - The result value, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeSuccessEnvelope(result: Any): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + stream.writeInt8(0); + this.messageCodec.writeValue(stream, result); + return stream.buffer; + } + + /** + * Encodes an error result into a binary envelope. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelope(errorCode: string, errorMessage: string, errorDetails: Any): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + stream.writeInt8(1); + this.messageCodec.writeValue(stream, errorCode); + this.messageCodec.writeValue(stream, errorMessage); + if (errorDetails instanceof Error) { + this.messageCodec.writeValue(stream, errorDetails.stack); + } else { + this.messageCodec.writeValue(stream, errorDetails); + } + return stream.buffer; + } + + /** + * Encodes an error result into a binary envelope with stacktrace. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @param errorStacktrace - The platform stacktrace, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelopeWithStacktrace(errorCode: string, errorMessage: string, errorDetails: Any, + errorStacktrace: string): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + stream.writeInt8(1); + this.messageCodec.writeValue(stream, errorCode); + this.messageCodec.writeValue(stream, errorMessage); + if (errorDetails instanceof Error) { + this.messageCodec.writeValue(stream, errorDetails.stack); + } else { + this.messageCodec.writeValue(stream, errorDetails); + } + this.messageCodec.writeValue(stream, errorStacktrace); + return stream.buffer; + } + + /** + * Decodes a result envelope from binary format. + * @param envelope - The binary envelope to decode + * @returns The decoded result value + * @throws FlutterException if the envelope contains an error + * @throws Error if the envelope is corrupted + */ + decodeEnvelope(envelope: ArrayBuffer): Any { + const buffer = ByteBuffer.from(envelope); + const flag = buffer.readInt8(); + switch (flag) { + case 0: { + const result: Any = this.messageCodec.readValue(buffer); + if (!buffer.hasRemaining()) { + return result; + } + // Falls through intentionally. + } + case 1: { + const code: Any = this.messageCodec.readValue(buffer); + const message: Any = this.messageCodec.readValue(buffer); + const details: Any = this.messageCodec.readValue(buffer); + if (typeof code == 'string' && (message == null || typeof message == 'string') && !buffer.hasRemaining()) { + throw new FlutterException(code, message, details); + } + } + } + throw new Error("Envelope corrupted"); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStringCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStringCodec.ets new file mode 100644 index 0000000..8d53c83 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/SendableStringCodec.ets @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on StringCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import SendableMessageCodec from './SendableMessageCodec'; +import StringUtils from '../../util/StringUtils'; + +/** + * A {@link SendableMessageCodec} using UTF-8 encoded String messages. + * + * This codec is guaranteed to be compatible with the corresponding `StringCodec` on the Dart side. + * These parts of the Flutter SDK are evolved synchronously. + */ +/** + * A sendable MessageCodec using UTF-8 encoded String messages. + * This codec can be used in background threads and worker contexts. + */ +@Sendable +export default class SendableStringCodec implements SendableMessageCodec { + /** Singleton instance of SendableStringCodec. */ + static readonly INSTANCE: SendableStringCodec = new SendableStringCodec(); + + /** + * Encodes a string message into binary format. + * @param message - The string message to encode, possibly null + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: string): ArrayBuffer { + if (message == null) { + return StringUtils.stringToArrayBuffer(""); + } + return StringUtils.stringToArrayBuffer(message); + } + + /** + * Decodes a binary message into a string. + * @param message - The binary message to decode, possibly null + * @returns The decoded string + */ + decodeMessage(message: ArrayBuffer | null): string { + if (message == null) { + return ""; + } + return StringUtils.arrayBufferToString(message); + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMessageCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMessageCodec.ets new file mode 100644 index 0000000..cf50f69 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMessageCodec.ets @@ -0,0 +1,416 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on StandardMessageCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import { ByteBuffer } from '../../util/ByteBuffer'; +import StringUtils from '../../util/StringUtils'; +import MessageCodec from './MessageCodec'; +import TreeMap from '@ohos.util.TreeMap'; +import HashMap from '@ohos.util.HashMap'; +import LightWeightMap from '@ohos.util.LightWeightMap'; +import PlainArray from '@ohos.util.PlainArray'; +import List from '@ohos.util.List'; +import LinkedList from '@ohos.util.LinkedList'; +import Any from './Any'; +import { ArrayList } from '@kit.ArkTS'; + +/** + * MessageCodec using the Flutter standard binary encoding. + * + * This codec is guaranteed to be compatible with the corresponding `StandardMessageCodec` + * on the Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * Supported messages are acyclic values of these forms: + * null + * Booleans + * number + * BigIntegers (see below) + * Int8Array, Int32Array, Float32Array, Float64Array + * Strings + * Array[] + * Lists of supported values + * Maps with supported keys and values + * + * + * On the Dart side, these values are represented as follows: + * null: null + * Boolean: bool + * Byte, Short, Integer, Long: int + * Float, Double: double + * String: String + * byte[]: Uint8List + * int[]: Int32List + * long[]: Int64List + * float[]: Float32List + * double[]: Float64List + * List: List + * Map: Map + * + * BigIntegers are represented in Dart as strings with the hexadecimal representation of the + * integer's value. + * + * To extend the codec, overwrite the writeValue and readValueOfType methods. + */ +export default class StandardMessageCodec implements MessageCodec { + private static TAG = "StandardMessageCodec#"; + /** Singleton instance of StandardMessageCodec. */ + static INSTANCE = new StandardMessageCodec(); + + /** + * Encodes a message into binary format using the standard encoding. + * @param message - The message to encode + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: Any): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)) + this.writeValue(stream, message); + return stream.buffer + } + + /** + * Decodes a binary message from the standard encoding. + * @param message - The binary message to decode, possibly null + * @returns The decoded message + */ + decodeMessage(message: ArrayBuffer | null): Any { + if (message == null) { + return null + } + const buffer = ByteBuffer.from(message) + return this.readValue(buffer) + } + + private static NULL = 0; + private static TRUE = 1; + private static FALSE = 2; + private static INT32 = 3; + private static INT64 = 4; + private static BIGINT = 5; + private static FLOAT64 = 6; + private static STRING = 7; + private static UINT8_ARRAY = 8; + private static INT32_ARRAY = 9; + private static INT64_ARRAY = 10; + private static FLOAT64_ARRAY = 11; + private static LIST = 12; + private static MAP = 13; + private static FLOAT32_ARRAY = 14; + private INT64_MAX = 9223372036854775807; + private INT64_MIN = -9223372036854775808; + + /** + * Writes a value to the byte buffer using the standard encoding. + * @param stream - The ByteBuffer to write to + * @param value - The value to write + * @returns The ByteBuffer stream + */ + writeValue(stream: ByteBuffer, value: Any): Any { + if (value == null || value == undefined) { + stream.writeInt8(StandardMessageCodec.NULL); + } else if (typeof value === "boolean") { + stream.writeInt8(value ? StandardMessageCodec.TRUE : StandardMessageCodec.FALSE) + } else if (typeof value === "number") { + if (Number.isInteger(value)) { //整型 + if (-0x7fffffff - 1 <= value && value <= 0x7fffffff) { //int32 + stream.writeInt8(StandardMessageCodec.INT32); + stream.writeInt32(value, true); + } else if (Number.MIN_SAFE_INTEGER <= value && value <= Number.MAX_SAFE_INTEGER) { //int64 number整型取值范围 + stream.writeInt8(StandardMessageCodec.INT64); + stream.writeInt64(value, true); + } else { //被判为整型的double型 + stream.writeInt8(StandardMessageCodec.FLOAT64); + this.writeAlignment(stream, 8); + stream.writeFloat64(value, true); + } + } else { //浮点型 + stream.writeInt8(StandardMessageCodec.FLOAT64); + this.writeAlignment(stream, 8); + stream.writeFloat64(value, true); + } + } else if (typeof value === "bigint") { + // The format is first the type byte (0x05), then the actual number + // as an ASCII string giving the hexadecimal representation of the + // integer, with the string's length as encoded by writeSize + // followed by the string bytes. + if (value >= this.INT64_MIN && value <= this.INT64_MAX) { + stream.writeInt8(StandardMessageCodec.INT64); + stream.writeBigInt64(value, true); + } else { + // Convert bigint to a hexadecimal string + stream.writeInt8(StandardMessageCodec.BIGINT); + const hexString = value.toString(16); + // Map each character in the hexadecimal string to its ASCII code + const asciiString = hexString.split('').map(char => char.charCodeAt(0)); + this.writeBytes(stream, Uint8Array.from(asciiString)); + } + } else if (typeof value === "string") { + stream.writeInt8(StandardMessageCodec.STRING); + let stringBuff = StringUtils.stringToArrayBuffer(value); + this.writeBytes(stream, new Uint8Array(stringBuff)); + } else if (value instanceof Uint8Array) { + stream.writeInt8(StandardMessageCodec.UINT8_ARRAY); + this.writeBytes(stream, value) + } else if (value instanceof Int32Array) { + stream.writeInt8(StandardMessageCodec.INT32_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 4); + value.forEach(item => stream.writeInt32(item, true)); + } else if (value instanceof BigInt64Array) { + stream.writeInt8(StandardMessageCodec.INT64_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 8); + value.forEach(item => stream.writeBigInt64(item, true)); + } else if (value instanceof Float32Array) { + stream.writeInt8(StandardMessageCodec.FLOAT32_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 4); + value.forEach(item => stream.writeFloat32(item, true)); + } else if (value instanceof Float64Array) { + stream.writeInt8(StandardMessageCodec.FLOAT64_ARRAY); + this.writeSize(stream, value.length); + this.writeAlignment(stream, 8); + value.forEach(item => stream.writeFloat64(item, true)); + } else if (value instanceof Array || value instanceof Int8Array || value instanceof Int16Array + || value instanceof Uint16Array || value instanceof Uint32Array || value instanceof List + || value instanceof LinkedList || value instanceof ArrayList) { + stream.writeInt8(StandardMessageCodec.LIST) + this.writeSize(stream, value.length); + value.forEach((item: Any): void => this.writeValue(stream, item)); + } else if (value instanceof Map) { + stream.writeInt8(StandardMessageCodec.MAP); + this.writeSize(stream, value.size); + value.forEach((value: Any, key: Any) => { + this.writeValue(stream, key); + this.writeValue(stream, value); + }); + } else if (value instanceof HashMap || value instanceof TreeMap || value instanceof LightWeightMap + || value instanceof PlainArray) { + stream.writeInt8(StandardMessageCodec.MAP); + this.writeSize(stream, value.length); + value.forEach((value: Any, key: Any) => { + this.writeValue(stream, key); + this.writeValue(stream, value); + }); + } else if (typeof value == 'object') { + let map: Map = new Map(); + Object.keys(value).forEach(key => { + map.set(key, value[key]); + }); + this.writeValue(stream, map); + } else { + throw new Error("Unsupported value: " + value); + stream.writeInt8(StandardMessageCodec.NULL); + } + return stream; + } + + /** + * Writes alignment padding to the stream. + * @param stream - The ByteBuffer to write to + * @param alignment - The alignment requirement (e.g., 4, 8) + */ + writeAlignment(stream: ByteBuffer, alignment: number) { + let mod: number = stream.byteOffset % alignment; + if (mod != 0) { + for (let i = 0; i < alignment - mod; i++) { + stream.writeInt8(0); + } + } + } + + /** + * Writes a size value to the stream using compact encoding. + * @param stream - The ByteBuffer to write to + * @param value - The size value to write + */ + writeSize(stream: ByteBuffer, value: number) { + if (value < 254) { + stream.writeUint8(value); + } else if (value <= 0xffff) { + stream.writeUint8(254); + stream.writeUint16(value, true); + } else { + stream.writeUint8(255); + stream.writeUint32(value, true); + } + } + + /** + * Writes a byte array to the stream. + * @param stream - The ByteBuffer to write to + * @param bytes - The byte array to write + */ + writeBytes(stream: ByteBuffer, bytes: Uint8Array) { + this.writeSize(stream, bytes.length) + stream.writeUint8Array(bytes); + } + + /** + * Reads a size value from the buffer using compact encoding. + * @param buffer - The ByteBuffer to read from + * @returns The size value + */ + readSize(buffer: ByteBuffer) { + let value = buffer.readUint8() & 0xff; + if (value < 254) { + return value; + } else if (value == 254) { + return buffer.readUint16(true); + } else { + return buffer.readUint32(true); + } + } + + /** + * Reads alignment padding from the buffer. + * @param buffer - The ByteBuffer to read from + * @param alignment - The alignment requirement (e.g., 4, 8) + */ + readAlignment(buffer: ByteBuffer, alignment: number) { + let mod = buffer.byteOffset % alignment; + if (mod != 0) { + buffer.skip(alignment - mod); + } + } + + /** + * Reads a value from the buffer using the standard encoding. + * @param buffer - The ByteBuffer to read from + * @returns The decoded value + */ + readValue(buffer: ByteBuffer): Any { + let type = buffer.readUint8() + return this.readValueOfType(type, buffer); + } + + /** + * Reads a byte array from the buffer. + * @param buffer - The ByteBuffer to read from + * @returns The byte array + */ + readBytes(buffer: ByteBuffer): Uint8Array { + let length = this.readSize(buffer); + let bytesBuffer = new ArrayBuffer(length); + let bytes = new Uint8Array(bytesBuffer); + bytes.set(buffer.readUint8Array(length)); + return bytes; + } + + /** + * Reads a value of a specific type from the buffer. + * @param type - The type code to read + * @param buffer - The ByteBuffer to read from + * @returns The decoded value + * @throws Error if the type is unknown or the message is corrupted + */ + readValueOfType(type: number, buffer: ByteBuffer): Any { + let result: Any; + switch (type) { + case StandardMessageCodec.NULL: + result = null; + break; + case StandardMessageCodec.TRUE: + result = true; + break; + case StandardMessageCodec.FALSE: + result = false; + break; + case StandardMessageCodec.INT32: + result = buffer.readInt32(true); + break; + case StandardMessageCodec.INT64: + result = buffer.readInt64(true); + if (Number.MIN_SAFE_INTEGER <= result && result <= Number.MAX_SAFE_INTEGER) { + result = Number(result); + } + break; + case StandardMessageCodec.BIGINT: + let bytes: Uint8Array = this.readBytes(buffer); + // Convert the byte array to a UTF-8 encoded string + const hexString: string = String.fromCharCode(...bytes); + // Parse the string as a hexadecimal BigInt + result = BigInt(`0x${hexString}`); + break; + case StandardMessageCodec.FLOAT64: + this.readAlignment(buffer, 8); + result = buffer.readFloat64(true) + break; + case StandardMessageCodec.STRING: { + let bytes: Uint8Array = this.readBytes(buffer); + result = StringUtils.uint8ArrayToString(bytes); + break; + } + case StandardMessageCodec.UINT8_ARRAY: { + result = this.readBytes(buffer); + break; + } + case StandardMessageCodec.INT32_ARRAY: { + let length = this.readSize(buffer); + let array = new Int32Array(length) + this.readAlignment(buffer, 4); + for (let i = 0; i < length; i++) { + array[i] = buffer.readInt32(true) + } + result = array; + break; + } + case StandardMessageCodec.INT64_ARRAY: { + let length = this.readSize(buffer); + let array = new BigInt64Array(length) + this.readAlignment(buffer, 8); + for (let i = 0; i < length; i++) { + array[i] = buffer.readBigInt64(true) + } + result = array; + break; + } + case StandardMessageCodec.FLOAT64_ARRAY: { + let length = this.readSize(buffer); + let array = new Float64Array(length) + this.readAlignment(buffer, 8); + for (let i = 0; i < length; i++) { + array[i] = buffer.readFloat64(true) + } + result = array; + break; + } + case StandardMessageCodec.LIST: { + let length = this.readSize(buffer); + let array: Array = new Array(length) + for (let i = 0; i < length; i++) { + array[i] = this.readValue(buffer) + } + result = array; + break; + } + case StandardMessageCodec.MAP: { + let size = this.readSize(buffer); + let map: Map = new Map() + for (let i = 0; i < size; i++) { + map.set(this.readValue(buffer), this.readValue(buffer)); + } + result = map; + break; + } + case StandardMessageCodec.FLOAT32_ARRAY: { + let length = this.readSize(buffer); + let array = new Float32Array(length); + this.readAlignment(buffer, 4); + for (let i = 0; i < length; i++) { + array[i] = buffer.readFloat32(true) + } + result = array; + break; + } + default: + throw new Error("Message corrupted, type=" + type); + } + return result; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMethodCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMethodCodec.ets new file mode 100644 index 0000000..f6059c0 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StandardMethodCodec.ets @@ -0,0 +1,152 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on StandardMethodCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import { ByteBuffer } from '../../util/ByteBuffer'; +import FlutterException from './FlutterException'; +import Any from './Any'; +import MethodCall from './MethodCall'; +import MethodCodec from './MethodCodec'; +import StandardMessageCodec from './StandardMessageCodec'; + +/** + * A {@link MethodCodec} using the Flutter standard binary encoding. + * + * This codec is guaranteed to be compatible with the corresponding StandardMethodCodec + * on the Dart side. These parts of the Flutter SDK are evolved synchronously. + * + * Values supported as method arguments and result payloads are those supported by {@link StandardMessageCodec}. + */ +export default class StandardMethodCodec implements MethodCodec { + private static TAG = "StandardMethodCodec"; + /** Singleton instance of StandardMethodCodec. */ + public static INSTANCE = new StandardMethodCodec(StandardMessageCodec.INSTANCE); + private messageCodec: StandardMessageCodec; + + /** + * Creates a new method codec based on the specified message codec. + * @param messageCodec - The StandardMessageCodec to use for encoding/decoding + */ + constructor(messageCodec: StandardMessageCodec) { + this.messageCodec = messageCodec; + } + + /** + * Encodes a method call into binary format. + * @param methodCall - The MethodCall to encode + * @returns The encoded method call as an ArrayBuffer + */ + encodeMethodCall(methodCall: MethodCall): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + this.messageCodec.writeValue(stream, methodCall.method); + this.messageCodec.writeValue(stream, methodCall.args); + return stream.buffer; + } + + /** + * Decodes a method call from binary format. + * @param methodCall - The binary message to decode + * @returns The decoded MethodCall + * @throws Error if the method call is corrupted + */ + decodeMethodCall(methodCall: ArrayBuffer): MethodCall { + const buffer = ByteBuffer.from(methodCall); + const method: Any = this.messageCodec.readValue(buffer); + const args: Any = this.messageCodec.readValue(buffer); + if (typeof method == 'string' && !buffer.hasRemaining()) { + return new MethodCall(method, args); + } + throw new Error("Method call corrupted"); + } + + /** + * Encodes a successful result into a binary envelope. + * @param result - The result value, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeSuccessEnvelope(result: Any): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + stream.writeInt8(0); + this.messageCodec.writeValue(stream, result); + return stream.buffer; + } + + /** + * Encodes an error result into a binary envelope. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelope(errorCode: string, errorMessage: string, errorDetails: Any): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + stream.writeInt8(1); + this.messageCodec.writeValue(stream, errorCode); + this.messageCodec.writeValue(stream, errorMessage); + if (errorDetails instanceof Error) { + this.messageCodec.writeValue(stream, errorDetails.stack); + } else { + this.messageCodec.writeValue(stream, errorDetails); + } + return stream.buffer; + } + + /** + * Encodes an error result into a binary envelope with stacktrace. + * @param errorCode - The error code + * @param errorMessage - The error message, possibly null + * @param errorDetails - Error details, possibly null + * @param errorStacktrace - The platform stacktrace, possibly null + * @returns The encoded envelope as an ArrayBuffer + */ + encodeErrorEnvelopeWithStacktrace(errorCode: string, errorMessage: string, errorDetails: Any, + errorStacktrace: string): ArrayBuffer { + const stream = ByteBuffer.from(new ArrayBuffer(1024)); + stream.writeInt8(1); + this.messageCodec.writeValue(stream, errorCode); + this.messageCodec.writeValue(stream, errorMessage); + if (errorDetails instanceof Error) { + this.messageCodec.writeValue(stream, errorDetails.stack); + } else { + this.messageCodec.writeValue(stream, errorDetails); + } + this.messageCodec.writeValue(stream, errorStacktrace); + return stream.buffer; + } + + /** + * Decodes a result envelope from binary format. + * @param envelope - The binary envelope to decode + * @returns The decoded result value + * @throws FlutterException if the envelope contains an error + * @throws Error if the envelope is corrupted + */ + decodeEnvelope(envelope: ArrayBuffer): Any { + const buffer = ByteBuffer.from(envelope); + const flag = buffer.readInt8(); + switch (flag) { + case 0: { + const result: Any = this.messageCodec.readValue(buffer); + if (!buffer.hasRemaining()) { + return result; + } + // Falls through intentionally. + } + case 1: { + const code: Any = this.messageCodec.readValue(buffer); + const message: Any = this.messageCodec.readValue(buffer); + const details: Any = this.messageCodec.readValue(buffer); + if (typeof code == 'string' && (message == null || typeof message == 'string') && !buffer.hasRemaining()) { + throw new FlutterException(code, message, details); + } + } + } + throw new Error("Envelope corrupted"); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StringCodec.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StringCodec.ets new file mode 100644 index 0000000..2205dd7 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/common/StringCodec.ets @@ -0,0 +1,47 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on StringCodec.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import StringUtils from '../../util/StringUtils'; +import MessageCodec from './MessageCodec'; + +/** + * A {@link MessageCodec} using UTF-8 encoded String messages. + * + * This codec is guaranteed to be compatible with the corresponding `StringCodec` on the + * Dart side. These parts of the Flutter SDK are evolved synchronously. + */ +export default class StringCodec implements MessageCodec { + /** Singleton instance of StringCodec. */ + static readonly INSTANCE = new StringCodec(); + + /** + * Encodes a string message into binary format. + * @param message - The string message to encode, possibly null + * @returns The encoded message as an ArrayBuffer + */ + encodeMessage(message: string): ArrayBuffer { + if (message == null) { + return StringUtils.stringToArrayBuffer(""); + } + return StringUtils.stringToArrayBuffer(message); + } + + /** + * Decodes a binary message into a string. + * @param message - The binary message to decode, possibly null + * @returns The decoded string + */ + decodeMessage(message: ArrayBuffer | null): string { + if (message == null) { + return ""; + } + return StringUtils.arrayBufferToString(message); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/ListenableEditingState.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/ListenableEditingState.ets new file mode 100644 index 0000000..2a6858a --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/ListenableEditingState.ets @@ -0,0 +1,974 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on ListenableEditingState.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import { TextEditState } from '../../embedding/engine/systemchannels/TextInputChannel'; +import Log from '../../util/Log'; +import inputMethod from '@ohos.inputMethod'; +import ArrayList from '@ohos.util.ArrayList'; +import { TextEditingDelta } from './TextEditingDelta'; +import TextInputChannel from '../../embedding/engine/systemchannels/TextInputChannel'; +import { FlutterTextUtils } from './TextUtils'; +import { KeyCode } from '@kit.InputKit'; +import KeyEventChannel, { SimulateKeyEvent } from '../../embedding/engine/systemchannels/KeyEventChannel'; +import { PasteboardUtils } from '../../util/PasteboardUtils'; + +const TAG = "ListenableEditingState"; + +/** + * Enumeration of delete operation states. + */ +enum DeleteStates { + /** Delete operation has started */ + START, + /** Delete operation is in progress */ + MOVING, + /** Delete operation has ended */ + END +} + +/** + * Manages the editable text state and notifies listeners of changes. + * This class tracks text content, selection, composing regions, and preview text. + */ +export class ListenableEditingState { + private TextInputChannel: TextInputChannel | null = null; + private keyEventChannel: KeyEventChannel | undefined; + private client: number = 0 + private leftDeleteState: DeleteStates = DeleteStates.END + private rightDeleteState: DeleteStates = DeleteStates.END + //Cache used to storage software keyboard input action + private mStringCache: string; + private mSelectionStartCache: number = 0; + private mSelectionEndCache: number = 0; + private mComposingStartCache: number = 0; + private mComposingEndCache: number = 0; + //used to compare with Cache + + private mListeners: ArrayList = new ArrayList(); + private mPendingListeners: ArrayList = new ArrayList(); + private mBatchTextEditingDeltas: ArrayList = new ArrayList(); + private mChangeNotificationDepth: number = 0; + private mBatchEditNestDepth: number = 0; + private mTextWhenBeginBatchEdit: string; + private mSelectionStartWhenBeginBatchEdit: number = 0; + private mSelectionEndWhenBeginBatchEdit: number = 0; + private mComposingStartWhenBeginBatchEdit: number = 0; + private mComposingEndWhenBeginBatchEdit: number = 0; + + // preview text + private mPreviewText: string = ""; + private mPreviewTextStart: number = 0; + private mPreviewTextEnd: number = 0; + private mLeftIdxOfPreviewTextRange: number = 0; + private mRightIdxOfPreviewTextRange: number = 0; + private mIsCursorIdxOutOfPreviewTextRange: boolean = false; + private mIsEnglishPreviewMode: boolean = false; + + /** + * Constructs a new ListenableEditingState instance. + * @param TextInputChannel - The TextInputChannel for communication, possibly null + * @param client - The client ID + * @param keyEventChannel - Optional KeyEventChannel for key event handling + */ + constructor(TextInputChannel:TextInputChannel | null,client:number, keyEventChannel?: KeyEventChannel) { + this.TextInputChannel = TextInputChannel; + this.keyEventChannel = keyEventChannel; + this.client = client + this.mStringCache = ""; + this.mTextWhenBeginBatchEdit = ""; + this.mSelectionStartCache = 0; + this.mSelectionEndCache = 0; + this.mComposingStartCache = -1; + this.mComposingEndCache = -1; + this.mPreviewText = ""; + } + + /** + * Extracts and clears the batch of text editing deltas. + * @returns A list of TextEditingDelta objects representing the changes + */ + extractBatchTextEditingDeltas(): ArrayList { + let currentBatchDeltas = new ArrayList(); + this.mBatchTextEditingDeltas.forEach((data) => { + currentBatchDeltas.add(data); + }) + this.mBatchTextEditingDeltas.clear(); + return currentBatchDeltas; + } + + /** + * Clears all batched text editing deltas. + */ + clearBatchDeltas(): void { + this.mBatchTextEditingDeltas.clear(); + } + + /** + * Replaces a range of text with new text. + * @param start - The start position of the range to replace + * @param end - The end position of the range to replace + * @param tb - The replacement text + * @param tbStart - The start position within the replacement text + * @param tbEnd - The end position within the replacement text + */ + replace(start: number, end: number, tb: String, tbStart: number, tbEnd: number): void { + const placeIndex = + this.mSelectionStartCache < this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + + this.mBatchTextEditingDeltas.add( + new TextEditingDelta( + this.mStringCache.toString(), + placeIndex + tbEnd, + placeIndex + tbEnd, + this.getComposingStart(), + this.getComposingEnd(), + start, + end + tbStart, + tb.toString() + )); + } + + /** + * Gets the current selection start position. + * @returns The selection start position + */ + getSelectionStart(): number { + return this.mSelectionStartCache; + } + + /** + * Gets the current selection end position. + * @returns The selection end position + */ + getSelectionEnd(): number { + return this.mSelectionEndCache; + } + + /** + * Gets the current composing region start position. + * @returns The composing start position, or -1 if no composing region + */ + getComposingStart(): number { + return this.mComposingStartCache; + } + + /** + * Gets the current composing region end position. + * @returns The composing end position, or -1 if no composing region + */ + getComposingEnd(): number { + return this.mComposingEndCache; + } + + /** + * Gets the current text content. + * @returns The text string + */ + getStringCache(): string { + return this.mStringCache; + } + + /** + * Gets the currently selected text. + * @returns The selected text, or empty string if selection is invalid + */ + getSelectionString(): string { + if (this.mSelectionStartCache < 0 || this.mSelectionEndCache > this.mStringCache.length) { + return ""; + } + return this.mStringCache.substring(this.mSelectionStartCache, this.mSelectionEndCache); + } + + /** + * Gets the current preview text from the input method. + * @returns The preview text + */ + getPreviewText(): string { + return this.mPreviewText; + } + + /** + * Gets the start position of the preview text in the original text. + * @returns The preview text start position + */ + getPreviewTextStart(): number { + return this.mPreviewTextStart; + } + + /** + * Gets the end position of the preview text in the original text. + * @returns The preview text end position + */ + getPreviewTextEnd(): number { + return this.mPreviewTextEnd; + } + + /** + * Gets the right index of the preview text range in the string cache. + * @returns The right index of the preview text range + */ + getRightIdxOfPreviewTextRange() : number { + return this.mRightIdxOfPreviewTextRange; + } + + /** + * Gets the left index of the preview text range in the string cache. + * @returns The left index of the preview text range + */ + getLeftIdxOfPreviewTextRange() : number { + return this.mLeftIdxOfPreviewTextRange; + } + + /** + * Sets the preview text. + * @param previewText - The preview text to set + */ + setPreviewText(previewText: string): void { + this.mPreviewText = previewText; + } + + /** + * Sets the start position of the preview text. + * @param previewTextStart - The start position + */ + setPreviewTextStart(previewTextStart: number): void { + this.mPreviewTextStart = previewTextStart; + } + + /** + * Sets the end position of the preview text. + * @param previewTextEnd - The end position + */ + setPreviewTextEnd(previewTextEnd: number): void { + this.mPreviewTextEnd = previewTextEnd; + } + + /** + * Updates the preview text range indices. + * @param leftIdx - The left index in the string cache + * @param rightIdx - The right index in the string cache + */ + updatePreviewTextRange(leftIdx: number, rightIdx: number): void { + this.mLeftIdxOfPreviewTextRange = leftIdx; + this.mRightIdxOfPreviewTextRange = rightIdx; + } + + /** + * Clears all preview text contents and resets related indices. + */ + clearPreviewTextContents() : void { + this.mPreviewText = ""; + this.mPreviewTextStart = 0; + this.mPreviewTextEnd = 0; + this.mLeftIdxOfPreviewTextRange = 0; + this.mRightIdxOfPreviewTextRange = 0; + } + + /** + * Sets whether the cursor index is out of the preview text range. + * @param hasChanged - True if the cursor is out of range, false otherwise + */ + setIsCursorIdxOutOfPreviewTextRange(hasChanged: boolean): void { + this.mIsCursorIdxOutOfPreviewTextRange = hasChanged; + } + + /** + * Gets whether the cursor index is out of the preview text range. + * @returns True if the cursor is out of range, false otherwise + */ + getIsCursorIdxOutOfPreviewTextRange(): boolean { + return this.mIsCursorIdxOutOfPreviewTextRange; + } + + /** + * Sets the selection start position. + * @param newSelectionStart - The new selection start position + */ + setSelectionStart(newSelectionStart: number): void { + this.mSelectionStartCache = newSelectionStart; + } + + /** + * Sets the selection end position. + * @param newSelectionEnd - The new selection end position + */ + setSelectionEnd(newSelectionEnd: number): void { + this.mSelectionEndCache = newSelectionEnd; + } + + /** + * Sets the composing region start position. + * @param newComposingStart - The new composing start position, or -1 to clear + */ + setComposingStart(newComposingStart: number): void { + this.mComposingStartCache = newComposingStart; + } + + /** + * Sets the composing region end position. + * @param newComposingEnd - The new composing end position, or -1 to clear + */ + setComposingEnd(newComposingEnd: number): void { + this.mComposingEndCache = newComposingEnd; + } + + /** + * Sets the text content. + * @param newStringCache - The new text content + */ + setStringCache(newStringCache: string): void { + this.mStringCache = newStringCache; + } + + /** + * Notifies a single listener of editing state changes. + * @param listener - The listener to notify + * @param textChanged - Whether the text has changed + * @param selectionChanged - Whether the selection has changed + * @param composingChanged - Whether the composing region has changed + */ + notifyListener(listener: EditingStateWatcher, + textChanged: boolean, + selectionChanged: boolean, + composingChanged: boolean): void { + this.mChangeNotificationDepth++; + listener.didChangeEditingState(textChanged, selectionChanged, composingChanged); + this.mChangeNotificationDepth--; + } + + /** + * Notifies all listeners if any changes have occurred. + * @param textChanged - Whether the text has changed + * @param selectionChanged - Whether the selection has changed + * @param composingChanged - Whether the composing region has changed + */ + notifyListenersIfNeeded(textChanged: boolean, selectionChanged: boolean, composingChanged: boolean) { + if (textChanged || selectionChanged || composingChanged) { + for (const listener of this.mListeners) { + this.notifyListener(listener, textChanged, selectionChanged, composingChanged); + } + } + } + + /** + * Handles insertion of preview text from the input method. + * @param text - The preview text to insert + * @param range - The range in the original text where preview text applies + */ + handleInsertPreviewTextEvent(text: string, range: inputMethod.Range): void { + // Determine whether it is in English preview input mode + if (range.start != -1 && range.end != -1 && text.indexOf("'") == -1) { + this.mIsEnglishPreviewMode = true; + } + // preview text callback with complete text contents,like a, a'b, a'b'c + // its text char do not appear one by one + this.setPreviewText(text); + this.setPreviewTextStart(range.start); + this.setPreviewTextEnd(range.end); + + // update preview text start idx and end idx + let leftIdxOfPreviewText: number = this.getSelectionStart(); + let rightIdxOfPreviewText: number = this.getSelectionStart() + text.length; + this.updatePreviewTextRange(leftIdxOfPreviewText, rightIdxOfPreviewText) + + if (this.mListeners == null) { + Log.e(TAG, "handleInsertPreviewTextEvent, mListeners is null"); + return; + } + this.notifyListenersIfNeeded(true, true, false); + } + + /** + * Handles insertion of final text from the input method. + * @param text - The text to insert + */ + handleInsertTextEvent(text: string): void { + // clear preview text cache before insert the final texts + if (this.getPreviewText().length != 0) { + this.setPreviewText(""); + } + // When previewTextChangeSelection async callback has been invoked, covering the previous preview text. + // But if that callback did not work, then manually insert the candidate word, covering the previous preview text. + if (this.getLeftIdxOfPreviewTextRange() < this.mSelectionStartCache && + (this.getLeftIdxOfPreviewTextRange() != this.getRightIdxOfPreviewTextRange())) { + this.mSelectionStartCache = this.getLeftIdxOfPreviewTextRange(); + this.mSelectionEndCache = this.getRightIdxOfPreviewTextRange(); + } else if (this.mIsEnglishPreviewMode) { + // To comply with the specifications of Xiaoyi-InputMethod, change the + // range of inserted English chars and add a space at the backend + this.mSelectionStartCache = this.getPreviewTextStart(); + this.mSelectionEndCache = this.getPreviewTextEnd(); + text += " "; + } + + let start = + this.mSelectionStartCache < this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + let end = this.mSelectionStartCache > this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + const length = text.length; + this.replace(start, end, text, 0, length); + + if (this.mStringCache.length == this.mSelectionStartCache) { + //Insert text one by one + let tempStr: string = this.mStringCache.substring(0, start) + text + this.mStringCache.substring(end); + this.mStringCache = tempStr; + this.setSelectionStart(this.mStringCache.length); + this.setSelectionEnd(this.mStringCache.length); + } else if (this.mStringCache.length > this.mSelectionStartCache) { + //Insert text in the middle of string + let tempStr: string = this.mStringCache.substring(0, start) + text + this.mStringCache.substring(end); + this.mStringCache = tempStr; + this.mSelectionStartCache = start + text.length; + this.mSelectionEndCache = this.mSelectionStartCache; + } + if (this.mListeners == null) { + Log.e(TAG, "mListeners is null"); + return; + } + this.notifyListenersIfNeeded(true, true, false); + // when preview text did insert, reset the params + this.updatePreviewTextRange(0, 0); + this.setPreviewTextStart(-1); + this.setPreviewTextEnd(-1); + this.mIsEnglishPreviewMode = false; + } + + /** + * Updates the text input state from Flutter. + * @param state - The new text edit state + */ + updateTextInputState(state: TextEditState): void { + if (this.leftDeleteState === DeleteStates.START) { + this.leftDeleteState = DeleteStates.MOVING; + } + if (this.rightDeleteState === DeleteStates.START) { + this.rightDeleteState = DeleteStates.MOVING; + } + this.beginBatchEdit(); + this.setStringCache(state.text); + if (state.hasSelection()) { + this.setSelectionStart(state.selectionStart); + this.setSelectionEnd(state.selectionEnd); + } else { + this.setSelectionStart(0); + this.setSelectionEnd(0); + } + this.endBatchEdit(); + } + + /** + * Begins a batch edit operation. + * Multiple edits can be batched together to reduce notifications. + */ + beginBatchEdit(): void { + this.mBatchEditNestDepth++; + if (this.mChangeNotificationDepth > 0) { + Log.e(TAG, "editing state should not be changed in a listener callback"); + } + if (this.mBatchEditNestDepth == 1 && !this.mListeners.isEmpty()) { + this.mTextWhenBeginBatchEdit = this.getStringCache(); + this.mSelectionStartWhenBeginBatchEdit = this.getSelectionStart(); + this.mSelectionEndWhenBeginBatchEdit = this.getSelectionEnd(); + this.mComposingStartWhenBeginBatchEdit = this.getComposingStart(); + this.mComposingEndWhenBeginBatchEdit = this.getComposingEnd(); + } + } + + /** + * Ends a batch edit operation and notifies listeners of all changes. + */ + endBatchEdit(): void { + if (this.mBatchEditNestDepth == 0) { + Log.e(TAG, "endBatchEdit called without a matching beginBatchEdit"); + return; + } + if (this.mBatchEditNestDepth == 1) { + Log.d(TAG, "mBatchEditNestDepth == 1"); + for (const listener of this.mPendingListeners) { + this.notifyListener(listener, true, true, true); + } + + if (!this.mListeners.isEmpty()) { + Log.d(TAG, "didFinishBatchEdit with " + this.mListeners.length + " listener(s)"); + const textChanged = !(this.mStringCache == this.mTextWhenBeginBatchEdit); + const selectionChanged = this.mSelectionStartWhenBeginBatchEdit != this.getSelectionStart() + || this.mSelectionEndWhenBeginBatchEdit != this.getSelectionEnd(); + const composingRegionChanged = this.mComposingStartWhenBeginBatchEdit != this.getComposingStart() + || this.mComposingEndWhenBeginBatchEdit != this.getComposingEnd(); + Log.d(TAG, "textChanged: " + textChanged + " selectionChanged: " + selectionChanged + + " composingRegionChanged: " + composingRegionChanged); + this.notifyListenersIfNeeded(textChanged, selectionChanged, composingRegionChanged); + } + } + for (const listener of this.mPendingListeners) { + this.mListeners.add(listener); + } + this.mPendingListeners.clear(); + this.mBatchEditNestDepth--; + } + + /** + * Adds a listener to be notified of editing state changes. + * @param listener - The listener to add + */ + addEditingStateListener(listener: EditingStateWatcher): void { + if (this.mChangeNotificationDepth > 0) { + Log.e(TAG, "adding a listener " + JSON.stringify(listener) + " in a listener callback"); + } + if (this.mBatchEditNestDepth > 0) { + Log.d(TAG, "a listener was added to EditingState while a batch edit was in progress"); + this.mPendingListeners.add(listener); + } else { + this.mListeners.add(listener); + } + } + + /** + * Removes an editing state listener. + * @param listener - The listener to remove + */ + removeEditingStateListener(listener: EditingStateWatcher): void { + if (this.mChangeNotificationDepth > 0) { + Log.e(TAG, "removing a listener " + JSON.stringify(listener) + " in a listener callback"); + } + this.mListeners.remove(listener); + if (this.mBatchEditNestDepth > 0) { + this.mPendingListeners.remove(listener); + } + } + + /** + * Marks the start of a deletion operation. + * @param code - The key code that triggered the deletion + */ + startDeleting(code: number) { + if (code === KeyCode.KEYCODE_FORWARD_DEL) { + this.rightDeleteState = DeleteStates.START + } else { + this.leftDeleteState = DeleteStates.START + } + } + + /** + * Marks the end of a deletion operation. + * @param code - The key code that triggered the deletion + */ + endDeletion(code: number) { + if (code === KeyCode.KEYCODE_FORWARD_DEL) { + this.rightDeleteState = DeleteStates.END + } else { + this.leftDeleteState = DeleteStates.END + } + } + + /** + * Handles a delete event from the input method. + * @param leftOrRight - True for delete right (forward delete), false for delete left (backspace) + * @param length - The number of characters to delete + * @param enableDeltaModel - Whether delta model is enabled + */ + handleDeleteEvent(leftOrRight: boolean, length: number, enableDeltaModel: boolean | undefined): void { + if (length === 0) { + return; + } + // clear preview text cache + if (this.getPreviewText().length != 0) { + this.setPreviewText(""); + } + + if (enableDeltaModel) { + let selectionStart = this.getSelectionStart(); + let selectionEnd = this.getSelectionEnd(); + if(selectionStart === 0 && selectionEnd === 0){ + // [selectionStart]和[selectionEnd]都为0时,删除文本范围为空 + return; + } + if (selectionStart === selectionEnd) { + // [selectionStart]和[selectionEnd]一致时为普通删除操作,需要修改偏移量 + this.setSelectionStart(this.mSelectionStartCache - length); + } + } + + let start = + this.mSelectionStartCache < this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + let end = this.mSelectionStartCache > this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + + if (leftOrRight == false && this.leftDeleteState !== DeleteStates.MOVING) { + //delete left + if (start == 0 && end == 0) { + return; + } + + let unicodeStart = start; + if (start == end) { + for (let i = 0; i < length; i++) { + unicodeStart = FlutterTextUtils.getOffsetBefore(this.mStringCache, unicodeStart); + if (unicodeStart === 0) { + break; + } + } + } + this.replace(unicodeStart, end, "", 0, 0); + this.mSelectionStartCache = unicodeStart; + let tempStr: string = this.mStringCache.slice(0, unicodeStart) + this.mStringCache.slice(end); + this.mStringCache = tempStr; + this.mSelectionEndCache = this.mSelectionStartCache; + } else if (leftOrRight == true && this.rightDeleteState !== DeleteStates.MOVING) { + //delete right + if (start == this.mStringCache.length) { + return; + } + let unicodeEnd = end; + if (start == end) { + for (let i = 0; i < length; i++) { + unicodeEnd = FlutterTextUtils.getOffsetAfter(this.mStringCache, unicodeEnd); + if (unicodeEnd === this.mStringCache.length) { + break; + } + } + } + this.replace(start, unicodeEnd, "", 0, 0); + this.mSelectionEndCache = start; + let tempStr: string = this.mStringCache.slice(0, start) + + (unicodeEnd >= this.mStringCache.length ? "" : this.mStringCache.slice(unicodeEnd)); + this.mStringCache = tempStr; + this.mSelectionStartCache = this.mSelectionEndCache; + } + this.notifyListenersIfNeeded(true, true, false); + } + + /** + * Handles a newline event from the input method. + */ + handleNewlineEvent(): void { + // 获取光标所在位置; + // 当光标移动前位置小于移动后的位置时,获取光标移动前位置;反之获取移动后位置 + let start = + this.mSelectionStartCache < this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + // 当光标移动前位置大于移动后的位置时,获取光标移动前位置;反之获取移动后位置 + let end = this.mSelectionStartCache > this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + + this.replace(start, end, '\n', 0, 1); + // 对光标位置和字符串长度进行对比,决定光标位置的计算方法 + if (this.mStringCache.length == this.mSelectionStartCache) { + //Insert newline one by one + let tempStr: string = this.mStringCache.substring(0, start) + '\n' + this.mStringCache.substring(end); + this.mStringCache = tempStr; + this.setSelectionStart(this.mStringCache.length); + this.setSelectionEnd(this.mStringCache.length); + } else if (this.mStringCache.length > this.mSelectionStartCache) { + //Insert newline in the middle of string + let tempStr: string = this.mStringCache.substring(0, start) + '\n' + this.mStringCache.substring(end); + this.mStringCache = tempStr; + this.mSelectionStartCache = start + 1; + this.mSelectionEndCache = this.mSelectionStartCache; + } + if (this.mListeners == null) { + Log.e(TAG, "mListeners is null"); + return; + } + this.notifyListenersIfNeeded(true, true, false); + } + + /** + * Handles a function key event from the input method. + * @param functionKey - The function key that was pressed + */ + handleFunctionKey(functionKey: inputMethod.FunctionKey): void { + if (!this.TextInputChannel) { + return + } + switch (functionKey.enterKeyType) { + case inputMethod.EnterKeyType.PREVIOUS: + this.TextInputChannel.previous(this.client); + break; + case inputMethod.EnterKeyType.UNSPECIFIED: + this.TextInputChannel.unspecifiedAction(this.client); + break; + case inputMethod.EnterKeyType.NEWLINE: + this.TextInputChannel.newline(this.client); + break; + case inputMethod.EnterKeyType.GO: + this.TextInputChannel.go(this.client); + break; + case inputMethod.EnterKeyType.SEARCH: + this.TextInputChannel.search(this.client); + break; + case inputMethod.EnterKeyType.SEND: + this.TextInputChannel.send(this.client); + break; + case inputMethod.EnterKeyType.NEXT: + this.TextInputChannel.next(this.client); + break; + case inputMethod.EnterKeyType.DONE: + this.TextInputChannel.done(this.client); + break; + } + } + + /** + * Handles a text selection by range event. + * @param range - The range to select + */ + handleSelectByRangeEvent(range: inputMethod.Range): void { + if (range.start === 0) { // cursor index updated at the start pos of text + this.setSelectionStart(0); + this.setSelectionEnd(0); + } else { // cursor index updated at the end pos of text + this.setSelectionStart(this.getStringCache().length); + this.setSelectionEnd(this.getStringCache().length); + } + this.notifyListenersIfNeeded(false, true, false); + } + + /** + * cursor moved at 2D-text with 4 directions + * aaaaaa aaaaaa| + * bbbbbbb|b -(cursor_up)-> bbbbbbbb + * cccc cccc + * + * aaaaaa aaaaaa + * bbbbbbb|b -(cursor_down)-> bbbbbbbb + * cccc cccc| + * + * aaaaaa aaaaaa + * bbbbbbb|b -(cursor_right)-> bbbbbbbb| + * cccc cccc + * + * aaaaaa aaaaaa + * bbbbbbb|b -(cursor_left)-> bbbbbb|bb + * cccc cccc + */ + /** + * Handles cursor movement events in 2D text. + * @param direction - The direction to move the cursor + */ + handleMoveCursorEvent(direction: inputMethod.Direction): void { + switch (direction) { + case inputMethod.Direction.CURSOR_LEFT: + case inputMethod.Direction.CURSOR_RIGHT: + case inputMethod.Direction.CURSOR_UP: + case inputMethod.Direction.CURSOR_DOWN: + // simulate the hardware-keyboard arrow-key pressed with any directions, + // and the cursor index in text cache will be moved + this.simulateHardwareCursorMovement(direction, false); + break; + default: + Log.w(TAG, `"Unknown cursor movement direction: ${direction}"`); + break; + } + } + + /** + * text-selection changed with cursor moving at 2D-text + * (...) -> denotes the selection range of text + * aaaaaa aaaa(aa + * bbbb|bbbb -(cursor_up)-> bbbb)bbbb + * cccc cccc + * + * aaaaaa aaaaaa + * bbbbbbb|b -(cursor_down)-> bbbbbbb(b + * cccc cccc) + * + * aaaaaa aaaaaa + * bb|bbbbbb -(cursor_right)-> bb(b)bbbbb + * cccc cccc + * + * aaaaaa aaaaaa + * bbbbbbb|b -(cursor_left)-> bbbbbb(b)b + * cccc cccc + */ + /** + * Handles text selection changes with cursor movement. + * @param movement - The movement that triggers the selection change + */ + handleSelectByMovementEvent(movement: inputMethod.Movement) : void { + switch (movement.direction) { + case inputMethod.Direction.CURSOR_LEFT: + case inputMethod.Direction.CURSOR_RIGHT: + case inputMethod.Direction.CURSOR_UP: + case inputMethod.Direction.CURSOR_DOWN: + // simulate the hardware-keyboard shift-key combined with arrow-key simultaneously pressed, + // and the text selection changed with cursor movement + this.simulateHardwareCursorMovement(movement.direction, true); + break; + default: + Log.w(TAG, `"Unknown cursor movement direction: ${movement.direction}"`); + break; + } + } + + /** + * Gets the text to the left of the cursor. + * @param length - The maximum number of characters to return + * @returns The text to the left of the cursor + */ + getLeftTextOfCursor(length: number) : string { + if (length <= 0) { + return ""; + } + const strCacheLen = this.getStringCache().length; + if (!strCacheLen) { + return ""; + } + const cursorIdx = this.getSelectionStart(); + let startIdx = cursorIdx - length; + if (startIdx < 0) { + startIdx = 0; + } + return this.getStringCache().substring(startIdx, cursorIdx); + } + + /** + * Gets the text to the right of the cursor. + * @param length - The maximum number of characters to return + * @returns The text to the right of the cursor + */ + getRightTextOfCursor(length: number) : string { + if (length <= 0) { + return ""; + } + const strCacheLen = this.getStringCache().length; + if (!strCacheLen) { + return ""; + } + const cursorIdx = this.getSelectionEnd(); + let endIdx = cursorIdx + length; + if (endIdx >= strCacheLen) { + endIdx = strCacheLen; + } + return this.getStringCache().substring(cursorIdx, endIdx); + } + + /** + * Handles extended actions like select all, cut, copy, and paste. + * @param action - The action to perform + */ + handleExtendActionEvent(action: inputMethod.ExtendAction) : void { + switch (action) { + case inputMethod.ExtendAction.SELECT_ALL: + // select all text from the start to end, and notify cursor state updating + this.setSelectionStart(0); + this.setSelectionEnd(this.getStringCache().length); + this.notifyListenersIfNeeded(false, true, false); + break; + case inputMethod.ExtendAction.CUT: + // set to copy text before cutting + const cutText = this.getStringCache().substring(this.getSelectionStart(), this.getSelectionEnd()); + PasteboardUtils.setCopyData(cutText); + // based on the text selection length (endIdx - startIdx) for cutting + this.handleDeleteEvent(false, this.getSelectionEnd() - this.getSelectionStart(), false); + break; + case inputMethod.ExtendAction.COPY: + // obtain the current selection range of text cache as the copy text + const copyText = this.getStringCache().substring(this.getSelectionStart(), this.getSelectionEnd()); + PasteboardUtils.setCopyData(copyText); + break; + case inputMethod.ExtendAction.PASTE: + // PasteboardUtils.getPasteDataAsync() is a async function, so must await the async paste operatopn + // in sync func, otherwise there can be a problem of timing inconsistency in the insertion of pasted text here. + const handlePasteDataAsync = async () => { + try { + const pasteText = await PasteboardUtils.getPasteDataAsync(); + if (pasteText) { // insert the paste text into the editing box + this.handleInsertTextEvent(pasteText); + } + } catch (err) { + Log.e(TAG, "get PasteData error: " + err); + } + }; + handlePasteDataAsync(); + break; + default: + Log.w(TAG, `"Unknown ExtendAction: ${action}"`); + break; + } + } + + /** + * This method is used to simulate the hard-keyboard key event in soft-keyboard mode, + * Case 1: cursor moving is simulated by sending arrow-key event to flutter SDK (Dart-side), + * hence the cursor will move up/down/left/right in editing box. + * Case 2: text-selection changed with cursor moving is simulated by sending shift-key combined with arrow-key events + * to flutter SDK, hence the text-selection range will be updated by moving cursor up/down/left/right in editing box. + * @param direction: cursor move direction from IMC callback + * @param isShiftPressed: determine whether shift-key is pressed + */ + /** + * Simulates hardware keyboard cursor movement by sending key events to Flutter. + * @param direction - The direction to move the cursor + * @param isShiftPressed - Whether shift key is pressed (for text selection) + * @private + */ + private simulateHardwareCursorMovement(direction: inputMethod.Direction, isShiftPressed: boolean) : void { + // Here, '选择' button in '文本编辑' function of soft-keyboard inputMethod apps is a simulation for hardware shift-key. + // Therefore, when '选择' button is pressed in soft-keyboard equals to shift-key pressed in hard-keyboard + enum SimulatedKeyText { + KEY_SHIFT = 'KEYCODE_SHIFT_LEFT', + KEY_ARROW_UP = 'KEYCODE_DPAD_UP', + KEY_ARROW_DOWN = 'KEYCODE_DPAD_DOWN', + KEY_ARROW_LEFT = 'KEYCODE_DPAD_LEFT', + KEY_ARROW_RIGHT = 'KEYCODE_DPAD_RIGHT', + KEY_UNKNOWN = 'KEYCODE_UNKNOWN' + } + if (isShiftPressed) { // simulate shift-key down + this.sendSimulatedKeyEvent(new SimulateKeyEvent(KeyCode.KEYCODE_SHIFT_LEFT, SimulatedKeyText.KEY_SHIFT), false); + } + let arrowEvent: SimulateKeyEvent; + switch (direction) { + case inputMethod.Direction.CURSOR_UP: // simulate hardware arrow-up key event + arrowEvent = new SimulateKeyEvent(KeyCode.KEYCODE_DPAD_UP, SimulatedKeyText.KEY_ARROW_UP); + break; + case inputMethod.Direction.CURSOR_DOWN: // simulate hardware arrow-down key event + arrowEvent = new SimulateKeyEvent(KeyCode.KEYCODE_DPAD_DOWN, SimulatedKeyText.KEY_ARROW_DOWN); + break; + case inputMethod.Direction.CURSOR_LEFT: // simulate hardware arrow-left key event + arrowEvent = new SimulateKeyEvent(KeyCode.KEYCODE_DPAD_LEFT, SimulatedKeyText.KEY_ARROW_LEFT); + break; + case inputMethod.Direction.CURSOR_RIGHT: // simulate hardware arrow-right key event + arrowEvent = new SimulateKeyEvent(KeyCode.KEYCODE_DPAD_RIGHT, SimulatedKeyText.KEY_ARROW_RIGHT); + break; + default: + arrowEvent = new SimulateKeyEvent(KeyCode.KEYCODE_UNKNOWN, SimulatedKeyText.KEY_UNKNOWN); + break; + } + this.sendSimulatedKeyEvent(arrowEvent, false); // simulate arrow-key down + this.sendSimulatedKeyEvent(arrowEvent, true); // simulate arrow-key up + if (isShiftPressed) { // simulate shift-key up + this.sendSimulatedKeyEvent(new SimulateKeyEvent(KeyCode.KEYCODE_SHIFT_LEFT, SimulatedKeyText.KEY_SHIFT), true); + } + } + + /** + * Sends a simulated key event to Flutter. + * @param event - The key event to simulate + * @param isKeyUp - True for key up event, false for key down event + * @private + */ + private sendSimulatedKeyEvent(event: SimulateKeyEvent, isKeyUp: boolean) : void { + this.keyEventChannel?.simulateSendFlutterKeyEvent( + event, isKeyUp, { + onFrameworkResponse: (isEventHandled: boolean): void => {} + }); + } +} + +/** + * Interface for objects that watch for editing state changes. + * Changing the editing state in a didChangeEditingState callback may cause unexpected behavior. + */ +export interface EditingStateWatcher { + /** + * Called when the editing state changes. + * @param textChanged - Whether the text has changed + * @param selectionChanged - Whether the selection has changed + * @param composingRegionChanged - Whether the composing region has changed + */ + didChangeEditingState(textChanged: boolean, selectionChanged: boolean, composingRegionChanged: boolean): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextEditingDelta.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextEditingDelta.ets new file mode 100644 index 0000000..3ca1533 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextEditingDelta.ets @@ -0,0 +1,117 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on TextEditingDelta.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import Log from '../../util/Log'; + +/** + * Represents a delta (change) in text editing state. + * This class tracks changes to text, selection, and composing regions. + */ +export class TextEditingDelta { + private static TAG = "TextEditingDelta"; + private oldText: string = ""; + private deltaText: string = ""; + private deltaStart: number = 0; + private deltaEnd: number = 0; + private newSelectionStart: number; + private newSelectionEnd: number; + private newComposingStart: number; + private newComposingEnd: number; + + /** + * Constructs a new TextEditingDelta instance. + * @param oldEditable - The text before the change + * @param selectionStart - The new selection start position + * @param selectionEnd - The new selection end position + * @param composingStart - The new composing region start position + * @param composingEnd - The new composing region end position + * @param replacementDestinationStart - Optional start position of the replacement destination + * @param replacementDestinationEnd - Optional end position of the replacement destination + * @param replacementSource - Optional replacement text + */ + constructor(oldEditable: string, + selectionStart: number, + selectionEnd: number, + composingStart: number, + composingEnd: number, + replacementDestinationStart?: number, + replacementDestinationEnd?: number, + replacementSource?: string) { + this.newSelectionStart = selectionStart; + this.newSelectionEnd = selectionEnd; + this.newComposingStart = composingStart; + this.newComposingEnd = composingEnd; + if (replacementDestinationStart === undefined || + replacementDestinationEnd === undefined || + replacementSource === undefined) { + this.setDeltas(oldEditable, "", -1, -1); + } else { + this.setDeltas( + oldEditable, + replacementSource, + replacementDestinationStart, + replacementDestinationEnd); + } + } + + /** + * Sets the delta information for this change. + * @param oldText - The text before the change + * @param newText - The replacement text + * @param newStart - The start position of the replacement + * @param newExtent - The end position of the replacement + */ + setDeltas(oldText: string, newText: string, newStart: number, newExtent: number): void { + this.oldText = oldText; + this.deltaText = newText; + this.deltaStart = newStart; + this.deltaEnd = newExtent; + } + + /** + * Converts this delta to a JSON representation. + * @returns A JSON object containing all delta information + */ + toJSON(): TextEditingDeltaJson { + let state: TextEditingDeltaJson = { + oldText: this.oldText.toString(), + deltaText: this.deltaText.toString(), + deltaStart: this.deltaStart, + deltaEnd: this.deltaEnd, + selectionBase: this.newSelectionStart, + selectionExtent: this.newSelectionEnd, + composingBase: this.newComposingStart, + composingExtent: this.newComposingEnd, + }; + return state; + } +} + +/** + * JSON representation of a text editing delta. + */ +export interface TextEditingDeltaJson { + /** The text before the change */ + oldText: string; + /** The replacement text */ + deltaText: string; + /** The start position of the replacement */ + deltaStart: number; + /** The end position of the replacement */ + deltaEnd: number; + /** The new selection start position */ + selectionBase: number; + /** The new selection end position */ + selectionExtent: number; + /** The new composing region start position */ + composingBase: number; + /** The new composing region end position */ + composingExtent: number; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextInputPlugin.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextInputPlugin.ets new file mode 100644 index 0000000..664800a --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextInputPlugin.ets @@ -0,0 +1,941 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on TextInputPlugin.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import TextInputChannel, { + Configuration, + TextEditState, + TextInputMethodHandler +} from '../../embedding/engine/systemchannels/TextInputChannel'; +import inputMethod from '@ohos.inputMethod'; +import Log from '../../util/Log'; +import { EditingStateWatcher, ListenableEditingState } from './ListenableEditingState'; +import Any from '../common/Any'; +import { inputDevice } from '@kit.InputKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { PointerDeviceKind } from '../../embedding/ohos/OhosTouchProcessor'; +import deviceInfo from '@ohos.deviceInfo'; +import KeyEventChannel from '../../embedding/engine/systemchannels/KeyEventChannel'; +import { appManager, bundleManager } from '@kit.AbilityKit'; + +const INPUT_SUPPORT_API = 15; +const PREIVEW_TEXT_SUPPORT_API = 17; +const sdkApiVersion: number = deviceInfo.sdkApiVersion; + +/** + * Plugin for handling text input in Flutter applications. + * This class manages the interaction between Flutter's text input system + * and the OpenHarmony input method framework. + */ +export default class TextInputPlugin implements EditingStateWatcher { + private static TAG = "TextInputPlugin"; + private textInputChannel: TextInputChannel; + private keyEventChannel: KeyEventChannel | undefined; + private mTextInputHandler: TextInputMethodHandlerImpl; + + /** + * Constructs a new TextInputPlugin instance. + * @param textInputChannel - The TextInputChannel for communication with Flutter + * @param viewId - The view ID for focus management + * @param keyEventChannel - Optional KeyEventChannel for key event handling + */ + constructor(textInputChannel: TextInputChannel, viewId: string, keyEventChannel?: KeyEventChannel) { + this.textInputChannel = textInputChannel; + this.keyEventChannel = keyEventChannel; + // viewId is used for requestFocus + this.mTextInputHandler = new TextInputMethodHandlerImpl(this, viewId); + this.textInputChannel.setTextInputMethodHandler(this.mTextInputHandler); + } + + /** + * Clears the current text input client. + */ + public clearTextInputClient() { + this.textInputChannel.textInputMethodHandler?.clearClient(); + } + + /** + * Sets the text input editing state. + * @param state - The new editing state + */ + setTextInputEditingState(state: TextEditState) { + + } + + /** + * Gets the current editing state. + * @returns The current ListenableEditingState instance + */ + getEditingState() { + return this.mTextInputHandler.mEditable; + } + + /** + * Handles changes to preview text editing state. + * This method updates the editing widget with preview text from the input method. + * @param inputTarget - The input target + * @param editable - The editable state containing preview text + */ + didChangePreviewTextEditingState(inputTarget: InputTarget, editable: ListenableEditingState): void { + let stringCache: string = editable.getStringCache(); + // obtain the start index of editing preview text + let startIdx: number = editable.getLeftIdxOfPreviewTextRange(); + let leftStringCache = stringCache.substring(0, startIdx); + let rightStringCache = stringCache.substring(startIdx, stringCache.length); + // concat the string cache and preview text and show on the TextField + let finalStringCache = leftStringCache + editable.getPreviewText() + rightStringCache; + // update the selection range + let newSelectionStart: number = startIdx + editable.getPreviewText().length; + let newSelectionEnd: number = startIdx + editable.getPreviewText().length; + + this.textInputChannel.updateEditingState(inputTarget.id, finalStringCache, + newSelectionStart, newSelectionEnd, + editable.getComposingStart(), editable.getComposingEnd()) + // notify current cursor index to the InputMethodFramework + this.mTextInputHandler.inputMethodController.changeSelection(finalStringCache, + newSelectionStart, newSelectionEnd); + } + + /** + * Called when the editing state changes. + * @param textChanged - Whether the text has changed + * @param selectionChanged - Whether the selection has changed + * @param composingRegionChanged - Whether the composing region has changed + */ + didChangeEditingState(textChanged: boolean, selectionChanged: boolean, composingRegionChanged: boolean): void { + let editable = this.mTextInputHandler.mEditable; + let inputTarget = this.mTextInputHandler.inputTarget; + let configuration = this.mTextInputHandler.configuration; + if (configuration != null && configuration.enableDeltaModel) { + this.textInputChannel.updateEditingStateWithDeltas(inputTarget.id, editable.extractBatchTextEditingDeltas()); + editable.clearBatchDeltas(); + } else if (sdkApiVersion >= PREIVEW_TEXT_SUPPORT_API && editable.getPreviewText() != "") { + this.didChangePreviewTextEditingState(inputTarget, editable); + } + else { + this.textInputChannel.updateEditingState(inputTarget.id, editable.getStringCache(), + editable.getSelectionStart(), editable.getSelectionEnd(), + editable.getComposingStart(), editable.getComposingEnd()) + } + } + + /** + * Detaches the text input plugin from the input method framework. + */ + detach(): void { + this.mTextInputHandler.inputMethodController.detach((err) => { + if (err) { + Log.e(TextInputPlugin.TAG, "Failed to detach: " + JSON.stringify(err)); + } + }) + } + + /** + * Destroys the text input plugin, hiding the keyboard and cleaning up resources. + */ + destroy() { + // Since the Dart side no longer listens to the lifecycle, + // the keyboard must be explicitly hidden before the engine is destroyed. + this.mTextInputHandler.hide(); + this.textInputChannel.setTextInputMethodHandler(null); + } +} + +const INPUT_TYPE_NAME = + ['NONE', 'TEXT', 'MULTILINE', 'NUMBER', 'PHONE', 'DATETIME', 'EMAIL_ADDRESS', 'URL', 'VISIBLE_PASSWORD'] + +/** + * Implementation of TextInputMethodHandler for OpenHarmony input method framework. + * This class handles all interactions with the system input method. + */ +class TextInputMethodHandlerImpl implements TextInputMethodHandler { + private static TAG = "TextInputMethodHandlerImpl"; + private textConfig: inputMethod.TextConfig; + /** The input method controller for managing the keyboard. */ + inputMethodController: inputMethod.InputMethodController; + /** The current input target. */ + inputTarget: InputTarget; + /** The current text input configuration, or null if not set. */ + public configuration: Configuration | null = null; + private lastKind?: PointerDeviceKind; + /** The editable state for text input. */ + mEditable: ListenableEditingState; + private mRestartInputPending: boolean = false; + private plugin: EditingStateWatcher | Any; + private imcFlag: boolean = false; + private keyboardStatus: inputMethod.KeyboardStatus = inputMethod.KeyboardStatus.HIDE; + private inputAttribute: inputMethod.InputAttribute = + { textInputType: inputMethod.TextInputType.TEXT, enterKeyType: inputMethod.EnterKeyType.NONE }; + private keyboardFocusState: boolean = false; + private focusViewId: string = ""; + // Record the type of soft keyboard that was triggered last time + private lastInputType?: inputMethod.TextInputType; + private isInputMethodAttached: boolean = false; + + /** + * Constructs a new TextInputMethodHandlerImpl instance. + * @param plugin - The TextInputPlugin instance + * @param viewId - The view ID for focus management + */ + constructor(plugin: TextInputPlugin | Any, viewId: string) { + this.textConfig = { + inputAttribute: this.inputAttribute + }; + this.plugin = plugin; + this.mEditable = new ListenableEditingState(null, 0); + this.inputMethodController = inputMethod.getController(); + this.inputTarget = new InputTarget(Type.NO_TARGET, 0); + this.focusViewId = viewId; + } + + /** + * Shows the text input keyboard if appropriate. + * The keyboard is only shown if the input type is not NONE. + */ + show(): void { + try { + let isPhysicalKeyboard = false; + inputDevice.getDeviceList((Error: Error, ids: Array) => { + for (let i = 0; i < ids.length; i++) { + const type = inputDevice.getKeyboardTypeSync(ids[i]); + if (type === inputDevice.KeyboardType.ALPHABETIC_KEYBOARD || + type === inputDevice.KeyboardType.DIGITAL_KEYBOARD) { + isPhysicalKeyboard = true; + break; + } + } + // 适配api20,在外接键盘状态下,阻止软键盘重复拉起 + if (isPhysicalKeyboard) { + this.keyboardStatus = inputMethod.KeyboardStatus.SHOW; + } + }) + } catch (error) { + Log.e(TextInputMethodHandlerImpl.TAG, + `Show function failed to query device. Code is ${error.code}, message is ${error.message}`) + } + if (this.canShowTextInput()) { + // Ensure the Xcomponent gains focus before the soft keyboard is displayed. + focusControl.requestFocus(this.focusViewId); + this.keyboardFocusState = true; + this.showTextInput(); + } else { + this.hide(); + } + } + + /** + * Hides the text input keyboard. + */ + hide(): void { + // Ensure the Xcomponent loses focus before the soft keyboard is hided. + focusControl.requestFocus("unfocus-xcomponent-node"); + this.keyboardFocusState = false; + this.hideTextInput(); + } + + /** + * Gets the current keyboard focus state. + * @returns True if the keyboard has focus, false otherwise + */ + getKeyboardFocusState() { + return this.keyboardFocusState; + } + + /** + * Requests autofill functionality. + */ + requestAutofill(): void { + + } + + /** + * Finishes the autofill context. + * @param shouldSave - Whether to save the autofill data + */ + finishAutofillContext(shouldSave: boolean): void { + + } + + /** + * Sets the text input client. + * @param textInputClientId - The client ID + * @param configuration - The input configuration + */ + setClient(textInputClientId: number, configuration: Configuration | null): void { + this.setTextInputClient(textInputClientId, configuration); + } + + /** + * Updates the input configuration. + * @param configuration - The new input configuration + */ + updateConfig(configuration: Configuration | null) { + if (configuration) { + this.lastKind = this.configuration?.deviceKind; + this.configuration = configuration; + if (configuration.inputType) { + this.textConfig.inputAttribute.textInputType = configuration.inputType.type; + this.textConfig.inputAttribute.enterKeyType = configuration.inputAction as Any; + } + } + } + + /** + * Sets the platform view client for text input. + * @param id - The platform view ID + * @param usesVirtualDisplay - Whether the platform view uses a virtual display + */ + setPlatformViewClient(id: number, usesVirtualDisplay: boolean): void { + + } + + /** + * Sets the editable size and transform. + * @param width - The width of the editable area + * @param height - The height of the editable area + * @param transform - The transformation matrix + */ + setEditableSizeAndTransform(width: number, height: number, transform: number[]): void { + + } + + /** + * Sets the cursor size and position. + * @param cursorInfo - The cursor information + */ + setCursorSizeAndPosition(cursorInfo: inputMethod.CursorInfo) { + try { + this.inputMethodController.updateCursor(cursorInfo, (err: BusinessError) => { + if (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to updateCursor:" + JSON.stringify(err)); + return; + } + }) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to updateCursor:" + JSON.stringify(err)); + } + } + + /** + * Sets the editing state from Flutter. + * @param editingState - The new editing state + */ + setEditingState(editingState: TextEditState): void { + Log.d(TextInputMethodHandlerImpl.TAG, + "text:" + editingState.text + " selectionStart:" + editingState.selectionStart + " selectionEnd:" + + editingState.selectionEnd + " composingStart:" + editingState.composingStart + " composingEnd" + + editingState.composingEnd); + this.mEditable.setPreviewText(""); + this.mEditable.setIsCursorIdxOutOfPreviewTextRange(false); + this.mEditable.updateTextInputState(editingState); + // notify current cursor index or selection range to the InputMethodFramework + this.inputMethodController.changeSelection("", + -1, -1, (err: BusinessError) => { + if (err) { + Log.e(TextInputMethodHandlerImpl.TAG, `Failed to changeSelection, code: ${err.code}, message: ${err.message}`); + return; + } + this.inputMethodController.changeSelection(editingState.text, + editingState.selectionStart, editingState.selectionEnd); + }); + if (sdkApiVersion >= PREIVEW_TEXT_SUPPORT_API) { + this.previewTextChangeSelection(editingState); + } + } + + /** + * Handles selection changes when preview text is active. + * If the cursor moves outside the preview text range, the preview text is inserted. + * @param editingState - The current editing state + */ + previewTextChangeSelection(editingState: TextEditState): void { + // if users only modify the cursor index without typing new preview text, + // clear the current preview text cache for avoiding inserting redundant preview text + // to insert at the current cursor position + let currCursorIndex: number = editingState.selectionStart; + let leftIdxOfPreviewText: number = this.mEditable.getLeftIdxOfPreviewTextRange(); + let rightIdxOfPreviewText: number = this.mEditable.getRightIdxOfPreviewTextRange(); + + let isCursorIdxOutOfPreviewTextRange = (leftIdxOfPreviewText != rightIdxOfPreviewText) && + (currCursorIndex < leftIdxOfPreviewText || currCursorIndex > rightIdxOfPreviewText) + // if user move the current cursor index out of editing preview text range, + // immediately inserting the first candidate word into the editing widget + if (isCursorIdxOutOfPreviewTextRange) { + // change selection async callback + this.inputMethodController.changeSelection(editingState.text, leftIdxOfPreviewText, rightIdxOfPreviewText) + .then(() => { + // force the cursor position at the end of editing preview text + editingState.selectionStart = rightIdxOfPreviewText; + editingState.selectionEnd = rightIdxOfPreviewText; + // update TextInputState and reset the preview text state + this.mEditable.updateTextInputState(editingState); + this.mEditable.setPreviewText(""); + }) + .catch((err: BusinessError) => { + console.error(`Failed to previewTextChangeSelection: ${JSON.stringify(err)}`); + }) + } + } + + /** + * Clears the current text input client. + */ + clearClient(): void { + this.clearTextInputClient(); + } + + private async showTextInput(): Promise { + if (!this.lastInputType) { + this.setLastInputType(this.configuration?.inputType?.type); + } + if (this.lastKind === undefined && this.configuration?.deviceKind !== undefined) { + this.lastKind = this.configuration.deviceKind; + } + if (this.keyboardStatus == inputMethod.KeyboardStatus.SHOW) { + // 增加键盘拉起状态时也调用attach,参数false则attach不会拉起键盘 + await this.attach(false); + if (this.lastKind != this.configuration?.deviceKind || + this.hasSecureKeyboardInSwitch()) { + if (deviceInfo.sdkApiVersion >= INPUT_SUPPORT_API) { + await this.inputMethodController.showTextInput(this.getRequestReason()); + } else { + await this.inputMethodController.showTextInput(); + } + if (this.configuration?.deviceKind !== undefined) { + this.lastKind = this.configuration.deviceKind; + } + } + this.setLastInputType(this.configuration?.inputType?.type); + return; + } + this.setLastInputType(this.configuration?.inputType?.type); + await this.attach(true); + if (this.configuration?.deviceKind !== undefined) { + this.lastKind = this.configuration.deviceKind; + } + if (!this.imcFlag) { + this.listenKeyBoardEvent(); + } + } + + private async hideTextInput(): Promise { + await this.inputMethodController.detach().then(() => { + this.keyboardStatus = inputMethod.KeyboardStatus.HIDE; + this.cancelListenKeyBoardEvent(); + }).catch((err: BusinessError) => { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to detach: " + JSON.stringify(err)); + this.keyboardStatus = inputMethod.KeyboardStatus.NONE; + }); + } + + /** + * Attaches to the input method controller. + * @param showKeyboard - Whether to show the keyboard immediately + * @private + */ + async attach(showKeyboard: boolean): Promise { + try { + // must register previewtext callbacks before attachment + if (sdkApiVersion >= PREIVEW_TEXT_SUPPORT_API) { + this.registerPreviewTextCallbacks(); + } + if (deviceInfo.sdkApiVersion >= INPUT_SUPPORT_API) { + await this.inputMethodController.attach(showKeyboard, this.textConfig, this.getRequestReason()).then(async () => { + await this.handleAttach(showKeyboard); + }); + } else { + await this.inputMethodController.attach(showKeyboard, this.textConfig).then(async () => { + await this.handleAttach(showKeyboard); + }); + } + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to attach:" + JSON.stringify(err)); + this.keyboardStatus = inputMethod.KeyboardStatus.NONE; + } + } + + private async handleAttach(showKeyboard: boolean): Promise { + let isDetached = false; + if (showKeyboard) { + isDetached = await this.detachIfBackground(); + } + if (isDetached) { + this.isInputMethodAttached = false; + this.keyboardStatus = inputMethod.KeyboardStatus.NONE; + } else { + this.isInputMethodAttached = true; + this.cacheMethodCall(); + this.keyboardStatus = inputMethod.KeyboardStatus.SHOW; + } + } + + private async detachIfBackground(): Promise { + try { + const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION; + const bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleFlags); + const currentInfo = await appManager.getRunningProcessInformation(); + const targetBundleName = bundleInfo?.name; + const currentProcess = currentInfo.find(info => info.bundleNames?.includes(targetBundleName)); + //STATE_FOREGROUND 代表进程处于前台,界面获焦并显示时状态为STATE_ACTIVE,参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-ability-appmanager#processstate10 + if ( + currentProcess && + (currentProcess.state === appManager.ProcessState.STATE_FOREGROUND || + currentProcess.state === appManager.ProcessState.STATE_BACKGROUND) + ) { + await this.inputMethodController.hideTextInput(); + return true; + } + } catch (err) { + Log.e( + TextInputMethodHandlerImpl.TAG, + `detachIfBackground error: ${JSON.stringify(err)}`, + ); + } + return false; + } + + cacheMethodCall(): void { + // Cache method calls when input method is attached; no-op if not used by this version + } + + /** + * Gets the request reason for showing the keyboard based on the input device kind. + * @returns The request reason for keyboard display + */ + getRequestReason() : inputMethod.RequestKeyboardReason { + let deviceKind : PointerDeviceKind = this.configuration?.deviceKind ?? PointerDeviceKind.UNKNOWN + Log.i(TextInputMethodHandlerImpl.TAG, "getRequestReason: deviceKind=" + deviceKind); + switch (deviceKind) { + case PointerDeviceKind.TOUCH: + return inputMethod.RequestKeyboardReason.TOUCH; + case PointerDeviceKind.MOUSE: + return inputMethod.RequestKeyboardReason.MOUSE; + case PointerDeviceKind.STYLUS: + case PointerDeviceKind.INVERTED_STYLUS: + case PointerDeviceKind.TRACKPAD: + return inputMethod.RequestKeyboardReason.OTHER; + default: + return inputMethod.RequestKeyboardReason.NONE; + } + } + + /** + * Handles focus state changes. + * @param focusState - The new focus state + */ + handleChangeFocus(focusState: boolean) { + if (focusState && this.keyboardFocusState) { + // When the app loses focus, the system automatically detaches the input method. + // Upon regaining focus, if the input method should be displayed, it must be reattached. + this.show(); + } + try { + inputDevice.getDeviceList((Error: Error, ids: Array) => { + let isPhysicalKeyboard = false; + for (let i = 0; i < ids.length; i++) { + const type = inputDevice.getKeyboardTypeSync(ids[i]); + if (type == inputDevice.KeyboardType.ALPHABETIC_KEYBOARD || type == inputDevice.KeyboardType.DIGITAL_KEYBOARD) { + isPhysicalKeyboard = true; + break; + } + } + + if(focusState && isPhysicalKeyboard && this.keyboardFocusState) { + this.cancelListenKeyBoardEvent(); + this.inputMethodController.detach().then(async () =>{ + await this.attach(true); + this.listenKeyBoardEvent(); + }) + } + }) + } catch (error) { + Log.e(TextInputMethodHandlerImpl.TAG, `Failed to query device. Code is ${error.code}, message is ${error.message}`) + } + } + + /** + * Updates the input attribute configuration. + */ + async updateAttribute(): Promise { + if (this.keyboardStatus != inputMethod.KeyboardStatus.SHOW) { + return; + } + try { + await this.inputMethodController.updateAttribute(this.inputAttribute); + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to updateAttribute:" + JSON.stringify(err)); + } + } + + /** + * Sets the text input client with the given configuration. + * @param client - The client ID + * @param configuration - The input configuration + */ + setTextInputClient(client: number, configuration: Configuration | null): void { + if (configuration) { + this.lastKind = this.configuration?.deviceKind; + this.configuration = configuration; + if (configuration.inputType) { + this.textConfig.inputAttribute.textInputType = configuration.inputType.type; + this.textConfig.inputAttribute.enterKeyType = configuration.inputAction as Any; + } + } + if (this.canShowTextInput()) { + this.inputTarget = new InputTarget(Type.FRAMEWORK_CLIENT, client); + } else { + this.inputTarget = new InputTarget(Type.NO_TARGET, client); + } + this.mEditable.removeEditingStateListener(this.plugin); + + this.mEditable = new ListenableEditingState( + this.plugin.textInputChannel, this.inputTarget.id, this.plugin.keyEventChannel); + + this.mRestartInputPending = true; + this.mEditable.addEditingStateListener(this.plugin); + + this.inputAttribute = this.textConfig.inputAttribute; + + this.updateAttribute(); + } + + setLastInputType(inputType?: inputMethod.TextInputType): void { + this.lastInputType = inputType; + } + + // It is used to determine whether there is a safe keyboard for the keyboard type + // awakened by the two input boxes when a soft keyboard is already suspended + // and switching input boxes + hasSecureKeyboardInSwitch(): boolean { + // Since this method is called in showTextInput to determine whether a secure + // keyboard needs to be pulled up, the parameter must be true + this.handleAttach(true); + return this.lastInputType === inputMethod.TextInputType.VISIBLE_PASSWORD || + this.configuration?.inputType?.type === inputMethod.TextInputType.VISIBLE_PASSWORD; + } + + /** + * Checks if text input can be shown. + * @returns True if text input can be shown, false if input type is NONE + */ + canShowTextInput(): boolean { + if (this.configuration == null || this.configuration.inputType == null) { + return true; + } + return this.configuration.inputType.type != inputMethod.TextInputType.NONE; + } + + /** + * Registers callbacks for preview text functionality. + */ + registerPreviewTextCallbacks(): void { + try { + this.inputMethodController.on("setPreviewText", this.setPreviewTextCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe setPreviewText:" + JSON.stringify(err)); + this.unregisterPreviewTextCallbacks(); + return; + } + + try { + this.inputMethodController.on("finishTextPreview", this.finishPreviewTextCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe finishTextPreview:" + JSON.stringify(err)); + this.unregisterPreviewTextCallbacks(); + return; + } + } + + /** + * Unregisters preview text callbacks. + */ + unregisterPreviewTextCallbacks(): void { + this.inputMethodController?.off("setPreviewText", this.setPreviewTextCallback) + this.inputMethodController?.off("finishTextPreview", this.finishPreviewTextCallback); + } + + /** + * Registers listeners for keyboard events from the input method framework. + */ + listenKeyBoardEvent(): void { + try { + this.inputMethodController.on('insertText', this.insertTextCallback); + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe insertText:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + try { + this.inputMethodController.on('deleteLeft', this.deleteLeftCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe deleteLeft:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + try { + this.inputMethodController.on('deleteRight', this.deleteRightCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe deleteRight:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + try { + this.inputMethodController.on('sendFunctionKey', this.sendFunctionKeyCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe sendFunctionKey:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + try { + this.inputMethodController.on('sendKeyboardStatus', this.sendKeyboardStatusCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe sendKeyboardStatus:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + try { + this.inputMethodController.on('selectByRange', this.selectByRangeCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe selectByRange:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + try { + this.inputMethodController.on('moveCursor', this.moveCursorCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe moveCursor:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + try { + this.inputMethodController.on('handleExtendAction', this.handleExtendActionCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe handleExtendAction:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + try { + this.inputMethodController.on('selectByMovement', this.selectByMovementCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe selectByMovement:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + try { + this.inputMethodController.on('getLeftTextOfCursor', this.getLeftTextOfCursorCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe getLeftTextOfCursor:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + try { + this.inputMethodController.on('getRightTextOfCursor', this.getRightTextOfCursorCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe getRightTextOfCursor:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + try { + this.inputMethodController.on('getTextIndexAtCursor', this.getTextIndexAtCursorCallback) + } catch (err) { + Log.e(TextInputMethodHandlerImpl.TAG, "Failed to subscribe getTextIndexAtCursor:" + JSON.stringify(err)); + this.cancelListenKeyBoardEvent(); + return; + } + + Log.d(TextInputMethodHandlerImpl.TAG, "listenKeyBoardEvent success"); + this.imcFlag = true; + } + + private setPreviewTextCallback = (text: string, range: inputMethod.Range) => { + Log.i(TextInputMethodHandlerImpl.TAG, + "setPreviewTextCallback: text = " + text + " range = " + JSON.stringify(range)); + this.mEditable.handleInsertPreviewTextEvent(text, range); + } + + private finishPreviewTextCallback = () => { + Log.i(TextInputMethodHandlerImpl.TAG, "finishPreviewTextCallback"); + // When editing preview text, meanwhile switch the app to the background and + // the preview text will automatically insert into the string cache of TextField + this.mEditable.handleInsertTextEvent(this.mEditable.getPreviewText()); + this.mEditable.clearPreviewTextContents(); + } + + private insertTextCallback = (text: string) => { + this.mEditable.handleInsertTextEvent(text); + // notify the current cursor index to InputMethodFramework for preview mode + this.inputMethodController.changeSelection(this.mEditable.getStringCache(), this.mEditable.getSelectionStart(), + this.mEditable.getSelectionEnd()); + } + + private deleteLeftCallback = (length: number) => { + this.mEditable.handleDeleteEvent(false, length, this.configuration?.enableDeltaModel); + this.inputMethodController.changeSelection(this.mEditable.getStringCache(), this.mEditable.getSelectionStart(), + this.mEditable.getSelectionEnd()); + } + + private deleteRightCallback = (length: number) => { + this.mEditable.handleDeleteEvent(true, length, this.configuration?.enableDeltaModel); + this.inputMethodController.changeSelection(this.mEditable.getStringCache(), this.mEditable.getSelectionStart(), + this.mEditable.getSelectionEnd()); + } + + private sendFunctionKeyCallback = (functionKey: inputMethod.FunctionKey) => { + if (functionKey.enterKeyType == inputMethod.EnterKeyType.NEWLINE) { + // insertText回调不会通知换行事件,需要在这里进行处理 + this.mEditable.handleNewlineEvent(); + } + this.mEditable.handleFunctionKey(functionKey); + } + + private sendKeyboardStatusCallback = (state: inputMethod.KeyboardStatus) => { + // api20开始,外接键盘状态下,点击输入框会拉起软键盘,此时要阻止onConnectionClosed,否则输入框会无法输入 + try { + let isPhysicalKeyboard = false; + inputDevice.getDeviceList((Error: Error, ids: Array) => { + for (let i = 0; i < ids.length; i++) { + const type = inputDevice.getKeyboardTypeSync(ids[i]); + if (type === inputDevice.KeyboardType.ALPHABETIC_KEYBOARD || + type === inputDevice.KeyboardType.DIGITAL_KEYBOARD) { + isPhysicalKeyboard = true; + return; + } + } + this.keyboardStatus = state; + if (state === inputMethod.KeyboardStatus.HIDE) { + this.plugin.textInputChannel.onConnectionClosed(this.inputTarget.id); + } + }) + } catch (error) { + Log.e(TextInputMethodHandlerImpl.TAG, + `SendKeyboardStatusCallback function failed to query device. Code is ${error.code}, message is ${error.message}`) + } + } + + // obtain the range to update cursor idx to start/end of text cache + private selectByRangeCallback = (range: inputMethod.Range) => { + this.mEditable.handleSelectByRangeEvent(range); + this.inputMethodController.changeSelection(this.mEditable.getStringCache(), this.mEditable.getSelectionStart(), + this.mEditable.getSelectionEnd()); + } + + // move cursor postion with left, right, up and down in editing widget + private moveCursorCallback = (direction: inputMethod.Direction) => { + this.mEditable.handleMoveCursorEvent(direction); + this.inputMethodController.changeSelection(this.mEditable.getStringCache(), this.mEditable.getSelectionStart(), + this.mEditable.getSelectionEnd()); + } + + // handle extend clipboard actions, like 'select all', 'cut', 'copy' and 'paste' + private handleExtendActionCallback = (action: inputMethod.ExtendAction) => { + this.mEditable.handleExtendActionEvent(action); + this.inputMethodController.changeSelection(this.mEditable.getStringCache(), this.mEditable.getSelectionStart(), + this.mEditable.getSelectionEnd()); + } + + // text selection range changed with cursor direction movement + private selectByMovementCallback = (movement: inputMethod.Movement) => { + this.mEditable.handleSelectByMovementEvent(movement); + this.inputMethodController.changeSelection(this.mEditable.getStringCache(), this.mEditable.getSelectionStart(), + this.mEditable.getSelectionEnd()); + } + + // return the left-side text string of current cursor index ("abc|345" -> return "abc") + private getLeftTextOfCursorCallback = (length: number) : string => { + const retText = this.mEditable.getLeftTextOfCursor(length); + Log.i(TextInputMethodHandlerImpl.TAG, "getLeftTextOfCursor: " + retText); + return retText; + } + + // return the right-side text string of current cursor index ("abc|345" -> return "345") + private getRightTextOfCursorCallback = (length: number) : string => { + const retText = this.mEditable.getRightTextOfCursor(length); + Log.i(TextInputMethodHandlerImpl.TAG, "getRightTextOfCursor: " + retText); + return retText; + } + + // return the current cursor index of editing text + private getTextIndexAtCursorCallback = () => { + const cursorIdx = this.mEditable.getSelectionStart(); + Log.i(TextInputMethodHandlerImpl.TAG, "getTextIndexAtCursor: " + cursorIdx); + return cursorIdx; + } + + /** + * Cancels all keyboard event listeners. + */ + cancelListenKeyBoardEvent(): void { + this.inputMethodController?.off('insertText', this.insertTextCallback); + this.inputMethodController?.off('deleteLeft', this.deleteLeftCallback); + this.inputMethodController?.off('deleteRight', this.deleteRightCallback); + this.inputMethodController?.off('sendFunctionKey', this.sendFunctionKeyCallback); + this.inputMethodController?.off('sendKeyboardStatus', this.sendKeyboardStatusCallback); + this.inputMethodController?.off('selectByRange', this.selectByRangeCallback); + this.inputMethodController?.off('moveCursor', this.moveCursorCallback); + this.inputMethodController?.off('handleExtendAction', this.handleExtendActionCallback); + this.inputMethodController?.off('selectByMovement', this.selectByMovementCallback); + this.inputMethodController?.off('getLeftTextOfCursor', this.getLeftTextOfCursorCallback); + this.inputMethodController?.off('getRightTextOfCursor', this.getRightTextOfCursorCallback); + this.inputMethodController?.off('getTextIndexAtCursor', this.getTextIndexAtCursorCallback); + this.imcFlag = false; + } + + /** + * Clears the text input client. + */ + public clearTextInputClient(): void { + if (this.inputTarget.type == Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) { + return; + } + this.mEditable.removeEditingStateListener(this.plugin); + this.configuration = null; + this.inputTarget = new InputTarget(Type.NO_TARGET, 0); + } +} + +/** + * Enumeration of input target types. + */ +enum Type { + /** No input target */ + NO_TARGET, + /** InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter framework. */ + FRAMEWORK_CLIENT, + /** InputConnection is managed by a platform view that is presented on a virtual display. */ + VIRTUAL_DISPLAY_PLATFORM_VIEW, + /** InputConnection is managed by a platform view that is presented on a physical display. */ + PHYSICAL_DISPLAY_PLATFORM_VIEW, +} + +/** + * Represents a target for text input operations. + */ +export class InputTarget { + /** The type of input target. */ + type: Type; + /** The unique identifier for this input target. */ + id: number; + + /** + * Constructs a new InputTarget instance. + * @param type - The type of input target + * @param id - The target ID + */ + constructor(type: Type, id: number) { + this.type = type; + this.id = id; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextUtils.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextUtils.ets new file mode 100644 index 0000000..3e0ae4c --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/editing/TextUtils.ets @@ -0,0 +1,440 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterTextUtils.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import FlutterNapi from '../../embedding/engine/FlutterNapi'; +import Log from '../../util/Log'; + +const LINE_FEED: number = 0x0A; +const CARRIAGE_RETURN: number = 0x0D; +const COMBINING_ENCLOSING_KEYCAP: number = 0x20E3; +const CANCEL_TAG: number = 0xE007F; +const ZERO_WIDTH_JOINER: number = 0x200D; + +const TAG = "TextUtils"; + +/** + * Utility class for text processing operations, including Unicode code point handling, + * emoji detection, and text offset calculations. + */ +export class FlutterTextUtils { + + /** + * Checks if a Unicode code point represents an emoji. + * @param code - The Unicode code point to check + * @returns True if the code point is an emoji, false otherwise + */ + static isEmoji(code: number): boolean { + return FlutterNapi.unicodeIsEmoji(code); + } + + /** + * Checks if a Unicode code point represents an emoji modifier. + * @param code - The Unicode code point to check + * @returns True if the code point is an emoji modifier, false otherwise + */ + static isEmojiModifier(code: number): boolean { + return FlutterNapi.unicodeIsEmojiModifier(code); + } + + /** + * Checks if a Unicode code point represents an emoji modifier base. + * @param code - The Unicode code point to check + * @returns True if the code point is an emoji modifier base, false otherwise + */ + static isEmojiModifierBase(code: number): boolean { + return FlutterNapi.unicodeIsEmojiModifierBase(code); + } + + /** + * Checks if a Unicode code point represents a variation selector. + * @param code - The Unicode code point to check + * @returns True if the code point is a variation selector, false otherwise + */ + static isVariationSelector(code: number): boolean { + return FlutterNapi.unicodeIsVariationSelector(code); + } + + /** + * Checks if a Unicode code point represents a regional indicator symbol. + * @param code - The Unicode code point to check + * @returns True if the code point is a regional indicator symbol, false otherwise + */ + static isRegionalIndicatorSymbol(code: number): boolean { + return FlutterNapi.unicodeIsRegionalIndicatorSymbol(code); + } + + /** + * Checks if a Unicode code point is a tag specification character. + * @param code - The Unicode code point to check + * @returns True if the code point is a tag specification character, false otherwise + */ + static isTagSpecChar(code: number): boolean { + return 0xE0020 <= code && code <= 0xE007E; + } + + /** + * Checks if a Unicode code point is a keycap base character (0-9, #, *). + * @param code - The Unicode code point to check + * @returns True if the code point is a keycap base, false otherwise + */ + static isKeycapBase(code: number): boolean { + return ('0'.charCodeAt(0) <= code && code <= '9'.charCodeAt(0)) || code == '#'.charCodeAt(0) || code == '*'.charCodeAt(0); + } + + /** + * Gets the Unicode code point before the specified offset in the text. + * Handles surrogate pairs correctly. + * @param text - The text to examine + * @param offset - The offset position + * @returns The Unicode code point before the offset + * @throws RangeError if the offset is out of range + */ + static codePointBefore(text: string, offset: number): number { + if (offset <= 0 || offset > text.length) { + throw new RangeError('Offset out of range'); + } + + // Get the character before the offset + const char = text[offset - 1]; + + // Check if it is a low surrogate (part of a surrogate pair) + if (offset > 1 && char >= '\uDC00' && char <= '\uDFFF') { + const prevChar = text[offset - 2]; + // Check if the previous character is a high surrogate + if (prevChar >= '\uD800' && prevChar <= '\uDBFF') { + // If it is, combine the surrogate pair into a full Unicode code point + return (prevChar.charCodeAt(0) - 0xD800) * 0x400 + (char.charCodeAt(0) - 0xDC00) + 0x10000; + } + } + + // Return the code point of the single character (if it's not a surrogate pair) + return char.charCodeAt(0); + } + + /** + * Gets the Unicode code point at the specified offset in the text. + * Handles surrogate pairs correctly. + * @param text - The text to examine + * @param offset - The offset position + * @returns The Unicode code point at the offset + * @throws RangeError if the offset is out of range + */ + static codePointAt(text: string, offset: number): number { + if (offset >= text.length) { + throw new RangeError('Offset out of range'); + } + let char = text[offset]; + + // Check if it is a high surrogate (part of a surrogate pair) + if (char >= '\uD800' && char <= '\uDBFF' && offset + 1 < text.length) { + const nextChar = text[offset + 1]; + // Check if the previous character is a low surrogate + if (nextChar >= '\uDC00' && nextChar <= '\uDFFF') { + // If it is, combine the surrogate pair into a full Unicode code point + return (char.charCodeAt(0) - 0xD800) * 0x400 + (nextChar.charCodeAt(0) - 0xDC00) + 0x10000; + } + } + return char.charCodeAt(0); + } + + /** + * Gets the number of UTF-16 code units required to represent a Unicode code point. + * @param codePoint - The Unicode code point + * @returns 1 for BMP characters (0x0000-0xFFFF), 2 for supplementary characters (0x10000-0x10FFFF) + */ + static charCount(codePoint: number): number { + // If the code point is in the BMP range (0x0000 - 0xFFFF), it needs 1 UTF-16 code unit + if (codePoint <= 0xFFFF) { + return 1; + } + // If the code point is in the supplementary range (0x10000 - 0x10FFFF), it needs 2 UTF-16 code units + return 2; + } + + /** + * Gets the offset before the current position, handling complex Unicode sequences + * such as emojis, regional indicators, keycaps, and variation selectors. + * @param text - The text to examine + * @param offset - The current offset position + * @returns The offset before the current position, accounting for Unicode sequences + */ + static getOffsetBefore(text: string, offset: number): number { + if (offset <= 1) { + return 0; + } + + let codePoint: number = FlutterTextUtils.codePointBefore(text, offset); + let deleteCharCount: number = FlutterTextUtils.charCount(codePoint); + let lastOffset: number = offset - deleteCharCount; + + if (lastOffset == 0) { + return 0; + } + + // Line Feed + if (codePoint == LINE_FEED) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + if (codePoint == CARRIAGE_RETURN) { + ++deleteCharCount; + } + return offset - deleteCharCount; + } + + // Flags + if (FlutterTextUtils.isRegionalIndicatorSymbol(codePoint)) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + let regionalIndicatorSymbolCount: number = 1; + while (lastOffset > 0 && FlutterTextUtils.isRegionalIndicatorSymbol(codePoint)) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + regionalIndicatorSymbolCount++; + } + if (FlutterTextUtils.isRegionalIndicatorSymbol(codePoint)) { + regionalIndicatorSymbolCount++; + } + if (regionalIndicatorSymbolCount % 2 == 0) { + deleteCharCount += 2; + } + return offset - deleteCharCount; + } + + // Keycaps + if (codePoint == COMBINING_ENCLOSING_KEYCAP) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + if (lastOffset > 0 && FlutterTextUtils.isVariationSelector(codePoint)) { + let tmpCodePoint: number = FlutterTextUtils.codePointBefore(text, lastOffset); + if (FlutterTextUtils.isKeycapBase(tmpCodePoint)) { + deleteCharCount += FlutterTextUtils.charCount(codePoint) + FlutterTextUtils.charCount(tmpCodePoint); + } + } else if (FlutterTextUtils.isKeycapBase(codePoint)) { + deleteCharCount += FlutterTextUtils.charCount(codePoint); + } + return offset - deleteCharCount; + } + + /** + * Following if statements for Emoji tag sequence and Variation selector are skipping these + * modifiers for going through the last statement that is for handling emojis. They return the + * offset if they don't find proper base characters + */ + // Emoji Tag Sequence + if (codePoint == CANCEL_TAG) { // tag_end + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + while (lastOffset > 0 && FlutterTextUtils.isTagSpecChar(codePoint)) { // tag_spec + deleteCharCount += FlutterTextUtils.charCount(codePoint); + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + } + if (!FlutterTextUtils.isEmoji(codePoint)) { // tag_base not found. Just delete the end. + return offset - 2; + } + deleteCharCount += FlutterTextUtils.charCount(codePoint); + } + + if (FlutterTextUtils.isVariationSelector(codePoint)) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + if (!FlutterTextUtils.isEmoji(codePoint)) { + return offset - deleteCharCount; + } + deleteCharCount += FlutterTextUtils.charCount(codePoint); + + lastOffset -= FlutterTextUtils.charCount(codePoint); + } + + if (FlutterTextUtils.isEmoji(codePoint)) { + let isZwj: boolean = false; + let lastSeenVariantSelectorCharCount: number = 0; + do { + if (isZwj) { + deleteCharCount += FlutterTextUtils.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; + isZwj = false; + } + lastSeenVariantSelectorCharCount = 0; + if (FlutterTextUtils.isEmojiModifier(codePoint)) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + if (lastOffset > 0 && FlutterTextUtils.isVariationSelector(codePoint)) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + if (!FlutterTextUtils.isEmoji(codePoint)) { + return offset - deleteCharCount; + } + lastSeenVariantSelectorCharCount = FlutterTextUtils.charCount(codePoint); + lastOffset -= FlutterTextUtils.charCount(codePoint); + } + if (FlutterTextUtils.isEmojiModifierBase(codePoint)) { + deleteCharCount += lastSeenVariantSelectorCharCount + FlutterTextUtils.charCount(codePoint); + } + break; + } + + if (lastOffset > 0) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + if (codePoint == ZERO_WIDTH_JOINER) { + isZwj = true; + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastOffset -= FlutterTextUtils.charCount(codePoint); + if (lastOffset > 0 && FlutterTextUtils.isVariationSelector(codePoint)) { + codePoint = FlutterTextUtils.codePointBefore(text, lastOffset); + lastSeenVariantSelectorCharCount = FlutterTextUtils.charCount(codePoint); + lastOffset -= FlutterTextUtils.charCount(codePoint); + } + } + } + + if (lastOffset == 0) { + break; + } + } while (isZwj && FlutterTextUtils.isEmoji(codePoint)); + + if (isZwj && lastOffset == 0) { + deleteCharCount += FlutterTextUtils.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; + isZwj = false; + } + } + + return offset - deleteCharCount; + } + + /** + * Gets the offset after the current position, handling complex Unicode sequences + * such as emojis, regional indicators, keycaps, and variation selectors. + * @param text - The text to examine + * @param offset - The current offset position + * @returns The offset after the current position, accounting for Unicode sequences + */ + static getOffsetAfter(text: string, offset: number): number { + const len = text.length; + if (offset >= len - 1) { + return len; + } + + let codePoint: number = FlutterTextUtils.codePointAt(text, offset); + let nextCharCount: number = FlutterTextUtils.charCount(codePoint); + let nextOffset: number = offset + nextCharCount; + + if (nextOffset == 0) { + return 0; + } + // Line Feed + if (codePoint == LINE_FEED) { + codePoint = FlutterTextUtils.codePointAt(text, nextOffset); + if (codePoint == CARRIAGE_RETURN) { + ++nextCharCount; + } + return offset + nextCharCount; + } + + // Flags + if (FlutterTextUtils.isRegionalIndicatorSymbol(codePoint)) { + if (nextOffset >= len - 1 + || !FlutterTextUtils.isRegionalIndicatorSymbol(FlutterTextUtils.codePointAt(text, nextOffset))) { + return offset + nextCharCount; + } + // In this case there are at least two regional indicator symbols ahead of + // offset. If those two regional indicator symbols are a pair that + // represent a region together, the next offset should be after both of + // them. + let regionalIndicatorSymbolCount: number = 0; + let regionOffset: number = offset; + while (regionOffset > 0 + && FlutterTextUtils.isRegionalIndicatorSymbol(FlutterTextUtils.codePointBefore(text, regionOffset))) { + regionOffset -= FlutterTextUtils.charCount(FlutterTextUtils.codePointBefore(text, regionOffset)); + regionalIndicatorSymbolCount++; + } + if (regionalIndicatorSymbolCount % 2 == 0) { + nextCharCount += 2; + } + return offset + nextCharCount; + } + + // Keycaps + if (FlutterTextUtils.isKeycapBase(codePoint)) { + nextCharCount += FlutterTextUtils.charCount(codePoint); + } + if (codePoint == COMBINING_ENCLOSING_KEYCAP) { + codePoint = FlutterTextUtils.codePointBefore(text, nextOffset); + nextOffset += FlutterTextUtils.charCount(codePoint); + if (nextOffset < len && FlutterTextUtils.isVariationSelector(codePoint)) { + let tmpCodePoint: number = FlutterTextUtils.codePointAt(text, nextOffset); + if (FlutterTextUtils.isKeycapBase(tmpCodePoint)) { + nextCharCount += FlutterTextUtils.charCount(codePoint) + FlutterTextUtils.charCount(tmpCodePoint); + } + } else if (FlutterTextUtils.isKeycapBase(codePoint)) { + nextCharCount += FlutterTextUtils.charCount(codePoint); + } + return offset + nextCharCount; + } + + if (FlutterTextUtils.isEmoji(codePoint)) { + let isZwj: boolean = false; + let lastSeenVariantSelectorCharCount: number = 0; + do { + if (isZwj) { + nextCharCount += FlutterTextUtils.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; + isZwj = false; + } + lastSeenVariantSelectorCharCount = 0; + if (FlutterTextUtils.isEmojiModifier(codePoint)) { + break; + } + + if (nextOffset < len) { + codePoint = FlutterTextUtils.codePointAt(text, nextOffset); + nextOffset += FlutterTextUtils.charCount(codePoint); + if (codePoint == COMBINING_ENCLOSING_KEYCAP) { + codePoint = FlutterTextUtils.codePointBefore(text, nextOffset); + nextOffset += FlutterTextUtils.charCount(codePoint); + if (nextOffset < len && FlutterTextUtils.isVariationSelector(codePoint)) { + let tmpCodePoint: number = FlutterTextUtils.codePointAt(text, nextOffset); + if (FlutterTextUtils.isKeycapBase(tmpCodePoint)) { + nextCharCount += FlutterTextUtils.charCount(codePoint) + FlutterTextUtils.charCount(tmpCodePoint); + } + } else if (FlutterTextUtils.isKeycapBase(codePoint)) { + nextCharCount += FlutterTextUtils.charCount(codePoint); + } + return offset + nextCharCount; + } + if (FlutterTextUtils.isEmojiModifier(codePoint)) { + nextCharCount += lastSeenVariantSelectorCharCount + FlutterTextUtils.charCount(codePoint); + break; + } + if (FlutterTextUtils.isVariationSelector(codePoint)) { + nextCharCount += lastSeenVariantSelectorCharCount + FlutterTextUtils.charCount(codePoint); + break; + } + if (codePoint == ZERO_WIDTH_JOINER) { + isZwj = true; + codePoint = FlutterTextUtils.codePointAt(text, nextOffset); + nextOffset += FlutterTextUtils.charCount(codePoint); + if (nextOffset < len && FlutterTextUtils.isVariationSelector(codePoint)) { + codePoint = FlutterTextUtils.codePointAt(text, nextOffset); + lastSeenVariantSelectorCharCount = FlutterTextUtils.charCount(codePoint); + nextOffset += FlutterTextUtils.charCount(codePoint); + } + } + } + + if (nextOffset >= len) { + break; + } + } while (isZwj && FlutterTextUtils.isEmoji(codePoint)); + + if (isZwj && nextOffset >= len) { + nextCharCount += FlutterTextUtils.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; + isZwj = false; + } + } + + return offset + nextCharCount; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/localization/LocalizationPlugin.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/localization/LocalizationPlugin.ets new file mode 100644 index 0000000..cf50893 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/localization/LocalizationPlugin.ets @@ -0,0 +1,123 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on LocalizationPlugin.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import LocalizationChannel, { + LocalizationMessageHandler +} from '../../embedding/engine/systemchannels/LocalizationChannel' +import common from '@ohos.app.ability.common'; +import intl from '@ohos.intl'; +import Log from '../../util/Log'; +import i18n from '@ohos.i18n'; + +const TAG = "LocalizationPlugin"; + +/** + * Plugin for handling localization in Flutter applications. + * This class manages the interaction between Flutter's localization system + * and the OpenHarmony resource management system. + */ +export default class LocalizationPlugin { + private localizationChannel: LocalizationChannel; + private context: common.Context; + + /** + * Converts a locale string to an intl.Locale object. + * @param localeString - The locale string (e.g., "en_US" or "zh-CN") + * @returns The corresponding intl.Locale object + */ + localeFromString(localeString: string): intl.Locale { + localeString = localeString.replace('_', '-'); + let parts: string[] = localeString.split('-', -1); + let languageCode = parts[0]; + let scriptCode = ""; + let countryCode = ""; + let index: number = 1; + + if (parts.length > index && parts[index].length == 4) { + scriptCode = parts[index]; + index++; + } + + if (parts.length > index && parts[index].length >= 2 && parts[index].length <= 3) { + countryCode = parts[index]; + index++; + } + return new intl.Locale(languageCode + '-' + countryCode + '-' + scriptCode); + } + + private localizationMessageHandler: LocalizationMessageHandler = + new enterGetStringResource((key: string, localeString: string | null) => { + + Log.i(TAG, "getStringResource,key: " + key + ",localeString: " + localeString); + let localContext: common.Context = this.context; + let stringToReturn: string | null = null; + // 获取资源管理器 + let resMgr = localContext.resourceManager; + + try { + // 如果localeString不为空,则更新为指定地区的资源管理器 + if (localeString) { + let overrideConfig = resMgr.getOverrideConfiguration(); + overrideConfig.locale = localeString; + let overrideResMgr = resMgr.getOverrideResourceManager(overrideConfig); + stringToReturn = overrideResMgr.getStringByNameSync(key); + } else { + stringToReturn = resMgr.getStringByNameSync(key); + } + } catch (e) { + Log.e(TAG, e); + return null; + } + + return stringToReturn; + }) + + /** + * Constructs a new LocalizationPlugin instance. + * @param context - The application context + * @param localizationChannel - The LocalizationChannel for communication with Flutter + */ + constructor(context: common.Context, localizationChannel: LocalizationChannel) { + this.context = context; + this.localizationChannel = localizationChannel; + this.localizationChannel.setLocalizationMessageHandler(this.localizationMessageHandler); + } + + /** + * Sends the system locale to Flutter. + */ + sendLocaleToFlutter(): void { + let systemLocale: string = i18n.System.getSystemLocale(); + let data: Array = []; + data.push(systemLocale); + this.localizationChannel.sendLocales(data); + } +} + +/** + * Implementation of LocalizationMessageHandler for getting string resources. + */ +class enterGetStringResource { + /** + * The function to get string resources. + * @param key - The resource key + * @param localeString - The locale string, or null to use the default locale + * @returns The localized string, or null if not found + */ + getStringResource: (key: string, localeString: string | null) => string | null + + /** + * Constructs a new enterGetStringResource instance. + * @param getStringResource - The function to get string resources + */ + constructor(getStringResource: (key: string, localeString: string | null) => string | null) { + this.getStringResource = getStringResource + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/mouse/MouseCursorPlugin.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/mouse/MouseCursorPlugin.ets new file mode 100644 index 0000000..21689d3 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/mouse/MouseCursorPlugin.ets @@ -0,0 +1,135 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on MouseCursorPlugin.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import MouseCursorChannel, { MouseCursorMethodHandler } from '../../embedding/engine/systemchannels/MouseCursorChannel'; +import pointer from '@ohos.multimodalInput.pointer'; +import HashMap from '@ohos.util.HashMap'; +import Log from '../../util/Log'; +import Any from '../common/Any'; + +const TAG: string = "MouseCursorPlugin"; + +/** + * Plugin for handling mouse cursor changes in Flutter applications. + * This class manages the interaction between Flutter's cursor system + * and the OpenHarmony pointer style system. + */ +export default class MouseCursorPlugin implements MouseCursorMethodHandler { + private mouseCursorChannel: MouseCursorChannel; + private systemCursorConstants: HashMap | null = null; + private windowId: number; + + /** + * Constructs a new MouseCursorPlugin instance. + * @param windowId - The window ID for setting pointer styles + * @param mouseCursorChannel - The MouseCursorChannel for communication with Flutter + */ + constructor(windowId: number, mouseCursorChannel: MouseCursorChannel) { + this.windowId = windowId; + this.mouseCursorChannel = mouseCursorChannel; + this.mouseCursorChannel.setMethodHandler(this); + } + + /** + * Activates a system cursor for the specified kind. + * @param kind - The cursor kind (e.g., "click", "text", "move") + */ + activateSystemCursor(kind: string): void { + if (this.windowId < 0) { + Log.e(TAG, "setPointerStyle failed: windowId is invalid: " + this.windowId); + return; + } + let pointStyle: pointer.PointerStyle = this.resolveSystemCursor(kind); + try { + pointer.setPointerStyle(this.windowId, pointStyle, (err: Any) => { + if (err) { + Log.e(TAG, "setPointerStyle callback error: kind=" + kind + ", err=" + JSON.stringify(err)); + } else { + Log.i(TAG, "setPointerStyle success: kind=" + kind); + } + }) + } catch (e) { + Log.e(TAG, "setPointerStyle exception: kind=" + kind + ", error=" + JSON.stringify(e)); + } + } + + private resolveSystemCursor(kind: string): pointer.PointerStyle { + if (this.systemCursorConstants == null) { + this.systemCursorConstants = new HashMap(); + this.systemCursorConstants.set("alias", pointer.PointerStyle.DEFAULT); + this.systemCursorConstants.set("allScroll", pointer.PointerStyle.MOVE); + this.systemCursorConstants.set("basic", pointer.PointerStyle.DEFAULT); + this.systemCursorConstants.set("cell", pointer.PointerStyle.DEFAULT); + this.systemCursorConstants.set("click", pointer.PointerStyle.HAND_POINTING); + this.systemCursorConstants.set("contextMenu", pointer.PointerStyle.DEFAULT); + this.systemCursorConstants.set("copy", pointer.PointerStyle.CURSOR_COPY); + this.systemCursorConstants.set("forbidden", pointer.PointerStyle.CURSOR_FORBID); + this.systemCursorConstants.set("grab", pointer.PointerStyle.HAND_OPEN); + this.systemCursorConstants.set("grabbing", pointer.PointerStyle.HAND_GRABBING); + this.systemCursorConstants.set("help", pointer.PointerStyle.HELP); + this.systemCursorConstants.set("move", pointer.PointerStyle.MOVE); + this.systemCursorConstants.set("none", pointer.PointerStyle.DEFAULT); + this.systemCursorConstants.set("noDrop", pointer.PointerStyle.DEFAULT); + this.systemCursorConstants.set("precise", pointer.PointerStyle.CROSS); + this.systemCursorConstants.set("text", pointer.PointerStyle.TEXT_CURSOR); + this.systemCursorConstants.set("resizeColum", pointer.PointerStyle.NORTH_SOUTH); + this.systemCursorConstants.set("resizeDown", pointer.PointerStyle.SOUTH); + this.systemCursorConstants.set("resizeDownLeft", pointer.PointerStyle.SOUTH_WEST); + this.systemCursorConstants.set("resizeDownRight", pointer.PointerStyle.SOUTH_EAST); + this.systemCursorConstants.set("resizeLeft", pointer.PointerStyle.WEST); + this.systemCursorConstants.set("resizeLeftRight", pointer.PointerStyle.RESIZE_LEFT_RIGHT); + this.systemCursorConstants.set("resizeRight", pointer.PointerStyle.EAST); + this.systemCursorConstants.set("resizeRow", pointer.PointerStyle.WEST_EAST); + this.systemCursorConstants.set("resizeUp", pointer.PointerStyle.NORTH); + this.systemCursorConstants.set("resizeUpDown", pointer.PointerStyle.RESIZE_UP_DOWN); + this.systemCursorConstants.set("resizeUpLeft", pointer.PointerStyle.NORTH_WEST); + this.systemCursorConstants.set("resizeUpRight", pointer.PointerStyle.NORTH_EAST); + this.systemCursorConstants.set("resizeUpLeftDownRight", pointer.PointerStyle.NORTH_WEST_SOUTH_EAST); + this.systemCursorConstants.set("resizeUpRightDownLeft", pointer.PointerStyle.NORTH_EAST_SOUTH_WEST); + this.systemCursorConstants.set("verticalText", pointer.PointerStyle.TEXT_CURSOR); + this.systemCursorConstants.set("wait", pointer.PointerStyle.DEFAULT); + this.systemCursorConstants.set("zoomIn", pointer.PointerStyle.ZOOM_IN); + this.systemCursorConstants.set("zoomOut", pointer.PointerStyle.ZOOM_OUT); + this.systemCursorConstants.set("middleBtnEast", pointer.PointerStyle.MIDDLE_BTN_EAST); + this.systemCursorConstants.set("middleBtnWest", pointer.PointerStyle.MIDDLE_BTN_WEST); + this.systemCursorConstants.set("middleBtnSouth", pointer.PointerStyle.MIDDLE_BTN_SOUTH); + this.systemCursorConstants.set("middleBtnNorth", pointer.PointerStyle.MIDDLE_BTN_NORTH); + this.systemCursorConstants.set("middleBtnNorthSouth", pointer.PointerStyle.MIDDLE_BTN_NORTH_SOUTH); + this.systemCursorConstants.set("middleBtnNorthEast", pointer.PointerStyle.MIDDLE_BTN_NORTH_EAST); + this.systemCursorConstants.set("middleBtnNorthWest", pointer.PointerStyle.MIDDLE_BTN_NORTH_WEST); + this.systemCursorConstants.set("middleBtnSouthEast", pointer.PointerStyle.MIDDLE_BTN_SOUTH_EAST); + this.systemCursorConstants.set("middleBtnSouthWest", pointer.PointerStyle.MIDDLE_BTN_SOUTH_WEST); + this.systemCursorConstants.set("middleBtnNorthSouthWestEast", + pointer.PointerStyle.MIDDLE_BTN_NORTH_SOUTH_WEST_EAST); + this.systemCursorConstants.set("horizontalTextCursor", pointer.PointerStyle.HORIZONTAL_TEXT_CURSOR); + this.systemCursorConstants.set("cursorCross", pointer.PointerStyle.CURSOR_CROSS); + this.systemCursorConstants.set("cursorCircle", pointer.PointerStyle.CURSOR_CIRCLE); + this.systemCursorConstants.set("loading", pointer.PointerStyle.LOADING); + this.systemCursorConstants.set("running", pointer.PointerStyle.RUNNING); + this.systemCursorConstants.set("colorSucker", pointer.PointerStyle.COLOR_SUCKER); + this.systemCursorConstants.set("screenshotChoose", pointer.PointerStyle.SCREENSHOT_CHOOSE); + this.systemCursorConstants.set("screenshotCursor", pointer.PointerStyle.SCREENSHOT_CURSOR); + } + let pointStyle: pointer.PointerStyle = this.systemCursorConstants.get(kind); + if (pointStyle === null) { + return pointer.PointerStyle.DEFAULT; + } + return pointStyle; + } + + /** + * Destroys the mouse cursor plugin and cleans up resources. + * The MouseCursorPlugin instance should not be used after calling this. + */ + destroy(): void { + this.mouseCursorChannel.setMethodHandler(null); + } +} + diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/CustomTouchEvent.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/CustomTouchEvent.ets new file mode 100644 index 0000000..6475db4 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/CustomTouchEvent.ets @@ -0,0 +1,184 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +*/ + +/** + * Custom implementation of TouchEvent for platform views. + * This class wraps touch event data for communication between Flutter and native views. + */ +export class CustomTouchEvent implements TouchEvent { + /** The type of touch event. */ + type: TouchType = 0; + /** Array of all touch points in this event. */ + touches: CustomTouchObject[]; + /** Array of touch points that changed in this event. */ + changedTouches: CustomTouchObject[]; + /** Function to stop event propagation. */ + stopPropagation: () => void = () => { + }; + /** The timestamp when the event occurred. */ + timestamp: number; + /** The source type of the touch event. */ + source: SourceType; + /** The pressure value of the touch. */ + pressure: number; + /** The X-axis tilt value of the touch. */ + tiltX: number; + /** The Y-axis tilt value of the touch. */ + tiltY: number; + /** The source tool type for the touch. */ + sourceTool: SourceTool; + + /** + * Constructs a new CustomTouchEvent instance. + * @param type - The touch event type + * @param touches - Array of all touch points + * @param changedTouches - Array of touch points that changed + * @param timestamp - The event timestamp + * @param source - The source type of the touch + * @param pressure - The pressure value + * @param tiltX - The X-axis tilt value + * @param tiltY - The Y-axis tilt value + * @param sourceTool - The source tool type + */ + constructor(type: TouchType, touches: CustomTouchObject[], changedTouches: CustomTouchObject[], timestamp: number, + source: SourceType, pressure: number, tiltX: number, tiltY: number, sourceTool: SourceTool) { + this.type = type; + this.touches = touches; + this.changedTouches = changedTouches; + this.timestamp = timestamp; + this.source = source; + this.pressure = pressure; + this.tiltX = tiltX; + this.tiltY = tiltY; + this.sourceTool = sourceTool; + } + + /** Function to prevent the default action. */ + preventDefault: () => void = () => { + }; + + /** + * Gets the modifier key state. + * @param keys - Array of key names to check + * @returns True if any of the keys are pressed + * @throws Error as this method is not implemented + */ + getModifierKeyState(keys: string[]): boolean { + throw new Error('Method not implemented.'); + } + + /** The event target for this touch event. */ + target: EventTarget = new CustomEventTarget(new CustomArea(0, 0, { x: 0, y: 0 }, { x: 0, y: 0 })); + + /** + * Gets historical touch points. + * @returns Array of historical points + * @throws Error as this method is not implemented + */ + getHistoricalPoints(): HistoricalPoint[] { + throw new Error('Method not implemented.'); + } +} + +/** + * Custom implementation of EventTarget for touch events. + * This class provides a target for touch events in platform views. + */ +class CustomEventTarget implements EventTarget { + /** The area associated with this event target */ + area: Area = new CustomArea(0, 0, { x: 0, y: 0 }, { x: 0, y: 0 }); + + /** + * Constructs a new CustomEventTarget instance. + * @param area - The area associated with this event target + */ + constructor(area: Area) { + this.area = area; + } +} + +/** + * Custom implementation of Area for touch events. + * This class represents a rectangular area with position and size information. + */ +class CustomArea implements Area { + /** The width of the area. */ + width: Length = 0; + /** The height of the area. */ + height: Length = 0; + /** The local position of the area. */ + position: Position = { x: 0, y: 0 }; + /** The global position of the area. */ + globalPosition: Position = { x: 0, y: 0 }; + + /** + * Constructs a new CustomArea instance. + * @param width - The width of the area + * @param height - The height of the area + * @param position - The local position + * @param globalPosition - The global position + */ + constructor(width: Length, height: Length, position: Position, globalPosition: Position) { + this.width = width; + this.height = height; + this.position = position; + this.globalPosition = globalPosition; + } +} + +/** + * Custom implementation of TouchObject for platform views. + */ +export class CustomTouchObject implements TouchObject { + /** The type of touch. */ + type: TouchType; + /** The unique identifier for this touch point. */ + id: number; + /** The X coordinate in display space. */ + displayX: number; + /** The Y coordinate in display space. */ + displayY: number; + /** The X coordinate in window space. */ + windowX: number; + /** The Y coordinate in window space. */ + windowY: number; + /** The X coordinate in screen space. */ + screenX: number; + /** The Y coordinate in screen space. */ + screenY: number; + /** The X coordinate in local space. */ + x: number; + /** The Y coordinate in local space. */ + y: number; + + /** + * Constructs a new CustomTouchObject instance. + * @param type - The touch type + * @param id - The touch point ID + * @param displayX - The X coordinate in display space + * @param displayY - The Y coordinate in display space + * @param windowX - The X coordinate in window space + * @param windowY - The Y coordinate in window space + * @param screenX - The X coordinate in screen space + * @param screenY - The Y coordinate in screen space + * @param x - The X coordinate in local space + * @param y - The Y coordinate in local space + */ + constructor(type: TouchType, id: number, displayX: number, displayY: number, windowX: number, windowY: number, + screenX: number, screenY: number, x: number, y: number) { + this.type = type; + this.id = id; + this.displayX = displayX; + this.displayY = displayY; + this.windowX = windowX; + this.windowY = windowY; + this.screenX = screenX; + this.screenY = screenY; + this.x = x; + this.y = y; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformView.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformView.ets new file mode 100644 index 0000000..0a80743 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformView.ets @@ -0,0 +1,127 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformView.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import { DVModel, DynamicView } from '../../view/DynamicView/dynamicView' + +/** + * Parameters for platform view rendering. + * This class contains the configuration needed to render a platform view. + */ +export declare class Params { + /** The text direction for the platform view */ + direction: Direction + /** The platform view instance to render */ + platformView: PlatformView +} + +export declare class PlatformViewVisibleAreaEventOptions { + enable: boolean // Enable variable area monitoring + ratios: Array // Ratio thresholds for visible area changes, e.g., [0.0, 1.0] means 0% to 100% visible + expectedUpdateInterval: number // Expected interval for visible area updates in milliseconds + onInactiveThreshold: number // Texture continuous production pause operation visible area threshold + onActiveThreshold: number // Texture continuous production activation operation visible area threshold +} + +// unit: ms +const defaultExpectedUpdateInterval: number = 1000; + +/** A handle to an DynamicView to be embedded in the Flutter hierarchy. */ +export default abstract class PlatformView { + + /** + * Gets the type of this platform view. + * @returns The view type string + */ + getType(): string { + return 'default'; + } + + /** Returns the DynamicView to be embedded in the Flutter hierarchy. */ + abstract getView(): WrappedBuilder<[Params]>; + + /** + * Called by the FlutterEngine that owns this PlatformView when the DynamicView responsible + * for rendering a Flutter UI is associated with the FlutterEngine. + * + * This means that our associated FlutterEngine can now render a UI and interact with the user. + * + * Some platform views may have unusual dependencies on the DynamicView that renders Flutter + * UIs, such as unique keyboard interactions. That DynamicView is provided here for those + * purposes. Use of this DynamicView should be avoided if it is not absolutely necessary, because + * depending on this DynamicView will tend to make platform view code more brittle to future + * changes. + * @param dvModel - The DynamicView model associated with the Flutter view + */ + onFlutterViewAttached(dvModel: DVModel): void { + } + + /** + * Called by the FlutterEngine that owns this PlatformView when the DynamicView responsible + * for rendering a Flutter UI is detached and disassociated from the FlutterEngine. + * + * This means that our associated FlutterEngine no longer has a rendering surface, or a user + * interaction surface of any kind. + * + * This platform view must release any references related to the DynamicView that was + * provided in onFlutterViewAttached. + */ + onFlutterViewDetached(): void { + } + + /** + * Disposes this platform view. + * + * The PlatformView object is unusable after this method is called. + * + * Plugins implementing PlatformView must clear all references to the DynamicView object and + * the PlatformView after this method is called. Failing to do so will result in a memory leak. + * + * References related to the DynamicView attached in onFlutterViewAttached + * must be released in dispose() to avoid memory leaks. + */ + abstract dispose(): void; + + /** + * Callback fired when the platform's input connection is locked, or should be used. + * + * This hook only exists for rare cases where the plugin relies on the state of the input + * connection. This probably doesn't need to be implemented. + */ + onInputConnectionLocked(): void { + } + + /** + * Callback fired when the platform input connection has been unlocked. + * + * This hook only exists for rare cases where the plugin relies on the state of the input + * connection. This probably doesn't need to be implemented. + */ + onInputConnectionUnlocked(): void { + } + + // Obtain the parameters related to the changes in the visible area of the external texture + getPlatformViewVisibleAreaEventOptions(): PlatformViewVisibleAreaEventOptions { + return { + enable: false, + ratios: [0.0, 1.0], + expectedUpdateInterval: defaultExpectedUpdateInterval, + onInactiveThreshold : 0.0, + onActiveThreshold : 1.0 + } as PlatformViewVisibleAreaEventOptions; + } + + // The operation to pause the continuous production of external textures, including animations and videos + onInactive(): void { + } + + // The operation to resume the continuous production of external textures, including animations and videos + onActive(): void { + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewFactory.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewFactory.ets new file mode 100644 index 0000000..74562f9 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewFactory.ets @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformViewFactory.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import MessageCodec from '../common/MessageCodec'; +import PlatformView from './PlatformView' +import common from '@ohos.app.ability.common'; +import Any from '../common/Any'; + +/** + * Factory for creating platform views. + * Subclasses must implement the create method to instantiate platform views. + */ +export default abstract class PlatformViewFactory { + private createArgsCodec: MessageCodec; + + /** + * Constructs a new PlatformViewFactory instance. + * @param createArgsCodec - The codec used to decode the args parameter of create + */ + constructor(createArgsCodec: MessageCodec) { + this.createArgsCodec = createArgsCodec; + } + + /** + * Creates a new platform view to be embedded in the Flutter hierarchy. + * + * @param context - The context to be used when creating the view, this is different than + * FlutterView's context + * @param viewId - Unique identifier for the created instance, this value is known on the Dart side + * @param args - Arguments sent from the Flutter app. The bytes for this value are decoded using the + * createArgsCodec argument passed to the constructor. This is null if createArgsCodec was + * null, or no arguments were sent from the Flutter app + * @returns A new PlatformView instance + */ + public abstract create(context: common.Context, viewId: number, args: Any): PlatformView; + + /** + * Returns the codec to be used for decoding the args parameter of create. + * @returns The MessageCodec instance + */ + getCreateArgsCodec(): MessageCodec { + return this.createArgsCodec; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewRegistry.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewRegistry.ets new file mode 100644 index 0000000..53860e8 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewRegistry.ets @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformViewRegistry.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import PlatformViewFactory from './PlatformViewFactory' + +/** + * Registry for platform view factories. + * + * Plugins can register factories for specific view types. + */ +export default interface PlatformViewRegistry { + /** + * Registers a factory for a platform view. + * + * @param viewTypeId - Unique identifier for the platform view's type + * @param factory - Factory for creating platform views of the specified type + * @returns True if succeeded, false if a factory is already registered for viewTypeId + */ + registerViewFactory(viewTypeId: string, factory: PlatformViewFactory): boolean; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewRegistryImpl.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewRegistryImpl.ets new file mode 100644 index 0000000..39f34f4 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewRegistryImpl.ets @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformViewRegistryImpl.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import HashMap from '@ohos.util.HashMap'; +import PlatformViewFactory from './PlatformViewFactory' +import PlatformViewRegistry from './PlatformViewRegistry' + +/** + * Implementation of PlatformViewRegistry for managing platform view factories. + */ +export default class PlatformViewRegistryImpl implements PlatformViewRegistry { + /** Maps a platform view type id to its factory. */ + private viewFactories: HashMap; + + /** + * Constructs a new PlatformViewRegistryImpl instance. + */ + constructor() { + this.viewFactories = new HashMap(); + } + + /** + * Registers a factory for a platform view. + * @param viewTypeId - Unique identifier for the platform view's type + * @param factory - Factory for creating platform views of the specified type + * @returns True if succeeded, false if a factory is already registered for viewTypeId + */ + registerViewFactory(viewTypeId: string, factory: PlatformViewFactory): boolean { + if (this.viewFactories.hasKey(viewTypeId)) { + return false; + } + + this.viewFactories.set(viewTypeId, factory); + return true; + } + + /** + * Gets the factory for a specific view type. + * @param viewTypeId - The view type ID + * @returns The PlatformViewFactory for the view type, or undefined if not found + */ + getFactory(viewTypeId: string): PlatformViewFactory { + return this.viewFactories.get(viewTypeId); + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewWrapper.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewWrapper.ets new file mode 100644 index 0000000..ac3858a --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewWrapper.ets @@ -0,0 +1,131 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import OhosTouchProcessor from '../../embedding/ohos/OhosTouchProcessor'; +import { DVModel, DVModelParameters } from '../../view/DynamicView/dynamicView'; +import { createDVModelFromJson } from '../../view/DynamicView/dynamicViewJson'; +import { RootDvModeManager } from './RootDvModelManager'; +import matrix4 from '@ohos.matrix4' +import Log from '../../util/Log'; +import Any from '../common/Any'; + +const TAG: string = "PlatformViewWrapper"; + +/** + * Wraps a platform view to intercept gestures and project this view onto a rendering target. + * + * An OpenHarmony platform view is composed by the engine using a TextureLayer. The view is embedded + * in the OpenHarmony view hierarchy like a normal DynamicView, but it's projected onto a rendering + * target, so it can be efficiently composed by the engine. + * + * Since the view is in the OpenHarmony view hierarchy, keyboard and accessibility interactions + * behave normally. + */ +export class PlatformViewWrapper { + private prevLeft: number = 0; + private prevTop: number = 0; + private left: number = 0; + private top: number = 0; + private bufferWidth: number = 0; + private bufferHeight: number = 0; + private touchProcessor: OhosTouchProcessor | null = null; + private model: DVModel | undefined; + + /** + * Sets the touch processor for handling touch events. + * @param newTouchProcessor - The OhosTouchProcessor instance + */ + public setTouchProcessor(newTouchProcessor: OhosTouchProcessor): void { + this.touchProcessor = newTouchProcessor; + } + + /** + * Constructs a new PlatformViewWrapper instance. + */ + constructor() { + } + + /** + * Gets the DynamicView model associated with this wrapper. + * @returns The DVModel instance + */ + public getDvModel(): DVModel { + return this.model!; + } + + /** + * Sets a parameter value in the DVModelParameters. + * @param params - The parameters object to modify + * @param key - The parameter key + * @param element - The value to set + */ + setParams: (params: DVModelParameters, key: string, element: Any) => void = + (params: DVModelParameters, key: string, element: Any): void => { + let params2 = params as Record; + params2[key] = element; + } + + /** + * Gets a parameter value from the DVModelParameters. + * @param params - The parameters object to read from + * @param element - The parameter key + * @returns The parameter value, or undefined if not found + */ + getParams: (params: DVModelParameters, element: string) => string | Any = + (params: DVModelParameters, element: string): string | Any => { + let params2 = params as Record; + return params2[element]; + } + + /** + * Sets the layout parameters for the platform view. + * @param parameters - The layout parameters to apply + */ + public setLayoutParams(parameters: DVModelParameters): void { + if (!this.model) { + return; + } + if (this.model.params == null) { + this.model.params = new DVModelParameters(); + } + this.setParams(this.model.params, "marginLeft", this.getParams(parameters, "marginLeft")); + this.setParams(this.model.params, "marginTop", this.getParams(parameters, "marginTop")); + this.left = this.getParams(parameters, "marginLeft"); + this.top = this.getParams(parameters, "marginTop"); + + this.setParams(this.model.params, "width", this.getParams(parameters, "width")); + this.setParams(this.model.params, "height", this.getParams(parameters, "height")); + } + + /** + * Adds a DynamicView model to this wrapper. + * @param model - The DVModel to add + */ + public addDvModel(model: DVModel): void { + this.model = model + } +} + +/** + * Parameters for DynamicView model creation. + * This class holds the basic structure for creating a DynamicView model. + */ +class DVModelParam { + /** The component type */ + compType: string + /** Array of child components */ + children: [] + + /** + * Constructs a new DVModelParam instance. + * @param compType - The component type + * @param children - Array of child components + */ + constructor(compType: string, children: []) { + this.compType = compType; + this.children = children; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewsController.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewsController.ets new file mode 100644 index 0000000..8c2e578 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformViewsController.ets @@ -0,0 +1,767 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on PlatformViewsController.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import PlatformViewsChannel, { + PlatformViewBufferResized, + PlatformViewCreationRequest, + PlatformViewResizeRequest, + PlatformViewsHandler, + PlatformViewTouch, + PlatformViewBufferSize +} from '../../../ets/embedding/engine/systemchannels/PlatformViewsChannel'; +import PlatformView, { Params } from './PlatformView'; +import { DVModelParameters, } from '../../view/DynamicView/dynamicView'; +import { createDVModelFromJson } from '../../view/DynamicView/dynamicViewJson'; +import display from '@ohos.display'; +import { FlutterView } from '../../view/FlutterView'; +import { TextureRegistry } from '../../view/TextureRegistry'; +import TextInputPlugin from '../editing/TextInputPlugin'; +import { PlatformViewWrapper } from './PlatformViewWrapper'; +import { FlutterOverlaySurface } from '../../embedding/engine/FlutterOverlaySurface'; +import HashSet from '@ohos.util.HashSet'; +import PlatformViewRegistry from './PlatformViewRegistry'; +import PlatformViewRegistryImpl from './PlatformViewRegistryImpl'; +import DartExecutor from '../../embedding/engine/dart/DartExecutor'; +import { FlutterMutatorView } from '../../embedding/engine/mutatorsstack/FlutterMutatorView'; +import Log from '../../util/Log' +import PlatformViewFactory from './PlatformViewFactory' +import { ByteBuffer } from '../../util/ByteBuffer'; +import Any from '../common/Any'; +import { ArrayList, Stack } from '@kit.ArkTS'; +import { CustomTouchEvent, CustomTouchObject } from './CustomTouchEvent'; +import { NodeRenderType } from '@kit.ArkUI'; +import { PlatformViewInfo } from '../../embedding/ohos/PlatformViewInfo'; +import { EmbeddingNodeController } from '../../embedding/ohos/EmbeddingNodeController'; + +/** + * JSON representation of a DynamicView model. + */ +class DVModelJson { + /** The component type. */ + compType: string + /** Array of child components. */ + children: Array + /** Component attributes. */ + attributes: Any + /** Component events. */ + events: Any + /** Optional build function. */ + build: Any + + /** + * Constructs a new DVModelJson instance. + * @param compType - The component type + * @param children - Array of child components + * @param attributes - Component attributes + * @param events - Component events + * @param build - Optional build function + */ + constructor(compType: string, children: Array, attributes: Any, events: Any, build?: Any) { + this.compType = compType + this.children = children + this.attributes = attributes + this.events = events; + this.build = build; + } +} +/** + * Enumeration of touch event types. + */ +enum TouchEventType { + /** Action code for when a primary pointer touched the screen. */ + ACTION_DOWN = 0, + /** Action code for when a primary pointer stopped touching the screen. */ + ACTION_UP = 1, + /** Action code for when the event only includes information about pointer movement. */ + ACTION_MOVE = 2, + /** Action code for when a motion event has been canceled. */ + ACTION_CANCEL = 3, + /** Action code for when a secondary pointer touched the screen. */ + ACTION_POINTER_DOWN = 5, + /** Action code for when a secondary pointer stopped touching the screen. */ + ACTION_POINTER_UP = 6, +} + +const TAG = "PlatformViewsController" + +/** + * Controller for managing platform views in Flutter applications. + * This class handles the creation, lifecycle, and interaction of native views embedded in Flutter. + */ +export default class PlatformViewsController implements PlatformViewsHandler { + private registry: PlatformViewRegistryImpl; + private context: Context | null = null; + private flutterView: FlutterView | null = null; + private textureRegistry: TextureRegistry | null = null; + private textInputPlugin: TextInputPlugin | null = null; + private platformViewsChannel: PlatformViewsChannel | null = null; + private nextOverlayLayerId: number = 0; + private focusViewId: number = -1; + private platformViews: Map; + private viewIdWithTextureId: Map; + private viewIdWithNodeController: Map; + private viewWrappers: Map; + private currentFrameUsedOverlayLayerIds: HashSet; + private currentFrameUsedPlatformViewIds: HashSet; + + /** + * Constructs a new PlatformViewsController instance. + */ + constructor() { + this.registry = new PlatformViewRegistryImpl(); + this.currentFrameUsedOverlayLayerIds = new HashSet(); + this.currentFrameUsedPlatformViewIds = new HashSet(); + this.viewWrappers = new Map(); + this.platformViews = new Map(); + this.viewIdWithTextureId = new Map(); + this.viewIdWithNodeController = new Map(); + } + + /** + * Creates a platform view for hybrid composition mode. + * @param request - The platform view creation request + */ + createForPlatformViewLayer(request: PlatformViewCreationRequest): void { + Log.i(TAG, "Enter createForPlatformViewLayer"); + this.ensureValidRequest(request); + + let platformView: PlatformView = this.createPlatformView(request); + + this.configureForHybridComposition(platformView, request); + } + + /** + * Disposes a platform view and releases all associated resources. + * @param viewId - The ID of the platform view to dispose + */ + dispose(viewId: number): void { + let platformView: PlatformView | null = this.platformViews.get(viewId) || null; + if (platformView == null) { + Log.e(TAG, "Disposing unknown platform view with id: " + viewId); + return; + } + if (this.focusViewId == viewId) { + this.clearFocus(viewId); + this.focusViewId = -1; + } + this.platformViews.delete(viewId); + let textureId = this.viewIdWithTextureId.get(viewId); + + if (textureId != undefined) { + this.textureRegistry!.unregisterTexture(textureId); + } + + this.viewIdWithNodeController.get(viewId)?.disposeFrameNode() + this.viewIdWithNodeController.delete(viewId); + + let viewWrapper: PlatformViewWrapper | null = this.viewWrappers.get(viewId) || null; + if (viewWrapper != null && this.flutterView) { + let index = this.flutterView.getDVModel().children.indexOf(viewWrapper.getDvModel()!); + if (index > -1) { + this.flutterView.getDVModel().children.splice(index, 1); + platformView.onFlutterViewDetached(); + } + } + this.viewWrappers.delete(viewId); + + try { + platformView.dispose(); + } catch (err) { + Log.e(TAG, "Disposing platform view threw an exception", err); + } + } + + /** + * Sets a parameter value in the DVModelParameters. + * @param params - The parameters object to modify + * @param key - The parameter key + * @param element - The value to set + */ + setParams: (params: DVModelParameters, key: string, element: Any) => void = + (params: DVModelParameters, key: string, element: Any): void => { + let params2 = params as Record; + params2[key] = element; + } + + /** + * Gets a parameter value from the DVModelParameters. + * @param params - The parameters object to read from + * @param key - The parameter key + * @returns The parameter value as a number + */ + getParams: (params: DVModelParameters, key: string) => number = (params: DVModelParameters, key: string): number => { + let params2 = params as Record; + return params2[key]; + } + + /** + * Resizes a platform view. + * @param request - The resize request containing new dimensions + * @param onComplete - Callback to invoke when resize is complete + */ + resize(request: PlatformViewResizeRequest, onComplete: PlatformViewBufferResized): void { + let physicalWidth: number = this.toPhysicalPixels(request.newLogicalWidth); + let physicalHeight: number = this.toPhysicalPixels(request.newLogicalHeight); + let viewId: number = request.viewId; + Log.i(TAG, + `Resize viewId ${viewId}, pw:${physicalWidth}, ph:${physicalHeight},lw:${request.newLogicalWidth}, lh:${request.newLogicalHeight}`); + + let viewWrapper = this.viewWrappers.get(request.viewId) + let params: DVModelParameters | undefined = viewWrapper?.getDvModel()!.params + + this.setParams(params!, "width", physicalWidth); + this.setParams(params!, "height", physicalHeight); + + let textureId = this.viewIdWithTextureId.get(viewId); + if (textureId != undefined) { + let density = this.getDisplayDensity(); + this.textureRegistry?.notifyTextureResizing(textureId, request.newLogicalWidth * density, request.newLogicalHeight * density); + } + + onComplete.run(new PlatformViewBufferSize(physicalWidth, physicalHeight)); + } + + /** + * Updates the offset position of a platform view. + * @param viewId - The ID of the platform view + * @param top - The top offset + * @param left - The left offset + */ + offset(viewId: number, top: number, left: number): void { + Log.i(TAG, `Offset is id${viewId}, t:${top}, l:${left}`); + + let viewWrapper = this.viewWrappers.get(viewId) + if (viewWrapper === undefined) { + return; + } + + let params: DVModelParameters | undefined = viewWrapper?.getDvModel()!.params; + if (!params) { + return; + } + // When the current value is NaN and the previous value is a normal value, + // the platformView is considered to have transitioned from visible to invisible. + // Use Number.isNaN; the page is considered invisible in the background + // only when both top and left values equal NaN. + if (Number.isNaN(top) && Number.isNaN(left)) { + let leftPre: number | undefined = this.getParams(params!, "left"); + let topPre: number | undefined = this.getParams(params!, "top"); + let compType: string | undefined = viewWrapper?.getDvModel().compType; + if ((leftPre !== undefined && !Number.isNaN(leftPre)) && + (topPre !== undefined && !Number.isNaN(topPre)) && + (compType === "NodeContainer")) { + const nodeController = (params as Record)?.nodeController; + if (nodeController instanceof EmbeddingNodeController) { + nodeController.notifyPlatformViewInvisible(); + } + } + } + this.setParams(params!, "left", left); + this.setParams(params!, "top", top); + } + + /** + * Sets the hover state for platform views. + * @param viewId - The ID of the platform view to set hover state for + */ + hover(viewId: number) { + for (let key of this.viewWrappers.keys()) { + let viewWrapper: undefined | PlatformViewWrapper = this.viewWrappers.get(key); + let dvModel = viewWrapper?.getDvModel(); + let params = dvModel?.getLayoutParams() as Record; + if (key == viewId) { + params["hover"] = true; + } else { + params["hover"] = false; + } + } + } + + /** + * Handles touch events for platform views. + * @param touch - The touch event information + */ + onTouch(touch: PlatformViewTouch): void { + let viewWrapper: undefined | PlatformViewWrapper = this.viewWrappers.get(touch.viewId) + this.focusViewId = touch.viewId; + if (viewWrapper != undefined) { + let dvModel = viewWrapper.getDvModel() + let params = dvModel.getLayoutParams() as Record; + // When receiving a DOWN action + if (touch.action === TouchEventType.ACTION_DOWN) { + // Set the current touch state to true + params['down'] = true + // When first receiving a touch DOWN event, dispatch all events stored in the list + let touchEventArray: Array | undefined = params['touchEvent'] as Array + if (touchEventArray !== undefined) { + let nodeController = params['nodeController'] as EmbeddingNodeController; + for (let it of touchEventArray) { + nodeController.postEvent(it) + } + // Clear the list after first dispatch + params['touchEvent'] = undefined + } + + // When first receiving a mouse PRESS event, dispatch all events stored in the list + let mouseEventArray: Array | undefined = params['mouseEvent'] as Array + if (mouseEventArray !== undefined) { + let nodeController = params['nodeController'] as EmbeddingNodeController; + for (let it of mouseEventArray) { + nodeController.postMouseEvent(it) + } + // Clear the list after first dispatch + params['mouseEvent'] = undefined + } + // When receiving an UP action + } else if (touch.action === TouchEventType.ACTION_UP || touch.action === TouchEventType.ACTION_CANCEL) { + // Set the touch state to false after finger is lifted. When multiple fingers are lifted suddenly, + // the final state returned is also ACTION_UP, so we use the UP state to indicate the user + // is no longer touching the platform view + params['down'] = false + } + } + } + + /** + * Sets the text direction for a platform view. + * @param viewId - The ID of the platform view + * @param direction - The text direction to set + */ + setDirection(viewId: number, direction: Direction): void { + let nodeController = this.viewIdWithNodeController.get(viewId) + if (nodeController != undefined) { + nodeController?.setRenderOption(this.flutterView!.getPlatformView()!, this.flutterView!.getSurfaceId(), + NodeRenderType.RENDER_TYPE_TEXTURE, direction) + nodeController?.rebuild() + } + } + + /** + * Validates if a direction value is valid. + * @param direction - The direction value to validate + * @returns True if the direction is valid, false otherwise + */ + validateDirection(direction: number): boolean { + return direction == Direction.Ltr || direction == Direction.Rtl || direction == Direction.Auto; + } + + /** + * Clears focus from a platform view. + * @param viewId - The ID of the platform view + */ + clearFocus(viewId: number): void { + const platformView = this.platformViews.get(viewId); + if (platformView == null) { + Log.e(TAG, "Setting direction to an unknown view with id: " + viewId); + return; + } + const embeddedView = platformView.getView(); + if (embeddedView == null) { + Log.e(TAG, "Setting direction to a null view with id: " + viewId); + return; + } + // Make the Xcomponent gain focus. + focusControl.requestFocus("unfocus-xcomponent-node"); + } + + /** + * Synchronizes the native view hierarchy. + * @param yes - Whether to synchronize + * @throws Error as this method is not implemented + */ + synchronizeToNativeViewHierarchy(yes: boolean): void { + throw new Error('Method not implemented.'); + } + + /** + * Creates a platform view for texture layer composition mode. + * @param request - The platform view creation request + * @returns The texture ID for the created view + */ + public createForTextureLayer(request: PlatformViewCreationRequest): number { + Log.i(TAG, "Enter createForTextureLayer"); + this.ensureValidRequest(request); + + let platformView: PlatformView = this.createPlatformView(request); + let textureId = this.configureForTextureLayerComposition(platformView, request); + this.viewIdWithTextureId.set(request.viewId, textureId); + return textureId; + } + + /** + * Ensures that a platform view creation request is valid. + * @param request - The request to validate + * @throws Error if the request is invalid + * @private + */ + private ensureValidRequest(request: PlatformViewCreationRequest): void { + if (!this.validateDirection(request.direction)) { + throw new Error("Trying to create a view with unknown direction value: " + + request.direction + + "(view id: " + + request.viewId + + ")") + } + } + + /** + * Creates a platform view instance from a factory. + * @param request - The platform view creation request + * @returns The created PlatformView instance + * @throws Error if the factory is not found or creation fails + * @private + */ + private createPlatformView(request: PlatformViewCreationRequest): PlatformView { + Log.i(TAG, "begin createPlatformView"); + const viewFactory: PlatformViewFactory = this.registry.getFactory(request.viewType); + if (viewFactory == null) { + throw new Error("Trying to create a platform view of unregistered type: " + request.viewType) + } + + let createParams: Any = null; + if (request.params != null) { + let byteParas: ByteBuffer = request.params as ByteBuffer; + createParams = viewFactory.getCreateArgsCodec().decodeMessage(byteParas.buffer); + } + + if (this.context == null) { + throw new Error('PlatformView#context is null.'); + } + let platformView = viewFactory.create(this.context, request.viewId, createParams); + + let embeddedView: WrappedBuilder<[Params]> = platformView.getView(); + if (embeddedView == null) { + throw new Error("PlatformView#getView() returned null, but an WrappedBuilder reference was expected."); + } + + this.platformViews.set(request.viewId, platformView); + return platformView; + } + + /** + * Configures the view for Hybrid Composition mode. + * @param platformView - The platform view to configure + * @param request - The creation request + * @private + */ + private configureForHybridComposition(platformView: PlatformView, request: PlatformViewCreationRequest): void { + Log.i(TAG, "Using hybrid composition for platform view: " + request.viewId); + } + + /** + * Configures the view for Texture Layer Composition mode. + * @param platformView - The platform view to configure + * @param request - The creation request + * @returns The texture ID for the view + * @private + */ + private configureForTextureLayerComposition(platformView: PlatformView, + request: PlatformViewCreationRequest): number { + Log.i(TAG, "Hosting view in view hierarchy for platform view: " + request.viewId); + let surfaceId: string = '0'; + let textureId: number = 0; + if (this.textureRegistry != null) { + textureId = this.textureRegistry!.getTextureId(); + surfaceId = this.textureRegistry!.registerTexture(textureId).getSurfaceId().toString(); + Log.i(TAG, "nodeController getSurfaceId: " + surfaceId); + this.flutterView!.setSurfaceId(surfaceId); + } + + let wrappedBuilder: WrappedBuilder<[Params]> = platformView.getView(); + this.flutterView?.setWrappedBuilder(wrappedBuilder); + this.flutterView?.setPlatformView(platformView); + let physicalWidth: number = this.toPhysicalPixels(request.logicalWidth); + let physicalHeight: number = this.toPhysicalPixels(request.logicalHeight); + + let nodeController = new EmbeddingNodeController(); + nodeController.setRenderOption(platformView, surfaceId, NodeRenderType.RENDER_TYPE_TEXTURE, request.direction); + this.viewIdWithNodeController.set(request.viewId, nodeController); + + let dvModel = createDVModelFromJson(new DVModelJson("NodeContainer", + [], + { + "width": physicalWidth, + "height": physicalHeight, + "nodeController": nodeController, + "left": request.logicalLeft, + "top": request.logicalTop + }, + {}, + undefined)); + let viewWrapper: PlatformViewWrapper = new PlatformViewWrapper(); + viewWrapper.addDvModel(dvModel); + this.viewWrappers.set(request.viewId, viewWrapper); + this.flutterView?.getDVModel().children.push(viewWrapper.getDvModel()) + platformView.onFlutterViewAttached(this.flutterView!.getDVModel()); + Log.i(TAG, "Create platform view success"); + return textureId; + } + + /** + * Attaches the controller to a context, texture registry, and Dart executor. + * @param context - The application context + * @param textureRegistry - The texture registry for managing textures + * @param dartExecutor - The Dart executor for communication + */ + public attach(context: Context, textureRegistry: TextureRegistry | null, dartExecutor: DartExecutor): void { + this.context = context; + this.textureRegistry = textureRegistry; + this.platformViewsChannel = new PlatformViewsChannel(dartExecutor); + this.platformViewsChannel.setPlatformViewsHandler(this); + } + + /** + * Detaches the controller and cleans up resources. + */ + public detach(): void { + if (this.platformViewsChannel != null) { + this.platformViewsChannel.setPlatformViewsHandler(null); + } + this.destroyOverlaySurfaces(); + this.platformViewsChannel = null; + this.context = null; + this.textureRegistry = null; + } + + /** + * Attaches the controller to a FlutterView. + * @param newFlutterView - The FlutterView to attach to + */ + public attachToView(newFlutterView: FlutterView) { + this.flutterView = newFlutterView; + } + + /** + * Detaches the controller from the FlutterView. + */ + public detachFromView(): void { + this.destroyOverlaySurfaces(); + this.removeOverlaySurfaces(); + this.flutterView = null; + } + + /** + * Gets the current FlutterView. + * @returns The FlutterView instance, or null if not attached + */ + public getFlutterView(): FlutterView | null { + return this.flutterView; + } + + /** + * Attaches a text input plugin to this controller. + * @param textInputPlugin - The TextInputPlugin instance + */ + public attachTextInputPlugin(textInputPlugin: TextInputPlugin): void { + this.textInputPlugin = textInputPlugin; + } + + /** + * Detaches the text input plugin from this controller. + */ + public detachTextInputPlugin(): void { + this.textInputPlugin = null; + } + + /** + * Gets the platform view registry. + * @returns The PlatformViewRegistry instance + */ + public getRegistry(): PlatformViewRegistry { + return this.registry; + } + + /** + * Called when the controller is detached from NAPI. + * Disposes all platform views. + */ + public onDetachedFromNapi(): void { + this.diposeAllViews(); + } + + /** + * Called before the Flutter engine restarts. + * Disposes all platform views. + */ + public onPreEngineRestart(): void { + this.diposeAllViews(); + } + + /** + * Gets the display density. + * @returns The display density value + * @private + */ + private getDisplayDensity(): number { + return display.getDefaultDisplaySync().densityPixels; + } + + /** + * Converts logical pixels to physical pixels. + * @param logicalPixels - The logical pixel value + * @returns The physical pixel value + * @private + */ + private toPhysicalPixels(logicalPixels: number): number { + return Math.round(px2vp(logicalPixels * this.getDisplayDensity())); + } + + /** + * Converts physical pixels to logical pixels using a specific density. + * @param physicalPixels - The physical pixel value + * @param displayDensity - The display density to use + * @returns The logical pixel value + * @private + */ + private toLogicalPixelsByDensity(physicalPixels: number, displayDensity: number): number { + return Math.round(physicalPixels / displayDensity); + } + + /** + * Converts physical pixels to logical pixels using the current display density. + * @param physicalPixels - The physical pixel value + * @returns The logical pixel value + * @private + */ + private toLogicalPixels(physicalPixels: number): number { + return this.toLogicalPixelsByDensity(physicalPixels, this.getDisplayDensity()); + } + + /** + * Disposes all platform views. + * @private + */ + private diposeAllViews(): void { + let viewKeys = this.platformViews.keys(); + for (let viewId of viewKeys) { + this.dispose(viewId); + } + } + + /** + * Initializes the root image view if needed. + * @private + */ + private initializeRootImageViewIfNeeded(): void { + } + + /** + * Called when an overlay surface should be displayed. + * @param id - The overlay surface ID + * @param x - The X position + * @param y - The Y position + * @param width - The width + * @param height - The height + */ + public onDisplayOverlaySurface(id: number, x: number, y: number, width: number, height: number): void { + } + + /** + * Called at the beginning of each frame. + * Clears the sets of used overlay layers and platform views. + */ + public onBeginFrame(): void { + this.currentFrameUsedOverlayLayerIds.clear(); + this.currentFrameUsedPlatformViewIds.clear(); + } + + /** + * Called at the end of each frame. + */ + public onEndFrame(): void { + } + + /** + * Finishes frame rendering. + * @param isFrameRenderedUsingImageReaders - Whether the frame was rendered using image readers + * @private + */ + private finishFrame(isFrameRenderedUsingImageReaders: boolean): void { + } + + /** + * Creates a new overlay surface. + * @returns A new FlutterOverlaySurface instance + */ + public createOverlaySurface(): FlutterOverlaySurface { + return new FlutterOverlaySurface(this.nextOverlayLayerId++); + } + + /** + * Destroys all overlay surfaces. + * @private + */ + private destroyOverlaySurfaces(): void { + } + + /** + * Removes overlay surfaces from the view hierarchy. + * @private + */ + private removeOverlaySurfaces(): void { + if (!(this.flutterView instanceof FlutterView)) { + return; + } + } + + /** + * Renders a platform view with the specified dimensions and position. + * @param surfaceId - The surface ID + * @param platformView - The platform view to render + * @param width - The width in pixels + * @param height - The height in pixels + * @param left - The left position in pixels + * @param top - The top position in pixels + */ + public render(surfaceId: number, platformView: PlatformView, + width: number, height: number, left: number, top: number) { + + let wrapper = this.viewWrappers.get(surfaceId); + if (wrapper != null) { + let params: DVModelParameters | undefined = wrapper?.getDvModel()!.params + + this.setParams(params!, "width", width); + this.setParams(params!, "height", height); + this.setParams(params!, "left", left); + this.setParams(params!, "top", top); + return; + } + + this.flutterView!.setSurfaceId(surfaceId.toString()); + let wrappedBuilder: WrappedBuilder<[Params]> = platformView.getView(); + this.flutterView?.setWrappedBuilder(wrappedBuilder); + this.flutterView?.setPlatformView(platformView); + + let nodeController = new EmbeddingNodeController(); + + nodeController.setRenderOption(platformView, surfaceId.toString(), NodeRenderType.RENDER_TYPE_TEXTURE, + Direction.Auto); + this.viewIdWithNodeController.set(surfaceId, nodeController); + + let dvModel = createDVModelFromJson(new DVModelJson("NodeContainer", + [], + { + "width": width, + "height": height, + "nodeController": nodeController, + "left": left, + "top": top + }, + {}, + undefined)); + + let viewWrapper: PlatformViewWrapper = new PlatformViewWrapper(); + viewWrapper.addDvModel(dvModel); + this.viewWrappers.set(surfaceId, viewWrapper); + this.flutterView?.getDVModel().children.push(viewWrapper.getDvModel()); + platformView.onFlutterViewAttached(this.flutterView!.getDVModel()); + this.platformViews.set(surfaceId, platformView!); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/RawPointerCoord.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/RawPointerCoord.ets new file mode 100644 index 0000000..bad2804 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/RawPointerCoord.ets @@ -0,0 +1,63 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +*/ + +/** + * Represents raw pointer coordinates with additional touch information. + * This class holds detailed information about a pointer event. + */ +export class RawPointerCoords { + private orientation: number = 0; + private pressure: number = 0; + private size: number = 0; + private toolMajor: number = 0; + private toolMinor: number = 0; + private touchMajor: number = 0; + private touchMinor: number = 0; + private x: number = 0; + private y: number = 0; + + /** + * Constructs a new RawPointerCoords instance. + * @param orientation - The orientation angle in radians + * @param pressure - The pressure value (0.0 to 1.0) + * @param size - The size value + * @param toolMajor - The major axis of the tool ellipse + * @param toolMinor - The minor axis of the tool ellipse + * @param touchMajor - The major axis of the touch ellipse + * @param touchMinor - The minor axis of the touch ellipse + * @param x - The X coordinate + * @param y - The Y coordinate + */ + constructor(orientation: number, pressure: number, size: number, toolMajor: number, toolMinor: number, + touchMajor: number, touchMinor: number, x: number, y: number) { + this.orientation = orientation; + this.pressure = pressure; + this.size = size; + this.toolMajor = toolMajor; + this.toolMinor = toolMinor; + this.touchMajor = touchMajor; + this.touchMinor = touchMinor; + this.x = x; + this.y = y; + } + + /** + * Gets the X coordinate. + * @returns The X coordinate value + */ + getX(): number { + return this.x; + } + + /** + * Gets the Y coordinate. + * @returns The Y coordinate value + */ + getY(): number { + return this.y; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/RootDvModelManager.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/RootDvModelManager.ets new file mode 100644 index 0000000..df11ae0 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/platform/RootDvModelManager.ets @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import { + DVModel, + DVModelChildren, + DVModelContainer, + DVModelEvents, + DVModelParameters +} from '../../view/DynamicView/dynamicView'; +import Log from '../../util/Log'; + +/** + * Manager for the root DynamicView model container. + * This class provides a singleton root container for all platform views. + */ +export class RootDvModeManager { + private static model: DVModel = + new DVModel("Stack", new DVModelParameters(), new DVModelEvents(), new DVModelChildren(), null); + private static container: DVModelContainer = new DVModelContainer(RootDvModeManager.model); + + /** + * Gets the root DynamicView model container. + * @returns The root DVModelContainer instance + */ + public static getRootDvMode(): DVModelContainer { + return RootDvModeManager.container; + } + + /** + * Adds a DynamicView model to the root container. + * @param model - The DVModel to add + */ + public static addDvModel(model: DVModel): void { + RootDvModeManager.container.model.children.push(model); + Log.i("flutter RootDvModeManager", 'DVModel: %{public}s', + JSON.stringify(RootDvModeManager.container.model.children) ?? ''); + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/view/SensitiveContentPlugin.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/view/SensitiveContentPlugin.ets new file mode 100644 index 0000000..309582a --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/plugin/view/SensitiveContentPlugin.ets @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + +import SensitiveContentChannel, { SensitiveContentMethodHandler } from '../../embedding/engine/systemchannels/SensitiveContentChannel'; +import Log from '../../util/Log'; +import { BusinessError } from '@kit.BasicServicesKit'; +import window from '@ohos.window'; + +import { + SENSITIVE_CONTENT_SENSITIVITY, + NOT_SENSITIVE_CONTENT_SENSITIVITY, +} from '../../embedding/engine/systemchannels/SensitiveContentChannel'; +const TAG = "SensitiveContentChannel"; +export default class SensitiveContentPlugin implements SensitiveContentMethodHandler { + private readonly sensitiveContentChannel: SensitiveContentChannel; + private currentContentSensitivity: number = NOT_SENSITIVE_CONTENT_SENSITIVITY; + + constructor( + sensitiveContentChannel: SensitiveContentChannel + ) { + this.sensitiveContentChannel = sensitiveContentChannel; + this.sensitiveContentChannel.setSensitiveContentMethodHandler(this); + } + + setContentSensitivity(requestedContentSensitivity: number): void { + let isPrivacyMode: boolean; + switch (requestedContentSensitivity) { + case SENSITIVE_CONTENT_SENSITIVITY: + isPrivacyMode = true; + break; + case NOT_SENSITIVE_CONTENT_SENSITIVITY: + isPrivacyMode = false; + break; + default: + isPrivacyMode = false; + requestedContentSensitivity = NOT_SENSITIVE_CONTENT_SENSITIVITY; + break; + } + if (this.currentContentSensitivity === requestedContentSensitivity) { + // Content sensitivity for the requested View already set to requestedContentSensitivity. + return; + } + try { + window.getLastWindow(getContext(), (err: BusinessError, data) => { + const errCode = err.code; + if (errCode) { + return; + } + // Set requestedContentSensitivity on the View. + let promise = data.setWindowPrivacyMode(isPrivacyMode); + promise.then(() => { + Log.d(TAG, "success to set the window to privacy mode."); + this.currentContentSensitivity = requestedContentSensitivity; + }).catch((err: BusinessError) => { + Log.e(TAG, "Failed to set the window to privacy mode. Cause: " + JSON.stringify(err)); + }); + }) + } catch (exception) { + Log.e(TAG, "Exception when setting window privacy mode: " + JSON.stringify(exception)); + } + } + + getContentSensitivity(): number { + return this.currentContentSensitivity; + } + + isSupported(): boolean { + return true; + } + + destroy(): void { + this.sensitiveContentChannel.setSensitiveContentMethodHandler(null); + } +} + diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/ByteBuffer.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/ByteBuffer.ets new file mode 100644 index 0000000..0b644ba --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/ByteBuffer.ets @@ -0,0 +1,809 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import util from '@ohos.util' +import StringUtils from './StringUtils' + +/** + * A byte buffer. + * + * Supports the following data types: + * - Bool + * - Int (8, 16, 32, 64) + * - Uint (8, 16, 32, 64) + * - BigInt (64) + * - String (utf8, utf16, and delimited) + * - TypedArray + * + */ +export class ByteBuffer { + /** + * Creates a byte buffer. + * @param source - The data source + * @param byteOffset - The byte offset + * @param byteLength - The byte length + * @returns A byte buffer + */ + static from(source: ArrayBuffer, byteOffset?: number, byteLength?: number): ByteBuffer { + const byteBuffer = new ByteBuffer() + byteBuffer.dataView = byteLength === undefined ? new DataView(source, byteOffset) : + new DataView(source, byteOffset, Math.min(source.byteLength, byteLength)) + byteBuffer.mByteOffset = byteBuffer.dataView.byteOffset + return byteBuffer + } + + private dataView?: DataView + /** The byte offset. */ + mByteOffset: number = 0 + + /** + * The byte offset. + * @returns The byte offset. + */ + get byteOffset(): number { + return this.mByteOffset + } + + /** + * Gets the byte length of the buffer. + * @returns The byte length + */ + get byteLength(): number { + return this.dataView?.byteLength ?? 0 + } + + /** + * The number of remaining bytes. + * @returns The number of bytes remaining. + */ + get bytesRemaining(): number { + return this.dataView ? this.dataView.byteLength - this.mByteOffset : 0; + } + + /** + * Checks if there are remaining bytes to read. + * @returns True if there are remaining bytes, false otherwise + */ + hasRemaining(): boolean { + return this.dataView != undefined && this.mByteOffset < this.dataView.byteLength; + } + + /** + * Gets the underlying ArrayBuffer up to the current offset. + * @returns The ArrayBuffer slice + */ + get buffer(): ArrayBuffer { + return this.dataView!.buffer.slice(0, this.mByteOffset) + } + + /** + * Skips the specified number of bytes. + * @param byteLength - The number of bytes to skip + */ + skip(byteLength: number): void { + this.mByteOffset += byteLength + } + + /** + * Resets the byte offset. + */ + reset(): void { + this.mByteOffset = this.dataView?.byteOffset ?? 0 + } + + /** + * Clears the byte buffer. + */ + clear(): void { + this.getUint8Array(0).fill(0) + } + + /** + * check buffer capacity. + */ + checkWriteCapacity(slen: number): void { + if (this.mByteOffset + slen > this.dataView!.byteLength) { + let newCapacity = this.dataView!.byteLength + (this.dataView!.byteLength >> 1); + if (newCapacity < this.dataView!.byteLength + slen + 512) { + newCapacity = this.dataView!.byteLength + slen + 512; + } + let newBuffer = new ArrayBuffer(newCapacity); + let newDataView = new DataView(newBuffer); + let oldUint8Array = new Uint8Array(this.dataView!.buffer); + let newUint8Array = new Uint8Array(newBuffer); + newUint8Array.set(oldUint8Array); + this.dataView = newDataView; + } + } + + /** + * Gets a boolean. + * @param byteOffset - The byte offset + */ + getBool(byteOffset: number): boolean { + return this.getInt8(byteOffset) !== 0 + } + + /** + * Reads the next boolean. + */ + readBool(): boolean { + return this.getInt8(this.mByteOffset++) !== 0 + } + + /** + * Sets a boolean. + * @param byteOffset - The byte offset + * @param value - The value + */ + setBool(byteOffset: number, value: boolean): void { + this.dataView?.setInt8(byteOffset, value ? 1 : 0) + } + + /** + * Writes the next boolean. + * @param value - The value + */ + writeBool(value: boolean): void { + this.checkWriteCapacity(1) + this.setInt8(this.mByteOffset++, value ? 1 : 0) + } + + /** + * Gets an signed byte. + * @param byteOffset - The byte offset + * @returns The value. + */ + getInt8(byteOffset: number): number { + return this.dataView?.getInt8(byteOffset) || 0 + } + + /** + * Reads the next signed byte. + * @returns The value. + */ + readInt8(): number { + return this.getInt8(this.mByteOffset++) + } + + /** + * Sets a signed byte. + * @param byteOffset - The byte offset + * @param value - The value + */ + setInt8(byteOffset: number, value: number): void { + this.dataView?.setInt8(byteOffset, value) + } + + /** + * Writes the next signed byte. + * @param value - The value + */ + writeInt8(value: number): void { + this.checkWriteCapacity(1) + this.setInt8(this.mByteOffset++, value) + } + + /** + * Gets an unsigned byte. + * @param byteOffset - The byte offset + * @returns The value. + */ + getUint8(byteOffset: number): number { + return this.dataView?.getUint8(byteOffset) || 0 + } + + /** + * Reads the next unsigned byte. + * @returns The value. + */ + readUint8(): number { + return this.getUint8(this.mByteOffset++) + } + + /** + * Sets an unsigned byte. + * @param byteOffset - The byte offset + * @param value - The value + */ + setUint8(byteOffset: number, value: number): void { + this.dataView?.setUint8(byteOffset, value) + } + + /** + * Writes the next signed byte. + * @param value - The value + */ + writeUint8(value: number): void { + this.checkWriteCapacity(1) + this.setUint8(this.mByteOffset++, value) + } + + /** + * Gets an signed short. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getInt16(byteOffset: number, littleEndian?: boolean): number { + return this.dataView?.getInt16(byteOffset, littleEndian) || 0 + } + + /** + * Reads the next signed short. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readInt16(littleEndian?: boolean): number { + const value = this.getInt16(this.mByteOffset, littleEndian) + this.mByteOffset += 2 + return value + } + + /** + * Sets a signed short. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setInt16(byteOffset: number, value: number, littleEndian?: boolean): void { + this.dataView?.setInt16(byteOffset, value, littleEndian) + } + + /** + * Writes the next signed short. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeInt16(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(2) + this.setInt16(this.mByteOffset, value, littleEndian) + this.mByteOffset += 2 + } + + /** + * Gets an unsigned short. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getUint16(byteOffset: number, littleEndian?: boolean): number { + return this.dataView?.getUint16(byteOffset, littleEndian) || 0 + } + + /** + * Reads the next unsigned short. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readUint16(littleEndian?: boolean): number { + const value = this.getUint16(this.mByteOffset, littleEndian) + this.mByteOffset += 2 + return value + } + + /** + * Sets an unsigned short. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setUint16(byteOffset: number, value: number, littleEndian?: boolean): void { + this.dataView?.setUint16(byteOffset, value, littleEndian) + } + + /** + * Writes the next signed short. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeUint16(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(2) + this.setUint16(this.mByteOffset, value, littleEndian) + this.mByteOffset += 2 + } + + /** + * Gets an signed integer. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getInt32(byteOffset: number, littleEndian?: boolean): number { + return this.dataView?.getInt32(byteOffset, littleEndian) ?? 0 + } + + /** + * Reads the next signed integer. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readInt32(littleEndian?: boolean): number { + const value = this.getInt32(this.mByteOffset, littleEndian) + this.mByteOffset += 4 + return value + } + + /** + * Sets a signed integer. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setInt32(byteOffset: number, value: number, littleEndian?: boolean): void { + this.dataView?.setInt32(byteOffset, value, littleEndian) + } + + /** + * Writes the next signed integer. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeInt32(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(4) + this.setInt32(this.mByteOffset, value, littleEndian) + this.mByteOffset += 4 + } + + /** + * Gets an unsigned integer. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getUint32(byteOffset: number, littleEndian?: boolean): number { + return this.dataView?.getUint32(byteOffset, littleEndian) ?? 0 + } + + /** + * Reads the next unsigned integer. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readUint32(littleEndian?: boolean): number { + const value = this.getUint32(this.mByteOffset, littleEndian) + this.mByteOffset += 4 + return value + } + + /** + * Sets an unsigned integer. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setUint32(byteOffset: number, value: number, littleEndian?: boolean): void { + this.dataView?.setUint32(byteOffset, value, littleEndian) + } + + /** + * Writes the next signed integer. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeUint32(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(4) + this.setUint32(this.mByteOffset, value, littleEndian) + this.mByteOffset += 4 + } + + /** + * Gets a float. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getFloat32(byteOffset: number, littleEndian?: boolean): number { + return this.dataView?.getFloat32(byteOffset, littleEndian) ?? 0 + } + + /** + * Reads the next float. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readFloat32(littleEndian?: boolean): number { + const value = this.getFloat32(this.mByteOffset, littleEndian) + this.mByteOffset += 4 + return value + } + + /** + * Sets a float. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setFloat32(byteOffset: number, value: number, littleEndian?: boolean): void { + this.dataView?.setFloat32(byteOffset, value, littleEndian) + } + + /** + * Writes the next float. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeFloat32(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(4) + this.setFloat32(this.mByteOffset, value, littleEndian) + this.mByteOffset += 4 + } + + /** + * Gets a double. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getFloat64(byteOffset: number, littleEndian?: boolean): number { + return this.dataView?.getFloat64(byteOffset, littleEndian) ?? 0 + } + + /** + * Reads the next double. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readFloat64(littleEndian?: boolean): number { + const value = this.getFloat64(this.mByteOffset, littleEndian) + this.mByteOffset += 8 + return value + } + + /** + * Sets a double. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setFloat64(byteOffset: number, value: number, littleEndian?: boolean): void { + this.dataView?.setFloat64(byteOffset, value, littleEndian) + } + + /** + * Writes the next double. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeFloat64(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(8) + this.setFloat64(this.mByteOffset, value, littleEndian) + this.mByteOffset += 8 + } + + /** + * Gets an signed long. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getBigInt64(byteOffset: number, littleEndian?: boolean): bigint { + return this.dataView?.getBigInt64(byteOffset, littleEndian) ?? BigInt(0) + } + + /** + * Reads the next signed long. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readBigInt64(littleEndian?: boolean): bigint { + const value = this.getBigInt64(this.mByteOffset, littleEndian) + this.mByteOffset += 8 + return value + } + + /** + * Sets a signed long. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setBigInt64(byteOffset: number, value: bigint, littleEndian?: boolean): void { + this.dataView?.setBigInt64(byteOffset, value, littleEndian) + } + + /** + * Writes the next signed long. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeBigInt64(value: bigint, littleEndian?: boolean): void { + this.checkWriteCapacity(8) + this.setBigInt64(this.mByteOffset, value, littleEndian) + this.mByteOffset += 8 + } + + /** + * Gets an unsigned long. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getBigUint64(byteOffset: number, littleEndian?: boolean): bigint { + return this.dataView?.getBigUint64(byteOffset, littleEndian) ?? BigInt(0) + } + + /** + * Reads the next unsigned long. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readBigUint64(littleEndian?: boolean): bigint { + const value = this.getBigUint64(this.mByteOffset, littleEndian) + this.mByteOffset += 8 + return value + } + + /** + * Sets an unsigned long. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setBigUint64(byteOffset: number, value: bigint, littleEndian?: boolean): void { + this.dataView?.setBigUint64(byteOffset, value, littleEndian) + } + + /** + * Writes the next unsigned long. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeBigUint64(value: bigint, littleEndian?: boolean): void { + this.checkWriteCapacity(8) + this.setBigUint64(this.mByteOffset, value, littleEndian) + this.mByteOffset += 8 + } + + /** + * Gets an signed long. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getInt64(byteOffset: number, littleEndian?: boolean): bigint { + return this.getBigInt64(byteOffset, littleEndian) + } + + /** + * Reads the next signed long. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readInt64(littleEndian?: boolean): bigint { + const value = this.getInt64(this.mByteOffset, littleEndian) + this.mByteOffset += 8 + return value + } + + /** + * Sets a signed long. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setInt64(byteOffset: number, value: number, littleEndian?: boolean): void { + this.setBigInt64(byteOffset, BigInt(value), littleEndian) + } + + /** + * Writes the next signed long. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeInt64(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(8) + this.setInt64(this.mByteOffset, value, littleEndian) + this.mByteOffset += 8 + } + + /** + * Gets an unsigned long. + * @param byteOffset - The byte offset + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + getUint64(byteOffset: number, littleEndian?: boolean): number { + return Number(this.getBigUint64(byteOffset, littleEndian)) + } + + /** + * Reads the next unsigned long. + * @param littleEndian - Whether the value is little endian + * @returns The value. + */ + readUint64(littleEndian?: boolean): number { + const value = this.getUint64(this.mByteOffset, littleEndian) + this.mByteOffset += 8 + return value + } + + /** + * Sets an unsigned long. + * @param byteOffset - The byte offset + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + setUint64(byteOffset: number, value: number, littleEndian?: boolean): void { + this.setBigUint64(byteOffset, BigInt(value), littleEndian) + } + + /** + * Writes the next signed long. + * @param value - The value + * @param littleEndian - Whether the value is little endian + */ + writeUint64(value: number, littleEndian?: boolean): void { + this.checkWriteCapacity(8) + this.setUint64(this.mByteOffset, value, littleEndian) + this.mByteOffset += 8 + } + + /** + * Gets an array of unsigned bytes. + * @param byteOffset - The byte offset + * @param byteLength - The byte length + * @returns The value. + */ + getUint8Array(byteOffset: number, byteLength?: number): Uint8Array { + return this.dataView == null ? + new Uint8Array(StringUtils.stringToArrayBuffer(""), byteOffset, byteLength) : + new Uint8Array(this.dataView?.buffer, this.dataView?.byteOffset + byteOffset, byteLength) + } + + /** + * Reads the next array of unsigned bytes. + * @param byteLength - The byte length + * @returns The value. + */ + readUint8Array(byteLength?: number): Uint8Array { + const value = this.getUint8Array(this.mByteOffset, byteLength) + this.mByteOffset += value.byteLength + return value + } + + /** + * Sets an array of unsigned bytes. + * @param byteOffset - The byte offset + * @param value - The value + */ + setUint8Array(byteOffset: number, value: Uint8Array): void { + const byteLength = value.byteLength + this.getUint8Array(byteOffset, byteLength).set(value) + } + + /** + * Writes the next array of unsigned bytes. + * @param value - The value + */ + writeUint8Array(value: Uint8Array): void { + this.checkWriteCapacity(value.byteLength) + this.setUint8Array(this.mByteOffset, value) + this.mByteOffset += value.byteLength + } + + /** + * Gets an array of unsigned shorts. + * @param byteOffset - The byte offset + * @param byteLength - The byte length + * @returns The value. + */ + getUint16Array(byteOffset: number, byteLength?: number): Uint16Array { + if (byteLength !== undefined) { + byteLength = Math.floor(byteLength / 2) + } + return this.dataView == null ? + new Uint16Array(StringUtils.stringToArrayBuffer(""), byteOffset, byteLength) : + new Uint16Array(this.dataView.buffer, this.dataView.byteOffset + byteOffset, byteLength) + } + + /** + * Reads the next array of unsigned shorts. + * @param byteLength - The byte length + * @returns The value. + */ + readUint16Array(byteLength?: number): Uint16Array { + const value = this.getUint16Array(this.mByteOffset, byteLength) + this.mByteOffset += value.byteLength + return value + } + + /** + * Sets an array of unsigned bytes. + * @param byteOffset - The byte offset + * @param value - The value + */ + setUint16Array(byteOffset: number, value: Uint16Array): void { + const byteLength = value.byteLength + this.getUint16Array(byteOffset, byteLength).set(value) + } + + /** + * Writes the next array of unsigned bytes. + * @param value - The value + */ + writeUint16Array(value: Uint16Array): void { + this.checkWriteCapacity(value.byteLength) + this.setUint16Array(this.mByteOffset, value) + this.mByteOffset += value.byteLength + } + + /** + * Gets a string. + * @param byteOffset - The byte offset + * @param byteLength - The byte length + * @param byteEncoding - The byte encoding + * @returns The value. + */ + getString(byteOffset: number, byteLength?: number, byteEncoding?: string): string { + const decoder = new util.TextDecoder(byteEncoding || "utf-8") + const encoded = this.getUint8Array(byteOffset, byteLength) + return decoder.decode(encoded) + } + + /** + * Reads the next string. + * @param byteLength - The byte length + * @param byteEncoding - The byte encoding + * @returns The value. + */ + readString(byteLength?: number, byteEncoding?: string): string { + const value = this.getString(this.mByteOffset, byteLength, byteEncoding) + if (byteLength === undefined) { + this.mByteOffset = this.dataView?.byteLength ?? 0 + } else { + this.mByteOffset += byteLength + } + return value + } + + /** + * Sets a string. + * @param byteOffset - The byte offset + * @param value - The string value + * @param byteEncoding - The byte encoding + * @returns The byte length. + */ + setString(byteOffset: number, value: string, byteEncoding?: string, write?: boolean): number { + if (byteEncoding && byteEncoding !== "utf-8") { + throw new TypeError("String encoding '" + byteEncoding + "' is not supported") + } + const encoder = new util.TextEncoder() + const byteLength = Math.min(this.dataView!.byteLength - byteOffset, value.length * 4) + if (write) { + this.checkWriteCapacity(byteLength) + } + const destination = this.getUint8Array(byteOffset, byteLength) + const written = encoder.encodeInto(value, destination).written + return written || 0 + } + + /** + * Writes the next a string. + * @param value - The string value + * @param byteEncoding - The byte encoding + */ + writeString(value: string, byteEncoding?: string): void { + const byteLength = this.setString(this.mByteOffset, value, byteEncoding, true) + this.mByteOffset += byteLength + } + + /** + * Formats to a string. + * @param format - The string format + * @returns The string. + */ + toString(format?: string): string { + return [...this.getUint8Array(0)].map((byte: number) => { + switch (format) { + case "hex": + return ("00" + byte.toString(16)).slice(-2) + default: + return byte.toString(10) + } + }).join(" ") + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/Json5ToJson.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/Json5ToJson.ets new file mode 100644 index 0000000..cb4d79c --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/Json5ToJson.ets @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ + + +/** + * Remove JSON5-style comments from text. + * Supports: // line comments, /* block comments *\/ + * Preserves anything inside string literals: "..." (and optionally '...' if present) + */ + +//Pass in a string and remove the comments from it +function stripComments(input: string) { + //Return value + let out = ''; + let i = 0; + + const len = input.length; + let inString = false; + let stringQuote = ''; + //Whether it is in an escape state (the previous character is \), + //used to handle \" or \' etc. + let escaped = false; + + //Loop through each character + while (i < len) { + const ch = input[i]; + const next = i + 1 < len ? input[i + 1] : ''; + + //If currently inside a string, output everything exactly as it is + if (inString) { + out += ch; + //If the previous character is a backslash causing this character to be escaped, + //then clear the escape state + if (escaped) { + escaped = false; + //When encountering a backslash, enter escape mode + } else if (ch === '\\') { + escaped = true; + } else if (ch === stringQuote) { + inString = false; + stringQuote = ''; + } + i++; + continue; + } + //Not inside a string; + //enter string mode when encountering a quotation mark + if (ch === '"' || ch === "'") { + inString = true; + stringQuote = ch; + out += ch; + i++; + continue; + } + //Detect and skip single-line comments + if (ch === '/' && next === '/') { + i += 2; + while (i < len && input[i] !== '\n' && input[i] !== '\r') i++; + continue; + } + //Detect and skip block comments + if (ch === '/' && next === '*') { + i += 2; + while (i < len) { + if (input[i] === '*' && i + 1 < len && input[i + 1] === '/') { + i += 2; + break; + } + i++; + } + continue; + } + out += ch; + i++; + } + return out; +} + + +export function json5Tojson(json5Text: string): string { + const noComments = stripComments(json5Text); + return noComments; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/Log.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/Log.ets new file mode 100644 index 0000000..d8b0dfc --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/Log.ets @@ -0,0 +1,119 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import HiLog from '@ohos.hilog'; +import BuildProfile from '../../../../BuildProfile'; + +const DOMAIN: number = 0x00FF; +const TAG = "Flutter"; +const SYMBOL = " --> "; + +/** + * Basic log class + */ +export default class Log { + private static _logLevel = HiLog.LogLevel.WARN; + + /** + * Sets the log level. + * + * @param level - The log level + */ + public static setLogLevel(level: HiLog.LogLevel) { + Log._logLevel = level; + } + + /** + * Outputs debug-level logs. + * + * @param tag - The log tag + * @param format - The log format string + * @param args - The log parameters + * @since 7 + */ + static d(tag: string, format: string, ...args: Object[]) { + if (Log.isLoggable(HiLog.LogLevel.DEBUG)) { + HiLog.debug(DOMAIN, TAG, tag + SYMBOL + format, args); + } + } + + /** + * Outputs info-level logs. + * + * @param tag - The log tag + * @param format - The log format string + * @param args - The log parameters + * @since 7 + */ + static i(tag: string, format: string, ...args: Object[]) { + if (Log.isLoggable(HiLog.LogLevel.INFO)) { + HiLog.info(DOMAIN, TAG, tag + SYMBOL + format, args); + } + } + + /** + * Outputs warning-level logs. + * + * @param tag - The log tag + * @param format - The log format string + * @param args - The log parameters + * @since 7 + */ + static w(tag: string, format: string, ...args: Object[]) { + if (Log.isLoggable(HiLog.LogLevel.WARN)) { + HiLog.warn(DOMAIN, TAG, tag + SYMBOL + format, args); + } + } + + /** + * Outputs error-level logs. + * + * @param tag - The log tag + * @param format - The log format string + * @param args - The log parameters + * @since 7 + */ + static e(tag: string, format: string, ...args: Object[]) { + if (Log.isLoggable(HiLog.LogLevel.ERROR)) { + args.forEach((item: Object, index: number) => { + if (item instanceof Error) { + args[index] = item.message + item.stack; + } + format += "%{public}s"; + }) + HiLog.error(DOMAIN, TAG, tag + SYMBOL + format, args); + } + } + + /** + * Outputs fatal-level logs. + * + * @param tag - The log tag + * @param format - The log format string + * @param args - The log parameters + * @since 7 + */ + static f(tag: string, format: string, ...args: Object[]) { + if (Log.isLoggable(HiLog.LogLevel.FATAL)) { + HiLog.fatal(DOMAIN, TAG, tag + SYMBOL + format, args); + } + } + + /** + * Checks whether logs of the specified tag and level can be printed. + * + * @param level - The log level + * @returns True if logs can be printed, false otherwise + * @since 7 + */ + private static isLoggable(level: HiLog.LogLevel): boolean { + let buildModeName: string = BuildProfile.BUILD_MODE_NAME.toLowerCase(); + if (buildModeName == 'release' || buildModeName == 'profile') { + return level >= Log._logLevel && HiLog.isLoggable(DOMAIN, TAG, level); + } + return HiLog.isLoggable(DOMAIN, TAG, level); + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/MessageChannelUtils.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/MessageChannelUtils.ets new file mode 100644 index 0000000..f08b175 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/MessageChannelUtils.ets @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import BasicMessageChannel from '../plugin/common/BasicMessageChannel'; +import { BinaryMessenger } from '../plugin/common/BinaryMessenger'; +import StringUtils from './StringUtils'; + +/** + * Utility class for message channel operations. + */ +export default class MessageChannelUtils { + /** + * Resizes a channel buffer. + * @param messenger - The BinaryMessenger to use + * @param channel - The channel name + * @param newSize - The new buffer size + */ + static resizeChannelBuffer(messenger: BinaryMessenger, channel: string, newSize: number) { + const dataStr = `resize\r${channel}\r${newSize}` + messenger.send(BasicMessageChannel.CHANNEL_BUFFERS_CHANNEL, StringUtils.stringToArrayBuffer(dataStr)); + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/PasteboardUtils.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/PasteboardUtils.ets new file mode 100644 index 0000000..cf44eeb --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/PasteboardUtils.ets @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2021-2024 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ +import { BusinessError, pasteboard } from "@kit.BasicServicesKit"; +import { abilityAccessCtrl } from "@kit.AbilityKit"; +import FlutterManager from "../embedding/ohos/FlutterManager"; +import Log from "./Log"; + +/** + * Utility class for pasteboard operations. + * Provides methods for copying and pasting data to/from the system pasteboard. + */ +export class PasteboardUtils { + private static TAG = "PasteboardUtils"; + + /** + * Copies text data to the OpenHarmony pasteboard. + * @param text - The text to copy + */ + static setCopyData(text: string) { + const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); + const systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); + try { + systemPasteboard.setDataSync(pasteData); + } catch (err) { + Log.w(PasteboardUtils.TAG, "Failed to set PasteData. Cause: " + JSON.stringify(err)); + } + } + + /** + * Gets paste data from the OpenHarmony pasteboard asynchronously. + * Requests READ_PASTEBOARD permission if needed. + * @returns A Promise that resolves to the pasted text, or empty string if no text is available. + */ + static getPasteDataAsync(): Promise { + return new Promise((resolve) => { + let atManager = abilityAccessCtrl.createAtManager(); + // request the ohos paste operation authority + atManager.requestPermissionsFromUser(FlutterManager.getInstance().getUIAbility().context, + ['ohos.permission.READ_PASTEBOARD']).then((data) => { + enum AuthResultStatus { + NOT_CONFIGURED = -1, + GRANTED = 0, + INVALID_REQ = 2 + } + + const authResult: number = data.authResults[0]; + switch (authResult) { + case AuthResultStatus.GRANTED: { + let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); + systemPasteboard.getData().then(async (pasteData: pasteboard.PasteData) => { + let pasteText: string = ''; + const recordCount: number = pasteData.getRecordCount(); + for (let i = 0; i < recordCount; i++) { + const record = pasteData.getRecord(i); + let text: string = ''; + if (typeof record.getValidTypes === 'function') { + // For api14 and above, click here. More formats are supported + text = await PasteboardUtils.getTargetTypesData(record); + } else if (record.mimeType === pasteboard.MIMETYPE_TEXT_HTML) { + const htmlText: StyledString = await StyledString.fromHtml(record.htmlText); + text = htmlText.getString(); + } else if (record.mimeType === pasteboard.MIMETYPE_TEXT_PLAIN) { + text = record.plainText; + } + pasteText += text; + } + resolve(pasteText); + }).catch((err: BusinessError) => { + Log.e(PasteboardUtils.TAG, "Failed to get PasteData. Cause: " + JSON.stringify(err)); + }); + break; + } + case AuthResultStatus.NOT_CONFIGURED: + case AuthResultStatus.INVALID_REQ: + default: { + Log.e(PasteboardUtils.TAG, "error code: " + authResult); + break; + } + } + }).catch((err: BusinessError) => { + Log.e(PasteboardUtils.TAG, "Failed to request permissions from user. Cause: " + JSON.stringify(err)); + }) + }); + } + + static async getTargetTypesData(record: pasteboard.PasteDataRecord): Promise { + let targetTypes: string[] = [ + pasteboard.MIMETYPE_TEXT_PLAIN, + pasteboard.MIMETYPE_TEXT_HTML + ]; + let tmpTypes: string[] = record.getValidTypes(targetTypes); + let str: string = ''; + for (let j = 0; j < tmpTypes.length; j++) { + let value = ''; + try { + value = await record.getData(tmpTypes[j]) as string; + } catch (error) { + Log.e(PasteboardUtils.TAG, "Failed to record.getData: " + JSON.stringify(error)); + } + if (value) { + if (tmpTypes[j] === pasteboard.MIMETYPE_TEXT_HTML) { + try { + const htmlText: StyledString = await StyledString.fromHtml(value); + str = htmlText.getString(); + } catch (error) { + Log.e(PasteboardUtils.TAG, "Failed to record.getData: " + JSON.stringify(error)); + } + } else { + str = value + } + break; + } + } + return str; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/PathUtils.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/PathUtils.ets new file mode 100644 index 0000000..629ff39 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/PathUtils.ets @@ -0,0 +1,53 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import common from '@ohos.app.ability.common'; +import fs from '@ohos.file.fs'; +import Log from './Log'; + +const TAG: string = "PathUtils"; + +/** + * Utility class for path operations in OpenHarmony. + * Provides methods for getting application directories. + */ +export default class PathUtils { + /** + * Gets the files directory for the application. + * @param context - The application context + * @returns The files directory path. + */ + static getFilesDir(context: common.Context): string { + return context.filesDir; + } + + /** + * Gets the cache directory for the application. + * @param context - The application context + * @returns The cache directory path. + */ + static getCacheDirectory(context: common.Context): string { + return context.cacheDir; + } + + /** + * Gets or creates the Flutter data directory. + * @param context - The application context + * @returns The Flutter data directory path, or null if creation fails. + */ + static getDataDirectory(context: common.Context): string | null { + const name = "flutter"; + const flutterDir = context.filesDir + "/" + name; + if (!fs.accessSync(flutterDir)) { + try { + fs.mkdirSync(flutterDir); + } catch (err) { + Log.e(TAG, "mkdirSync failed err:" + err); + return null; + } + } + return flutterDir; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/StringUtils.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/StringUtils.ets new file mode 100644 index 0000000..9bd3b4d --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/StringUtils.ets @@ -0,0 +1,68 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import flutter from 'libflutter.so' + +/** + * Utility class for string operations. + * Provides methods for converting between strings and ArrayBuffer/Uint8Array. + */ +export default class StringUtils { + + /** + * Converts a string to an ArrayBuffer using UTF-8 encoding. + * @param str - The string to convert. + * @returns The ArrayBuffer containing the UTF-8 encoded string. + */ + static stringToArrayBuffer(str: string): ArrayBuffer { + if (str.length == 0) { + return new ArrayBuffer(0); + } + return flutter.nativeEncodeUtf8(str).buffer; + } + + /** + * Converts an ArrayBuffer to a string using UTF-8 decoding. + * @param buffer - The ArrayBuffer to convert. + * @returns The decoded string, or empty string if buffer is empty. + */ + static arrayBufferToString(buffer: ArrayBuffer): string { + if (buffer.byteLength <= 0) { + return ""; + } + return flutter.nativeDecodeUtf8(new Uint8Array(buffer)); + } + + /** + * Converts a Uint8Array to a string using UTF-8 decoding. + * @param buffer - The Uint8Array to convert. + * @returns The decoded string, or empty string if buffer is empty. + */ + static uint8ArrayToString(buffer: Uint8Array): string { + if (buffer.length <= 0) { + return ""; + } + return flutter.nativeDecodeUtf8(buffer); + } + + /** + * Checks if a string is not empty. + * @param str - The string to check. + * @returns True if the string is not null and has length > 0, false otherwise. + */ + static isNotEmpty(str: string): boolean { + return str != null && str.length > 0; + } + + /** + * Checks if a string is empty. + * @param str - The string to check + * @returns True if the string is null, undefined, or has length 0, false otherwise. + */ + static isEmpty(str: string): boolean { + return (!str) || str.length == 0; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/ToolUtils.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/ToolUtils.ets new file mode 100644 index 0000000..292c0aa --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/ToolUtils.ets @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ +import Any from '../plugin/common/Any'; + +/** + * Utility class for object operations. + */ +export default class ToolUtils { + /** + * Checks if a value is an object. + * @param object - The value to check + * @returns True if the value is an object, false otherwise. + */ + static isObj(object: Object): boolean { + return object && typeof (object) == 'object'; + } + + /** + * Checks if an object implements a specific method (interface check). + * @param obj - The object to check + * @param method - The method name to check for + * @returns True if the object has the method and it's a function, false otherwise. + */ + static implementsInterface(obj: Any, method: string): boolean { + return Reflect.has(obj, method) && typeof obj[method] === 'function' + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/TraceSection.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/TraceSection.ets new file mode 100644 index 0000000..f7c1f91 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/util/TraceSection.ets @@ -0,0 +1,59 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on TraceSection.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import hiTraceMeter from '@ohos.hiTraceMeter' + +/** + * Utility class for tracing code sections using hiTraceMeter. + * Provides methods to begin and end trace sections with automatic name cropping. + */ +export class TraceSection { + /** The current task ID for trace sections. */ + static taskId: number = 0; + + /** + * Crops a section name to ensure it stays below 124 characters. + * @param sectionName - The section name to crop + * @returns The cropped section name. + * @private + */ + private static cropSectionName(sectionName: string): string { + return sectionName.length < 124 ? sectionName : sectionName.substring(0, 124) + "..."; + } + + /** + * Wraps Trace.beginSection to ensure that the line length stays below 127 code units. + * + * @param sectionName - The string to display as the section name in the trace + * @returns The task ID for this trace section + */ + public static begin(sectionName: string): number { + TraceSection.taskId++; + hiTraceMeter.startTrace(TraceSection.cropSectionName(sectionName), TraceSection.taskId); + return TraceSection.taskId; + } + + /** + * Ends a trace section. + * @param sectionName - The section name to end. + */ + public static end(sectionName: string): void { + hiTraceMeter.finishTrace(TraceSection.cropSectionName(sectionName), TraceSection.taskId); + } + + /** + * Ends a trace section with a specific task ID. + * @param sectionName - The section name to end. + * @param id - The task ID to use. + */ + public static endWithId(sectionName: string, id: number): void { + hiTraceMeter.finishTrace(TraceSection.cropSectionName(sectionName), id); + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/DynamicView/dynamicView.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/DynamicView/dynamicView.ets new file mode 100644 index 0000000..a757281 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/DynamicView/dynamicView.ets @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2021-2024 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ +import matrix4 from '@ohos.matrix4'; +import Any from '../../plugin/common/Any'; +import Log from '../../util/Log'; + +/** + * Dynamic View creation + * from a recursive data structure + * + * exported @Component: DynamicView + * exported view model classes: + * - DVModelContainer + * - DVModel + * - DVModelParameters + * - DVModelEvents + * - DVModelChildren + * + * The purpose of exporting the DVModel classes + * is to make them available to the converter from + * JD's XML format and the expression parser. These + * components are expected to generate and update the + * DVModel. + * + * An application written by JS should only import + * DynamicView, DVModelContainer to be used in their own ArkUI + * container view. + */ + +/** + * View Model classes + */ + +/** + * Parameters for DynamicView model components. + * This class is observed to enable reactive updates when parameters change. + * It serves as a container for component attributes and properties. + */ +@Observed +export class DVModelParameters extends Object { + /* empty, just get any instance wrapped inside an ObservedObject + with the help of the decoration */ +} + +/** + * Events for DynamicView model components. + * This class is observed to enable reactive updates when events change. + * It serves as a container for component event handlers. + */ +@Observed +export class DVModelEvents extends Object { + /* empty, just get any instance wrapped inside an ObservedObject + with the help of the decoration */ +} + +/** + * Children array for DynamicView model components. + * This class is observed to enable reactive updates when children change. + * It serves as a container for child DVModel instances. + */ +@Observed +export class DVModelChildren extends Array { + /* empty, just get any instance wrapped inside an ObservedObject + with the help of the decoration */ +} + +let nextId: number = 1; + +/** + * Model class representing a dynamic view component. + * This class holds all information needed to render a component dynamically, + * including its type, parameters, events, children, and optional builder. + */ +@Observed +export class DVModel { + /** The unique identifier for this model. */ + id_: number; + /** The component type (e.g., "Column", "Row", "Text", "Image"). */ + compType: string; + /** The component parameters. */ + params: DVModelParameters; + /** The component events. */ + events: DVModelEvents; + /** The child components. */ + children: DVModelChildren; + /** Optional custom builder function. */ + builder: Any; + + /** + * Constructs a new DVModel instance. + * @param compType - The component type (e.g., "Column", "Row", "Text", "Image") + * @param params - The component parameters + * @param events - The component events + * @param children - The child components + * @param builder - Optional custom builder function + */ + constructor(compType: string, params: DVModelParameters, events: DVModelEvents, children: DVModelChildren, + builder?: Any) { + this.id_ = nextId++; + this.compType = compType; + this.params = params ?? new DVModelParameters; + this.events = events; + this.children = children; + this.builder = builder; + } + + /** + * Gets the layout parameters for this model. + * @returns The DVModelParameters instance + */ + public getLayoutParams(): DVModelParameters { + return this.params; + } +} + +/** + * Container for the root DVModel object. + * This class wraps the root model to provide a container structure. + */ +export class DVModelContainer { + /** The root DVModel instance. */ + model: DVModel; + + /** + * Constructs a new DVModelContainer instance. + * @param model - The root DVModel to contain + */ + constructor(model: DVModel) { + this.model = model; + } +} + +/** + DynamicView is the @Component that does all the work: + + The following 4 features are the key solution elements for dynamic View + construction and update: + + 1. The if statement decides which framework component to create. + We can not use a factory function here, because that would requite calling + a regular function inside build() or a @Builder function. + + 2. Take note of the @Builder for Row, Column containers: + These functions create DynamicView Views inside a DynamicView + view. This behaviour is why we talk about DynamicView as a 'recursive' View. + All @Builder functions are member functions of the DynamicView @Component to + retain access ('this.xyz') to its decorated state variables. + + 3. The @Extend functions execute attribute and event handler registration functions + for all attributes and events permissable on the framework component, irrespective + if DVModelParameters or DVModelEvents objects includes a value or not. If not + the attribute or event is set to 'undefined' by intention. This is required to unset + any previously set value. + + 4. The scope ('this') of any lambda registered as an event hander function, e.g. for onClick, + is the @Component, in which the DVModel object is initialized. This said, it is advised to initialize + the DVModel object in the @Component that is parent to outmost DynamicView. Thereby, + any event handler function is able to mutate decorated state variables of that @Component + + */ + +@Component +export struct DynamicView { + @ObjectLink model: DVModel; + @ObjectLink children: DVModelChildren; + @ObjectLink params: DVModelParameters; + @ObjectLink events: DVModelEvents; + @BuilderParam customBuilder?: ($$: BuilderParams) => void; + + /** + * Gets a parameter value from the parameters object. + * @param params - The parameters object + * @param element - The parameter key + * @returns The parameter value, or undefined if not found + */ + getParams: (params: DVModelParameters, element: string) => string | Any = + (params: DVModelParameters, element: string): string | Any => { + let params2 = params as Record; + return params2[element]; + } + + /** + * Gets an event handler from the events object. + * @param events - The events object + * @param element - The event key + * @returns The event handler, or undefined if not found + */ + getEvents: (events: DVModelEvents, element: string) => Any = (events: DVModelEvents, element: string): Any => { + let events2 = events as Record; + return events2[element]; + } + + @Styles + common_attrs() { + .width(this.getParams(this.params, "width")) + .height(this.getParams(this.params, "height")) + .backgroundColor(this.getParams(this.params, "backgroundColor")) + .onClick(this.getEvents(this.events, "onClick")) + .margin({ + left: this.getParams(this.params, "marginLeft"), + right: this.getParams(this.params, "marginRight"), + top: this.getParams(this.params, "marginTop"), + bottom: this.getParams(this.params, "marginBottom") + }) + .onTouch(this.getEvents(this.events, "onTouch")) + .onFocus(this.getEvents(this.events, "onFocus")) + .onBlur(this.getEvents(this.events, "onBlur")) + .translate({ + x: this.getParams(this.params, "translateX"), + y: this.getParams(this.params, "translateY"), + z: this.getParams(this.params, "translateZ") + }) + .transform(this.getParams(this.params, "matrix")) + .direction(this.getParams(this.params, "direction")) + } + + @Styles + clip_attrs() { + .clip(this.getParams(this.params, "rectWidth") ? new Rect({ + width: this.getParams(this.params, "rectWidth"), + height: this.getParams(this.params, "rectHeight"), + radius: this.getParams(this.params, "rectRadius") + }) : null) + .clip(this.getParams(this.params, "pathWidth") ? new Path({ + width: this.getParams(this.params, "pathWidth"), + height: this.getParams(this.params, "pathHeight"), + commands: this.getParams(this.params, "pathCommands") + }) : null) + } + + @Builder + buildChildren() { + ForEach(this.children, + (child: Any) => { + DynamicView({ + model: child as DVModel, + params: child.params, + events: child.events, + children: child.children, + customBuilder: child.builder + }) + }, + (child: Any) => `${child.id_}` + ) + } + + @Builder + buildRow() { + Row() { + this.buildChildren() + } + .common_attrs() + .clip_attrs() + } + + @Builder + buildColumn() { + Column() { + this.buildChildren() + } + .common_attrs() + .clip_attrs() + } + + @Builder + buildStack() { + Stack() { + this.buildChildren() + } + .common_attrs() + .clip_attrs() + .alignContent(this.getParams(this.params, "alignContent")) + } + + @Builder + buildText() { + Text(`${this.getParams(this.params, "value")}`) + .common_attrs() + .fontColor(this.getParams(this.params, "fontColor")) + } + + @Builder + buildImage() { + Image(this.getParams(this.params, "src")) + .common_attrs() + } + + @Builder + buildButton() { + Button(this.getParams(this.params, "value")) + .common_attrs() + } + + @Builder + buildNodeContainer() { + NodeContainer(this.getParams(this.params, "nodeController")) + .common_attrs() + .position({ + x: (this.params as Record)['left'] as number, + y: (this.params as Record)['top'] as number + }) + } + + @Builder + buildCustom() { + if (this.customBuilder) { + this.customBuilder(new BuilderParams(this.params)); + } + } + + build() { + if (this.model.compType == "Column") { + this.buildColumn() + } else if (this.model.compType == "Row") { + this.buildRow() + } else if (this.model.compType == "Stack") { + this.buildStack() + } else if (this.model.compType == "Text") { + this.buildText() + } else if (this.model.compType == "Image") { + this.buildImage() + } else if (this.model.compType == "Button") { + this.buildButton() + } else if (this.model.compType == "NodeContainer") { + this.buildNodeContainer() + } else { + this.buildCustom() + } + } +} + +/** + * Parameters passed to custom builder functions. + * This class wraps DVModelParameters for use in custom builders. + */ +export class BuilderParams { + /** The DVModelParameters to wrap. */ + params: DVModelParameters; + + /** + * Constructs a new BuilderParams instance. + * @param params - The DVModelParameters to wrap + */ + constructor(params: DVModelParameters) { + this.params = params; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/DynamicView/dynamicViewJson.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/DynamicView/dynamicViewJson.ets new file mode 100644 index 0000000..afc3412 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/DynamicView/dynamicViewJson.ets @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021-2024 Huawei Device Co., Ltd. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE_HW file. + */ +import Any from '../../plugin/common/Any'; + +import { DVModel, DVModelParameters, DVModelEvents, DVModelChildren } from "./dynamicView"; +import Log from '../../util/Log'; +const TAG = "dynamicViewJson"; + +/** + * Creates a DVModel instance from a JSON object. + * This function recursively parses the JSON structure to build a complete DVModel tree. + * @param json - The JSON object representing the view structure + * @returns A DVModel instance created from the JSON + */ +export function createDVModelFromJson(json: Object): DVModel { + + /** + * Helper function to create children from a JSON array. + * @param children - Array of child JSON objects + * @returns A DVModelChildren instance containing parsed child models + * @private + */ + let createChildrenFrom: (children: Array) => DVModelChildren = (children: Array): DVModelChildren => { + let result = new DVModelChildren(); + if (Array.isArray(children)) { + (children as Array).forEach(child => { + const childView = createDVModelFromJson(child); + if (childView != undefined) { + result.push(childView); + } + }); + } + return result; + } + + /** + * Helper function to set a parameter or event value. + * @param result - The parameters or events object to modify + * @param key - The key to set + * @param element - The source object containing the value + * @private + */ + let setParams: (result: DVModelParameters | DVModelEvents, key: Any, element: Object) => void = + (result: DVModelParameters, key: Any, element: Any): void => { + let newResult = result as Record; + newResult[key] = element[key]; + } + + /** + * Helper function to create parameters from a JSON attributes object. + * @param attributes - The attributes JSON object + * @returns A DVModelParameters instance + * @private + */ + let createAttributesFrom: (attributes: Object) => DVModelParameters = (attributes: Object): DVModelParameters => { + let result = new DVModelParameters(); + if ((typeof attributes == "object") && (!Array.isArray(attributes))) { + Object.keys(attributes).forEach(k => { + setParams(result, k, attributes) + }); + } + return result; + } + + /** + * Helper function to create events from a JSON events object. + * @param events - The events JSON object + * @returns A DVModelEvents instance + * @private + */ + let createEventsFrom: (events: Object) => DVModelEvents = (events: Object): DVModelEvents => { + let result = new DVModelEvents(); + if ((typeof events == "object") && (!Array.isArray(events))) { + Object.keys(events).forEach(k => { + setParams(result, k, events) + }); + } + return result; + } + + if (typeof json !== 'object') { + Log.e(TAG, "createDVModelFromJson: input is not JSON"); + return new DVModel("", "", "", createChildrenFrom([])); + } + + let jsonObject = json as Record; + return new DVModel( + jsonObject["compType"], + createAttributesFrom(jsonObject["attributes"]), + createEventsFrom(jsonObject["events"]), + createChildrenFrom(jsonObject["children"]), + jsonObject["build"] + ); +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterCallbackInformation.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterCallbackInformation.ets new file mode 100644 index 0000000..9afb0f1 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterCallbackInformation.ets @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterCallbackInformation.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +import FlutterNapi from '../embedding/engine/FlutterNapi'; + +/** + * A class representing information for a callback registered using `PluginUtilities` from `dart:ui`. + * + * This class holds information about callbacks that can be invoked from native code. + */ +export class FlutterCallbackInformation { + /** The name of the callback function. */ + callbackName?: string; + /** The class name containing the callback. */ + callbackClassName?: string; + /** The library path where the callback is defined. */ + callbackLibraryPath?: string; + + /** + * Gets callback information for a given handle. + * + * @param handle - The handle for the callback, generated by `PluginUtilities.getCallbackHandle` in + * `dart:ui` + * @returns An instance of FlutterCallbackInformation for the provided handle, or null if not found + */ + static lookupCallbackInformation(handle: number): FlutterCallbackInformation | null { + return FlutterNapi.nativeLookupCallbackInformation(handle); + } + + /** + * Constructs a new FlutterCallbackInformation instance. + * @param callbackName - The name of the callback function + * @param callbackClassName - The class name containing the callback + * @param callbackLibraryPath - The library path where the callback is defined + */ + constructor(callbackName?: string, callbackClassName?: string, callbackLibraryPath?: string) { + this.callbackName = callbackName; + this.callbackClassName = callbackClassName; + this.callbackLibraryPath = callbackLibraryPath; + } + + /** + * Initializes the callback information. + * @param callbackName - The name of the callback function + * @param callbackClassName - The class name containing the callback + * @param callbackLibraryPath - The library path where the callback is defined + */ + init(callbackName: string, callbackClassName: string, callbackLibraryPath: string) { + this.callbackName = callbackName; + this.callbackClassName = callbackClassName; + this.callbackLibraryPath = callbackLibraryPath; + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterRunArguments.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterRunArguments.ets new file mode 100644 index 0000000..7a32ede --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterRunArguments.ets @@ -0,0 +1,34 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on FlutterRunArguments.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ + +/** + * A class containing arguments for entering a FlutterNativeView's isolate for the first time. + * Contains the bundle path, entrypoint, and library path. + */ +export default class FlutterRunArguments { + /** The path to the Flutter bundle. */ + public bundlePath: string; + /** The entrypoint function name. */ + public entrypoint: string; + /** The path to the Dart library. */ + public libraryPath: string; + + /** + * Constructs a new FlutterRunArguments instance. + * @param bundlePath - The path to the Flutter bundle + * @param entrypoint - The entrypoint function name + * @param libraryPath - The path to the Dart library + */ + constructor(bundlePath: string, entrypoint: string, libraryPath: string) { + this.bundlePath = bundlePath; + this.entrypoint = entrypoint; + this.libraryPath = libraryPath; + } +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterView.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterView.ets new file mode 100644 index 0000000..09fad2b --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/FlutterView.ets @@ -0,0 +1,1212 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +*/ + +import FlutterEngine from '../embedding/engine/FlutterEngine'; +import Log from '../util/Log'; +import { DVModel, DVModelChildren, DVModelEvents, DVModelParameters } from './DynamicView/dynamicView'; +import { display } from '@kit.ArkUI' +import FlutterManager from '../embedding/ohos/FlutterManager'; +import window from '@ohos.window'; +import KeyboardManager from '../embedding/ohos/KeyboardManager'; +import MouseCursorPlugin from '../plugin/mouse/MouseCursorPlugin'; +import Settings from '../embedding/ohos/Settings'; +import ArrayList from '@ohos.util.ArrayList'; +import { EmbeddingNodeController } from '../embedding/ohos/EmbeddingNodeController'; +import PlatformView, { Params } from '../plugin/platform/PlatformView'; +import hiTraceMeter from '@ohos.hiTraceMeter' +import { JSON } from '@kit.ArkTS'; +import TextInputPlugin from '../plugin/editing/TextInputPlugin'; +import { accessibility } from '@kit.AccessibilityKit'; +import { uiObserver } from '@kit.ArkUI'; +import { common } from '@kit.AbilityKit'; +import { deviceInfo } from '@kit.BasicServicesKit'; +import KeyEventChannel from '../embedding/engine/systemchannels/KeyEventChannel'; +import StatusBarClickChannel from '../embedding/engine/systemchannels/StatusBarClickChannel'; +import { commonEventManager, BusinessError } from '@kit.BasicServicesKit'; + +const TAG = "FlutterViewTag"; +const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS'; +const DPI_SCALE_RESET: number = -1; + +/** + * Metrics describing the viewport dimensions and insets. + * This class holds information about the physical dimensions, padding, insets, and display features. + */ +export class ViewportMetrics { + /** Device pixel ratio for converting logical to physical pixels. */ + devicePixelRatio: number = 1.0; + /** Physical width of the viewport in pixels. */ + physicalWidth: number = 0; + /** Physical height of the viewport in pixels. */ + physicalHeight: number = 0; + /** Top padding of the viewport in physical pixels. */ + physicalViewPaddingTop: number = 0; + /** Right padding of the viewport in physical pixels. */ + physicalViewPaddingRight: number = 0; + /** Bottom padding of the viewport in physical pixels. */ + physicalViewPaddingBottom: number = 0; + /** Left padding of the viewport in physical pixels. */ + physicalViewPaddingLeft: number = 0; + /** Top inset of the viewport in physical pixels. */ + physicalViewInsetTop: number = 0; + /** Right inset of the viewport in physical pixels. */ + physicalViewInsetRight: number = 0; + /** Bottom inset of the viewport in physical pixels. */ + physicalViewInsetBottom: number = 0; + /** Left inset of the viewport in physical pixels. */ + physicalViewInsetLeft: number = 0; + /** Top system gesture inset in physical pixels. */ + systemGestureInsetTop: number = 0; + /** Right system gesture inset in physical pixels. */ + systemGestureInsetRight: number = 0; + /** Bottom system gesture inset in physical pixels. */ + systemGestureInsetBottom: number = 0; + /** Left system gesture inset in physical pixels. */ + systemGestureInsetLeft: number = 0; + /** Physical touch slop value, or -1 if not set. */ + physicalTouchSlop: number = -1; + /** List of display features such as folds, hinges, or cutouts. */ + displayFeatures: ArrayList = new ArrayList(); + + /** + * Creates a deep copy of this ViewportMetrics instance. + * @returns A new ViewportMetrics instance with copied values + */ + clone(): ViewportMetrics { + const copy = new ViewportMetrics(); + copy.devicePixelRatio = this.devicePixelRatio; + copy.physicalWidth = this.physicalWidth; + copy.physicalHeight = this.physicalHeight; + copy.physicalViewPaddingTop = this.physicalViewPaddingTop; + copy.physicalViewPaddingRight = this.physicalViewPaddingRight; + copy.physicalViewPaddingBottom = this.physicalViewPaddingBottom; + copy.physicalViewPaddingLeft = this.physicalViewPaddingLeft; + copy.physicalViewInsetTop = this.physicalViewInsetTop; + copy.physicalViewInsetRight = this.physicalViewInsetRight; + copy.physicalViewInsetBottom = this.physicalViewInsetBottom; + copy.physicalViewInsetLeft = this.physicalViewInsetLeft; + copy.systemGestureInsetTop = this.systemGestureInsetTop; + copy.systemGestureInsetRight = this.systemGestureInsetRight; + copy.systemGestureInsetBottom = this.systemGestureInsetBottom; + copy.systemGestureInsetLeft = this.systemGestureInsetLeft; + copy.physicalTouchSlop = this.physicalTouchSlop; + copy.displayFeatures = this.displayFeatures; + return copy; + } + + /** + * Checks if this ViewportMetrics is equal to another. + * @param other - The other ViewportMetrics to compare with + * @returns True if all metrics are equal, false otherwise + */ + isEqual(other: ViewportMetrics): boolean { + return this.devicePixelRatio === other.devicePixelRatio && + this.physicalWidth === other.physicalWidth && + this.physicalHeight === other.physicalHeight && + this.physicalViewPaddingTop === other.physicalViewPaddingTop && + this.physicalViewPaddingRight === other.physicalViewPaddingRight && + this.physicalViewPaddingBottom === other.physicalViewPaddingBottom && + this.physicalViewPaddingLeft === other.physicalViewPaddingLeft && + this.physicalViewInsetTop === other.physicalViewInsetTop && + this.physicalViewInsetRight === other.physicalViewInsetRight && + this.physicalViewInsetBottom === other.physicalViewInsetBottom && + this.physicalViewInsetLeft === other.physicalViewInsetLeft && + this.systemGestureInsetTop === other.systemGestureInsetTop && + this.systemGestureInsetRight === other.systemGestureInsetRight && + this.systemGestureInsetBottom === other.systemGestureInsetBottom && + this.systemGestureInsetLeft === other.systemGestureInsetLeft && + this.physicalTouchSlop === other.physicalTouchSlop && + this.displayFeatures === other.displayFeatures; + } +} + +/** + * Represents a display feature such as a fold, hinge, or cutout. + */ +export class DisplayFeature { + /** Bounding rectangle of the display feature. */ + bound: display.Rect; + /** Type of the display feature (fold, hinge, cutout, etc.). */ + type: DisplayFeatureType; + /** State of the display feature. */ + state: DisplayFeatureState; + + /** + * Constructs a new DisplayFeature instance. + * @param bound - The bounding rectangle of the feature + * @param type - The type of display feature + * @param state - The state of the display feature + */ + constructor(bound: display.Rect, type: DisplayFeatureType, state: DisplayFeatureState) { + this.bound = bound; + this.type = type; + this.state = state; + } + + /** + * Gets the bounding rectangle of the display feature. + * @returns The bounding rectangle + */ + getBound(): display.Rect { + return this.bound; + } + + /** + * Gets the type of the display feature. + * @returns The display feature type + */ + getType(): DisplayFeatureType { + return this.type; + } + + /** + * Gets the state of the display feature. + * @returns The display feature state + */ + getState(): DisplayFeatureState { + return this.state + } + + /** + * Sets the bounding rectangle of the display feature. + * @param bound - The bounding rectangle to set + */ + setBound(bound: display.Rect): void { + this.bound = bound; + } + + /** + * Sets the type of the display feature. + * @param type - The display feature type to set + */ + setType(type: DisplayFeatureType): void { + this.type = type; + } + + /** + * Sets the state of the display feature. + * @param state - The display feature state to set + */ + setState(state: DisplayFeatureState): void { + this.state = state; + } +} + +/** + * Enumeration of display feature types. + */ +export enum DisplayFeatureType { + /** Unknown display feature type */ + UNKNOWN = 0, + /** Fold display feature */ + FOLD = 1, + /** Hinge display feature */ + HINGE = 2, + /** Cutout display feature */ + CUTOUT = 3 +} + +/** + * Enumeration of display feature states. + */ +export enum DisplayFeatureState { + /** Unknown state */ + UNKNOWN = 0, + /** Flat posture */ + POSTURE_FLAT = 1, + /** Half-opened posture */ + POSTURE_HALF_OPENED = 2, +} + +/** + * Enumeration of display fold status. + */ +export enum DisplayFoldStatus { + /** Unknown fold status */ + FOLD_STATUS_UNKNOWN = 0, + /** Display is expanded */ + FOLD_STATUS_EXPANDED = 1, + /** Display is folded */ + FOLD_STATUS_FOLDED = 2, + /** Display is half-folded */ + FOLD_STATUS_HALF_FOLDED = 3 +} + +type callbackNumber = () => number + +/** + * Parameters for platform view layout. + * @deprecated since 3.7 + */ +export class PlatformViewParas { + /** The width of the platform view. */ + width: number = 0.0; + /** The height of the platform view. */ + height: number = 0.0; + /** The top position of the platform view. */ + top: number = 0.0; + /** The left position of the platform view. */ + left: number = 0.0; + /** The layout direction for the platform view. */ + direction: Direction = Direction.Auto; + + /** + * Sets the layout values. + * @param width - The width + * @param height - The height + * @param top - The top position + * @param left - The left position + */ + setValue(width: number, height: number, top: number, left: number): void { + this.width = width; + this.height = height; + this.top = top; + this.left = left; + } + + /** + * Sets the offset position. + * @param top - The top offset + * @param left - The left offset + */ + setOffset(top: number, left: number): void { + this.top = top; + this.left = left; + } +} + +/** + * Main view class for rendering Flutter content in OpenHarmony applications. + * This class manages the Flutter engine, viewport metrics, keyboard handling, + * and all interactions between Flutter and the native platform. + */ +export class FlutterView { + private flutterEngine: FlutterEngine | null = null + private id: string = "" + private isActive: boolean = true + private dVModel: DVModel = + new DVModel("Stack", new DVModelParameters(), new DVModelEvents(), new DVModelChildren(), null); + /** The wrapped builder for creating the view, or undefined if not set. */ + private wrapBuilder: WrappedBuilder<[Params]> | undefined = undefined; + private platformView: PlatformView | undefined = undefined; + private isSurfaceAvailableForRendering: boolean = false + private viewportMetrics = new ViewportMetrics(); + private displayInfo?: display.Display; + private keyboardManager: KeyboardManager | null = null; + private statusBarClickChannel: StatusBarClickChannel|null = null; + private mainWindow: window.Window | null = null; + private mouseCursorPlugin?: MouseCursorPlugin; + private textInputPlugin?: TextInputPlugin; + private uiContext?: UIContext | undefined; + private context: Context; + private settings?: Settings; + private mFirstFrameListeners: ArrayList; + private mFirstPreloadFrameListeners: ArrayList; + private isFlutterUiDisplayed: boolean = false; + private isFlutterUiPreload: boolean = false; + private surfaceId: string = "0"; + private nodeController: EmbeddingNodeController = new EmbeddingNodeController(); + private platformViewSize: PlatformViewParas = new PlatformViewParas(); + private checkFullScreen: boolean = true; + private checkKeyboard: boolean = true; + private checkGesture: boolean = true; + private checkAiBar: boolean = true; + private frameCache: boolean = true; + private paddingTop?: number; + private paddingBottom?: number; + private systemAvoidArea: window.AvoidArea; + private navigationAvoidArea: window.AvoidArea; + private gestureAvoidArea: window.AvoidArea; + private keyboardAvoidArea: window.AvoidArea; + private needSetViewport: boolean = false; + private windowPosition: window.Rect | null = null; + // 默认值5,参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pangesture + private callbackValue: callbackNumber = () => 5; + // DPI update flag to distinguish between system DPI updates and custom DPI updates + private isDevicePixelRatioAdaptive: boolean = false; + private lastHeight: number = 0; + private statusBarClickSubscriber: commonEventManager.CommonEventSubscriber | null = null; + + /** + * Constructs a new FlutterView instance. + * @param viewId - The unique identifier for this view + * @param context - The application context + */ + constructor(viewId: string, context: Context) { + this.id = viewId; + this.context = context; + this.displayInfo = display.getDefaultDisplaySync(); + this.viewportMetrics.devicePixelRatio = this.displayInfo?.densityPixels; + this.buildDisplayFeatures(display.getFoldStatus()); + + this.mainWindow = FlutterManager.getInstance() + .getWindowStage(FlutterManager.getInstance().getUIAbility(context)) + ?.getMainWindowSync(); + this.mFirstFrameListeners = new ArrayList(); + this.mFirstPreloadFrameListeners = new ArrayList(); + + this.mainWindow?.on('windowSizeChange', this.windowSizeChangeCallback); + this.mainWindow?.on('avoidAreaChange', this.avoidAreaChangeCallback); + this.mainWindow?.on('windowStatusChange', this.windowStatusChangeCallback); + this.mainWindow?.on('keyboardHeightChange', this.keyboardHeightChangeCallback); + this.mainWindow?.on('windowRectChange', this.windowRectChangeCallback); + //监听系统无障碍服务状态改变 + accessibility.on('accessibilityStateChange', this.accessibilityStateChangeCallback); + + this.systemAvoidArea = this.mainWindow?.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); + this.navigationAvoidArea = this.mainWindow?.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); + this.gestureAvoidArea = this.mainWindow?.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM_GESTURE); + this.keyboardAvoidArea = this.mainWindow?.getWindowAvoidArea(window.AvoidAreaType.TYPE_KEYBOARD); + commonEventManager.createSubscriber( + { events: ["usual.event.CLICK_STATUSBAR"] }, this.onStatusBarClick); + + // 监听折叠状态的改变 + display?.on('foldStatusChange', this.foldStatusChangeCallback); + + // Subscribes to display changes. Example: event that the display size is changed. + try { + display.on("change", this.displayChangeCallback); + } catch (e) { + Log.e(TAG, "displayInfo error" + JSON.stringify(e)); + } + this.context.eventHub.on('changeDevicePixelRatio', this.onDevicePixelRatioChange); + } + + onDevicePixelRatioChange = (dpiScaleFactor: number) => { + try { + // Get display information + this.displayInfo = display.getDefaultDisplaySync(); + + // Set DPI for text input channel + this.flutterEngine?.getTextInputChannel()?.setDevicePixelRatio(this.displayInfo.densityPixels); + + // Calculate device pixel ratio + // When dpiScaleFactor is DPI_SCALE_RESET, reset DPI to system default + let newDevicePixelRatio: number; + if (dpiScaleFactor === DPI_SCALE_RESET) { + newDevicePixelRatio = this.displayInfo.densityPixels; + Log.d(TAG, "Resetting device pixel ratio to system default: " + newDevicePixelRatio); + // When resetting DPI, set the flag to false + this.isDevicePixelRatioAdaptive = false; + } else { + newDevicePixelRatio = this.displayInfo.densityPixels * dpiScaleFactor; + Log.d(TAG, "Scaling device pixel ratio by factor " + dpiScaleFactor + ": " + newDevicePixelRatio); + // When customizing DPI, set the flag to true + this.isDevicePixelRatioAdaptive = true; + } + + // Only update when DPI actually changes + if (newDevicePixelRatio !== this.viewportMetrics.devicePixelRatio) { + this.viewportMetrics.devicePixelRatio = newDevicePixelRatio; + Log.i(TAG, "Device pixel ratio updated: " + newDevicePixelRatio + + " (scale factor: " + dpiScaleFactor + ", system DPI: " + this.displayInfo.densityPixels + ")"); + this.needSetViewport = true; + this.onAreaChange(null); + } + } catch (e) { + Log.e(TAG, "Error updating device pixel ratio: " + JSON.stringify(e)); + } + } + + /** + * Sets the callback for getting touch slop value. + * @param callback - The callback function that returns the touch slop value + */ + setTouchSlopCallbackValue(callback: callbackNumber) { + this.callbackValue = callback; + } + + private async buildDisplayFeatures(foldStatus: display.FoldStatus) { + let displayFeatures: ArrayList = new ArrayList(); + const displayInfo = display.getDefaultDisplaySync(); + /* + There are some bugs about getCurrentFoldCreaseRegion: + * 1. the crease region area is inaccurate + * 2. the creaseRegion.displayId and displayInfo.id are always both 0, in which case it is unable to + * distinguish whether the crease region is on the current screen. + * So do not add FOLD feature until the bugs are fixed. + */ + // if (display.isFoldable()) { + // let state: DisplayFeatureState = DisplayFeatureState.UNKNOWN; + // if (foldStatus == display.FoldStatus.FOLD_STATUS_EXPANDED) { + // state = DisplayFeatureState.POSTURE_FLAT; + // } else if (foldStatus == display.FoldStatus.FOLD_STATUS_HALF_FOLDED) { + // state = DisplayFeatureState.POSTURE_HALF_OPENED; + // } else { + // state = DisplayFeatureState.UNKNOWN; + // } + // let creaseRegion: display.FoldCreaseRegion = display.getCurrentFoldCreaseRegion(); + // if (creaseRegion.displayId == displayInfo.id) { + // for (let bound of creaseRegion.creaseRects) { + // displayFeatures.add(new DisplayFeature(bound, DisplayFeatureType.FOLD, state)); + // } + // } + // } + + let cutoutInfos = await displayInfo?.getCutoutInfo(); + for (let bound of cutoutInfos.boundingRects) { + displayFeatures.add(new DisplayFeature(bound, DisplayFeatureType.CUTOUT, DisplayFeatureState.UNKNOWN)); + } + Log.d(TAG, `device displayFeatures is : ${JSON.stringify(displayFeatures)}`); + this.viewportMetrics.displayFeatures = displayFeatures; + this.updateViewportMetrics(); + } + + private routerPageUpdateCallback = (info: uiObserver.RouterPageInfo) => { + if (this.getKeyboardHeight() !== 0 && info.state === uiObserver.RouterPageState.ON_PAGE_SHOW) { + this.flutterEngine?.getTextInputChannel()?.textInputMethodHandler?.hide(); + } + } + + private densityUpdateCallback = (info: uiObserver.DensityInfo) => { + try { + const sysDensity = display.getDefaultDisplaySync().densityPixels; + const customDensity = info.density; + if (sysDensity > 0 && customDensity > 0 && Number.isFinite(customDensity)) { + const scale = customDensity / sysDensity; + Log.i(TAG, "densityUpdateCallback: customDensity=" + customDensity + ", sysDensity=" + + sysDensity + ", scale=" + scale); + this.onDevicePixelRatioChange(scale); + } + } catch (e) { + Log.e(TAG, "densityUpdateCallback error: " + JSON.stringify(e)); + } + } + + private avoidAreaChangeCallback = (data: window.AvoidAreaOptions) => { + Log.i(TAG, "avoidAreaChangeCallback, type=" + data.type + ", area=" + JSON.stringify(data.area)); + + switch (data.type) { + case window.AvoidAreaType.TYPE_SYSTEM: + this.systemAvoidArea = data.area; + break; + case window.AvoidAreaType.TYPE_SYSTEM_GESTURE: + this.gestureAvoidArea = data.area; + break; + case window.AvoidAreaType.TYPE_KEYBOARD: + if (this.getKeyboardHeight() > 0) { + this.keyboardAvoidArea = data.area; + } else { + this.keyboardAvoidArea.bottomRect = { left: 0, top: 0, width: 0, height: 0 }; + } + break; + case window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR: + this.navigationAvoidArea = data.area; + break; + default: + break; + } + if (this.isAttachedToFlutterEngine()) { + this.onAreaChange(null); + } + } + private windowSizeChangeCallback = (data: window.Size) => { + Log.i(TAG, `windowSizeChangeCallback: width=${data.width}, height=${data.height}, lastHeight=${this.lastHeight}`); + + // Only handle when height changes + if (this.lastHeight !== data.height) { + try { + // If DPI has been customized, do not process system DPI changes + this.displayInfo = display.getDefaultDisplaySync(); + this.flutterEngine?.getTextInputChannel()?.setDevicePixelRatio(this.displayInfo.densityPixels); + + let devicePixelRatio: number = this.displayInfo.densityPixels; + Log.i(TAG, `windowSizeChangeCallback devicePixelRatio: ${devicePixelRatio}, viewportMetrics.devicePixelRatio: ${this.viewportMetrics.devicePixelRatio}`); + + if (devicePixelRatio !== this.viewportMetrics.devicePixelRatio) { + this.viewportMetrics.devicePixelRatio = devicePixelRatio; + Log.i(TAG, `windowSizeChangeCallback: Updated devicePixelRatio to ${devicePixelRatio}`); + this.needSetViewport = true; + } + } catch (e) { + Log.e(TAG, `windowSizeChangeCallback error: ${JSON.stringify(e)}`); + } + } + + // Update lastHeight + this.lastHeight = data.height; + + // Notify area change, this method handles all necessary view updates + if (this.isAttachedToFlutterEngine()) { + this.onAreaChange(null); + } + } + private windowStatusChangeCallback = (data: window.WindowStatusType) => { + Log.i(TAG, "windowStatusChangeCallback " + data); + if (this.isAttachedToFlutterEngine()) { + FlutterManager.getInstance().getFullScreenListener().onScreenStateChanged(data); + } + }; + private displayChangeCallback = (data: number) => { + // If DPI has been customized, do not process system DPI changes + if (this.isDevicePixelRatioAdaptive) { + Log.d(TAG, "Skipping display change callback due to custom DPI setting"); + return; + } + + this.displayInfo = display.getDefaultDisplaySync(); + this.flutterEngine?.getTextInputChannel()?.setDevicePixelRatio(this.displayInfo.densityPixels); + let devicePixelRatio: number = this.displayInfo?.densityPixels; + Log.i(TAG, "Display on: " + JSON.stringify(this.displayInfo) + ". Display id:" + JSON.stringify(data)) + if (devicePixelRatio != this.viewportMetrics.devicePixelRatio) { + this.viewportMetrics.devicePixelRatio = devicePixelRatio; + this.needSetViewport = true; + this.onAreaChange(null); + } + this.flutterEngine?.getFlutterNapi()?.updateRefreshRate(this.displayInfo?.refreshRate); + } + private keyboardHeightChangeCallback = (data: number) => { + Log.i(TAG, "keyboardHeightChangeCallback " + data); + if (this.keyboardAvoidArea) { + this.keyboardAvoidArea.bottomRect.height = data; + } + this.onAreaChange(null); + }; + private windowRectChangeCallback = (data: window.RectChangeOptions) => { + Log.i(TAG, "windowRectChangeCallback " + data); + this.windowPosition = data.rect as window.Rect; + this.flutterEngine?.getTextInputChannel()?.setWindowPosition(this.windowPosition); + } + private accessibilityStateChangeCallback = (data: boolean) => { + Log.i(TAG, `subscribe accessibility state change, result: ${JSON.stringify(data)}`); + this.flutterEngine?.getFlutterNapi()?.accessibilityStateChange(data); + } + private foldStatusChangeCallback = (data: display.FoldStatus) => { + Log.d(TAG, `Fold status change to ${JSON.stringify(data)}`) + this.buildDisplayFeatures(data); + } + private windowTitleButtonRectChangeCallback = (data: window.TitleButtonRect) => { + Log.i(TAG, "windowTitleButtonRectChangeCallback " + JSON.stringify(data)); + FlutterManager.getInstance().handleWindowDecorSafeArea(this.id, this.context); + } + + /** + * Gets the view ID. + * @returns The view ID + */ + getId(): string { + return this.id; + } + + /** + * Sets the active state of the view. + * @param value - True to activate, false to deactivate + */ + setActive(value: boolean): void { + this.isActive = value; + } + + /** + * Gets the active state of the view. + * @returns True if active, false otherwise + */ + getActive(): boolean { + return this.isActive; + } + + /** + * Sets the surface ID for rendering. + * @param surfaceId - The surface ID + */ + setSurfaceId(surfaceId: string): void { + this.surfaceId = surfaceId; + } + + /** + * Gets the surface ID. + * @returns The surface ID + */ + getSurfaceId(): string { + return this.surfaceId; + } + + /** + * Gets the embedding node controller. + * @deprecated since 3.7 + * @returns The EmbeddingNodeController instance + */ + getEmbeddingNodeController(): EmbeddingNodeController { + return this.nodeController; + } + + /** + * Sets the wrapped builder for platform views. + * @param wrappedBuilder - The WrappedBuilder instance + */ + setWrappedBuilder(wrappedBuilder: WrappedBuilder<[Params]>) { + this.wrapBuilder = wrappedBuilder; + } + + /** + * Gets the wrapped builder. + * @returns The WrappedBuilder instance, or undefined if not set + */ + getWrappedBuilder(): WrappedBuilder<[Params]> | undefined { + return this.wrapBuilder; + } + + /** + * Sets the platform view. + * @param platformView - The PlatformView instance + */ + setPlatformView(platformView: PlatformView) { + this.platformView = platformView; + } + + /** + * Gets the platform view. + * @returns The PlatformView instance, or undefined if not set + */ + getPlatformView(): PlatformView | undefined { + return this.platformView; + } + + /** + * Gets the platform view size. + * @deprecated since 3.7 + * @returns The PlatformViewParas instance + */ + getPlatformViewSize(): PlatformViewParas { + return this.platformViewSize; + } + + /** + * Gets the DynamicView model. + * @returns The DVModel instance + */ + getDVModel() { + return this.dVModel; + } + + /** + * Gets the current keyboard height. + * @returns The keyboard height in pixels, or 0 if keyboard is not visible + */ + getKeyboardHeight() { + return this.keyboardAvoidArea?.bottomRect.height + } + + /** + * Called when the view is being destroyed. + * Cleans up all event listeners and resources. + */ + onDestroy() { + try { + uiObserver.off('routerPageUpdate', this.uiContext as UIContext, this.routerPageUpdateCallback); + uiObserver.off('densityUpdate', this.uiContext as UIContext, this.densityUpdateCallback); + this.mainWindow?.off('windowSizeChange', this.windowSizeChangeCallback); + this.mainWindow?.off('avoidAreaChange', this.avoidAreaChangeCallback); + this.mainWindow?.off('windowStatusChange', this.windowStatusChangeCallback); + this.mainWindow?.off('keyboardHeightChange', this.keyboardHeightChangeCallback); + this.mainWindow?.off('windowRectChange', this.windowRectChangeCallback); + this.mainWindow?.off('windowTitleButtonRectChange', this.windowTitleButtonRectChangeCallback); + accessibility.off('accessibilityStateChange', this.accessibilityStateChangeCallback); + display.off('foldStatusChange', this.foldStatusChangeCallback); + this.context.eventHub.off('changeDevicePixelRatio', this.onDevicePixelRatioChange); + } catch (e) { + Log.e(TAG, "mainWindow off error: " + JSON.stringify(e)); + } + this.mainWindow = null; + + try { + display.off("change", this.displayChangeCallback); + } catch (e) { + Log.e(TAG, "displayInfo off error" + JSON.stringify(e)); + } + FlutterManager.getInstance().deleteFlutterView(this.id, this); + + this.nodeController.disposeFrameNode(); + } + + /** + * Attaches this view to a Flutter engine. + * @param flutterEngine - The FlutterEngine to attach to + */ + attachToFlutterEngine(flutterEngine: FlutterEngine): void { + hiTraceMeter.startTrace("attachToFlutterEngine", 0); + if (this.isAttachedToFlutterEngine()) { + if (flutterEngine == this.flutterEngine) { + Log.i(TAG, "Already attached to this engine. Doing nothing."); + return; + } + // Detach from a previous FlutterEngine so we can attach to this new one.f + Log.i( + TAG, + "Currently attached to a different engine. Detaching and then attaching" + + " to new engine."); + this.detachFromFlutterEngine(); + } + Log.i(TAG, "attachToFlutterEngine"); + this.flutterEngine = flutterEngine; + this.flutterEngine?.getFlutterNapi().xComponentAttachFlutterEngine(this.id) + this.flutterEngine?.getFlutterNapi()?.updateRefreshRate(this.displayInfo!.refreshRate) + this.flutterEngine?.getFlutterNapi()?.updateSize(this.displayInfo!.width, this.displayInfo!.height) + this.flutterEngine?.getFlutterNapi()?.updateDensity(this.displayInfo!.densityPixels) + this.flutterEngine?.getFlutterNapi().enableFrameCache(this.frameCache); + if (accessibility.isOpenAccessibilitySync()) { + this.flutterEngine?.getFlutterNapi()?.accessibilityStateChange(true); + } + flutterEngine.getPlatformViewsController()?.attachToView(this); + + let newArea: Area | null = { + width: px2vp(this.displayInfo!.width), + height: px2vp(this.displayInfo!.height), + position: { x: 0, y: 0 }, + globalPosition: { x: 0, y: 0 } + }; + if (this.viewportMetrics.physicalWidth != 0 || this.viewportMetrics.physicalHeight != 0) { + newArea = null; + } + this.onAreaChange(newArea, true); + + this.context.eventHub.on(EVENT_BACK_PRESS, () => { + if (this?.getKeyboardHeight() == 0) { + this.flutterEngine?.getNavigationChannel()?.popRoute(); + } else { + this.flutterEngine?.getTextInputChannel()?.textInputMethodHandler?.hide(); + } + }); + + let windowId = this.mainWindow?.getWindowProperties()?.id ?? 0 + this.mouseCursorPlugin = new MouseCursorPlugin(windowId, this.flutterEngine?.getMouseCursorChannel()!); + this.textInputPlugin = new TextInputPlugin(this.flutterEngine?.getTextInputChannel()!, this.id, + new KeyEventChannel(this.flutterEngine.dartExecutor)); + this.statusBarClickChannel = new StatusBarClickChannel(flutterEngine.dartExecutor) + this.keyboardManager = new KeyboardManager(flutterEngine, this.textInputPlugin!); + this.settings = new Settings(this.flutterEngine.getSettingsChannel()!); + this.sendSettings(); + this.isFlutterUiDisplayed = this.flutterEngine.getFlutterNapi().isDisplayingFlutterUi; + this.isFlutterUiPreload = this.flutterEngine.getFlutterNapi().isPreloadedFlutterUi; + if (this.isFlutterUiPreload) { + this.onFirstFrame(1); + } + if (this.isFlutterUiDisplayed) { + this.onFirstFrame(); + } + if (this.isSurfaceAvailableForRendering) { + this.flutterEngine?.processPendingMessages(); + } + hiTraceMeter.finishTrace("attachToFlutterEngine", 0); + } + + /** + * Called before drawing a frame. + * @param width - The width of the drawing area, defaults to display width + * @param height - The height of the drawing area, defaults to display height + */ + preDraw(width: number = 0, height: number = 0): void { + if (this.isAttachedToFlutterEngine()) { + if (width == 0 || height == 0) { + width = this.displayInfo!.width; + height = this.displayInfo!.height; + } + this.flutterEngine?.getFlutterNapi().xComponentPreDraw(this.id, width, height); + } + } + + /** + * Detaches this view from the Flutter engine. + * Cleans up all engine-related resources. + */ + detachFromFlutterEngine(): void { + Log.i(TAG, "detachFromFlutterEngine"); + if (!this.isAttachedToFlutterEngine()) { + Log.d(TAG, "FlutterView not attached to an engine. Not detaching."); + return; + } + if (this.isSurfaceAvailableForRendering) { + this.flutterEngine!!.getFlutterNapi().xComponentDetachFlutterEngine(this.id) + } + this.flutterEngine?.getPlatformViewsController()?.detachFromView(); + this.flutterEngine = null; + this.keyboardManager = null; + this.textInputPlugin?.destroy(); + this.context?.eventHub.off(EVENT_BACK_PRESS); + } + + /** + * Called when the window is created. + * Initializes the UIContext and sends settings to Flutter. + */ + onWindowCreated() { + Log.d(TAG, "received onwindowCreated."); + let _UIContext = this.mainWindow?.getUIContext(); + this.uiContext = _UIContext; + // Listen to route navigation (Flutter navigating to HarmonyOS native), soft keyboard management + uiObserver.on('routerPageUpdate', this.uiContext as UIContext, this.routerPageUpdateCallback); + uiObserver.on('densityUpdate', this.uiContext as UIContext, this.densityUpdateCallback); + this.sendSettings(); + this.mainWindow?.on('windowTitleButtonRectChange', this.windowTitleButtonRectChangeCallback); + Log.d(TAG, "uiContext init and sendSettings finished."); + } + + /** + * Sends system settings to Flutter. + */ + sendSettings(): void { + if (this.uiContext != undefined && this.isAttachedToFlutterEngine()) { + this.settings?.sendSettings(this.uiContext.getMediaQuery()); + } else { + Log.e(TAG, "UIContext is null, cannot send Settings!"); + } + } + + /** + * Called when the rendering surface is created. + * Marks the surface as available and processes pending messages. + */ + onSurfaceCreated() { + this.isSurfaceAvailableForRendering = true; + this.flutterEngine?.processPendingMessages(); + } + + /** + * Called when the rendering surface is destroyed. + * Marks the surface as unavailable and detaches from the engine. + */ + onSurfaceDestroyed() { + this.isSurfaceAvailableForRendering = false; + if (this.isAttachedToFlutterEngine()) { + this.flutterEngine!!.getFlutterNapi().xComponentDetachFlutterEngine(this.id) + } + } + + /** + * Called when the view area changes. + * Updates viewport metrics based on the new area and avoid areas. + * @param newArea - The new area, or null to use current display dimensions + * @param setFullScreen - Whether to set fullscreen mode + */ + onAreaChange(newArea: Area | null, setFullScreen: boolean = false) { + const originalMetrics = this.viewportMetrics.clone(); + if (newArea != null) { + this.viewportMetrics.physicalWidth = vp2px(newArea.width as number); + this.viewportMetrics.physicalHeight = vp2px(newArea.height as number); + } + let fullScreen = false + // 根据是否全屏显示,设置标题栏高度 + if (this.checkFullScreen && + (setFullScreen || FlutterManager.getInstance().getFullScreenListener().useFullScreen())) { // 全屏显示 + fullScreen = true + if (this.paddingTop != undefined) { + this.viewportMetrics.physicalViewPaddingTop = this.paddingTop; + } else { + this.viewportMetrics.physicalViewPaddingTop = + this.systemAvoidArea?.topRect.height ?? this.viewportMetrics.physicalViewPaddingTop; + } + if (this.paddingBottom != undefined) { + this.viewportMetrics.physicalViewPaddingBottom = this.paddingBottom; + } else { + this.viewportMetrics.physicalViewPaddingBottom = + this.systemAvoidArea?.bottomRect.height ?? this.viewportMetrics.physicalViewPaddingBottom; + } + } else { // 非全屏显示 + this.viewportMetrics.physicalViewPaddingTop = this.paddingTop ?? 0; + this.viewportMetrics.physicalViewPaddingBottom = this.paddingBottom ?? 0; + } + + this.viewportMetrics.physicalViewPaddingLeft = + this.systemAvoidArea?.leftRect.width ?? this.viewportMetrics.physicalViewPaddingLeft; + this.viewportMetrics.physicalViewPaddingRight = + this.systemAvoidArea?.rightRect.width ?? this.viewportMetrics.physicalViewPaddingRight; + + this.onKeyboardAreaChange(fullScreen) + this.onAiBarAreaChange(fullScreen) + this.onGestureAreaChange(fullScreen) + if (!this.viewportMetrics.isEqual(originalMetrics) || this.needSetViewport) { + if (!this.updateViewportMetrics()) { + this.needSetViewport = true; + } else { + this.needSetViewport = false; + } + } + } + + private onAiBarAreaChange(fullScreen: boolean = false) { + if (this.checkAiBar && this.navigationAvoidArea != null && fullScreen) { + this.viewportMetrics.physicalViewPaddingBottom = + Math.max(this.navigationAvoidArea?.bottomRect.height, this.viewportMetrics.physicalViewPaddingBottom) + } + } + + private onKeyboardAreaChange(fullScreen: boolean = false) { + if (this.checkKeyboard && fullScreen) { + this.viewportMetrics.physicalViewInsetTop = + this.keyboardAvoidArea?.topRect.height ?? this.viewportMetrics.physicalViewInsetTop + this.viewportMetrics.physicalViewInsetLeft = + this.keyboardAvoidArea?.leftRect.width ?? this.viewportMetrics.physicalViewInsetLeft + this.viewportMetrics.physicalViewInsetBottom = + this.keyboardAvoidArea?.bottomRect.height ?? this.viewportMetrics.physicalViewInsetBottom + this.viewportMetrics.physicalViewInsetRight = + this.keyboardAvoidArea?.rightRect.width ?? this.viewportMetrics.physicalViewInsetRight + } else { + this.viewportMetrics.physicalViewInsetTop = 0 + this.viewportMetrics.physicalViewInsetLeft = 0 + this.viewportMetrics.physicalViewInsetBottom = 0 + this.viewportMetrics.physicalViewInsetRight = 0 + } + } + + private onGestureAreaChange(fullScreen: boolean = false) { + if (this.checkGesture && fullScreen) { + this.viewportMetrics.systemGestureInsetTop = + this.gestureAvoidArea?.topRect.height ?? this.viewportMetrics.systemGestureInsetTop + this.viewportMetrics.systemGestureInsetLeft = + this.gestureAvoidArea?.leftRect.width ?? this.viewportMetrics.systemGestureInsetLeft + this.viewportMetrics.systemGestureInsetBottom = + Math.max(this.navigationAvoidArea?.bottomRect.height, this.gestureAvoidArea?.bottomRect.height) + this.viewportMetrics.systemGestureInsetRight = + this.gestureAvoidArea?.rightRect.width ?? this.viewportMetrics.systemGestureInsetRight + } else { + this.viewportMetrics.systemGestureInsetTop = 0 + this.viewportMetrics.systemGestureInsetLeft = 0 + this.viewportMetrics.systemGestureInsetBottom = 0 + this.viewportMetrics.systemGestureInsetRight = 0 + } + } + + /** + * Checks if this view is attached to a Flutter engine. + * @returns True if attached, false otherwise + */ + public isAttachedToFlutterEngine(): boolean { + return this.flutterEngine != null + } + + /** + * Checks if this view is attached to an engine with the specified shell holder ID. + * @param id - The shell holder ID to check + * @returns True if attached to an engine with the specified ID, false otherwise + */ + public isSameEngineShellHolderId(id: number): boolean { + if (this.flutterEngine) { + let flutterNapi = this.flutterEngine.getFlutterNapi(); + if (flutterNapi.nativeShellHolderId == id && id != 0) { + return true; + } + } + return false; + } + + private updateViewportMetrics(): boolean { + if (this.isAttachedToFlutterEngine()) { + const displayFeatures = this.viewportMetrics.displayFeatures; + let displayFeatureBound: number[] = new Array(displayFeatures.length * 4); + let displayFeatureType: number[] = new Array(displayFeatures.length); + let displayFeatureStatus: number[] = new Array(displayFeatures.length); + for (let i = 0; i < displayFeatures.length; i++) { + let singleFeatureBound = displayFeatures[i].getBound(); + displayFeatureBound[4 * i] = singleFeatureBound.left; + displayFeatureBound[4 * i + 1] = singleFeatureBound.top + displayFeatureBound[4 * i + 2] = singleFeatureBound.left + singleFeatureBound.width; + displayFeatureBound[4 * i + 3] = singleFeatureBound.top + singleFeatureBound.height; + displayFeatureType[i] = displayFeatures[i].getType(); + displayFeatureStatus[i] = displayFeatures[i].getState(); + } + + this.viewportMetrics.physicalTouchSlop = this.callbackValue(); + this?.flutterEngine?.getFlutterNapi()?.setViewportMetrics(this.viewportMetrics.devicePixelRatio, + this.viewportMetrics.physicalWidth, + this.viewportMetrics.physicalHeight, + this.viewportMetrics.physicalViewPaddingTop, + this.viewportMetrics.physicalViewPaddingRight, + this.viewportMetrics.physicalViewPaddingBottom, + this.viewportMetrics.physicalViewPaddingLeft, + this.viewportMetrics.physicalViewInsetTop, + this.viewportMetrics.physicalViewInsetRight, + this.viewportMetrics.physicalViewInsetBottom, + this.viewportMetrics.physicalViewInsetLeft, + this.viewportMetrics.systemGestureInsetTop, + this.viewportMetrics.systemGestureInsetRight, + this.viewportMetrics.systemGestureInsetBottom, + this.viewportMetrics.systemGestureInsetLeft, + this.viewportMetrics.physicalTouchSlop, + displayFeatureBound, + displayFeatureType, + displayFeatureStatus) + return true + } + return false + } + + onStatusBarClick = (err: BusinessError, subscriber: commonEventManager.CommonEventSubscriber) => { + this.statusBarClickSubscriber = subscriber + if(subscriber !== null) { + commonEventManager.subscribe(subscriber, (err, data) => { + this.statusBarClickChannel?.sendClick() + }) + } + } + + /** + * Handles key events before they reach the input method editor. + * @param event - The key event + * @returns True if the event was handled, false otherwise + */ + onKeyPreIme(event: KeyEvent): boolean { + return this.keyboardManager?.onKeyPreIme(event) ?? false; + } + + /** + * Handles key events. + * @param event - The key event + * @returns True if the event was handled, false otherwise + */ + onKeyEvent(event: KeyEvent): boolean { + return this.keyboardManager?.onKeyEvent(event) ?? false; + } + + /** + * Handles mouse wheel events. + * @param eventType - The event type + * @param event - The pan gesture event + */ + onMouseWheel(eventType: string, event: PanGestureEvent) { + if (deviceInfo.sdkApiVersion < 15) { // API15 及以后通过轴事件处理滚动 + this.flutterEngine?.getFlutterNapi()?.xComponentDisPatchMouseWheel(this.id, eventType, event); + } + } + + /** + * Adds a listener for the first frame event. + * @param listener - The listener to add + */ + addFirstFrameListener(listener: FirstFrameListener) { + this.mFirstFrameListeners.add(listener); + } + + /** + * Removes a first frame listener. + * @param listener - The listener to remove + */ + removeFirstFrameListener(listener: FirstFrameListener) { + this.mFirstFrameListeners.remove(listener); + } + + /** + * Adds a listener for the first preload frame event. + * @param listener - The listener to add + */ + addFirstPreloadFrameListener(listener: FirstPreloadFrameListener) { + this.mFirstPreloadFrameListeners.add(listener); + } + + /** + * Removes a first preload frame listener. + * @param listener - The listener to remove + */ + removeFirstPreloadFrameListener(listener: FirstPreloadFrameListener) { + this.mFirstPreloadFrameListeners.remove(listener); + } + + /** + * Checks if the first frame has been rendered. + * @returns True if the first frame has been rendered, false otherwise + */ + hasRenderedFirstFrame(): boolean { + return this.isFlutterUiDisplayed; + } + + /** + * Called when the first frame is rendered. + * Notifies all registered listeners. + * @param isPreload - 1 for preload frame, 0 for normal first frame + */ + onFirstFrame(isPreload: number = 0) { + if (isPreload) { + let listeners = this.mFirstPreloadFrameListeners.clone(); + listeners.forEach((listener) => { + listener.onFirstPreloadFrame(); + }) + } else { + let listeners = this.mFirstFrameListeners.clone(); + listeners.forEach((listener) => { + listener.onFirstFrame(); + }) + } + } + + /** + * Sets whether to check fullscreen mode. + * @param check - True to check fullscreen, false otherwise + */ + setCheckFullScreen(check: boolean) { + this.checkFullScreen = check; + } + + /** + * Sets whether to check keyboard area. + * @param check - True to check keyboard area, false otherwise + */ + setCheckKeyboard(check: boolean) { + this.checkKeyboard = check + } + + /** + * Sets whether to check gesture area. + * @param check - True to check gesture area, false otherwise + */ + setCheckGesture(check: boolean) { + this.checkGesture = check + } + + /** + * Sets whether to check AI bar area. + * @param check - True to check AI bar area, false otherwise + */ + setCheckAiBar(check: boolean) { + this.checkAiBar = check + } + + /** + * Sets the top padding value. + * @param paddingTop - The top padding value, or undefined to use default + */ + setPaddingTop(paddingTop?: number) { + this.paddingTop = paddingTop; + this.onAreaChange(null); + } + + /** + * Sets the bottom padding value. + * @param paddingBottom - The bottom padding value, or undefined to use default + */ + setPaddingBottom(paddingBottom?: number) { + this.paddingBottom = paddingBottom; + this.onAreaChange(null); + } + + /** + * Enables or disables frame caching. + * @param cacheEnable - True to enable frame cache, false to disable + */ + enableFrameCache(cacheEnable: boolean) { + this.frameCache = cacheEnable; + if (this.isAttachedToFlutterEngine()) { + this.flutterEngine?.getFlutterNapi().enableFrameCache(cacheEnable); + } + } +} + +/** + * Listener interface for first frame events. + */ +export interface FirstFrameListener { + /** + * Called when the first frame is rendered. + */ + onFirstFrame(): void; +} + +/** + * Listener interface for first preload frame events. + */ +export interface FirstPreloadFrameListener { + /** + * Called when the first preload frame is rendered. + */ + onFirstPreloadFrame(): void; +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/TextureRegistry.ets b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/TextureRegistry.ets new file mode 100644 index 0000000..6a7382d --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/ets/view/TextureRegistry.ets @@ -0,0 +1,92 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. All rights reserved. +* Use of this source code is governed by a BSD-style license that can be +* found in the LICENSE_KHZG file. +* +* Based on TextureRegistry.java originally written by +* Copyright (C) 2013 The Flutter Authors. +* +*/ +import image from '@ohos.multimedia.image'; + +/** + * Registry of backend textures used with a single FlutterView instance. + * Entries may be embedded into the Flutter view using the Texture widget. + * Textures can be created from surface textures, image receivers, or pixel maps. + */ +export interface TextureRegistry { + + createSurfaceTexture(): SurfaceTextureEntry; + + getTextureId(): number; + + registerTexture(textureId: number): SurfaceTextureEntry; + + registerSurfaceTexture(receiver: image.ImageReceiver): SurfaceTextureEntry; + + registerPixelMap(pixelMap: PixelMap): number; + + setTextureBackGroundPixelMap(textureId: number, pixelMap: PixelMap): void; + + /** + * @deprecated since 3.7 + */ + setTextureBackGroundColor(textureId: number, color: number): void; + + setTextureBufferSize(textureId: number, width: number, height: number): void; + + notifyTextureResizing(textureId: number, width: number, height: number): void; + + /** + * @deprecated since 3.22 + * @useinstead TextureRegistry#setExternalNativeImagePtr + */ + setExternalNativeImage(textureId: number, native_image: number): boolean; + + setExternalNativeImagePtr(textureId: number, native_image: bigint): boolean; + + resetExternalTexture(textureId: number, need_surfaceId: boolean): number; + + unregisterTexture(textureId: number): void; + + onTrimMemory(level: number): void; +} + +/** + * Entry representing a surface texture registered with the texture registry. + */ +export interface SurfaceTextureEntry { + getTextureId(): number; + + getSurfaceId(): number; + + /* + * This return value is OHNativeWindow* in native code. + * Once converted to OHNativeWindow*, it can be used to create an EGLSurface or VkSurface for rendering. + * This OHNativeWindow* needn't be released when invoking unregisterTexture. + */ + getNativeWindowId(): number; + + release(): void; +} + +/** + * Listener for frame consumption events. + */ +export interface OnFrameConsumedListener { + /** + * Called when a frame has been consumed. + */ + onFrameConsumed(): void; +} + +/** + * Listener for memory trim events. + */ +export interface OnTrimMemoryListener { + /** + * Called when memory should be trimmed. + * @param level - The memory trim level + */ + onTrimMemory(level: number): void; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/module.json b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/module.json new file mode 100644 index 0000000..a3a1dfa --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/@ohos/flutter_ohos/src/main/module.json @@ -0,0 +1,30 @@ +{ + "app": { + "bundleName": "com.example.config", + "debug": false, + "versionCode": 1000000, + "versionName": "1.0.0", + "minAPIVersion": 50000012, + "targetAPIVersion": 60001021, + "apiReleaseType": "Release", + "targetMinorAPIVersion": 0, + "targetPatchAPIVersion": 0, + "compileSdkVersion": "6.0.1.112", + "compileSdkType": "HarmonyOS", + "appEnvironments": [], + "bundleType": "app", + "buildMode": "release" + }, + "module": { + "name": "flutter", + "type": "har", + "deviceTypes": [ + "default" + ], + "packageName": "@ohos/flutter_ohos", + "installationFree": false, + "virtualMachine": "ark", + "compileMode": "esmodule", + "dependencies": [] + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/BuildProfile.ets b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/BuildProfile.ets new file mode 100644 index 0000000..e599d28 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0-e34a685f4b'; +export const BUILD_MODE_NAME = 'release'; +export const DEBUG = false; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/Index.ets b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/Index.ets new file mode 100644 index 0000000..e69de29 diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/build-profile.json5 b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/build-profile.json5 new file mode 100644 index 0000000..e3fb899 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/build-profile.json5 @@ -0,0 +1,34 @@ +{ + "apiType": "stageMode", + "buildOption": { + "nativeLib": { + "debugSymbol": { + "strip": false, + "exclude": [] + } + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + }, + "consumerFiles": [ + "./consumer-rules.txt" + ] + } + }, + }, + ], + "targets": [ + { + "name": "default" + } + ] +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/consumer-rules.txt b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/consumer-rules.txt new file mode 100644 index 0000000..e69de29 diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/hvigorfile.ts b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/hvigorfile.ts new file mode 100644 index 0000000..4218707 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/libs/arm64-v8a/libflutter.so b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/libs/arm64-v8a/libflutter.so new file mode 100644 index 0000000..901632c Binary files /dev/null and b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/libs/arm64-v8a/libflutter.so differ diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/obfuscation-rules.txt b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/obfuscation-rules.txt new file mode 100644 index 0000000..272efb6 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/obfuscation.txt b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/obfuscation.txt new file mode 100644 index 0000000..e69de29 diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/oh-package.json5 b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/oh-package.json5 new file mode 100644 index 0000000..739d5e1 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/oh-package.json5 @@ -0,0 +1 @@ +{"name":"flutter_native_arm64_v8a","version":"1.0.0-e34a685f4b","description":"Place so files for flutter on ohos.","main":"Index.ets","author":"","license":"Apache-2.0","dependencies":{},"metadata":{"sourceRoots":["./src/main"],"debug":false,"nativeDebugSymbol":false},"compatibleSdkVersionStage":"beta1","compatibleSdkVersion":12,"compatibleSdkType":"HarmonyOS","obfuscated":false} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/src/main/module.json b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/src/main/module.json new file mode 100644 index 0000000..c09f8ef --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_arm64_v8a/src/main/module.json @@ -0,0 +1,30 @@ +{ + "app": { + "bundleName": "com.example.config", + "debug": false, + "versionCode": 1000000, + "versionName": "1.0.0", + "minAPIVersion": 50000012, + "targetAPIVersion": 60001021, + "apiReleaseType": "Release", + "targetMinorAPIVersion": 0, + "targetPatchAPIVersion": 0, + "compileSdkVersion": "6.0.1.112", + "compileSdkType": "HarmonyOS", + "appEnvironments": [], + "bundleType": "app", + "buildMode": "release" + }, + "module": { + "name": "flutter_native", + "type": "har", + "deviceTypes": [ + "default" + ], + "packageName": "flutter_native_arm64_v8a", + "installationFree": false, + "virtualMachine": "ark", + "compileMode": "esmodule", + "dependencies": [] + } +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/BuildProfile.ets b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/BuildProfile.ets new file mode 100644 index 0000000..e599d28 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0-e34a685f4b'; +export const BUILD_MODE_NAME = 'release'; +export const DEBUG = false; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/Index.ets b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/Index.ets new file mode 100644 index 0000000..e69de29 diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/build-profile.json5 b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/build-profile.json5 new file mode 100644 index 0000000..e3fb899 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/build-profile.json5 @@ -0,0 +1,34 @@ +{ + "apiType": "stageMode", + "buildOption": { + "nativeLib": { + "debugSymbol": { + "strip": false, + "exclude": [] + } + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + }, + "consumerFiles": [ + "./consumer-rules.txt" + ] + } + }, + }, + ], + "targets": [ + { + "name": "default" + } + ] +} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/consumer-rules.txt b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/consumer-rules.txt new file mode 100644 index 0000000..e69de29 diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/hvigorfile.ts b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/hvigorfile.ts new file mode 100644 index 0000000..4218707 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/dist/MomKitchen_Setup_0.99.1.exe b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/libs/x86_64/libflutter.so similarity index 61% rename from dist/MomKitchen_Setup_0.99.1.exe rename to packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/libs/x86_64/libflutter.so index edd7048..6a1ed1f 100644 Binary files a/dist/MomKitchen_Setup_0.99.1.exe and b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/libs/x86_64/libflutter.so differ diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/obfuscation-rules.txt b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/obfuscation-rules.txt new file mode 100644 index 0000000..272efb6 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/obfuscation.txt b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/obfuscation.txt new file mode 100644 index 0000000..e69de29 diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/oh-package.json5 b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/oh-package.json5 new file mode 100644 index 0000000..d375780 --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/oh-package.json5 @@ -0,0 +1 @@ +{"name":"flutter_native_x86_64","version":"1.0.0-e34a685f4b","description":"Place so files for flutter on ohos.","main":"Index.ets","author":"","license":"Apache-2.0","dependencies":{},"metadata":{"sourceRoots":["./src/main"],"debug":false,"nativeDebugSymbol":false},"compatibleSdkVersionStage":"beta1","compatibleSdkVersion":12,"compatibleSdkType":"HarmonyOS","obfuscated":false} diff --git a/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/src/main/module.json b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/src/main/module.json new file mode 100644 index 0000000..3f478ff --- /dev/null +++ b/packages/fluttertoast_ohos/ohos/oh_modules/flutter_native_x86_64/src/main/module.json @@ -0,0 +1,30 @@ +{ + "app": { + "bundleName": "com.example.config", + "debug": false, + "versionCode": 1000000, + "versionName": "1.0.0", + "minAPIVersion": 50000012, + "targetAPIVersion": 60001021, + "apiReleaseType": "Release", + "targetMinorAPIVersion": 0, + "targetPatchAPIVersion": 0, + "compileSdkVersion": "6.0.1.112", + "compileSdkType": "HarmonyOS", + "appEnvironments": [], + "bundleType": "app", + "buildMode": "release" + }, + "module": { + "name": "flutter_native", + "type": "har", + "deviceTypes": [ + "default" + ], + "packageName": "flutter_native_x86_64", + "installationFree": false, + "virtualMachine": "ark", + "compileMode": "esmodule", + "dependencies": [] + } +} diff --git a/packages/本地已适配鸿蒙的库.md b/packages/本地已适配鸿蒙的库.md index 2da0cbe..e40c48e 100644 --- a/packages/本地已适配鸿蒙的库.md +++ b/packages/本地已适配鸿蒙的库.md @@ -1,6 +1,6 @@ # 鸿蒙适配方案 -> 文档创建: 2026-04-09 | 最后更新: 2026-04-14 +> 文档创建: 2026-04-09 | 最后更新: 2026-04-22 > 适配策略: 纯 Dart 包零成本适配 + 原生插件完整适配 --- @@ -156,6 +156,7 @@ flutter pub get && flutter analyze --no-pub | 6 | flutter_card_swiper | 7.2.0 | 7.2.0-ohos.1 | 2026-04-14 | ✅ analyze 通过 | | 7 | qr | 3.0.2 | 3.0.2-ohos.1 | 2026-04-19 | ✅ pub get 通过 | | 8 | mailer | 7.1.0 | 7.1.0-ohos.1 | 2026-04-19 | ✅ pub get 通过 | +| 9 | mobile_scanner | 7.2.0 | 7.2.0+ohos | 2026-04-22 | ✅ 原生插件,含鸿蒙ets实现 | ### 5.2 各包克隆命令速查 @@ -186,6 +187,10 @@ git clone --depth 1 --branch v3.0.2 https://github.com/kevmoo/qr.dart.git qr # 8. mailer git clone --depth 1 --branch v7.1.0 https://github.com/dart-mailer/mailer.git mailer + +# 9. mobile_scanner(官方v7.2.0 + 鸿蒙适配合并) +git clone --depth 1 --branch v7.2.0 https://github.com/juliansteenbakker/mobile_scanner.git mobile_scanner +# 合并鸿蒙适配:ohos/ 目录、ohos_surface_producer_delegate.dart、surface_producer_delegate.dart、rotated_preview.dart 鸿蒙旋转修复 ``` ### 5.3 各包使用示例 @@ -242,6 +247,17 @@ final message = Message()..from = Address('user@gmail.com')..recipients.add('tar final sendReport = await send(message, smtpServer); ``` +**mobile_scanner** +```dart +import 'package:mobile_scanner/mobile_scanner.dart'; +MobileScanner( + controller: MobileScannerController(), + onDetect: (result) { + print(result.barcodes.first.rawValue); + }, +) +``` + --- ## 六、项目依赖兼容性总览 @@ -256,6 +272,7 @@ final sendReport = await send(message, smtpServer); | flutter_card_swiper | 本地 path | ✅ | ✅ | 纯 Dart | | qr | 本地 path | ✅ | ✅ | 纯 Dart,QR码生成 | | mailer | 本地 path | ❌ | ✅ | 纯 Dart,SMTP客户端,Web不支持 | +| mobile_scanner | 本地 path | ✅ | ✅ | 原生插件,扫码/二维码,v7.2.0+鸿蒙适配 | | hive_ce | pub.dev | ✅ | ✅ | 纯 Dart | | get / dio / logger / intl | pub.dev | ✅ | ✅ | 纯 Dart | | shared_preferences | pub.dev | ✅ | ✅ | localStorage | diff --git a/pubspec.lock b/pubspec.lock index dc7aad2..0e34e4d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -768,6 +768,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + path: "packages/mobile_scanner" + relative: true + source: path + version: "7.2.0" nm: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ee8f542..e5e9bde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.3.0+260421 +version: 1.3.1+26042102 environment: sdk: ^3.9.2 @@ -121,6 +121,11 @@ dependencies: git: url: https://gitcode.com/openharmony-sig/flutter_plus_plugins.git path: packages/device_info_plus/device_info_plus + + mobile_scanner: + path: packages/mobile_scanner + + flutter_staggered_grid_view: @@ -203,6 +208,16 @@ flutter: - assets/md/tips/advanced/ - assets/md/tips/learn/ + # 字体配置:NotoSansSC 子集化版本(GB2312 6763字 + 常用符号),从完整版 32MB 缩减至 6.5MB + # 原始字体来源:https://github.com/notofonts/noto-cjk/releases + # 子集化工具:fontTools pyftsubset + fonts: + - family: NotoSansSC + fonts: + - asset: assets/fonts/NotoSansSC-Regular.otf + - asset: assets/fonts/NotoSansSC-Bold.otf + weight: 700 + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/scripts/add_image_border.py b/scripts/add_image_border.py deleted file mode 100644 index 0f52ea2..0000000 --- a/scripts/add_image_border.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -from PIL import Image, ImageDraw, ImageFilter -import os - -def add_border_and_resize(input_path, output_path, target_size=(1920, 1080), border_color=(255, 255, 255), border_width=20): - img = Image.open(input_path) - original_width, original_height = img.size - aspect_ratio = original_width / original_height - target_aspect = target_size[0] / target_size[1] - - if aspect_ratio > target_aspect: - new_width = target_size[0] - border_width * 2 - new_height = int(new_width / aspect_ratio) - else: - new_height = target_size[1] - border_width * 2 - new_width = int(new_height * aspect_ratio) - - img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - output_img = Image.new('RGB', target_size, border_color) - paste_x = (target_size[0] - new_width) // 2 - paste_y = (target_size[1] - new_height) // 2 - - shadow_offset = 8 - shadow_rect = [ - paste_x + shadow_offset, - paste_y + shadow_offset, - paste_x + new_width + shadow_offset, - paste_y + new_height + shadow_offset - ] - shadow_layer = Image.new('RGBA', target_size, (0, 0, 0, 0)) - shadow_draw = ImageDraw.Draw(shadow_layer) - shadow_draw.rectangle(shadow_rect, fill=(0, 0, 0, 60)) - shadow_blurred = shadow_layer.filter(ImageFilter.GaussianBlur(radius=10)) - - final_image = Image.new('RGB', target_size, border_color) - - if shadow_blurred.mode == 'RGBA': - final_image.paste(shadow_blurred, (0, 0), shadow_blurred) - else: - final_rgb_shadow = shadow_blurred.convert('RGB') - final_image.paste(final_rgb_shadow, (0, 0)) - - final_image.paste(img_resized, (paste_x, paste_y)) - - draw = ImageDraw.Draw(final_image) - rect = [paste_x - 2, paste_y - 2, paste_x + new_width + 2, paste_y + new_height + 2] - for i in range(3): - offset = i + 1 - current_rect = [ - rect[0] - offset, - rect[1] - offset, - rect[2] + offset, - rect[3] + offset - ] - draw.rectangle(current_rect, outline=(180, 200, 220)) - - for i in range(border_width): - current_rect = [ - rect[0] - i - 3, - rect[1] - i - 3, - rect[2] + i + 3, - rect[3] + i + 3 - ] - alpha = int(255 * (1 - i / border_width)) - color_with_alpha = (200 + i*2, 210 + i*2, 230 + i*2) - draw.rectangle(current_rect, outline=color_with_alpha) - - final_image.save(output_path, quality=95) - print(f'[OK] Processed: {os.path.basename(input_path)} -> {output_path}') - return True - -def main(): - input_files = [ - r'e:\project\flutter\f\mom_kitchen\docs\design\1.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\2.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\3.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\4.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\5.jpg', - ] - - output_dir = r'e:\project\flutter\f\mom_kitchen\docs\design\processed' - os.makedirs(output_dir, exist_ok=True) - - print('='*50) - print('Image Border Processing Script') - print(f'Target Size: 1920 x 1080 px') - print(f'Processing: {len(input_files)} images') - print('='*50) - print() - - success_count = 0 - for input_file in input_files: - if os.path.exists(input_file): - filename = os.path.basename(input_file) - output_path = os.path.join(output_dir, f'bordered_{filename}') - try: - add_border_and_resize(input_file, output_path) - success_count += 1 - except Exception as e: - print(f'[ERROR] Failed to process: {filename} - {str(e)}') - import traceback - traceback.print_exc() - else: - print(f'[WARNING] File not found: {input_file}') - - print() - print('='*50) - print(f'[DONE] Processed {success_count}/{len(input_files)} images successfully!') - print(f'Output directory: {output_dir}') - print('='*50) - -if __name__ == '__main__': - main() diff --git a/scripts/add_image_border_portrait.py b/scripts/add_image_border_portrait.py deleted file mode 100644 index c2ff66a..0000000 --- a/scripts/add_image_border_portrait.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -from PIL import Image, ImageDraw, ImageFilter -import os - -def add_border_and_resize(input_path, output_path, target_size=(1080, 1920), border_color=(255, 255, 255), border_width=20): - img = Image.open(input_path) - original_width, original_height = img.size - aspect_ratio = original_width / original_height - target_aspect = target_size[0] / target_size[1] - - if aspect_ratio > target_aspect: - new_width = target_size[0] - border_width * 2 - new_height = int(new_width / aspect_ratio) - else: - new_height = target_size[1] - border_width * 2 - new_width = int(new_height * aspect_ratio) - - img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - paste_x = (target_size[0] - new_width) // 2 - paste_y = (target_size[1] - new_height) // 2 - - shadow_offset = 8 - shadow_rect = [ - paste_x + shadow_offset, - paste_y + shadow_offset, - paste_x + new_width + shadow_offset, - paste_y + new_height + shadow_offset - ] - shadow_layer = Image.new('RGBA', target_size, (0, 0, 0, 0)) - shadow_draw = ImageDraw.Draw(shadow_layer) - shadow_draw.rectangle(shadow_rect, fill=(0, 0, 0, 60)) - shadow_blurred = shadow_layer.filter(ImageFilter.GaussianBlur(radius=10)) - - final_image = Image.new('RGB', target_size, border_color) - - if shadow_blurred.mode == 'RGBA': - final_image.paste(shadow_blurred, (0, 0), shadow_blurred) - else: - final_rgb_shadow = shadow_blurred.convert('RGB') - final_image.paste(final_rgb_shadow, (0, 0)) - - final_image.paste(img_resized, (paste_x, paste_y)) - - draw = ImageDraw.Draw(final_image) - rect = [paste_x - 2, paste_y - 2, paste_x + new_width + 2, paste_y + new_height + 2] - for i in range(3): - offset = i + 1 - current_rect = [ - rect[0] - offset, - rect[1] - offset, - rect[2] + offset, - rect[3] + offset - ] - draw.rectangle(current_rect, outline=(180, 200, 220)) - - for i in range(border_width): - current_rect = [ - rect[0] - i - 3, - rect[1] - i - 3, - rect[2] + i + 3, - rect[3] + i + 3 - ] - color_with_alpha = (200 + i*2, 210 + i*2, 230 + i*2) - draw.rectangle(current_rect, outline=color_with_alpha) - - final_image.save(output_path, quality=95) - print(f'[OK] Processed: {os.path.basename(input_path)} -> {output_path}') - return True - -def main(): - input_files = [ - r'e:\project\flutter\f\mom_kitchen\docs\design\1.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\2.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\3.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\4.jpg', - r'e:\project\flutter\f\mom_kitchen\docs\design\5.jpg', - ] - - output_dir = r'e:\project\flutter\f\mom_kitchen\docs\design\processed\portrait' - os.makedirs(output_dir, exist_ok=True) - - print('='*50) - print('Image Border Processing Script (Portrait Mode)') - print(f'Target Size: 1080 x 1920 px (Vertical)') - print(f'Processing: {len(input_files)} images') - print('='*50) - print() - - success_count = 0 - for input_file in input_files: - if os.path.exists(input_file): - filename = os.path.basename(input_file) - output_path = os.path.join(output_dir, f'bordered_portrait_{filename}') - try: - add_border_and_resize(input_file, output_path) - success_count += 1 - except Exception as e: - print(f'[ERROR] Failed to process: {filename} - {str(e)}') - import traceback - traceback.print_exc() - else: - print(f'[WARNING] File not found: {input_file}') - - print() - print('='*50) - print(f'[DONE] Processed {success_count}/{len(input_files)} images successfully!') - print(f'Output directory: {output_dir}') - print('='*50) - -if __name__ == '__main__': - main() diff --git a/scripts/test_detail_id.dart b/scripts/test_detail_id.dart deleted file mode 100644 index bc295e7..0000000 --- a/scripts/test_detail_id.dart +++ /dev/null @@ -1,64 +0,0 @@ -// 2026-04-13 | test_detail_id.dart | 详情页ID测试 | 验证详情页API返回数据 -// 运行: dart run scripts/test_detail_id.dart - -import 'dart:convert'; -import 'dart:io'; - -const String baseUrl = 'https://eat.wktyl.com/api'; - -Future main() async { - print('=== 详情页ID测试 ===\n'); - - // 测试ID 32390(从日志中获取) - const testId = 32390; - - print('📡 测试ID: $testId'); - await testDetailApi(testId); - - // 再测试一个有效的ID - print('\n📡 测试另一个ID: 46518'); - await testDetailApi(46518); -} - -Future testDetailApi(int id) async { - final client = HttpClient(); - try { - final url = Uri.parse('$baseUrl/api.php?act=full&id=$id&_refresh=1'); - print(' 请求URL: $url'); - - final request = await client.getUrl(url); - request.headers.set('Accept', 'application/json'); - - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - - print(' 状态码: ${response.statusCode}'); - print(' 响应长度: ${body.length}'); - - if (body.isEmpty) { - print(' ❌ 响应体为空'); - return; - } - - final json = jsonDecode(body) as Map; - print(' code: ${json['code']}'); - print(' message: ${json['message']}'); - - final data = json['data'] as Map?; - if (data == null) { - print(' ❌ data字段为null'); - return; - } - - print(' ✅ 数据加载成功'); - print(' id: ${data['id']}'); - print(' title: ${data['title']}'); - print(' pic_id: ${data['pic_id']}'); - print(' cover: ${data['cover']}'); - print(' intro: ${(data['intro'] as String?)?.substring(0, (data['intro'] as String?)?.length.clamp(0, 50) ?? 0)}...'); - } catch (e) { - print(' ❌ 请求错误: $e'); - } finally { - client.close(); - } -} diff --git a/scripts/test_discover_image.dart b/scripts/test_discover_image.dart deleted file mode 100644 index 081e224..0000000 --- a/scripts/test_discover_image.dart +++ /dev/null @@ -1,146 +0,0 @@ -// 2026-04-14 | test_discover_image.dart | Discover图片数据验证 | 验证api_discover.php返回的cover/picId字段 -// 运行: dart run scripts/test_discover_image.dart - -import 'dart:convert'; -import 'dart:io'; - -const String baseUrl = 'https://eat.wktyl.com/api'; - -Future main() async { - print('=== Discover 图片数据验证 ===\n'); - - await testDiscoverApi(); - await testDetailApi(); - await testImageUrl(); -} - -Future testDiscoverApi() async { - print('📡 测试 api_discover.php 接口'); - final client = HttpClient(); - try { - final url = Uri.parse('$baseUrl/api_discover.php?total=10'); - print(' 请求URL: $url'); - - final request = await client.getUrl(url); - request.headers.set('Accept', 'application/json'); - - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - - if (body.isEmpty) { - print(' ❌ 响应体为空'); - return; - } - - final json = jsonDecode(body) as Map; - print(' code: ${json['code']}'); - - final data = json['data'] as Map?; - if (data == null) { - print(' ❌ data字段为null'); - return; - } - - final recipes = data['recipes'] as List?; - if (recipes == null || recipes.isEmpty) { - print(' ❌ recipes为空'); - return; - } - - print(' 菜谱数量: ${recipes.length}'); - print(''); - - for (int i = 0; i < recipes.length && i < 5; i++) { - final recipe = recipes[i] as Map; - print(' ─── 菜谱 #${i + 1} ───'); - print(' id: ${recipe['id']}'); - print(' title: ${recipe['title']}'); - print(' cover: ${recipe['cover']}'); - print(' pic_id: ${recipe['pic_id']}'); - print(' picId: ${recipe['picId']}'); - print(' pic: ${recipe['pic']}'); - - final allKeys = recipe.keys.toList(); - print(' 所有字段: $allKeys'); - - final cover = recipe['cover'] ?? ''; - if (cover.isNotEmpty) { - final regex = RegExp(r'/pic/(\d+)[ab]?\.(jpg|png|webp)$'); - final match = regex.firstMatch(cover.toString()); - if (match != null) { - print(' ✅ 从cover提取picId: ${match.group(1)}'); - } else { - print(' ⚠️ cover不匹配picId正则: $cover'); - } - } else { - print(' ❌ cover为空'); - } - print(''); - } - } catch (e) { - print(' ❌ 请求失败: $e'); - } finally { - client.close(); - } -} - -Future testDetailApi() async { - print('\n📡 测试 api.php?act=detail 接口(对比picId字段)'); - final client = HttpClient(); - try { - final url = Uri.parse('$baseUrl/api.php?act=detail&id=32390'); - print(' 请求URL: $url'); - - final request = await client.getUrl(url); - request.headers.set('Accept', 'application/json'); - - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - - if (body.isEmpty) return; - - final json = jsonDecode(body) as Map; - final data = json['data'] as Map?; - if (data == null) return; - - print(' id: ${data['id']}'); - print(' title: ${data['title']}'); - print(' cover: ${data['cover']}'); - print(' pic_id: ${data['pic_id']}'); - print(' picId: ${data['picId']}'); - print(' pic: ${data['pic']}'); - - final allKeys = data.keys.toList(); - print(' 所有字段: $allKeys'); - } catch (e) { - print(' ❌ 请求失败: $e'); - } finally { - client.close(); - } -} - -Future testImageUrl() async { - print('\n📡 测试图片URL可访问性'); - final client = HttpClient(); - try { - final testUrls = [ - 'https://eat.wktyl.com/api/assets/pic/32390a.jpg', - 'https://eat.wktyl.com/api/assets/pic/32390b.jpg', - 'https://eat.wktyl.com/api/assets/pic/32390.jpg', - ]; - - for (final url in testUrls) { - try { - final request = await client.getUrl(Uri.parse(url)); - final response = await request.close(); - print( - ' $url → ${response.statusCode} (${response.contentLength} bytes)', - ); - } catch (e) { - print(' $url → ❌ $e'); - } - } - } finally { - client.close(); - } -} diff --git a/scripts/test_filter_steps.dart b/scripts/test_filter_steps.dart deleted file mode 100644 index 4709bf4..0000000 --- a/scripts/test_filter_steps.dart +++ /dev/null @@ -1,156 +0,0 @@ -// 2026-04-20 | test_filter_steps.dart | 动态筛选接口测试 | 验证filter_steps动态筛选+api_filter分类 -// 运行: dart run scripts/test_filter_steps.dart - -import 'dart:convert'; -import 'dart:io'; - -const String baseUrl = 'https://eat.wktyl.com/api'; - -Future main() async { - print('=== 动态筛选接口测试 ===\n'); - - print('━━━ 1. api_filter: 获取菜谱大类 ━━━'); - final mainCats = await getJson('api_filter.php', {'act': 'recipe_main_categories'}); - if (mainCats != null) { - final list = mainCats['data']?['list'] as List? ?? []; - print(' 大类数量: ${list.length}'); - for (final c in list) { - final m = c as Map; - print(' 📂 ${m['name']} (ID:${m['id']}): ${m['recipe_count']}道'); - } - } - - print('\n━━━ 2. api_filter: 获取中国菜子类 ━━━'); - final subCats = await getJson('api_filter.php', {'act': 'recipe_sub_categories', 'parent_id': '12'}); - if (subCats != null) { - final list = subCats['data']?['list'] as List? ?? []; - print(' 子类数量: ${list.length}'); - for (final c in list.take(5)) { - final m = c as Map; - print(' 📂 ${m['name']} (ID:${m['id']}): ${m['recipe_count']}道'); - } - } - - print('\n━━━ 3. filter_steps: 无筛选条件 ━━━'); - final fs1 = await fetchFilterSteps(); - if (fs1 != null) { - print(' 匹配菜谱数: ${fs1['matched_count']}'); - final opts = fs1['available_options'] as List? ?? []; - print(' 可用分类数: ${opts.length}'); - for (final o in opts.take(3)) { - final m = o as Map; - print(' 📂 ${m['name']}: ${m['count']}道 (${(m['children'] as List?)?.length ?? 0}子类)'); - } - final tags1 = fs1['available_tags'] as List? ?? []; - print(' 可用标签数: ${tags1.length}'); - for (final t in tags1.take(5)) { - final tm = t as Map; - print(' 🏷️ ${tm['name']}: ${tm['count']}道'); - } - } - - print('\n━━━ 4. filter_steps: 选分类[12] ━━━'); - final fs2 = await fetchFilterSteps(categories: [12]); - if (fs2 != null) { - print(' 匹配菜谱数: ${fs2['matched_count']}'); - final tags2 = fs2['available_tags'] as List? ?? []; - print(' 可用标签数: ${tags2.length}'); - for (final t in tags2.take(5)) { - final tm = t as Map; - print(' 🏷️ ${tm['name']}: ${tm['count']}道'); - } - } - - print('\n━━━ 5. filter_steps: 分类[12]+标签[1] ━━━'); - final fs3 = await fetchFilterSteps(categories: [12], tags: [1]); - if (fs3 != null) { - print(' 匹配菜谱数: ${fs3['matched_count']}'); - final tags3 = fs3['available_tags'] as List? ?? []; - print(' 可用标签数: ${tags3.length}'); - for (final t in tags3.take(5)) { - final tm = t as Map; - print(' 🏷️ ${tm['name']}: ${tm['count']}道'); - } - } - - print('\n━━━ 6. 验证动态筛选效果 ━━━'); - final c1 = fs1?['matched_count'] ?? 0; - final c2 = fs2?['matched_count'] ?? 0; - final c3 = fs3?['matched_count'] ?? 0; - print(' 无筛选: $c1 道'); - print(' 选分类: $c2 道'); - print(' 分类+标签: $c3 道'); - if (c2 < c1 && c3 < c2) { - print(' ✅ 动态筛选正常:选项越多,匹配越少'); - } else if (c2 <= c1 && c3 <= c2) { - print(' ⚠️ 动态筛选部分正常'); - } else { - print(' ❌ 动态筛选异常'); - } - - print('\n━━━ 7. filter_apply: 获取推荐菜谱 ━━━'); - final applyResult = await fetchFilterApply(categories: [12], tags: [1], count: 3); - if (applyResult != null) { - final recipes = applyResult['recipes'] as List? ?? []; - print(' 返回菜谱数: ${recipes.length}'); - print(' 总匹配数: ${applyResult['total_matched']}'); - for (final r in recipes) { - final rm = r as Map; - print(' 🍳 ${rm['title']} (ID: ${rm['id']})'); - } - } - - print('\n=== 测试完成 ==='); -} - -Future?> getJson(String endpoint, Map params) async { - try { - final uri = Uri.parse('$baseUrl/$endpoint').replace(queryParameters: params); - final response = await HttpClient() - .getUrl(uri) - .then((r) => r.close()) - .timeout(const Duration(seconds: 15)); - final body = await response.transform(utf8.decoder).join(); - return json.decode(body) as Map; - } catch (e) { - print(' ❌ 请求失败: $e'); - return null; - } -} - -Future?> fetchFilterSteps({ - List? categories, - List? tags, -}) async { - final params = {'act': 'filter_steps'}; - if (categories != null && categories.isNotEmpty) { - params['category'] = categories.join(','); - } - if (tags != null && tags.isNotEmpty) { - params['tag'] = tags.join(','); - } - final data = await getJson('api_what_to_eat.php', params); - if (data != null && data['code'] == 200) { - return data['data'] as Map?; - } - return null; -} - -Future?> fetchFilterApply({ - List? categories, - List? tags, - int count = 5, -}) async { - final params = {'act': 'filter_apply', 'count': '$count'}; - if (categories != null && categories.isNotEmpty) { - params['category'] = categories.join(','); - } - if (tags != null && tags.isNotEmpty) { - params['tag'] = tags.join(','); - } - final data = await getJson('api_what_to_eat.php', params); - if (data != null && data['code'] == 200) { - return data['data'] as Map?; - } - return null; -} diff --git a/scripts/test_ingredient_cache.dart b/scripts/test_ingredient_cache.dart deleted file mode 100644 index b96fa76..0000000 --- a/scripts/test_ingredient_cache.dart +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 文件: test_ingredient_cache.dart - * 名称: 食材缓存测试脚本 - * 作用: 验证食材缓存服务的读写功能 - * 创建: 2026-04-14 - */ - -import 'dart:io'; - -void main() async { - print('========================================'); - print('食材缓存测试脚本'); - print('========================================\n'); - - // 测试 API 接口 - await testIngredientApi(); - - print('\n========================================'); - print('测试完成'); - print('========================================'); -} - -Future testIngredientApi() async { - final testIds = [849, 1248, 1, 1206, 1209]; - - for (final id in testIds) { - print('\n--- 测试食材 ID: $id ---'); - - try { - final client = HttpClient(); - final request = await client.getUrl( - Uri.parse('https://eat.wktyl.com/api/api.php?act=ingredient_detail&id=$id'), - ); - - final response = await request.close(); - final body = await response.transform(const SystemEncoding().decoder).join(); - - print('状态码: ${response.statusCode}'); - print('响应长度: ${body.length} 字符'); - - if (body.isNotEmpty && body.startsWith('{')) { - print('✅ API 响应正常'); - } else { - print('❌ API 响应异常'); - } - - client.close(); - } catch (e) { - print('❌ 请求失败: $e'); - } - - // 延迟避免请求过快 - await Future.delayed(const Duration(milliseconds: 500)); - } -} diff --git a/web/index.html b/web/index.html index 584f18a..2300d23 100644 --- a/web/index.html +++ b/web/index.html @@ -14,11 +14,15 @@ This is a placeholder for base href that will be replaced by the value of the `--base-href` argument provided to `flutter build`. --> - + + + - + diff --git a/web/manifest.json b/web/manifest.json index ec69f64..9012738 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "mom_kitchen", - "short_name": "mom_kitchen", + "name": "小妈厨房", + "short_name": "小妈厨房 Cute kitchen", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "A new Flutter project.", + "description": " 你的贴身美食搭档.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [