From f4a28f9ea53d3236ce30d4fd0f7bd0f80eb562b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:16:13 +0000 Subject: [PATCH 1/5] Initial plan From 35c9e2ad91c0a17553ff280cfd4db0c0cadf07ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:24:49 +0000 Subject: [PATCH 2/5] Migrate flutter_boost to federated plugin architecture Co-authored-by: 0xZOne <26625149+0xZOne@users.noreply.github.com> --- CHANGELOG.md | 5 + FEDERATED_PLUGIN_MIGRATION.md | 303 +++++++++ example/pubspec.yaml | 7 +- flutter_boost_android/.gitignore | 27 + flutter_boost_android/CHANGELOG.md | 4 + flutter_boost_android/LICENSE | 21 + flutter_boost_android/README.md | 7 + flutter_boost_android/analysis_options.yaml | 5 + flutter_boost_android/android/build.gradle | 56 ++ .../android/gradle.properties | 2 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + flutter_boost_android/android/gradlew | 172 +++++ flutter_boost_android/android/gradlew.bat | 84 +++ flutter_boost_android/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 6 + .../com/idlefish/flutterboost/Assert.java | 106 +++ .../idlefish/flutterboost/EventListener.java | 11 + .../FBPlatformViewsController.java | 109 ++++ .../idlefish/flutterboost/FlutterBoost.java | 360 ++++++++++ .../flutterboost/FlutterBoostDelegate.java | 13 + .../flutterboost/FlutterBoostPlugin.java | 425 ++++++++++++ .../FlutterBoostRouteOptions.java | 84 +++ .../FlutterBoostSetupOptions.java | 138 ++++ .../flutterboost/FlutterBoostUtils.java | 168 +++++ .../flutterboost/ListenerRemover.java | 12 + .../com/idlefish/flutterboost/Messages.java | 613 ++++++++++++++++++ .../containers/ContainerThemeMgr.java | 52 ++ .../FlutterActivityLaunchConfigs.java | 23 + .../containers/FlutterBoostActivity.java | 436 +++++++++++++ .../containers/FlutterBoostFragment.java | 530 +++++++++++++++ .../containers/FlutterContainerManager.java | 117 ++++ .../containers/FlutterTextureHooker.java | 143 ++++ .../containers/FlutterViewContainer.java | 23 + .../containers/LifecycleStage.java | 14 + .../lib/flutter_boost_android.dart | 8 + flutter_boost_android/pubspec.yaml | 31 + flutter_boost_ios/.gitignore | 27 + flutter_boost_ios/CHANGELOG.md | 4 + flutter_boost_ios/LICENSE | 21 + flutter_boost_ios/README.md | 7 + flutter_boost_ios/analysis_options.yaml | 5 + flutter_boost_ios/ios/Assets/.gitkeep | 0 flutter_boost_ios/ios/Classes/FlutterBoost.h | 105 +++ flutter_boost_ios/ios/Classes/FlutterBoost.m | 227 +++++++ .../ios/Classes/FlutterBoostDelegate.h | 43 ++ .../ios/Classes/FlutterBoostPlugin.h | 47 ++ .../ios/Classes/FlutterBoostPlugin.m | 209 ++++++ flutter_boost_ios/ios/Classes/Options.h | 75 +++ flutter_boost_ios/ios/Classes/Options.m | 52 ++ .../Classes/container/FBFlutterContainer.h | 33 + .../container/FBFlutterContainerManager.h | 36 + .../container/FBFlutterContainerManager.m | 87 +++ .../container/FBFlutterViewContainer.h | 41 ++ .../container/FBFlutterViewContainer.m | 332 ++++++++++ .../ios/Classes/container/FBLifecycle.h | 28 + .../ios/Classes/container/FBLifecycle.m | 44 ++ flutter_boost_ios/ios/Classes/messages.h | 86 +++ flutter_boost_ios/ios/Classes/messages.m | 526 +++++++++++++++ .../ios/flutter_boost_ios.podspec | 36 + flutter_boost_ios/lib/flutter_boost_ios.dart | 8 + flutter_boost_ios/pubspec.yaml | 30 + flutter_boost_platform_interface/.gitignore | 27 + flutter_boost_platform_interface/CHANGELOG.md | 4 + flutter_boost_platform_interface/LICENSE | 21 + flutter_boost_platform_interface/README.md | 13 + .../analysis_options.yaml | 5 + .../lib/flutter_boost_platform_interface.dart | 9 + .../src/flutter_boost_platform_interface.dart | 43 ++ .../lib/src/messages.dart | 519 +++++++++++++++ .../lib/src/method_channel_flutter_boost.dart | 22 + flutter_boost_platform_interface/pubspec.yaml | 23 + lib/flutter_boost.dart | 4 + lib/src/messages.dart | 524 +-------------- pubspec.yaml | 11 +- 75 files changed, 6933 insertions(+), 522 deletions(-) create mode 100644 FEDERATED_PLUGIN_MIGRATION.md create mode 100644 flutter_boost_android/.gitignore create mode 100644 flutter_boost_android/CHANGELOG.md create mode 100755 flutter_boost_android/LICENSE create mode 100644 flutter_boost_android/README.md create mode 100644 flutter_boost_android/analysis_options.yaml create mode 100755 flutter_boost_android/android/build.gradle create mode 100755 flutter_boost_android/android/gradle.properties create mode 100755 flutter_boost_android/android/gradle/wrapper/gradle-wrapper.jar create mode 100755 flutter_boost_android/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 flutter_boost_android/android/gradlew create mode 100755 flutter_boost_android/android/gradlew.bat create mode 100755 flutter_boost_android/android/settings.gradle create mode 100755 flutter_boost_android/android/src/main/AndroidManifest.xml create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Assert.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/EventListener.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FBPlatformViewsController.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostDelegate.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostPlugin.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostRouteOptions.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostSetupOptions.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostUtils.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/ListenerRemover.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Messages.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/ContainerThemeMgr.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterActivityLaunchConfigs.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostActivity.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostFragment.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterContainerManager.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterTextureHooker.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterViewContainer.java create mode 100644 flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/LifecycleStage.java create mode 100644 flutter_boost_android/lib/flutter_boost_android.dart create mode 100644 flutter_boost_android/pubspec.yaml create mode 100644 flutter_boost_ios/.gitignore create mode 100644 flutter_boost_ios/CHANGELOG.md create mode 100755 flutter_boost_ios/LICENSE create mode 100644 flutter_boost_ios/README.md create mode 100644 flutter_boost_ios/analysis_options.yaml create mode 100755 flutter_boost_ios/ios/Assets/.gitkeep create mode 100644 flutter_boost_ios/ios/Classes/FlutterBoost.h create mode 100644 flutter_boost_ios/ios/Classes/FlutterBoost.m create mode 100644 flutter_boost_ios/ios/Classes/FlutterBoostDelegate.h create mode 100644 flutter_boost_ios/ios/Classes/FlutterBoostPlugin.h create mode 100644 flutter_boost_ios/ios/Classes/FlutterBoostPlugin.m create mode 100644 flutter_boost_ios/ios/Classes/Options.h create mode 100644 flutter_boost_ios/ios/Classes/Options.m create mode 100644 flutter_boost_ios/ios/Classes/container/FBFlutterContainer.h create mode 100644 flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.h create mode 100644 flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.m create mode 100644 flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.h create mode 100644 flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.m create mode 100644 flutter_boost_ios/ios/Classes/container/FBLifecycle.h create mode 100644 flutter_boost_ios/ios/Classes/container/FBLifecycle.m create mode 100644 flutter_boost_ios/ios/Classes/messages.h create mode 100644 flutter_boost_ios/ios/Classes/messages.m create mode 100755 flutter_boost_ios/ios/flutter_boost_ios.podspec create mode 100644 flutter_boost_ios/lib/flutter_boost_ios.dart create mode 100644 flutter_boost_ios/pubspec.yaml create mode 100644 flutter_boost_platform_interface/.gitignore create mode 100644 flutter_boost_platform_interface/CHANGELOG.md create mode 100755 flutter_boost_platform_interface/LICENSE create mode 100644 flutter_boost_platform_interface/README.md create mode 100644 flutter_boost_platform_interface/analysis_options.yaml create mode 100644 flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart create mode 100644 flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart create mode 100644 flutter_boost_platform_interface/lib/src/messages.dart create mode 100644 flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart create mode 100644 flutter_boost_platform_interface/pubspec.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index df946d50d..c42a9aac4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT 1. [dart]添加HDR/HEIC/HEIF/TIFF/WBMP/WEBP等图片格式的测试案例 +2. **[架构] 将 flutter_boost 迁移为联邦插件架构** + - 新增 `flutter_boost_platform_interface` 包(平台接口层) + - 新增 `flutter_boost_android` 包(Android 平台实现) + - 新增 `flutter_boost_ios` 包(iOS 平台实现) + - 详细迁移记录请参阅 `FEDERATED_PLUGIN_MIGRATION.md` ## 5.0.2 1. Adapt to the official engine's App Lifecycle state changes diff --git a/FEDERATED_PLUGIN_MIGRATION.md b/FEDERATED_PLUGIN_MIGRATION.md new file mode 100644 index 000000000..9ad2940af --- /dev/null +++ b/FEDERATED_PLUGIN_MIGRATION.md @@ -0,0 +1,303 @@ +# Flutter Boost 联邦插件迁移记录 + +本文档记录了将 `flutter_boost` 插件迁移为 Flutter 联邦插件架构的过程。 + +## 迁移概述 + +### 什么是联邦插件 + +联邦插件(Federated Plugin)是 Flutter 官方推荐的插件架构,它将插件拆分为多个独立的包: + +1. **平台接口包** (`flutter_boost_platform_interface`): 定义平台无关的抽象接口 +2. **平台实现包** (`flutter_boost_android`, `flutter_boost_ios`): 各平台的具体实现 +3. **应用层包** (`flutter_boost`): 对外暴露 API,聚合各平台实现 + +### 迁移优势 + +- 更好的代码组织和模块化 +- 支持添加新平台而无需修改现有代码 +- 允许第三方提供替代的平台实现 +- 更清晰的依赖关系 + +--- + +## 迁移步骤详细记录 + +### 1. 创建 `flutter_boost_platform_interface` 包 + +**目录**: `flutter_boost_platform_interface/` + +#### 1.1 创建 `pubspec.yaml` + +**文件**: `flutter_boost_platform_interface/pubspec.yaml` + +**修改内容**: +- 创建新的 pubspec.yaml 文件 +- 添加 `plugin_platform_interface` 依赖 +- 保持与主包相同的版本号 (5.0.2) + +**原因**: 平台接口包需要依赖 `plugin_platform_interface` 来实现标准的平台接口模式。 + +#### 1.2 迁移 `messages.dart` + +**文件**: `flutter_boost_platform_interface/lib/src/messages.dart` + +**修改内容**: +- 将原 `lib/src/messages.dart` 复制到平台接口包 +- 保持 Pigeon 生成的代码不变 + +**原因**: `messages.dart` 包含了 `NativeRouterApi` 和 `FlutterRouterApi` 的定义,是 Flutter 与原生平台通信的核心接口,应该放在平台接口层。 + +#### 1.3 创建平台接口抽象类 + +**文件**: `flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart` + +**修改内容**: +- 创建 `FlutterBoostPlatform` 抽象类 +- 继承 `PlatformInterface` +- 定义平台接口方法 + +**原因**: 根据联邦插件最佳实践,平台接口需要继承 `PlatformInterface` 并提供标准的实例访问模式。 + +#### 1.4 创建方法通道默认实现 + +**文件**: `flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart` + +**修改内容**: +- 创建 `MethodChannelFlutterBoost` 类 +- 实现 `FlutterBoostPlatform` 接口 +- 提供基于方法通道的默认实现 + +**原因**: 提供默认的平台通信实现,各平台实现包可以继承或替换此实现。 + +#### 1.5 创建主库导出文件 + +**文件**: `flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart` + +**修改内容**: +- 创建库导出文件 +- 导出所有公开 API + +**原因**: 统一的导出点,方便其他包引用。 + +--- + +### 2. 创建 `flutter_boost_android` 包 + +**目录**: `flutter_boost_android/` + +#### 2.1 创建 `pubspec.yaml` + +**文件**: `flutter_boost_android/pubspec.yaml` + +**修改内容**: +- 创建新的 pubspec.yaml 文件 +- 添加对 `flutter_boost_platform_interface` 的依赖 +- 配置 `flutter.plugin.implements` 为 `flutter_boost` +- 配置 `flutter.plugin.platforms.android` 指定原生插件类 + +**原因**: 联邦插件的平台实现包需要声明它实现了哪个插件,并指定原生插件类。 + +#### 2.2 迁移 Android 原生代码 + +**操作**: 将 `android/` 目录复制到 `flutter_boost_android/android/` + +**修改内容**: +- 复制所有 Android 原生代码 +- 保持包名 `com.idlefish.flutterboost` 不变 +- 保持插件类 `FlutterBoostPlugin` 不变 + +**原因**: Android 原生代码无需修改,只需移动到对应的平台实现包中。 + +#### 2.3 创建空的 Dart 库文件 + +**文件**: `flutter_boost_android/lib/flutter_boost_android.dart` + +**修改内容**: +- 创建空的库文件(平台实现包不需要导出 Dart 代码) + +**原因**: 平台实现包主要提供原生实现,Dart 层面不需要导出内容。 + +--- + +### 3. 创建 `flutter_boost_ios` 包 + +**目录**: `flutter_boost_ios/` + +#### 3.1 创建 `pubspec.yaml` + +**文件**: `flutter_boost_ios/pubspec.yaml` + +**修改内容**: +- 创建新的 pubspec.yaml 文件 +- 添加对 `flutter_boost_platform_interface` 的依赖 +- 配置 `flutter.plugin.implements` 为 `flutter_boost` +- 配置 `flutter.plugin.platforms.ios` 指定原生插件类 + +**原因**: 联邦插件的平台实现包需要声明它实现了哪个插件,并指定原生插件类。 + +#### 3.2 迁移 iOS 原生代码 + +**操作**: 将 `ios/` 目录复制到 `flutter_boost_ios/ios/` + +**修改内容**: +- 复制所有 iOS 原生代码 +- 重命名 podspec 文件为 `flutter_boost_ios.podspec` +- 更新 podspec 中的 `s.name` 为 `flutter_boost_ios` +- 更新版本号为 `5.0.2` +- 更新 homepage 为正确的 GitHub 地址 + +**原因**: iOS 原生代码需要移动到对应的平台实现包,并更新 podspec 配置。 + +#### 3.3 创建空的 Dart 库文件 + +**文件**: `flutter_boost_ios/lib/flutter_boost_ios.dart` + +**修改内容**: +- 创建空的库文件(平台实现包不需要导出 Dart 代码) + +**原因**: 平台实现包主要提供原生实现,Dart 层面不需要导出内容。 + +--- + +### 4. 修改主包 `flutter_boost` + +**目录**: `flutter_boost/`(根目录) + +#### 4.1 更新 `pubspec.yaml` + +**文件**: `pubspec.yaml` + +**修改内容**: +- 添加对 `flutter_boost_platform_interface` 的依赖 +- 添加对 `flutter_boost_android` 的依赖 +- 添加对 `flutter_boost_ios` 的依赖 +- 修改 `flutter.plugin.platforms.android` 配置,使用 `default_package: flutter_boost_android` +- 修改 `flutter.plugin.platforms.ios` 配置,使用 `default_package: flutter_boost_ios` +- 移除原来的 `package` 和 `pluginClass` 配置 + +**原因**: 联邦插件的主包需要聚合所有平台实现包,并使用 `default_package` 指定默认的平台实现。 + +#### 4.2 更新 `lib/flutter_boost.dart` + +**文件**: `lib/flutter_boost.dart` + +**修改内容**: +- 添加从 `flutter_boost_platform_interface` 导出核心类型 +- 保持其他导出不变 + +**原因**: 主包需要重新导出平台接口中的公共类型,保持 API 向后兼容。 + +#### 4.3 更新 `lib/src/messages.dart` + +**文件**: `lib/src/messages.dart` + +**修改内容**: +- 将文件内容替换为重新导出语句 +- 从 `flutter_boost_platform_interface` 重新导出所有消息类型 + +**原因**: 避免代码重复,统一使用平台接口包中的定义。 + +--- + +### 5. 更新 Example 目录 + +**目录**: `example/` + +#### 5.1 更新 `example/pubspec.yaml` + +**文件**: `example/pubspec.yaml` + +**修改内容**: +- 更新 SDK 版本约束为 `>=3.2.0 <4.0.0` +- 将 `flutter_boost` 从 `dev_dependencies` 移动到 `dependencies` + +**原因**: +- SDK 版本需要与主包保持一致 +- `flutter_boost` 是 example 的运行时依赖,应放在 `dependencies` 中 + +--- + +## 目录结构对比 + +### 迁移前 + +``` +flutter_boost/ +├── android/ # Android 原生代码 +├── ios/ # iOS 原生代码 +├── lib/ +│ ├── flutter_boost.dart # 主库文件 +│ └── src/ +│ ├── messages.dart # Pigeon 生成的消息代码 +│ └── ... # 其他 Dart 文件 +├── example/ # 示例项目 +└── pubspec.yaml +``` + +### 迁移后 + +``` +flutter_boost/ +├── flutter_boost_platform_interface/ # 平台接口包 +│ ├── lib/ +│ │ ├── flutter_boost_platform_interface.dart +│ │ └── src/ +│ │ ├── flutter_boost_platform_interface.dart +│ │ ├── method_channel_flutter_boost.dart +│ │ └── messages.dart +│ └── pubspec.yaml +├── flutter_boost_android/ # Android 平台实现包 +│ ├── android/ # Android 原生代码 +│ ├── lib/ +│ │ └── flutter_boost_android.dart +│ └── pubspec.yaml +├── flutter_boost_ios/ # iOS 平台实现包 +│ ├── ios/ # iOS 原生代码 +│ ├── lib/ +│ │ └── flutter_boost_ios.dart +│ └── pubspec.yaml +├── android/ # 保留原 Android 目录(可选删除) +├── ios/ # 保留原 iOS 目录(可选删除) +├── lib/ +│ ├── flutter_boost.dart # 主库文件(更新) +│ └── src/ +│ ├── messages.dart # 重新导出 +│ └── ... # 其他 Dart 文件(不变) +├── example/ # 示例项目(更新) +└── pubspec.yaml # 更新依赖配置 +``` + +--- + +## 功能保持不变 + +此次迁移仅涉及代码组织结构的调整,以下功能保持完全不变: + +1. **Flutter 路由 API**: `BoostNavigator`, `FlutterBoostApp` 等 +2. **原生通信**: `NativeRouterApi`, `FlutterRouterApi` +3. **生命周期管理**: `BoostLifecycleBinding`, `PageVisibilityBinding` +4. **容器管理**: `BoostContainer`, `BoostContainerWidget` +5. **事件通道**: `BoostChannel` +6. **拦截器**: `BoostInterceptor` +7. **所有原生实现**: Android Java 代码和 iOS Objective-C 代码 + +--- + +## 验证清单 + +- [x] 平台接口包结构正确 +- [x] Android 平台实现包结构正确 +- [x] iOS 平台实现包结构正确 +- [x] 主包依赖配置正确 +- [x] Example 依赖配置正确 +- [x] 所有公开 API 保持向后兼容 +- [x] 原生代码无修改 + +--- + +## 参考资料 + +- [Flutter 联邦插件文档](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#federated-plugins) +- [plugin_platform_interface 包](https://pub.dev/packages/plugin_platform_interface) +- [Flutter 插件最佳实践](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-platforms) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d97809406..8a82babce 100755 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,11 +10,13 @@ description: Demonstrates how to use the flutter_boost plugin. version: 1.0.0+1 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=3.2.0 <4.0.0' dependencies: flutter: sdk: flutter + flutter_boost: + path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -28,9 +30,6 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_boost: - path: ../ - # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/flutter_boost_android/.gitignore b/flutter_boost_android/.gitignore new file mode 100644 index 000000000..6d11bab8b --- /dev/null +++ b/flutter_boost_android/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +pubspec.lock diff --git a/flutter_boost_android/CHANGELOG.md b/flutter_boost_android/CHANGELOG.md new file mode 100644 index 000000000..88c63d483 --- /dev/null +++ b/flutter_boost_android/CHANGELOG.md @@ -0,0 +1,4 @@ +## 5.0.2 + +* Initial release of the Android implementation for flutter_boost. +* Extracted from `flutter_boost` as part of federated plugin migration. diff --git a/flutter_boost_android/LICENSE b/flutter_boost_android/LICENSE new file mode 100755 index 000000000..88ed1cb37 --- /dev/null +++ b/flutter_boost_android/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alibaba Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_boost_android/README.md b/flutter_boost_android/README.md new file mode 100644 index 000000000..0c2155bdd --- /dev/null +++ b/flutter_boost_android/README.md @@ -0,0 +1,7 @@ +# flutter_boost_android + +The Android implementation of [flutter_boost](https://pub.dev/packages/flutter_boost). + +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you can simply use `flutter_boost` normally. This package will be automatically included in your app when you do. diff --git a/flutter_boost_android/analysis_options.yaml b/flutter_boost_android/analysis_options.yaml new file mode 100644 index 000000000..8d81c200c --- /dev/null +++ b/flutter_boost_android/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false diff --git a/flutter_boost_android/android/build.gradle b/flutter_boost_android/android/build.gradle new file mode 100755 index 000000000..89e0adc70 --- /dev/null +++ b/flutter_boost_android/android/build.gradle @@ -0,0 +1,56 @@ +group 'com.idlefish.flutterboost' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.idlefish.flutterboost' + } + compileSdkVersion 31 + buildToolsVersion '30.0.2' + defaultConfig { + minSdkVersion 16 + targetSdkVersion 31 + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + lintOptions { + disable 'InvalidPackage' + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + compileOnly 'com.google.android.material:material:1.0.0' + compileOnly 'com.alibaba:fastjson:1.2.41' + +} + +ext { + groupId = 'com.taobao.fleamarket' + artifactId = "FlutterBoost" +} + diff --git a/flutter_boost_android/android/gradle.properties b/flutter_boost_android/android/gradle.properties new file mode 100755 index 000000000..7be3d8b46 --- /dev/null +++ b/flutter_boost_android/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true diff --git a/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.jar b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..13536770052936a92b204cc34e72284a03a6903c GIT binary patch literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/flutter_boost_android/android/gradlew.bat b/flutter_boost_android/android/gradlew.bat new file mode 100755 index 000000000..e95643d6a --- /dev/null +++ b/flutter_boost_android/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/flutter_boost_android/android/settings.gradle b/flutter_boost_android/android/settings.gradle new file mode 100755 index 000000000..4e421b91c --- /dev/null +++ b/flutter_boost_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_boost' diff --git a/flutter_boost_android/android/src/main/AndroidManifest.xml b/flutter_boost_android/android/src/main/AndroidManifest.xml new file mode 100755 index 000000000..a6c6bf96b --- /dev/null +++ b/flutter_boost_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Assert.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Assert.java new file mode 100644 index 000000000..8fe6cadcb --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Assert.java @@ -0,0 +1,106 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.lang.AssertionError; + +/** + * A set of assert methods. Messages are only displayed when an assert fails. + * + */ +public class Assert { + /** + * Protect constructor since it is a static only class + */ + protected Assert() { + } + + /** + * Asserts that a condition is true. If it isn't it throws + * an AssertionError with the given message. + */ + static public void assertTrue(String message, boolean condition) { + if (!condition) { + fail(message); + } + } + + /** + * Asserts that a condition is true. If it isn't it throws + * an AssertionError. + */ + static public void assertTrue(boolean condition) { + assertTrue(null, condition); + } + + /** + * Asserts that a condition is false. If it isn't it throws + * an AssertionError with the given message. + */ + static public void assertFalse(String message, boolean condition) { + assertTrue(message, !condition); + } + + /** + * Asserts that a condition is false. If it isn't it throws + * an AssertionError. + */ + static public void assertFalse(boolean condition) { + assertFalse(null, condition); + } + + /** + * Fails a test with the given message. + */ + static public void fail(String message) { + if (message == null) { + throw new AssertionError(); + } + throw new AssertionError(message); + } + + /** + * Fails a test with no message. + */ + static public void fail() { + fail(null); + } + + /** + * Asserts that an object isn't null. + */ + static public void assertNotNull(Object object) { + assertNotNull(null, object); + } + + /** + * Asserts that an object isn't null. If it is + * an AssertionError is thrown with the given message. + */ + static public void assertNotNull(String message, Object object) { + assertTrue(message, object != null); + } + + /** + * Asserts that an object is null. If it isn't an {@link AssertionError} is + * thrown. + * Message contains: Expected: but was: object + * + * @param object Object to check or null + */ + static public void assertNull(Object object) { + if (object != null) { + assertNull("Expected: but was: " + object.toString(), object); + } + } + + /** + * Asserts that an object is null. If it is not + * an AssertionError is thrown with the given message. + */ + static public void assertNull(String message, Object object) { + assertTrue(message, object == null); + } +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/EventListener.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/EventListener.java new file mode 100644 index 000000000..0d58be9c2 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/EventListener.java @@ -0,0 +1,11 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.util.Map; + +public interface EventListener { + void onEvent(String key, Map args); +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FBPlatformViewsController.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FBPlatformViewsController.java new file mode 100644 index 000000000..7e5c4327a --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FBPlatformViewsController.java @@ -0,0 +1,109 @@ +package com.idlefish.flutterboost; + +import android.content.Context; +import android.view.View; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorView; +import io.flutter.plugin.platform.PlatformViewsController; +import io.flutter.view.TextureRegistry; + +/** + * + * fix issues: + * + * + * + * @author : Joe Chan + * @date : 2024/5/22 13:35 + */ +public class FBPlatformViewsController extends PlatformViewsController { + + private Context appCtx; + + /** + * 记录PlatformViewsController绑定使用的FlutterView + */ + private FlutterView curFlutterView = null; + + /** + * 占位FlutterView,用于防止不执行完整detach后,内部channelHandler继续响应时,出现空指针异常。 + */ + private FlutterView dummyFlutterView = null; + + public FBPlatformViewsController() { + super(); + } + + @Override + public void attach(@Nullable Context context, @NonNull TextureRegistry textureRegistry, + @NonNull DartExecutor dartExecutor) { + if (appCtx == null && context != null) { + appCtx = context.getApplicationContext(); + dummyFlutterView = new FlutterView(appCtx); + } + super.attach(context, textureRegistry, dartExecutor); + } + + @Override + public void detach() { + // 不执行完整的detach,这样就使内部channelHandler正确响应,同时避免platformView触摸事件无法响应 + // super.detach(); + // 使用反射将内部context变量设置为null,一方面解决重新attach时的异常,另一方面解决内存泄漏 + try { + Field contextF = getClass().getSuperclass().getDeclaredField("context"); + contextF.setAccessible(true); + contextF.set(this, null); + } catch (Exception ignore) { + } + destroyOverlaySurfaces(); + } + + @Override + public void attachToView(@NonNull FlutterView newFlutterView) { + if (curFlutterView == null) { + super.attachToView(newFlutterView); + curFlutterView = newFlutterView; + } else if (newFlutterView != curFlutterView) { + removePlatformWrapperOrParents(); + super.attachToView(newFlutterView); + curFlutterView = newFlutterView; + } + } + + + @Override + public void detachFromView() { + if (curFlutterView != null) { + super.detachFromView(); + curFlutterView = null; + //将占位FlutterView绑定上去 + attachToView(dummyFlutterView); + } + } + + public void removePlatformWrapperOrParents() { + if (curFlutterView != null) { + List needRemoveViews = new ArrayList<>(); + int childCount = curFlutterView.getChildCount(); + for (int i = 0; i < childCount; i++) { + View view = curFlutterView.getChildAt(i); + if (view.getClass().getName().contains("PlatformViewWrapper") || view instanceof FlutterMutatorView) { + needRemoveViews.add(view); + } + } + if (!needRemoveViews.isEmpty()) { + for (View needRemoveView : needRemoveViews) { + curFlutterView.removeView(needRemoveView); + } + } + } + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java new file mode 100644 index 000000000..8cad78dca --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java @@ -0,0 +1,360 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +import com.idlefish.flutterboost.containers.FlutterContainerManager; +import com.idlefish.flutterboost.containers.FlutterViewContainer; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import io.flutter.embedding.android.FlutterEngineProvider; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.view.FlutterMain; + +public class FlutterBoost { + public static final String ENGINE_ID = "flutter_boost_default_engine"; + + private LinkedList activityQueue = null; + private FlutterBoostPlugin plugin; + private boolean isBackForegroundEventOverridden = false; + private boolean isAppInBackground = false; + + + private FlutterBoost() { + } + + private static class LazyHolder { + static final FlutterBoost INSTANCE = new FlutterBoost(); + } + + public static FlutterBoost instance() { + return LazyHolder.INSTANCE; + } + + public interface Callback { + void onStart(FlutterEngine engine); + } + + /** + * Initializes engine and plugin. + * + * @param application the application + * @param delegate the FlutterBoostDelegate + * @param callback Invoke the callback when the engine was started. + */ + public void setup(Application application, FlutterBoostDelegate delegate, Callback callback) { + setup(application, delegate, callback, FlutterBoostSetupOptions.createDefault()); + } + + public void setup(Application application, FlutterBoostDelegate delegate, Callback callback, FlutterBoostSetupOptions options) { + if (options == null) { + options = FlutterBoostSetupOptions.createDefault(); + } + isBackForegroundEventOverridden = options.shouldOverrideBackForegroundEvent(); + FlutterBoostUtils.setDebugLoggingEnabled(options.isDebugLoggingEnabled()); + + // 1. initialize default engine + FlutterEngine engine = getEngine(); + if (engine == null) { + // First, get engine from option.flutterEngineProvider + if (options.flutterEngineProvider() != null) { + FlutterEngineProvider provider = options.flutterEngineProvider(); + engine = provider.provideFlutterEngine(application); + } + + if (engine == null) { + // Second, when the engine from option.flutterEngineProvider is null, + // we should create a new engine + engine = new FlutterEngine(application, null, null, new FBPlatformViewsController(), options.shellArgs(), true); + } + + // Cache the created FlutterEngine in the FlutterEngineCache. + FlutterEngineCache.getInstance().put(ENGINE_ID, engine); + } + + if (!engine.getDartExecutor().isExecutingDart()) { + // Pre-warm the cached FlutterEngine. + engine.getNavigationChannel().setInitialRoute(options.initialRoute()); + engine.getDartExecutor().executeDartEntrypoint(new DartExecutor.DartEntrypoint( + FlutterMain.findAppBundlePath(), options.dartEntrypoint()), options.dartEntrypointArgs()); + } + if (callback != null) callback.onStart(engine); + + // 2. set delegate + getPlugin().setDelegate(delegate); + + //3. register ActivityLifecycleCallbacks + setupActivityLifecycleCallback(application, isBackForegroundEventOverridden); + } + + /** + * Releases the engine resource. + */ + public void tearDown() { + FlutterEngine engine = getEngine(); + if (engine != null) { + engine.destroy(); + FlutterEngineCache.getInstance().remove(ENGINE_ID); + } + activityQueue = null; + plugin = null; + isBackForegroundEventOverridden = false; + isAppInBackground = false; + } + + /** + * Gets the FlutterBoostPlugin. + * + * @return the FlutterBoostPlugin. + */ + public FlutterBoostPlugin getPlugin() { + if (plugin == null) { + FlutterEngine engine = getEngine(); + if (engine == null) { + throw new RuntimeException("FlutterBoost might *not* have been initialized yet!!!"); + } + plugin = FlutterBoostUtils.getPlugin(engine); + } + return plugin; + } + + /** + * Gets the FlutterEngine in use. + * + * @return the FlutterEngine + */ + public FlutterEngine getEngine() { + return FlutterEngineCache.getInstance().get(ENGINE_ID); + } + + /** + * Gets the current activity. + * + * @return the current activity + */ + public Activity currentActivity() { + if (activityQueue != null && !activityQueue.isEmpty()) { + return activityQueue.peek(); + } else { + return null; + } + } + + /** + * Informs FlutterBoost of the back/foreground state. + * + * @param background a boolean indicating if the app goes to background + * or foreground. + */ + public void dispatchBackForegroundEvent(boolean background) { + if (!isBackForegroundEventOverridden) { + throw new RuntimeException("Oops! You should set override enable first by FlutterBoostSetupOptions."); + } + + if (background) { + getPlugin().onBackground(); + } else { + getPlugin().onForeground(); + } + setAppIsInBackground(background); + } + + /** + * Gets the FlutterView container with uniqueId. + *

+ * This is a legacy API for backwards compatibility. + * + * @param uniqueId The uniqueId of the container + * @return a FlutterView container + */ + public FlutterViewContainer findFlutterViewContainerById(String uniqueId) { + return FlutterContainerManager.instance().findContainerById(uniqueId); + } + + /** + * Gets the topmost container + *

+ * This is a legacy API for backwards compatibility. + * + * @return the topmost container + */ + public FlutterViewContainer getTopContainer() { + return FlutterContainerManager.instance().getTopContainer(); + } + + /** + * @param name The Flutter route name. + * @param arguments The bussiness arguments. + * @deprecated use open(FlutterBoostRouteOptions options) instead + * Open a Flutter page with name and arguments. + */ + public void open(String name, Map arguments) { + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(name) + .arguments(arguments) + .build(); + getPlugin().getDelegate().pushFlutterRoute(options); + } + + /** + * Use FlutterBoostRouteOptions to open a new Page + * + * @param options FlutterBoostRouteOptions object + */ + public void open(FlutterBoostRouteOptions options) { + getPlugin().getDelegate().pushFlutterRoute(options); + } + + /** + * Close the Flutter page with uniqueId. + * + * @param uniqueId The uniqueId of the Flutter page + */ + public void close(String uniqueId) { + Messages.CommonParams params = new Messages.CommonParams(); + params.setUniqueId(uniqueId); + getPlugin().popRoute(params,new Messages.Result(){ + @Override + public void success(Void result) { + } + + @Override + public void error(Throwable t) { + } + }); + } + + /** + * Change the application life cycle of Flutter + */ + public void changeFlutterAppLifecycle(int state) { + getPlugin().changeFlutterAppLifecycle(state); + } + + /** + * Add a event listener + * + * @param listener + * @return ListenerRemover, you can use this to remove this listener + */ + public ListenerRemover addEventListener(String key, EventListener listener) { + return getPlugin().addEventListener(key, listener); + } + + /** + * Send the event to flutter + * + * @param key the key of this event + * @param args the arguments of this event + */ + public void sendEventToFlutter(String key, Map args) { + getPlugin().sendEventToFlutter(key, args); + } + + public boolean isAppInBackground() { + return isAppInBackground; + } + + /*package*/ void setAppIsInBackground(boolean inBackground) { + isAppInBackground = inBackground; + } + + private void setupActivityLifecycleCallback(Application application, boolean isBackForegroundEventOverridden) { + application.registerActivityLifecycleCallbacks(new BoostActivityLifecycle(isBackForegroundEventOverridden)); + } + + private class BoostActivityLifecycle implements Application.ActivityLifecycleCallbacks { + private int activityReferences = 0; + private boolean isActivityChangingConfigurations = false; + private boolean isBackForegroundEventOverridden = false; + + public BoostActivityLifecycle(boolean isBackForegroundEventOverridden) { + this.isBackForegroundEventOverridden = isBackForegroundEventOverridden; + } + + private void dispatchForegroundEvent() { + if (isBackForegroundEventOverridden) { + return; + } + + FlutterBoost.instance().setAppIsInBackground(false); + FlutterBoost.instance().getPlugin().onForeground(); + } + + private void dispatchBackgroundEvent() { + if (isBackForegroundEventOverridden) { + return; + } + + FlutterBoost.instance().setAppIsInBackground(true); + FlutterBoost.instance().getPlugin().onBackground(); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (activityQueue == null) { + activityQueue = new LinkedList(); + } + activityQueue.addFirst(activity); + } + + @Override + public void onActivityStarted(Activity activity) { + if (++activityReferences == 1 && !isActivityChangingConfigurations) { + // App enters foreground + dispatchForegroundEvent(); + } + } + + @Override + public void onActivityResumed(Activity activity) { + if (activityQueue == null) { + activityQueue = new LinkedList(); + activityQueue.addFirst(activity); + } else if(activityQueue.isEmpty()) { + activityQueue.addFirst(activity); + } else if (activityQueue.peek() != activity) { + //针对多tab且每个tab都为Activity,在切换时并不会走remove,所以先从队列中删除再加入 + activityQueue.removeFirstOccurrence(activity); + activityQueue.addFirst(activity); + } + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + isActivityChangingConfigurations = activity.isChangingConfigurations(); + if (--activityReferences == 0 && !isActivityChangingConfigurations) { + // App enters background + dispatchBackgroundEvent(); + } + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + if (activityQueue != null && !activityQueue.isEmpty()) { + activityQueue.remove(activity); + } + } + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostDelegate.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostDelegate.java new file mode 100644 index 000000000..0aa9e49d1 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostDelegate.java @@ -0,0 +1,13 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +public interface FlutterBoostDelegate { + void pushNativeRoute(FlutterBoostRouteOptions options); + void pushFlutterRoute(FlutterBoostRouteOptions options); + default boolean popRoute(FlutterBoostRouteOptions options){ + return false; + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostPlugin.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostPlugin.java new file mode 100644 index 000000000..cee1228df --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostPlugin.java @@ -0,0 +1,425 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import android.util.Log; +import android.util.SparseArray; + +import com.idlefish.flutterboost.Messages.CommonParams; +import com.idlefish.flutterboost.Messages.FlutterRouterApi; +import com.idlefish.flutterboost.Messages.NativeRouterApi; +import com.idlefish.flutterboost.Messages.StackInfo; +import com.idlefish.flutterboost.containers.FlutterContainerManager; +import com.idlefish.flutterboost.containers.FlutterViewContainer; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; + +public class FlutterBoostPlugin implements FlutterPlugin, NativeRouterApi, ActivityAware { + private static final String TAG = "FlutterBoost_java"; + private static final String APP_LIFECYCLE_CHANGED_KEY = "app_lifecycle_changed_key"; + private static final String LIFECYCLE_STATE = "lifecycleState"; + // See https://github.com/flutter/engine/pull/42418 for details + private static final int FLUTTER_APP_STATE_RESUMED = 1; + private static final int FLUTTER_APP_STATE_PAUSED = 4; + + private FlutterEngine engine; + private FlutterRouterApi channel; + private FlutterBoostDelegate delegate; + private StackInfo dartStack; + private SparseArray pageNames; + private int requestCode = 1000; + + private HashMap> listenersTable = new HashMap<>(); + + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + public FlutterRouterApi getChannel() { + return channel; + } + + public void setDelegate(FlutterBoostDelegate delegate) { + this.delegate = delegate; + } + + public FlutterBoostDelegate getDelegate() { + return delegate; + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onAttachedToEngine: " + this); + NativeRouterApi.setup(binding.getBinaryMessenger(), this); + engine = binding.getFlutterEngine(); + channel = new FlutterRouterApi(binding.getBinaryMessenger()); + pageNames = new SparseArray(); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetachedFromEngine: " + this); + engine = null; + channel = null; + } + + @Override + public void pushNativeRoute(CommonParams params) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushNativeRoute: " + params.getUniqueId() + ", " + this); + if (delegate != null) { + requestCode++; + if (pageNames != null) { + pageNames.put(requestCode, params.getPageName()); + } + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(params.getPageName()) + .arguments((Map) (Object) params.getArguments()) + .requestCode(requestCode) + .build(); + delegate.pushNativeRoute(options); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* set delegate!"); + } + } + + @Override + public void pushFlutterRoute(CommonParams params) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushFlutterRoute: " + params.getUniqueId() + ", " + this); + if (delegate != null) { + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(params.getPageName()) + .uniqueId(params.getUniqueId()) + .opaque(params.getOpaque()) + .arguments((Map) (Object) params.getArguments()) + .build(); + delegate.pushFlutterRoute(options); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* set delegate!"); + } + } + + @Override + public void popRoute(CommonParams params, Messages.Result result) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#popRoute: " + params.getUniqueId() + ", " + this); + if (delegate != null) { + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(params.getPageName()) + .uniqueId(params.getUniqueId()) + .arguments((Map) (Object) params.getArguments()) + .build(); + boolean isHandle = delegate.popRoute(options); + //isHandle代表是否已经自定义处理,如果未自定义处理走默认逻辑 + if (!isHandle) { + String uniqueId = params.getUniqueId(); + if (uniqueId != null) { + FlutterViewContainer container = FlutterContainerManager.instance().findContainerById(uniqueId); + if (container != null) { + container.finishContainer((Map) (Object) params.getArguments()); + } + result.success(null); + } else { + throw new RuntimeException("Oops!! The unique id is null!"); + } + } else { + //被拦截处理了,那么直接通知result + result.success(null); + } + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* set delegate!"); + } + } + + @Override + public StackInfo getStackFromHost() { + if (dartStack == null) { + return StackInfo.fromMap(new HashMap()); + } + if (isDebugLoggingEnabled()) Log.d(TAG, "#getStackFromHost: " + dartStack + ", " + this); + return dartStack; + } + + @Override + public void saveStackToHost(StackInfo arg) { + dartStack = arg; + if (isDebugLoggingEnabled()) Log.d(TAG, "#saveStackToHost: " + dartStack + ", " + this); + } + + @Override + public void sendEventToNative(CommonParams arg) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#sendEventToNative: " + this); + //deal with the event from flutter side + String key = arg.getKey(); + Map arguments = arg.getArguments(); + assert (key != null); + + if (arguments == null) { + arguments = new HashMap<>(); + } + + List listeners = listenersTable.get(key); + if (listeners == null) { + return; + } + + for (EventListener listener : listeners) { + listener.onEvent(key, arguments); + } + } + + ListenerRemover addEventListener(String key, EventListener listener) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#addEventListener: " + key + ", " + this); + assert (key != null && listener != null); + + LinkedList listeners = listenersTable.get(key); + if (listeners == null) { + listeners = new LinkedList<>(); + listenersTable.put(key, listeners); + } + listeners.add(listener); + + LinkedList finalListeners = listeners; + return () -> finalListeners.remove(listener); + } + + void sendEventToFlutter(String key, Map args) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#sendEventToFlutter: " + key + ", " + this); + Messages.CommonParams params = new Messages.CommonParams(); + params.setKey(key); + params.setArguments(args); + getChannel().sendEventToFlutter(params, reply -> {}); + } + + void changeFlutterAppLifecycle(int state) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#changeFlutterAppLifecycle: " + state + ", " + this); + assert (state == FLUTTER_APP_STATE_PAUSED || state == FLUTTER_APP_STATE_RESUMED); + Map arguments = new HashMap(); + arguments.put(LIFECYCLE_STATE, state); + sendEventToFlutter(APP_LIFECYCLE_CHANGED_KEY, arguments); + } + + private void checkEngineState() { + if (engine == null || !engine.getDartExecutor().isExecutingDart()) { + throw new RuntimeException("The engine is not ready for use. " + + "The message may be drop silently by the engine. " + + "You should check 'DartExecutor.isExecutingDart()' first!"); + } + } + + public void pushRoute(String uniqueId, String pageName, Map arguments, + final FlutterRouterApi.Reply callback) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushRoute start: " + pageName + ", " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + params.setPageName(pageName); + params.setArguments(arguments); + channel.pushRoute(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushRoute end: " + pageName + ", " + uniqueId); + if (callback != null) { + callback.reply(null); + } + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void popRoute(String uniqueId, final FlutterRouterApi.Reply callback) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#popRoute start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.popRoute(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#popRoute end: " + uniqueId + ", " + this); + if (callback != null) { + callback.reply(null); + } + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onBackPressed() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed start: " + this); + if (channel != null) { + checkEngineState(); + channel.onBackPressed(reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed end: " + this); + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void removeRoute(String uniqueId, final FlutterRouterApi.Reply callback) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#removeRoute start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.removeRoute(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#removeRoute end: " + uniqueId + ", " + this); + if (callback != null) { + callback.reply(null); + } + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onForeground() { + Log.d(TAG, "## onForeground start: " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + channel.onForeground(params, reply -> { + Log.d(TAG, "## onForeground end: " + this); + }); + + // The scheduling frames are resumed when [onForeground] is called. + changeFlutterAppLifecycle(FLUTTER_APP_STATE_RESUMED); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onBackground() { + Log.d(TAG, "## onBackground start: " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + channel.onBackground(params, reply -> { + Log.d(TAG, "## onBackground end: " + this); + }); + + // The scheduling frames are paused when [onBackground] is called. + changeFlutterAppLifecycle(FLUTTER_APP_STATE_PAUSED); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onContainerShow(String uniqueId) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerShow start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.onContainerShow(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerShow end: " + uniqueId + ", " + this); + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onContainerHide(String uniqueId) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerHide start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.onContainerHide(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerHide end: " + uniqueId + ", " + this); + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onContainerCreated(FlutterViewContainer container) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerCreated: " + container.getUniqueId() + ", " + this); + FlutterContainerManager.instance().addContainer(container.getUniqueId(), container); + if (FlutterContainerManager.instance().getContainerSize() == 1) { + changeFlutterAppLifecycle(FLUTTER_APP_STATE_RESUMED); + } + } + + public void onContainerAppeared(FlutterViewContainer container, Runnable onPushRouteComplete) { + String uniqueId = container.getUniqueId(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerAppeared: " + uniqueId + ", " + this); + FlutterContainerManager.instance().activateContainer(uniqueId, container); + pushRoute(uniqueId, container.getUrl(), container.getUrlParams(), reply -> { + if (FlutterContainerManager.instance().isTopContainer(uniqueId)) { + if (onPushRouteComplete != null) { + onPushRouteComplete.run(); + } + } + }); + //onContainerDisappeared并非异步触发,为了匹配对应,onContainerShow也不做异步 + onContainerShow(uniqueId); + } + + public void onContainerDisappeared(FlutterViewContainer container) { + String uniqueId = container.getUniqueId(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerDisappeared: " + uniqueId + ", " + this); + onContainerHide(uniqueId); + } + + public void onContainerDestroyed(FlutterViewContainer container) { + String uniqueId = container.getUniqueId(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerDestroyed: " + uniqueId + ", " + this); + removeRoute(uniqueId, reply -> {}); + FlutterContainerManager.instance().removeContainer(uniqueId); + if (FlutterContainerManager.instance().getContainerSize() == 0) { + changeFlutterAppLifecycle(FLUTTER_APP_STATE_PAUSED); + } + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onAttachedToActivity: " + this); + activityPluginBinding.addActivityResultListener((requestCode, resultCode, intent) -> { + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + String pageName = pageNames.get(requestCode); + pageNames.remove(requestCode); + if (null != pageName) { + params.setPageName(pageName); + if (intent != null) { + Map result = FlutterBoostUtils.bundleToMap(intent.getExtras()); + params.setArguments(result); + } + + // Get a result back from an activity when it ends. + channel.onNativeResult(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onNativeResult return, pageName=" + pageName + ", " + this); + }); + } + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + return true; + }); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetachedFromActivityForConfigChanges: " + this); + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onReattachedToActivityForConfigChanges: " + this); + } + + @Override + public void onDetachedFromActivity() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetachedFromActivity: " + this); + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostRouteOptions.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostRouteOptions.java new file mode 100644 index 000000000..6fb73b06c --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostRouteOptions.java @@ -0,0 +1,84 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.util.Map; + +public class FlutterBoostRouteOptions { + private final String pageName; + private final Map arguments; + private final int requestCode; + private final String uniqueId; + private final boolean opaque; + + private FlutterBoostRouteOptions(FlutterBoostRouteOptions.Builder builder) { + this.pageName = builder.pageName; + this.arguments = builder.arguments; + this.requestCode = builder.requestCode; + this.uniqueId = builder.uniqueId; + this.opaque = builder.opaque; + } + + public String pageName() { + return pageName; + } + + public Map arguments() { + return arguments; + } + + public int requestCode() { + return requestCode; + } + + public String uniqueId() { + return uniqueId; + } + + public boolean opaque() { + return opaque; + } + + public static class Builder { + private String pageName; + private Map arguments; + private int requestCode; + private String uniqueId; + private boolean opaque = true; + + public Builder() { + } + + public FlutterBoostRouteOptions.Builder pageName(String pageName) { + this.pageName = pageName; + return this; + } + + public FlutterBoostRouteOptions.Builder arguments(Map arguments) { + this.arguments = arguments; + return this; + } + + public FlutterBoostRouteOptions.Builder requestCode(int requestCode) { + this.requestCode = requestCode; + return this; + } + + public FlutterBoostRouteOptions.Builder uniqueId(String uniqueId) { + this.uniqueId = uniqueId; + return this; + } + + public FlutterBoostRouteOptions.Builder opaque(boolean opaque) { + this.opaque = opaque; + return this; + } + + public FlutterBoostRouteOptions build() { + return new FlutterBoostRouteOptions(this); + } + } + +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostSetupOptions.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostSetupOptions.java new file mode 100644 index 000000000..3a7780ce3 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostSetupOptions.java @@ -0,0 +1,138 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.util.List; + +import io.flutter.embedding.android.FlutterEngineProvider; + +public class FlutterBoostSetupOptions { + private final String initialRoute; + private final String dartEntrypoint; + private final List dartEntrypointArgs; + private final String[] shellArgs; + private final boolean isDebugLoggingEnabled; + private final boolean shouldOverrideBackForegroundEvent; + private FlutterEngineProvider flutterEngineProvider; + + private FlutterBoostSetupOptions(Builder builder) { + this.initialRoute = builder.initialRoute; + this.dartEntrypoint = builder.dartEntrypoint; + this.dartEntrypointArgs = builder.dartEntrypointArgs; + this.shellArgs = builder.shellArgs; + this.isDebugLoggingEnabled = builder.isDebugLoggingEnabled; + this.shouldOverrideBackForegroundEvent = builder.shouldOverrideBackForegroundEvent; + this.flutterEngineProvider = builder.flutterEngineProvider; + } + + public static FlutterBoostSetupOptions createDefault() { + return new Builder().build(); + } + + public String initialRoute() { + return initialRoute; + } + + public String dartEntrypoint() { + return dartEntrypoint; + } + + public List dartEntrypointArgs() { + return dartEntrypointArgs; + } + + public String[] shellArgs() { + return shellArgs; + } + + public FlutterEngineProvider flutterEngineProvider() { + return flutterEngineProvider; + } + + public boolean isDebugLoggingEnabled() { + return isDebugLoggingEnabled; + } + + public boolean shouldOverrideBackForegroundEvent() { + return shouldOverrideBackForegroundEvent; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('['); + if (shellArgs == null || shellArgs.length == 0) { + sb.append(']'); + } else { + for (int i = 0; ; i++) { + sb.append(String.valueOf(shellArgs[i])); + if (i == shellArgs.length - 1) { + sb.append(']'); + break; + } + sb.append(", "); + } + } + return "initialRoute:" + this.initialRoute + + ", dartEntrypoint:" + this.dartEntrypoint + + ", isDebugLoggingEnabled: " + this.isDebugLoggingEnabled + + ", shouldOverrideBackForegroundEvent:" + this.shouldOverrideBackForegroundEvent + + ", shellArgs:" + sb.toString(); + } + + public static class Builder { + private String initialRoute = "/"; + private String dartEntrypoint = "main"; + private List dartEntrypointArgs; + private boolean isDebugLoggingEnabled = false; + private boolean shouldOverrideBackForegroundEvent = false; + private String[] shellArgs; + private FlutterEngineProvider flutterEngineProvider; + + public Builder() { + } + + public Builder initialRoute(String initialRoute){ + this.initialRoute = initialRoute; + return this; + } + + public Builder dartEntrypoint(String dartEntrypoint){ + this.dartEntrypoint = dartEntrypoint; + return this; + } + + public Builder dartEntrypointArgs(List args) { + this.dartEntrypointArgs = args; + return this; + } + + public Builder shellArgs(String[] shellArgs){ + this.shellArgs = shellArgs; + return this; + } + + public Builder flutterEngineProvider(FlutterEngineProvider flutterEngineProvider) { + this.flutterEngineProvider = flutterEngineProvider; + return this; + } + + public Builder isDebugLoggingEnabled(boolean enable) { + isDebugLoggingEnabled = enable; + return this; + } + + // Determines whether to override back/foreground event. + public Builder shouldOverrideBackForegroundEvent(boolean override) { + shouldOverrideBackForegroundEvent = override; + return this; + } + + public FlutterBoostSetupOptions build() { + FlutterBoostSetupOptions options = new FlutterBoostSetupOptions(this); + return options; + } + } +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostUtils.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostUtils.java new file mode 100644 index 000000000..deb486d71 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostUtils.java @@ -0,0 +1,168 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import android.app.Activity; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.WindowInsetsControllerCompat; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.platform.PlatformPlugin; + +/** + * Helper methods to deal with common tasks. + */ +public class FlutterBoostUtils { + // Control whether the internal debugging logs are turned on. + private static boolean sEnableDebugLogging = false; + + public static void setDebugLoggingEnabled(boolean enable) { + sEnableDebugLogging = enable; + } + + public static boolean isDebugLoggingEnabled() { + return sEnableDebugLogging; + } + + public static String createUniqueId(String name) { + return UUID.randomUUID().toString() + "_" + name; + } + + public static FlutterBoostPlugin getPlugin(FlutterEngine engine) { + if (engine != null) { + try { + Class pluginClass = + (Class) Class.forName("com.idlefish.flutterboost.FlutterBoostPlugin"); + return (FlutterBoostPlugin) engine.getPlugins().get(pluginClass); + } catch (Throwable t) { + t.printStackTrace(); + } + } + return null; + } + + public static Map bundleToMap(Bundle bundle) { + Map map = new HashMap<>(); + if (bundle == null || bundle.keySet().isEmpty()) { + return map; + } + Set keys = bundle.keySet(); + for (String key : keys) { + Object value = bundle.get(key); + if (value instanceof Bundle) { + map.put(key, bundleToMap(bundle.getBundle(key))); + } else if (value != null) { + map.put(key, value); + } + } + return map; + } + + public static FlutterView findFlutterView(View view) { + if (view instanceof FlutterView) { + return (FlutterView) view; + } + if (view instanceof ViewGroup) { + ViewGroup vp = (ViewGroup) view; + for (int i = 0; i < vp.getChildCount(); i++) { + View child = vp.getChildAt(i); + FlutterView fv = findFlutterView(child); + if (fv != null) { + return fv; + } + } + } + return null; + } + + @Nullable + public static PlatformChannel.SystemChromeStyle getCurrentSystemUiOverlayTheme(PlatformPlugin platformPlugin, boolean copy) { + if (platformPlugin != null) { + try { + Field field = platformPlugin.getClass().getDeclaredField("currentTheme"); + field.setAccessible(true); + PlatformChannel.SystemChromeStyle style = + (PlatformChannel.SystemChromeStyle) field.get(platformPlugin); + if (!copy || style == null) { + return style; + } else { + return copySystemChromeStyle(style); + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + return null; + } + + public static PlatformChannel.SystemChromeStyle copySystemChromeStyle(PlatformChannel.SystemChromeStyle style) { + if (style == null) { + return null; + } + return new PlatformChannel.SystemChromeStyle( + style.statusBarColor, + style.statusBarIconBrightness, + style.systemStatusBarContrastEnforced, + style.systemNavigationBarColor, + style.systemNavigationBarIconBrightness, + style.systemNavigationBarDividerColor, + style.systemNavigationBarContrastEnforced + ); + } + + public static PlatformChannel.SystemChromeStyle mergeSystemChromeStyle(PlatformChannel.SystemChromeStyle old, PlatformChannel.SystemChromeStyle ne_w) { + if (ne_w == null) { + return copySystemChromeStyle(old); + } + if (old == null) { + return copySystemChromeStyle(ne_w); + } + return new PlatformChannel.SystemChromeStyle( + ne_w.statusBarColor != null ? ne_w.statusBarColor : old.statusBarColor, + ne_w.statusBarIconBrightness != null ? ne_w.statusBarIconBrightness : old.statusBarIconBrightness, + ne_w.systemStatusBarContrastEnforced != null ? ne_w.systemStatusBarContrastEnforced : old.systemStatusBarContrastEnforced, + ne_w.systemNavigationBarColor != null ? ne_w.systemNavigationBarColor : old.systemNavigationBarColor, + ne_w.systemNavigationBarIconBrightness != null ? ne_w.systemNavigationBarIconBrightness : old.systemNavigationBarIconBrightness, + ne_w.systemNavigationBarDividerColor != null ? ne_w.systemNavigationBarDividerColor : old.systemNavigationBarDividerColor, + ne_w.systemNavigationBarContrastEnforced != null ? ne_w.systemNavigationBarContrastEnforced : old.systemNavigationBarContrastEnforced + ); + } + + public static void setSystemChromeSystemUIOverlayStyle(@NonNull PlatformPlugin platformPlugin, + @NonNull PlatformChannel.SystemChromeStyle systemChromeStyle) { + try { + Method mth = platformPlugin.getClass().getDeclaredMethod( + "setSystemChromeSystemUIOverlayStyle", PlatformChannel.SystemChromeStyle.class); + mth.setAccessible(true); + mth.invoke(platformPlugin, systemChromeStyle); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/ListenerRemover.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/ListenerRemover.java new file mode 100644 index 000000000..7077e6901 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/ListenerRemover.java @@ -0,0 +1,12 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +/** + * The interface to remove the EventListener added in list + */ +public interface ListenerRemover { + void remove(); +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Messages.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Messages.java new file mode 100644 index 000000000..c4ff23a59 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Messages.java @@ -0,0 +1,613 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package com.idlefish.flutterboost; + +import android.util.Log; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class Messages { + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class CommonParams { + private Boolean opaque; + public Boolean getOpaque() { return opaque; } + public void setOpaque(Boolean setterArg) { + this.opaque = setterArg; + } + + private String key; + public String getKey() { return key; } + public void setKey(String setterArg) { + this.key = setterArg; + } + + private String pageName; + public String getPageName() { return pageName; } + public void setPageName(String setterArg) { + this.pageName = setterArg; + } + + private String uniqueId; + public String getUniqueId() { return uniqueId; } + public void setUniqueId(String setterArg) { + this.uniqueId = setterArg; + } + + private Map arguments; + public Map getArguments() { return arguments; } + public void setArguments(Map setterArg) { + this.arguments = setterArg; + } + + public static final class Builder { + private Boolean opaque; + public Builder setOpaque(Boolean setterArg) { + this.opaque = setterArg; + return this; + } + private String key; + public Builder setKey(String setterArg) { + this.key = setterArg; + return this; + } + private String pageName; + public Builder setPageName(String setterArg) { + this.pageName = setterArg; + return this; + } + private String uniqueId; + public Builder setUniqueId(String setterArg) { + this.uniqueId = setterArg; + return this; + } + private Map arguments; + public Builder setArguments(Map setterArg) { + this.arguments = setterArg; + return this; + } + public CommonParams build() { + CommonParams pigeonReturn = new CommonParams(); + pigeonReturn.setOpaque(opaque); + pigeonReturn.setKey(key); + pigeonReturn.setPageName(pageName); + pigeonReturn.setUniqueId(uniqueId); + pigeonReturn.setArguments(arguments); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("opaque", opaque); + toMapResult.put("key", key); + toMapResult.put("pageName", pageName); + toMapResult.put("uniqueId", uniqueId); + toMapResult.put("arguments", arguments); + return toMapResult; + } + static CommonParams fromMap(Map map) { + CommonParams pigeonResult = new CommonParams(); + Object opaque = map.get("opaque"); + pigeonResult.setOpaque((Boolean)opaque); + Object key = map.get("key"); + pigeonResult.setKey((String)key); + Object pageName = map.get("pageName"); + pigeonResult.setPageName((String)pageName); + Object uniqueId = map.get("uniqueId"); + pigeonResult.setUniqueId((String)uniqueId); + Object arguments = map.get("arguments"); + pigeonResult.setArguments((Map)arguments); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class StackInfo { + private List ids; + public List getIds() { return ids; } + public void setIds(List setterArg) { + this.ids = setterArg; + } + + private Map containers; + public Map getContainers() { return containers; } + public void setContainers(Map setterArg) { + this.containers = setterArg; + } + + public static final class Builder { + private List ids; + public Builder setIds(List setterArg) { + this.ids = setterArg; + return this; + } + private Map containers; + public Builder setContainers(Map setterArg) { + this.containers = setterArg; + return this; + } + public StackInfo build() { + StackInfo pigeonReturn = new StackInfo(); + pigeonReturn.setIds(ids); + pigeonReturn.setContainers(containers); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("ids", ids); + toMapResult.put("containers", containers); + return toMapResult; + } + static StackInfo fromMap(Map map) { + StackInfo pigeonResult = new StackInfo(); + Object ids = map.get("ids"); + pigeonResult.setIds((List)ids); + Object containers = map.get("containers"); + pigeonResult.setContainers((Map)containers); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class FlutterContainer { + private List pages; + public List getPages() { return pages; } + public void setPages(List setterArg) { + this.pages = setterArg; + } + + public static final class Builder { + private List pages; + public Builder setPages(List setterArg) { + this.pages = setterArg; + return this; + } + public FlutterContainer build() { + FlutterContainer pigeonReturn = new FlutterContainer(); + pigeonReturn.setPages(pages); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("pages", pages); + return toMapResult; + } + static FlutterContainer fromMap(Map map) { + FlutterContainer pigeonResult = new FlutterContainer(); + Object pages = map.get("pages"); + pigeonResult.setPages((List)pages); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class FlutterPage { + private Boolean withContainer; + public Boolean getWithContainer() { return withContainer; } + public void setWithContainer(Boolean setterArg) { + this.withContainer = setterArg; + } + + private String pageName; + public String getPageName() { return pageName; } + public void setPageName(String setterArg) { + this.pageName = setterArg; + } + + private String uniqueId; + public String getUniqueId() { return uniqueId; } + public void setUniqueId(String setterArg) { + this.uniqueId = setterArg; + } + + private Map arguments; + public Map getArguments() { return arguments; } + public void setArguments(Map setterArg) { + this.arguments = setterArg; + } + + public static final class Builder { + private Boolean withContainer; + public Builder setWithContainer(Boolean setterArg) { + this.withContainer = setterArg; + return this; + } + private String pageName; + public Builder setPageName(String setterArg) { + this.pageName = setterArg; + return this; + } + private String uniqueId; + public Builder setUniqueId(String setterArg) { + this.uniqueId = setterArg; + return this; + } + private Map arguments; + public Builder setArguments(Map setterArg) { + this.arguments = setterArg; + return this; + } + public FlutterPage build() { + FlutterPage pigeonReturn = new FlutterPage(); + pigeonReturn.setWithContainer(withContainer); + pigeonReturn.setPageName(pageName); + pigeonReturn.setUniqueId(uniqueId); + pigeonReturn.setArguments(arguments); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("withContainer", withContainer); + toMapResult.put("pageName", pageName); + toMapResult.put("uniqueId", uniqueId); + toMapResult.put("arguments", arguments); + return toMapResult; + } + static FlutterPage fromMap(Map map) { + FlutterPage pigeonResult = new FlutterPage(); + Object withContainer = map.get("withContainer"); + pigeonResult.setWithContainer((Boolean)withContainer); + Object pageName = map.get("pageName"); + pigeonResult.setPageName((String)pageName); + Object uniqueId = map.get("uniqueId"); + pigeonResult.setUniqueId((String)uniqueId); + Object arguments = map.get("arguments"); + pigeonResult.setArguments((Map)arguments); + return pigeonResult; + } + } + + public interface Result { + void success(T result); + void error(Throwable error); + } + private static class NativeRouterApiCodec extends StandardMessageCodec { + public static final NativeRouterApiCodec INSTANCE = new NativeRouterApiCodec(); + private NativeRouterApiCodec() {} + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte)128: + return CommonParams.fromMap((Map) readValue(buffer)); + + case (byte)129: + return FlutterContainer.fromMap((Map) readValue(buffer)); + + case (byte)130: + return FlutterPage.fromMap((Map) readValue(buffer)); + + case (byte)131: + return StackInfo.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof CommonParams) { + stream.write(128); + writeValue(stream, ((CommonParams) value).toMap()); + } else + if (value instanceof FlutterContainer) { + stream.write(129); + writeValue(stream, ((FlutterContainer) value).toMap()); + } else + if (value instanceof FlutterPage) { + stream.write(130); + writeValue(stream, ((FlutterPage) value).toMap()); + } else + if (value instanceof StackInfo) { + stream.write(131); + writeValue(stream, ((StackInfo) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + public interface NativeRouterApi { + void pushNativeRoute(CommonParams param); + void pushFlutterRoute(CommonParams param); + void popRoute(CommonParams param, Result result); + StackInfo getStackFromHost(); + void saveStackToHost(StackInfo stack); + void sendEventToNative(CommonParams params); + + /** The codec used by NativeRouterApi. */ + static MessageCodec getCodec() { + return NativeRouterApiCodec.INSTANCE; + } + + /** Sets up an instance of `NativeRouterApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, NativeRouterApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.pushNativeRoute", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramArg = (CommonParams)args.get(0); + if (paramArg == null) { + throw new NullPointerException("paramArg unexpectedly null."); + } + api.pushNativeRoute(paramArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramArg = (CommonParams)args.get(0); + if (paramArg == null) { + throw new NullPointerException("paramArg unexpectedly null."); + } + api.pushFlutterRoute(paramArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.popRoute", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramArg = (CommonParams)args.get(0); + if (paramArg == null) { + throw new NullPointerException("paramArg unexpectedly null."); + } + Result resultCallback = new Result() { + public void success(Void result) { + wrapped.put("result", null); + reply.reply(wrapped); + } + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.popRoute(paramArg, resultCallback); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.getStackFromHost", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + StackInfo output = api.getStackFromHost(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.saveStackToHost", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + StackInfo stackArg = (StackInfo)args.get(0); + if (stackArg == null) { + throw new NullPointerException("stackArg unexpectedly null."); + } + api.saveStackToHost(stackArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.sendEventToNative", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramsArg = (CommonParams)args.get(0); + if (paramsArg == null) { + throw new NullPointerException("paramsArg unexpectedly null."); + } + api.sendEventToNative(paramsArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class FlutterRouterApiCodec extends StandardMessageCodec { + public static final FlutterRouterApiCodec INSTANCE = new FlutterRouterApiCodec(); + private FlutterRouterApiCodec() {} + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte)128: + return CommonParams.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof CommonParams) { + stream.write(128); + writeValue(stream, ((CommonParams) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + public static class FlutterRouterApi { + private final BinaryMessenger binaryMessenger; + public FlutterRouterApi(BinaryMessenger argBinaryMessenger){ + this.binaryMessenger = argBinaryMessenger; + } + public interface Reply { + void reply(T reply); + } + static MessageCodec getCodec() { + return FlutterRouterApiCodec.INSTANCE; + } + + public void pushRoute(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.pushRoute", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void popRoute(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.popRoute", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void removeRoute(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.removeRoute", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onForeground(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onForeground", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onBackground(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onBackground", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onNativeResult(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onNativeResult", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onContainerShow(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onContainerShow", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onContainerHide(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onContainerHide", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void sendEventToFlutter(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onBackPressed(Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onBackPressed", getCodec()); + channel.send(null, channelReply -> { + callback.reply(null); + }); + } + } + private static Map wrapError(Throwable exception) { + Map errorMap = new HashMap<>(); + errorMap.put("message", exception.toString()); + errorMap.put("code", exception.getClass().getSimpleName()); + errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + return errorMap; + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/ContainerThemeMgr.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/ContainerThemeMgr.java new file mode 100644 index 000000000..a510d70f8 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/ContainerThemeMgr.java @@ -0,0 +1,52 @@ +package com.idlefish.flutterboost.containers; + +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.util.HashMap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; + +/** + * @author : Joe Chan + * @date : 2024/3/8 11:30 + */ +public class ContainerThemeMgr { + private static final HashMap themes = + new HashMap<>(); + private static PlatformChannel.SystemChromeStyle finalStyle; + + @UiThread + public static void onActivityPause(@NonNull FlutterBoostActivity activity, PlatformChannel.SystemChromeStyle restoreTheme) { + finalStyle = null; + if (activity.platformPlugin == null) { + return; + } + int hash = activity.hashCode(); + PlatformChannel.SystemChromeStyle style = + FlutterBoostUtils.getCurrentSystemUiOverlayTheme(activity.platformPlugin, true); + PlatformChannel.SystemChromeStyle mergedStyle = FlutterBoostUtils.mergeSystemChromeStyle(restoreTheme, style); + if (mergedStyle != null) { + themes.put(hash, mergedStyle); + } + } + + @UiThread + public static void onActivityDestroy(@NonNull FlutterBoostActivity activity) { + PlatformChannel.SystemChromeStyle style = themes.remove(activity.hashCode()); + if (themes.isEmpty()) { + finalStyle = style; + } + } + + @Nullable + public static PlatformChannel.SystemChromeStyle findTheme(@NonNull FlutterBoostActivity activity) { + return themes.get(activity.hashCode()); + } + + public static PlatformChannel.SystemChromeStyle getFinalStyle() { + return FlutterBoostUtils.copySystemChromeStyle(finalStyle); + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterActivityLaunchConfigs.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterActivityLaunchConfigs.java new file mode 100644 index 000000000..0050e4d8a --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterActivityLaunchConfigs.java @@ -0,0 +1,23 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +public class FlutterActivityLaunchConfigs { + + // Intent extra arguments. + public static final String EXTRA_BACKGROUND_MODE = "background_mode"; + public static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; + public static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = + "destroy_engine_with_activity"; + public static final String EXTRA_ENABLE_STATE_RESTORATION = "enable_state_restoration"; + public static final String EXTRA_URL = "url"; + public static final String EXTRA_URL_PARAM = "url_param"; + public static final String EXTRA_UNIQUE_ID = "unique_id"; + + // for onActivityResult + public static final String ACTIVITY_RESULT_KEY = "ActivityResult"; + + private FlutterActivityLaunchConfigs() {} +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostActivity.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostActivity.java new file mode 100644 index 000000000..80982cbdd --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostActivity.java @@ -0,0 +1,436 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +import com.idlefish.flutterboost.Assert; +import com.idlefish.flutterboost.FlutterBoost; +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; +import io.flutter.embedding.android.FlutterTextureView; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.android.RenderMode; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.platform.PlatformPlugin; + +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.ACTIVITY_RESULT_KEY; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_UNIQUE_ID; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL_PARAM; + +public class FlutterBoostActivity extends FlutterActivity implements FlutterViewContainer { + private static final String TAG = "FlutterBoost_java"; + private final String who = UUID.randomUUID().toString(); + private final FlutterTextureHooker textureHooker =new FlutterTextureHooker(); + private FlutterView flutterView; + protected PlatformPlugin platformPlugin; + private LifecycleStage stage; + private boolean isAttached = false; + + PlatformChannel.SystemChromeStyle restoreTheme = null; + + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onCreate: " + this); + final FlutterContainerManager containerManager = FlutterContainerManager.instance(); + FlutterViewContainer top = containerManager.getTopContainer(); + if (top != this && top instanceof FlutterBoostActivity) { + // find the theme of the previous container + restoreTheme = ContainerThemeMgr.findTheme((FlutterBoostActivity) top); + } else if (top == null) { + // this is the first active container, try to get the theme of the last-destroyed container + restoreTheme = ContainerThemeMgr.getFinalStyle(); + } + super.onCreate(savedInstanceState); + stage = LifecycleStage.ON_CREATE; + flutterView = FlutterBoostUtils.findFlutterView(getWindow().getDecorView()); + flutterView.detachFromFlutterEngine(); // Avoid failure when attaching to engine in |onResume|. + FlutterBoost.instance().getPlugin().onContainerCreated(this); + } + + @Override + public void detachFromFlutterEngine() { + /** + * TODO:// Override and do nothing to avoid destroying + * FlutterView unexpectedly. + */ + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromFlutterEngine: " + this); + } + + @Override + public boolean shouldDispatchAppLifecycleState() { + return false; + } + + /** + * 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}. + */ + @Override + public boolean attachToEngineAutomatically() { + return false; + } + + @Override + // This method is called right before the activity's onPause() callback. + public void onUserLeaveHint() { + super.onUserLeaveHint(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUserLeaveHint: " + this); + } + + @Override + protected void onStart() { + super.onStart(); + stage = LifecycleStage.ON_START; + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStart: " + this); + } + + @Override + protected void onStop() { + super.onStop(); + stage = LifecycleStage.ON_STOP; + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStop: " + this); + } + + @Override + public void onResume() { + super.onResume(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onResume: " + this + ", isOpaque=" + isOpaque()); + final FlutterContainerManager containerManager = FlutterContainerManager.instance(); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + FlutterViewContainer top = containerManager.getTopActivityContainer(); + boolean isActiveContainer = containerManager.isActiveContainer(this); + if (isActiveContainer && top != null && top != this && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_RESUME; + + + // try to detach *prevous* container from the engine. + FlutterViewContainer top = containerManager.getTopContainer(); + if (top != null && top != this) top.detachFromEngineIfNeeded(); + + FlutterBoost.instance().getPlugin().onContainerAppeared(this, () -> { + // attach new container to the engine. + attachToEngineIfNeeded(); + textureHooker.onFlutterTextureViewRestoreState(); + // Since we takeover PlatformPlugin from FlutterActivityAndFragmentDelegate, + // the system UI overlays can't be updated in |onPostResume| callback. So we + // update system UI overlays to match Flutter's desired system chrome style here. + onUpdateSystemUiOverlays(); + }); + } + + // Update system UI overlays to match Flutter's desired system chrome style + protected void onUpdateSystemUiOverlays() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUpdateSystemUiOverlays: " + this); + Assert.assertNotNull(platformPlugin); + platformPlugin.updateSystemUiOverlays(); + } + + @Override + protected void onPause() { + super.onPause(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onPause: " + this + ", isOpaque=" + isOpaque()); + // update the restoreTheme of this container + ContainerThemeMgr.onActivityPause(this, restoreTheme); + restoreTheme = ContainerThemeMgr.findTheme(this); + FlutterViewContainer top = FlutterContainerManager.instance().getTopActivityContainer(); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + if (top != null && top != this && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_PAUSE; + + FlutterBoost.instance().getPlugin().onContainerDisappeared(this); + + // We defer |performDetach| call to new Flutter container's |onResume|. + setIsFlutterUiDisplayed(false); + } + + @Override + public void onFlutterTextureViewCreated(FlutterTextureView flutterTextureView) { + super.onFlutterTextureViewCreated(flutterTextureView); + textureHooker.hookFlutterTextureView(flutterTextureView); + } + + private void performAttach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performAttach: " + this); + + // Attach plugins to the activity. + getFlutterEngine().getActivityControlSurface().attachToActivity(getExclusiveAppComponent(), getLifecycle()); + + if (platformPlugin == null) { + platformPlugin = new PlatformPlugin(getActivity(), getFlutterEngine().getPlatformChannel(), this); + // Set the restoreTheme to current container + if (restoreTheme != null) { + FlutterBoostUtils.setSystemChromeSystemUIOverlayStyle(platformPlugin, restoreTheme); + } + } + + // Attach rendering pipeline. + flutterView.attachToFlutterEngine(getFlutterEngine()); + } + + private void performDetach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performDetach: " + this); + + // Plugins are no longer attached to the activity. + getFlutterEngine().getActivityControlSurface().detachFromActivity(); + + // Release Flutter's control of UI such as system chrome. + releasePlatformChannel(); + + // Detach rendering pipeline. + flutterView.detachFromFlutterEngine(); + } + + private void releasePlatformChannel() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#releasePlatformChannel: " + this); + if (platformPlugin != null) { + platformPlugin.destroy(); + platformPlugin = null; + } + } + + // Fix black screen when activity transition + private void setIsFlutterUiDisplayed(boolean isDisplayed) { + try { + FlutterRenderer flutterRenderer = getFlutterEngine().getRenderer(); + Field isDisplayingFlutterUiField = FlutterRenderer.class.getDeclaredField("isDisplayingFlutterUi"); + isDisplayingFlutterUiField.setAccessible(true); + isDisplayingFlutterUiField.setBoolean(flutterRenderer, false); + assert(!flutterRenderer.isDisplayingFlutterUi()); + } catch (Exception e) { + Log.e(TAG, "You *should* keep fields in io.flutter.embedding.engine.renderer.FlutterRenderer."); + e.printStackTrace(); + } + } + + public void attachToEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#attachToEngineIfNeeded: " + this); + if (!isAttached) { + performAttach(); + isAttached = true; + } + } + + @Override + public void detachFromEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromEngineIfNeeded: " + this); + if (isAttached) { + performDetach(); + isAttached = false; + } + } + + @Override + protected void onDestroy() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDestroy: " + this); + ContainerThemeMgr.onActivityDestroy(this); + stage = LifecycleStage.ON_DESTROY; + detachFromEngineIfNeeded(); + textureHooker.onFlutterTextureViewRelease(); + FlutterBoost.instance().getPlugin().onContainerDestroyed(this); + + // Call super's onDestroy + super.onDestroy(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onConfigurationChanged: " + (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? "LANDSCAPE" : "PORTRAIT") + ", " + this); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onSaveInstanceState: " + this); + } + + @Override + public boolean shouldRestoreAndSaveState() { + if (getIntent().hasExtra(EXTRA_ENABLE_STATE_RESTORATION)) { + return getIntent().getBooleanExtra(EXTRA_ENABLE_STATE_RESTORATION, false); + } + // Defaults to |true|. + return true; + } + + @Override + public PlatformPlugin providePlatformPlugin(Activity activity, FlutterEngine flutterEngine) { + // We takeover |PlatformPlugin| here. + return null; + } + + @Override + public boolean shouldDestroyEngineWithHost() { + // The |FlutterEngine| should outlive this FlutterActivity. + return false; + } + + @Override + public boolean shouldAttachEngineToActivity() { + // We manually manage the relationship between the Activity and FlutterEngine here. + return false; + } + + @Override + public void onBackPressed() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed: " + this); + // Intercept the user's press of the back key. + FlutterBoost.instance().getPlugin().onBackPressed(); + } + + @Override + public RenderMode getRenderMode() { + // Default to |FlutterTextureView|. + return RenderMode.texture; + } + + @Override + public Activity getContextActivity() { + return this; + } + + @Override + public void finishContainer(Map result) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#finishContainer: " + this); + if (result != null) { + Intent intent = new Intent(); + intent.putExtra(ACTIVITY_RESULT_KEY, new HashMap(result)); + setResult(Activity.RESULT_OK, intent); + } + finish(); + } + + @Override + public String getUrl() { + if (!getIntent().hasExtra(EXTRA_URL)) { + Log.e(TAG, "Oops! The activity url are *MISSED*! You should override" + + " the |getUrl|, or set url via |CachedEngineIntentBuilder.url|."); + return null; + } + return getIntent().getStringExtra(EXTRA_URL); + } + + @Override + public Map getUrlParams() { + return (HashMap)getIntent().getSerializableExtra(EXTRA_URL_PARAM); + } + + @Override + public String getUniqueId() { + if (!getIntent().hasExtra(EXTRA_UNIQUE_ID)) { + return this.who; + } + return getIntent().getStringExtra(EXTRA_UNIQUE_ID); + } + + @Override + public String getCachedEngineId() { + return FlutterBoost.ENGINE_ID; + } + + @Override + public boolean isOpaque() { + return getBackgroundMode() == BackgroundMode.opaque; + } + + @Override + public boolean isPausing() { + return (stage == LifecycleStage.ON_PAUSE || stage == LifecycleStage.ON_STOP) && !isFinishing(); + } + + public static class CachedEngineIntentBuilder { + private final Class activityClass; + private boolean destroyEngineWithActivity = false; + private String backgroundMode = BackgroundMode.opaque.name(); + private String url; + private HashMap params; + private String uniqueId; + + public CachedEngineIntentBuilder(Class activityClass) { + this.activityClass = activityClass; + } + + + public FlutterBoostActivity.CachedEngineIntentBuilder destroyEngineWithActivity(boolean destroyEngineWithActivity) { + this.destroyEngineWithActivity = destroyEngineWithActivity; + return this; + } + + + public FlutterBoostActivity.CachedEngineIntentBuilder backgroundMode(BackgroundMode backgroundMode) { + this.backgroundMode = backgroundMode.name(); + return this; + } + + public FlutterBoostActivity.CachedEngineIntentBuilder url(String url) { + this.url = url; + return this; + } + + public FlutterBoostActivity.CachedEngineIntentBuilder urlParams(Map params) { + this.params = (params instanceof HashMap) ? (HashMap)params : new HashMap(params); + return this; + } + + public FlutterBoostActivity.CachedEngineIntentBuilder uniqueId(String uniqueId) { + this.uniqueId = uniqueId; + return this; + } + + public Intent build(Context context) { + return new Intent(context, activityClass) + .putExtra(EXTRA_CACHED_ENGINE_ID, FlutterBoost.ENGINE_ID) // default engine + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, destroyEngineWithActivity) + .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode) + .putExtra(EXTRA_URL, url) + .putExtra(EXTRA_URL_PARAM, params) + .putExtra(EXTRA_UNIQUE_ID, uniqueId != null ? uniqueId : FlutterBoostUtils.createUniqueId(url)); + } + } + +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostFragment.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostFragment.java new file mode 100644 index 000000000..3e24df9eb --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostFragment.java @@ -0,0 +1,530 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.ACTIVITY_RESULT_KEY; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_UNIQUE_ID; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL_PARAM; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.idlefish.flutterboost.Assert; +import com.idlefish.flutterboost.FlutterBoost; +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import io.flutter.embedding.android.FlutterFragment; +import io.flutter.embedding.android.FlutterTextureView; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.android.RenderMode; +import io.flutter.embedding.android.TransparencyMode; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.platform.PlatformPlugin; + +public class FlutterBoostFragment extends FlutterFragment implements FlutterViewContainer { + private static final String TAG = "FlutterBoost_java"; + private final String who = UUID.randomUUID().toString(); + private final FlutterTextureHooker textureHooker=new FlutterTextureHooker(); + private FlutterView flutterView; + private PlatformPlugin platformPlugin; + private LifecycleStage stage; + private boolean isAttached = false; + private boolean isFinishing = false; + + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + @Override + public void detachFromFlutterEngine() { + /** + * TODO:// Override and do nothing to avoid destroying + * FlutterView unexpectedly. + */ + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromFlutterEngine: " + this); + } + + @Override + public boolean shouldDispatchAppLifecycleState() { + return false; + } + + /** + * 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}. + */ + @Override + public boolean attachToEngineAutomatically() { + return false; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onCreate: " + this); + stage = LifecycleStage.ON_CREATE; + } + + @Override + public void onStart() { + super.onStart(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStart: " + this); + } + + @Override + public void onDestroy() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDestroy: " + this); + stage = LifecycleStage.ON_DESTROY; + textureHooker.onFlutterTextureViewRelease(); + detachFromEngineIfNeeded(); + + // Call super's onDestroy + super.onDestroy(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onAttach: " + this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onCreateView: " + this); + FlutterBoost.instance().getPlugin().onContainerCreated(this); + View view = super.onCreateView(inflater, container, savedInstanceState); + flutterView = FlutterBoostUtils.findFlutterView(view); + // Detach FlutterView from engine before |onResume|. + flutterView.detachFromFlutterEngine(); + if (view == flutterView) { + // fix https://github.com/alibaba/flutter_boost/issues/1732 + FrameLayout frameLayout = new FrameLayout(view.getContext()); + frameLayout.addView(view); + return frameLayout; + } + return view; + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onHiddenChanged: hidden=" + hidden + ", " + this); + // If |onHiddenChanged| method is called before the |onCreateView|, + // we just return here. + if (flutterView == null) return; + if (hidden) { + didFragmentHide(); + } else { + didFragmentShow(() -> {}); + } + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (isDebugLoggingEnabled()) Log.d(TAG, "#setUserVisibleHint: isVisibleToUser=" + isVisibleToUser + ", " + this); + // If |setUserVisibleHint| method is called before the |onCreateView|, + // we just return here. + if (flutterView == null) return; + if (isVisibleToUser) { + didFragmentShow(() -> {}); + } else { + didFragmentHide(); + } + } + + @Override + public void onResume() { + super.onResume(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onResume: isHidden=" + isHidden() + ", " + this); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + final FlutterContainerManager containerManager = FlutterContainerManager.instance(); + FlutterViewContainer top = containerManager.getTopActivityContainer(); + boolean isActiveContainer = containerManager.isActiveContainer(this); + if (isActiveContainer && top != null && top != this.getContextActivity() && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_RESUME; + if (!isHidden()) { + didFragmentShow(() -> { + // Update system UI overlays to match Flutter's desired system chrome style + onUpdateSystemUiOverlays(); + }); + } + } + + // Update system UI overlays to match Flutter's desired system chrome style + protected void onUpdateSystemUiOverlays() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUpdateSystemUiOverlays: " + this); + Assert.assertNotNull(platformPlugin); + platformPlugin.updateSystemUiOverlays(); + } + + @Override + public RenderMode getRenderMode() { + // Default to |FlutterTextureView|. + return RenderMode.texture; + } + + @Override + public void onPause() { + super.onPause(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onPause: " + this + ", isFinshing=" + isFinishing); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + FlutterViewContainer top = FlutterContainerManager.instance().getTopActivityContainer(); + if (top != null && top != this.getContextActivity() && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_PAUSE; + didFragmentHide(); + } + + @Override + public void onStop() { + super.onStop(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStop: " + this); + stage = LifecycleStage.ON_STOP; + } + + @Override + public void onDestroyView() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDestroyView: " + this); + FlutterBoost.instance().getPlugin().onContainerDestroyed(this); + + super.onDestroyView(); + } + + @Override + public void onDetach() { + super.onDetach(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetach: " + this); + } + + @Override + // This method is called right before the activity's onPause() callback. + public void onUserLeaveHint() { + super.onUserLeaveHint(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUserLeaveHint: " + this); + } + + @Override + public void onBackPressed() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed: " + this); + // Intercept the user's press of the back key. + FlutterBoost.instance().getPlugin().onBackPressed(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onConfigurationChanged: " + (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? "LANDSCAPE" : "PORTRAIT") + ", " + this); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onSaveInstanceState: " + this); + } + + @Override + public boolean shouldRestoreAndSaveState() { + if (getArguments().containsKey(ARG_ENABLE_STATE_RESTORATION)) { + return getArguments().getBoolean(ARG_ENABLE_STATE_RESTORATION); + } + // Defaults to |true|. + return true; + } + + @Override + public PlatformPlugin providePlatformPlugin(Activity activity, FlutterEngine flutterEngine) { + // We takeover |PlatformPlugin| here. + return null; + } + + @Override + public boolean shouldDestroyEngineWithHost() { + // The |FlutterEngine| should outlive this FlutterFragment. + return false; + } + + @Override + public void onFlutterTextureViewCreated(FlutterTextureView flutterTextureView) { + super.onFlutterTextureViewCreated(flutterTextureView); + textureHooker.hookFlutterTextureView(flutterTextureView); + } + + @Override + public Activity getContextActivity() { + return getActivity(); + } + + @Override + public void finishContainer(Map result) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#finishContainer: " + this); + isFinishing = true; + if (result != null) { + Intent intent = new Intent(); + intent.putExtra(ACTIVITY_RESULT_KEY, new HashMap(result)); + getActivity().setResult(Activity.RESULT_OK, intent); + } + onFinishContainer(); + } + + // finish activity container + protected void onFinishContainer() { + getActivity().finish(); + } + + @Override + public String getUrl() { + if (!getArguments().containsKey(EXTRA_URL)) { + throw new RuntimeException("Oops! The fragment url are *MISSED*! You should " + + "override the |getUrl|, or set url via CachedEngineFragmentBuilder."); + } + return getArguments().getString(EXTRA_URL); + } + + @Override + public Map getUrlParams() { + return (HashMap)getArguments().getSerializable(EXTRA_URL_PARAM); + } + + @Override + public String getUniqueId() { + return getArguments().getString(EXTRA_UNIQUE_ID, this.who); + } + + @Override + public String getCachedEngineId() { + return FlutterBoost.ENGINE_ID; + } + + @Override + public boolean isPausing() { + return (stage == LifecycleStage.ON_PAUSE || stage == LifecycleStage.ON_STOP) && !isFinishing; + } + + protected void didFragmentShow(Runnable onComplete) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#didFragmentShow: " + this + ", isOpaque=" + isOpaque()); + + // try to detach *prevous* container from the engine. + FlutterViewContainer top = FlutterContainerManager.instance().getTopContainer(); + if (top != null && top != this) top.detachFromEngineIfNeeded(); + + FlutterBoost.instance().getPlugin().onContainerAppeared(this, () -> { + // attach new container to the engine. + attachToEngineIfNeeded(); + textureHooker.onFlutterTextureViewRestoreState(); + onComplete.run(); + }); + } + + protected void didFragmentHide() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#didFragmentHide: " + this + ", isOpaque=" + isOpaque()); + FlutterBoost.instance().getPlugin().onContainerDisappeared(this); + // We defer |performDetach| call to new Flutter container's |onResume|; + // performDetach(); + } + + private void performAttach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performAttach: " + this); + + // Attach plugins to the activity. + getFlutterEngine().getActivityControlSurface().attachToActivity(getExclusiveAppComponent(), getLifecycle()); + + if (platformPlugin == null) { + platformPlugin = new PlatformPlugin(getActivity(), getFlutterEngine().getPlatformChannel(), this); + } + + // Attach rendering pipeline. + flutterView.attachToFlutterEngine(getFlutterEngine()); + } + + private void performDetach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performDetach: " + this); + + // Plugins are no longer attached to the activity. + getFlutterEngine().getActivityControlSurface().detachFromActivity(); + + // Release Flutter's control of UI such as system chrome. + releasePlatformChannel(); + + // Detach rendering pipeline. + flutterView.detachFromFlutterEngine(); + } + + private void releasePlatformChannel() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#releasePlatformChannel: " + this); + if (platformPlugin != null) { + platformPlugin.destroy(); + platformPlugin = null; + } + } + + public void attachToEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#attachToEngineIfNeeded: " + this); + if (!isAttached) { + performAttach(); + isAttached = true; + } + } + + @Override + public void detachFromEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromEngineIfNeeded: " + this); + if (isAttached) { + performDetach(); + isAttached = false; + } + } + + // Defaults to {@link TransparencyMode#opaque}. + @Override + public TransparencyMode getTransparencyMode() { + String transparencyModeName = + getArguments() + .getString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, TransparencyMode.opaque.name()); + return TransparencyMode.valueOf(transparencyModeName); + } + + @Override + public boolean isOpaque() { + return getTransparencyMode() == TransparencyMode.opaque; + } + + public static class CachedEngineFragmentBuilder { + private final Class fragmentClass; + private boolean destroyEngineWithFragment = false; + private RenderMode renderMode = RenderMode.surface; + private TransparencyMode transparencyMode = TransparencyMode.opaque; + private boolean shouldAttachEngineToActivity = true; + private String url = "/"; + private HashMap params; + private String uniqueId; + + public CachedEngineFragmentBuilder() { + this(FlutterBoostFragment.class); + } + + public CachedEngineFragmentBuilder(Class subclass) { + fragmentClass = subclass; + } + + public CachedEngineFragmentBuilder url(String url) { + this.url = url; + return this; + } + + public CachedEngineFragmentBuilder urlParams(Map params) { + this.params = (params instanceof HashMap) ? (HashMap)params : new HashMap(params); + return this; + } + + public CachedEngineFragmentBuilder uniqueId(String uniqueId) { + this.uniqueId = uniqueId; + return this; + } + + public CachedEngineFragmentBuilder destroyEngineWithFragment( + boolean destroyEngineWithFragment) { + this.destroyEngineWithFragment = destroyEngineWithFragment; + return this; + } + + public CachedEngineFragmentBuilder renderMode( RenderMode renderMode) { + this.renderMode = renderMode; + return this; + } + + public CachedEngineFragmentBuilder transparencyMode( + TransparencyMode transparencyMode) { + this.transparencyMode = transparencyMode; + return this; + } + + public CachedEngineFragmentBuilder shouldAttachEngineToActivity( + boolean shouldAttachEngineToActivity) { + this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; + return this; + } + + /** + * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}. + * + *

Subclasses should override this method to add new properties to the {@link Bundle}. + * Subclasses must call through to the super method to collect all existing property values. + */ + protected Bundle createArgs() { + Bundle args = new Bundle(); + args.putString(ARG_CACHED_ENGINE_ID, FlutterBoost.ENGINE_ID); + args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, destroyEngineWithFragment); + args.putString( + ARG_FLUTTERVIEW_RENDER_MODE, + renderMode != null ? renderMode.name() : RenderMode.surface.name()); + args.putString( + ARG_FLUTTERVIEW_TRANSPARENCY_MODE, + transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name()); + args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); + args.putString(EXTRA_URL, url); + args.putSerializable(EXTRA_URL_PARAM, params); + args.putString(EXTRA_UNIQUE_ID, uniqueId != null ? uniqueId : FlutterBoostUtils.createUniqueId(url)); + return args; + } + + /** + * Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on + * properties set on this {@code CachedEngineFragmentBuilder}. + */ + public T build() { + try { + @SuppressWarnings("unchecked") + T frag = (T) fragmentClass.getDeclaredConstructor().newInstance(); + if (frag == null) { + throw new RuntimeException( + "The FlutterFragment subclass sent in the constructor (" + + fragmentClass.getCanonicalName() + + ") does not match the expected return type."); + } + + Bundle args = createArgs(); + frag.setArguments(args); + + return frag; + } catch (Exception e) { + throw new RuntimeException( + "Could not instantiate FlutterFragment subclass (" + fragmentClass.getName() + ")", e); + } + } + } + +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterContainerManager.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterContainerManager.java new file mode 100644 index 000000000..dc5b27480 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterContainerManager.java @@ -0,0 +1,117 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.app.Activity; +import android.os.Build; +import android.util.Log; + +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +public class FlutterContainerManager { + private static final String TAG = "FlutterBoost_java"; + + private FlutterContainerManager() {} + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + private static class LazyHolder { + static final FlutterContainerManager INSTANCE = new FlutterContainerManager(); + } + + public static FlutterContainerManager instance() { + return FlutterContainerManager.LazyHolder.INSTANCE; + } + + private final Map allContainers = new HashMap<>(); + private final LinkedList activeContainers = new LinkedList<>(); + + // onContainerCreated + public void addContainer(String uniqueId, FlutterViewContainer container) { + allContainers.put(uniqueId, container); + if (isDebugLoggingEnabled()) Log.d(TAG, "#addContainer: " + uniqueId + ", " + this); + } + + // onContainerAppeared + public void activateContainer(String uniqueId, FlutterViewContainer container) { + if (uniqueId == null || container == null) return; + assert(allContainers.containsKey(uniqueId)); + + if (activeContainers.contains(container)) { + activeContainers.remove(container); + } + activeContainers.add(container); + if (isDebugLoggingEnabled()) Log.d(TAG, "#activateContainer: " + uniqueId + "," + this); + } + + // onContainerDestroyed + public void removeContainer(String uniqueId) { + if (uniqueId == null) return; + FlutterViewContainer container = allContainers.remove(uniqueId); + activeContainers.remove(container); + if (isDebugLoggingEnabled()) Log.d(TAG, "#removeContainer: " + uniqueId + ", " + this); + } + + + public FlutterViewContainer findContainerById(String uniqueId) { + if (allContainers.containsKey(uniqueId)) { + return allContainers.get(uniqueId); + } + return null; + } + + public boolean isActiveContainer(FlutterViewContainer container) { + return activeContainers.contains(container); + } + + public FlutterViewContainer getTopContainer() { + if (activeContainers.size() > 0) { + return activeContainers.getLast(); + } + return null; + } + + public FlutterViewContainer getTopActivityContainer() { + final int size = activeContainers.size(); + if (size == 0) { + return null; + } + for (int i = size - 1; i >= 0; i--) { + final FlutterViewContainer container = activeContainers.get(i); + if (container instanceof Activity) { + return container; + } + } + return null; + } + + public boolean isTopContainer(String uniqueId) { + FlutterViewContainer top = getTopContainer(); + if (top != null && top.getUniqueId() == uniqueId) { + return true; + } + return false; + } + + public int getContainerSize() { + return allContainers.size(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("activeContainers=" + activeContainers.size() + ", ["); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + activeContainers.forEach((value) -> sb.append(value.getUrl() + ',')); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterTextureHooker.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterTextureHooker.java new file mode 100644 index 000000000..6d3e46da6 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterTextureHooker.java @@ -0,0 +1,143 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.graphics.SurfaceTexture; +import android.os.Build; +import android.view.Surface; +import android.view.TextureView; + +import com.idlefish.flutterboost.FlutterBoost; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import io.flutter.embedding.android.FlutterTextureView; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.renderer.FlutterRenderer; + + +class FlutterTextureHooker { + private SurfaceTexture restoreSurface; + private FlutterTextureView flutterTextureView; + private boolean isNeedRestoreState = false; + + /** + * Release surface when Activity.onDestroy / Fragment.onDestroy. + * Stop render when finish the last flutter boost container. + */ + public void onFlutterTextureViewRelease() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + int containerSize = FlutterContainerManager.instance().getContainerSize(); + if (containerSize == 1) { + FlutterEngine engine = FlutterBoost.instance().getEngine(); + FlutterRenderer renderer = engine.getRenderer(); + renderer.stopRenderingToSurface(); + } + if (restoreSurface != null) { + restoreSurface.release(); + restoreSurface = null; + } + } + } + + /** + * Restore last surface for os version below Android.M. + * Call from Activity.onResume / Fragment.didFragmentShow. + */ + public void onFlutterTextureViewRestoreState() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + if (restoreSurface != null && flutterTextureView != null && isNeedRestoreState) { + try { + Class aClass = flutterTextureView.getClass(); + + Field isSurfaceAvailableForRendering = aClass.getDeclaredField( + "isSurfaceAvailableForRendering"); + isSurfaceAvailableForRendering.setAccessible(true); + isSurfaceAvailableForRendering.set(flutterTextureView, true); + boolean next = false; + try { + Field isAttachedToFlutterRenderer = aClass.getDeclaredField( + "isAttachedToFlutterRenderer"); + isAttachedToFlutterRenderer.setAccessible(true); + next = isAttachedToFlutterRenderer.getBoolean(flutterTextureView); + } catch (NoSuchFieldException ignore) { + Method shouldNotify = aClass.getDeclaredMethod("shouldNotify"); + shouldNotify.setAccessible(true); + next = (Boolean) shouldNotify.invoke(flutterTextureView); + } + if (next) { + FlutterEngine engine = FlutterBoost.instance().getEngine(); + if (engine != null) { + + FlutterRenderer flutterRenderer = engine.getRenderer(); + Surface surface = new Surface(restoreSurface); + flutterRenderer.startRenderingToSurface(surface, false); + + try { + flutterTextureView.setSurfaceTexture(restoreSurface); + } catch (Exception e) { + e.printStackTrace(); + } + } + restoreSurface = null; + isNeedRestoreState = false; + } + } catch (Exception e) { + // https://github.com/alibaba/flutter_boost/issues/1560 + throw new RuntimeException("You *SHOULD* keep FlutterTextureView: -keep class io.flutter.embedding.android.FlutterTextureView { *; }.", e); + } + } + } + } + + /** + * Hook FlutterTextureView for os version below Android.M. + */ + public void hookFlutterTextureView(FlutterTextureView flutterTextureView) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + if (flutterTextureView != null) { + TextureView.SurfaceTextureListener surfaceTextureListener = flutterTextureView.getSurfaceTextureListener(); + this.flutterTextureView = flutterTextureView; + this.flutterTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + surfaceTextureListener.onSurfaceTextureAvailable(surface, width, height); + + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + surfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + try { + Class aClass = + flutterTextureView.getClass(); + Field isSurfaceAvailableForRendering = aClass.getDeclaredField( + "isSurfaceAvailableForRendering"); + isSurfaceAvailableForRendering.setAccessible(true); + isSurfaceAvailableForRendering.set(flutterTextureView, false); + } catch (Exception e) { + // https://github.com/alibaba/flutter_boost/issues/1560 + throw new RuntimeException("You *SHOULD* keep FlutterTextureView: -keep class io.flutter.embedding.android.FlutterTextureView { *; }.", e); + } + isNeedRestoreState = true; + //return false, handle the last frame of surfaceTexture ourselves; + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + surfaceTextureListener.onSurfaceTextureUpdated(surface); + restoreSurface = surface; + } + }); + } + } + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterViewContainer.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterViewContainer.java new file mode 100644 index 000000000..1c5c95a82 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterViewContainer.java @@ -0,0 +1,23 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.app.Activity; + +import java.util.Map; + +/** + * A container which contains the FlutterView + */ +public interface FlutterViewContainer { + Activity getContextActivity(); + String getUrl(); + Map getUrlParams(); + String getUniqueId(); + void finishContainer(Map result); + default boolean isPausing() { return false; } + default boolean isOpaque() { return true; } + default void detachFromEngineIfNeeded() {} +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/LifecycleStage.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/LifecycleStage.java new file mode 100644 index 000000000..a430f23ea --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/LifecycleStage.java @@ -0,0 +1,14 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +enum LifecycleStage { + ON_CREATE, + ON_START, + ON_RESUME, + ON_PAUSE, + ON_STOP, + ON_DESTROY +} \ No newline at end of file diff --git a/flutter_boost_android/lib/flutter_boost_android.dart b/flutter_boost_android/lib/flutter_boost_android.dart new file mode 100644 index 000000000..8e67c6783 --- /dev/null +++ b/flutter_boost_android/lib/flutter_boost_android.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +library flutter_boost_android; + +// This package does not export any Dart code to the app-facing package. +// It only provides the Android platform implementation. diff --git a/flutter_boost_android/pubspec.yaml b/flutter_boost_android/pubspec.yaml new file mode 100644 index 000000000..4720ed2e0 --- /dev/null +++ b/flutter_boost_android/pubspec.yaml @@ -0,0 +1,31 @@ +# Copyright (c) 2019 Alibaba Group. All rights reserved. +# Use of this source code is governed by a MIT license that can be +# found in the LICENSE file. + +name: flutter_boost_android +description: Android implementation of the flutter_boost plugin. +version: 5.0.2 +homepage: https://github.com/alibaba/flutter_boost + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + flutter_boost_platform_interface: + path: ../flutter_boost_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + plugin: + implements: flutter_boost + platforms: + android: + package: com.idlefish.flutterboost + pluginClass: FlutterBoostPlugin diff --git a/flutter_boost_ios/.gitignore b/flutter_boost_ios/.gitignore new file mode 100644 index 000000000..6d11bab8b --- /dev/null +++ b/flutter_boost_ios/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +pubspec.lock diff --git a/flutter_boost_ios/CHANGELOG.md b/flutter_boost_ios/CHANGELOG.md new file mode 100644 index 000000000..06b328475 --- /dev/null +++ b/flutter_boost_ios/CHANGELOG.md @@ -0,0 +1,4 @@ +## 5.0.2 + +* Initial release of the iOS implementation for flutter_boost. +* Extracted from `flutter_boost` as part of federated plugin migration. diff --git a/flutter_boost_ios/LICENSE b/flutter_boost_ios/LICENSE new file mode 100755 index 000000000..88ed1cb37 --- /dev/null +++ b/flutter_boost_ios/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alibaba Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_boost_ios/README.md b/flutter_boost_ios/README.md new file mode 100644 index 000000000..4d2658ac4 --- /dev/null +++ b/flutter_boost_ios/README.md @@ -0,0 +1,7 @@ +# flutter_boost_ios + +The iOS implementation of [flutter_boost](https://pub.dev/packages/flutter_boost). + +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you can simply use `flutter_boost` normally. This package will be automatically included in your app when you do. diff --git a/flutter_boost_ios/analysis_options.yaml b/flutter_boost_ios/analysis_options.yaml new file mode 100644 index 000000000..8d81c200c --- /dev/null +++ b/flutter_boost_ios/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false diff --git a/flutter_boost_ios/ios/Assets/.gitkeep b/flutter_boost_ios/ios/Assets/.gitkeep new file mode 100755 index 000000000..e69de29bb diff --git a/flutter_boost_ios/ios/Classes/FlutterBoost.h b/flutter_boost_ios/ios/Classes/FlutterBoost.h new file mode 100644 index 000000000..8c2b94921 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoost.h @@ -0,0 +1,105 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FlutterBoostDelegate.h" +#import "FlutterBoostPlugin.h" +#import "FBFlutterViewContainer.h" +#import "FlutterBoostDelegate.h" +#import "FlutterBoostPlugin.h" +#import "FBFlutterViewContainer.h" +#import "Options.h" +#import "messages.h" + + +@interface FlutterBoost : NSObject + +#pragma mark + +- (FlutterEngine*)engine; + +- (FlutterBoostPlugin*)plugin; + +- (FlutterViewController *)currentViewController; + +#pragma mark + +/// Boost全局单例 ++ (instancetype)instance; + +/// 初始化 +/// @param application 全局Application实例,如未设置engine参数,则默认从Application做engine的绑定 +/// @param delegate FlutterBoostDelegate的实例,用于实现Push和Pop的具体策略(Native侧如何Push,以及需要Push一个新的FlutterViewController时的具体动作),以及Engine的部分初始化策略 +/// @param callback 初始化完成以后的回调, +/// TODO 设计需要再review下 callback并不是异步的感觉没有必要。 +- (void)setup:(UIApplication*)application delegate:(id)delegate callback:(void (^)(FlutterEngine *engine))callback; + + +/// 利用自定义配置进行初始化 +/// @param application 全局Application实例,如未设置engine参数,则默认从Application做engine的绑定 +/// @param delegate FlutterBoostDelegate的实例,用于实现Push和Pop的具体策略 +/// @param callback 初始化完成以后的回调 +/// @param options 启动的配置,如果需要自定义请使用此参数 +- (void)setup:(UIApplication*)application delegate:(id)delegate callback:(void (^)(FlutterEngine *engine))callback options:(FlutterBoostSetupOptions*)options; + +/// 关闭页面,混合栈推荐使用的用于操作页面的接口 +/// @param uniqueId 关闭的页面唯一ID符 +- (void)close:(NSString *)uniqueId; + +/// ( 已废弃,之后有新参数可能不支持此方法 !!! ) +/// 打开新页面(默认以push方式),混合栈推荐使用的用于操作页面的接口 +/// 通过arguments可以设置为以present方式打开页面:arguments:@{@"present":@(YES)} +/// @param pageName 打开的页面资源定位符 +/// @param arguments 传入页面的参数; 若有特殊逻辑,可以通过这个参数设置回调的id +/// @param completion 页面open操作完成的回调,注意,从原生调用此方法跳转此参数才会生效 +- (void)open:(NSString *)pageName arguments:(NSDictionary *)arguments completion:(void(^)(BOOL)) completion; + + +/// (推荐使用)利用启动参数配置开启新页面 +/// @param options 配置参数 +- (void)open:(FlutterBoostRouteOptions* )options; + + +/// 将原生页面的数据回传到flutter侧的页面的的方法 +/// @param pageName 这个页面在路由表中的名字,和flutter侧BoostNavigator.push(name)中的name一样 +/// @param arguments 你想传的参数 +- (void)sendResultToFlutterWithPageName:(NSString*)pageName arguments:(NSDictionary*) arguments; + +/// 添加一个事件监听 +/// @param listener FBEventListener类型的函数 +/// @param key 事件标识符 +/// @return 用于移除监听器的一个函数,直接调用此函数可以移除此监听器避免内存泄漏 +- (FBVoidCallback)addEventListener:(FBEventListener)listener + forName:(NSString *)key; + +/// 将自定义事件传递给flutter侧 +/// @param key 事件的标识符 +/// @param arguments 事件的参数 +- (void)sendEventToFlutterWith:(NSString*)key arguments:(NSDictionary*)arguments; + +/// 卸载引擎 +- (void)unsetFlutterBoost; + +@end + diff --git a/flutter_boost_ios/ios/Classes/FlutterBoost.m b/flutter_boost_ios/ios/Classes/FlutterBoost.m new file mode 100644 index 000000000..6c724e250 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoost.m @@ -0,0 +1,227 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import +#import "FlutterBoost.h" +#import "FlutterBoostPlugin.h" +#import "Options.h" + +@interface FlutterBoost () +@property (nonatomic, strong) FlutterEngine* engine; +@property (nonatomic, strong) FlutterBoostPlugin* plugin; +@end + +@implementation FlutterBoost +- (void)setup:(UIApplication*)application + delegate:(id)delegate + callback:(void (^)(FlutterEngine *engine))callback { + // 调用默认的配置参数进行初始化 + [self setup:application + delegate:delegate + callback:callback + options:FlutterBoostSetupOptions.createDefault]; +} + +- (void)setup:(UIApplication*)application delegate:(id)delegate + callback:(void (^)(FlutterEngine *engine))callback + options:(FlutterBoostSetupOptions*)options { + if ([delegate respondsToSelector:@selector(engine)]) { + self.engine = delegate.engine; + } else { + self.engine = [[FlutterEngine alloc ] initWithName:@"io.flutter" project:options.dartObject]; + } + + // 从options中获取参数 + NSString* initialRoute = options.initalRoute; + NSString* dartEntrypointFunctionName = options.dartEntryPoint; + NSArray* dartEntryPointArgs = options.dartEntryPointArgs; + + void(^engineRun)(void) = ^(void) { + [self.engine runWithEntrypoint:dartEntrypointFunctionName + libraryURI:nil + initialRoute:initialRoute + entrypointArgs:dartEntryPointArgs]; + + // 根据配置提前预热引擎,配置默认预热引擎 + if (options.warmUpEngine){ + [self warmUpEngine]; + } + + Class clazz = NSClassFromString(@"GeneratedPluginRegistrant"); + SEL selector = NSSelectorFromString(@"registerWithRegistry:"); + if (clazz && selector && self.engine) { + if ([clazz respondsToSelector:selector]) { + ((void (*)(id, SEL, NSObject*registry))[clazz methodForSelector:selector])(clazz, selector, self.engine); + } + } + + self.plugin= [FlutterBoostPlugin getPlugin:self.engine]; + self.plugin.delegate=delegate; + + if (callback) { + callback(self.engine); + } + }; + + if ([NSThread isMainThread]) { + engineRun(); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + engineRun(); + }); + } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; +} + +- (void)unsetFlutterBoost { + void (^engineDestroy)(void) = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + self.plugin.delegate = nil; + self.plugin = nil; + + [self.engine destroyContext]; + self.engine = nil; + }; + + if ([NSThread isMainThread]){ + engineDestroy(); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + engineDestroy(); + }); + } +} + +/// 提前预热引擎 +- (void)warmUpEngine { + FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.engine + nibName:nil bundle:nil]; + [vc beginAppearanceTransition:YES animated:NO]; + [vc endAppearanceTransition]; + [vc beginAppearanceTransition:NO animated:NO]; + [vc endAppearanceTransition]; +} + ++ (instancetype)instance { + static id _instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _instance = [self.class new]; + }); + + return _instance; +} + + +#pragma mark - Some properties. + +- (FlutterViewController *) currentViewController{ + return self.engine.viewController; +} + +#pragma mark - open/close Page +- (void)open:(NSString *)pageName arguments:(NSDictionary *)arguments + completion:(void(^)(BOOL)) completion { + FlutterBoostRouteOptions* options = [[FlutterBoostRouteOptions alloc]init]; + options.pageName = pageName; + options.arguments = arguments; + options.completion = completion; + + [self.plugin.delegate pushFlutterRoute:options]; +} + +- (void)open:(FlutterBoostRouteOptions* )options{ + [self.plugin.delegate pushFlutterRoute:options]; +} + +- (void)close:(NSString *)uniqueId { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.uniqueId=uniqueId; + [self.plugin.flutterApi popRouteParam:params + completion:^(NSError* error) { + }]; +} + +- (void)sendResultToFlutterWithPageName:(NSString*)pageName + arguments:(NSDictionary*) arguments { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.pageName = pageName; + params.arguments = arguments; + + [self.plugin.flutterApi onNativeResultParam:params + completion:^(NSError * error) { + }]; +} + + +- (void)applicationDidEnterBackground:(UIApplication *)application { + FBCommonParams* params = [[FBCommonParams alloc] init]; + [ self.plugin.flutterApi onBackgroundParam:params + completion:^(NSError * error) { + }]; +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + FBCommonParams* params = [[FBCommonParams alloc] init]; + [self.plugin.flutterApi onForegroundParam:params + completion:^(NSError * error) { + }]; +} + +- (FBVoidCallback)addEventListener:(FBEventListener)listener + forName:(NSString *)key { + return [self.plugin addEventListener:listener forName:key]; +} + +- (void)sendEventToFlutterWith:(NSString*)key + arguments:(NSDictionary*)arguments { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.key = key; + params.arguments = arguments; + [self.plugin.flutterApi sendEventToFlutterParam:params + completion:^(NSError * error) { + }]; +} + + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:UIApplicationDidEnterBackgroundNotification object:nil]; +} +@end diff --git a/flutter_boost_ios/ios/Classes/FlutterBoostDelegate.h b/flutter_boost_ios/ios/Classes/FlutterBoostDelegate.h new file mode 100644 index 000000000..194b97fda --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoostDelegate.h @@ -0,0 +1,43 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "messages.h" +#import "Options.h" +#import + +@protocol FlutterBoostDelegate +@optional +- (FlutterEngine*) engine; +@required + +// 如果框架发现您输入的路由表在flutter里面注册的路由表中找不到,那么就会调用此方法来push一个纯原生页面 +- (void) pushNativeRoute:(NSString *) pageName arguments:(NSDictionary *) arguments; + +// 当框架的withContainer为true的时候,会调用此方法来做原生的push +- (void) pushFlutterRoute:(FlutterBoostRouteOptions *)options; + +// 当pop调用涉及到原生容器的时候,此方法将会被调用 +- (void) popRoute:(FlutterBoostRouteOptions *)options; +@end diff --git a/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.h b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.h new file mode 100644 index 000000000..fbee90d17 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.h @@ -0,0 +1,47 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import +#import "messages.h" +#import "FlutterBoostDelegate.h" +#import "FBFlutterContainer.h" + +typedef void (^FBEventListener) (NSString *name, NSDictionary *arguments); +typedef void (^FBVoidCallback)(void); + +@interface FlutterBoostPlugin : NSObject +@property (nonatomic, strong) id delegate; +@property(nonatomic, strong) FBFlutterRouterApi* flutterApi; + +- (void)containerCreated:(id)container; +- (void)containerWillAppear:(id)container; +- (void)containerAppeared:(id)container; +- (void)containerDisappeared:(id)container; +- (void)containerDestroyed:(id)container; +- (void)onBackSwipe; + +- (FBVoidCallback)addEventListener:(FBEventListener)listener forName:(NSString *)key; ++ (FlutterBoostPlugin* )getPlugin:(FlutterEngine*)engine ; +@end diff --git a/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.m b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.m new file mode 100644 index 000000000..ab89f2fc1 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.m @@ -0,0 +1,209 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import +#import "FlutterBoostPlugin.h" +#import "messages.h" +#import "FlutterBoost.h" +#import "FBFlutterContainerManager.h" +#import "FBLifecycle.h" + +@interface FlutterBoostPlugin () +@property(nonatomic, strong) FBFlutterContainerManager* containerManager; +@property(nonatomic, strong) FBStackInfo* stackInfo; +@property(nonatomic, strong) NSMutableDictionary*>* listenersTable; +@end + +@implementation FlutterBoostPlugin +- (void)containerCreated:(id)vc { + [self.containerManager addContainer:vc forUniqueId:vc.uniqueIDString]; + if (self.containerManager.containerSize == 1) { + [FBLifecycle resume]; + } +} + +- (void)containerWillAppear:(id)vc { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.pageName = vc.name; + params.arguments = vc.params; + params.uniqueId = vc.uniqueId; + params.opaque = [[NSNumber alloc] initWithBool:vc.opaque]; + + [self.flutterApi pushRouteParam:params + completion:^(NSError * e) { + }]; + [self.containerManager activeContainer:vc + forUniqueId:vc.uniqueIDString]; +} + +- (void)containerAppeared:(id)vc { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.uniqueId = vc.uniqueId; + [self.flutterApi onContainerShowParam:params + completion:^(NSError * e) { + }]; +} + +- (void)containerDisappeared:(id)vc { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.uniqueId = vc.uniqueId; + [self.flutterApi onContainerHideParam:params + completion:^(NSError * e) { + }]; +} + +- (void)onBackSwipe { + [self.flutterApi onBackPressedWithCompletion: ^(NSError * e) { + }]; +} + +- (void)containerDestroyed:(id)vc { + FBCommonParams* params =[[FBCommonParams alloc] init]; + params.pageName = vc.name; + params.arguments = vc.params; + params.uniqueId = vc.uniqueId; + [self.flutterApi removeRouteParam:params + completion:^(NSError * e) { + }]; + [self.containerManager removeContainerByUniqueId:vc.uniqueIDString]; + if (self.containerManager.containerSize == 0) { + [FBLifecycle pause]; + } +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterBoostPlugin* plugin = [[FlutterBoostPlugin alloc] initWithMessenger:(registrar.messenger)]; + [registrar publish:plugin]; + FBNativeRouterApiSetup(registrar.messenger, plugin); +} + ++ (FlutterBoostPlugin* )getPlugin:(FlutterEngine*)engine{ + NSObject *published = [engine valuePublishedByPlugin:@"FlutterBoostPlugin"]; + if ([published isKindOfClass:[FlutterBoostPlugin class]]) { + FlutterBoostPlugin *plugin = (FlutterBoostPlugin *)published; + return plugin; + } + return nil; +} + +- (instancetype)initWithMessenger:(id)messenger { + self = [super init]; + if (self) { + _flutterApi = [[FBFlutterRouterApi alloc] initWithBinaryMessenger:messenger]; + _containerManager= [FBFlutterContainerManager new]; + _listenersTable = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)pushNativeRouteParam:(FBCommonParams*)input + error:(FlutterError *_Nullable *_Nonnull)error { + [self.delegate pushNativeRoute:input.pageName arguments:input.arguments]; +} + +- (void)pushFlutterRouteParam:(FBCommonParams*)input + error:(FlutterError *_Nullable *_Nonnull)error { + FlutterBoostRouteOptions* options = [[FlutterBoostRouteOptions alloc]init]; + options.pageName = input.pageName; + options.uniqueId = input.uniqueId; + options.arguments = input.arguments; + options.opaque = [input.opaque boolValue]; + + // 因为这里是flutter端开启新容器push一个页面,所以这里原生用不着,所以这里completion传一个空的即可 + options.completion = ^(BOOL completion) { + }; + + [self.delegate pushFlutterRoute: options]; +} + +- (void)popRouteParam:(FBCommonParams *)input + completion:(void(^)(FlutterError *_Nullable))completion { + if ([self.containerManager findContainerByUniqueId:input.uniqueId]) { + // 封装成options传回代理 + FlutterBoostRouteOptions* options = [[FlutterBoostRouteOptions alloc]init]; + options.pageName = input.pageName; + options.uniqueId = input.uniqueId; + options.arguments = input.arguments; + options.completion = ^(BOOL ret) { + }; + + // 调用代理回调给调用层 + [self.delegate popRoute:options]; + completion(nil); + } else { + completion([FlutterError errorWithCode:@"Invalid uniqueId" + message:@"No container to pop." + details:nil]); + } +} + +- (nullable FBStackInfo *)getStackFromHostWithError:(FlutterError *_Nullable *_Nonnull)error { + if (self.stackInfo == nil) { + return [[FBStackInfo alloc] init]; + } + return self.stackInfo; +} + +- (void)saveStackToHostStack:(FBStackInfo *)stack + error:(FlutterError *_Nullable *_Nonnull)error { + self.stackInfo = stack; +} + +// flutter端将会调用此方法给native发送信息,所以这里将是接收事件的逻辑 +- (void)sendEventToNativeParams:(FBCommonParams *)params + error:(FlutterError *_Nullable *_Nonnull)error { + NSString* key = params.key; + NSDictionary* args = params.arguments; + + assert(key != nil); + + // 如果arg是null,那么就生成一个空的字典传过去,避免null造成的崩溃 + if (args == nil) { + args = [NSDictionary dictionary]; + } + + // 从总事件表中找到和key对应的事件监听者列表 + NSMutableArray* listeners = self.listenersTable[key]; + + if (listeners == nil) return; + for (FBEventListener listener in listeners) { + listener(key,args); + } +} + +- (FBVoidCallback)addEventListener:(FBEventListener)listener + forName:(NSString *)key { + assert(key != nil && listener != nil); + NSMutableArray* listeners = self.listenersTable[key]; + if (listeners == nil) { + listeners = [[NSMutableArray alloc] init]; + self.listenersTable[key] = listeners; + } + + [listeners addObject:listener]; + + return ^{ + [listeners removeObject:listener]; + }; +} +@end diff --git a/flutter_boost_ios/ios/Classes/Options.h b/flutter_boost_ios/ios/Classes/Options.h new file mode 100644 index 000000000..361561eeb --- /dev/null +++ b/flutter_boost_ios/ios/Classes/Options.h @@ -0,0 +1,75 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +#import + +//此文件用用于配置FlutterBoost各种配置文件 + +///启动参数配置 +@interface FlutterBoostSetupOptions : NSObject + +///初始路由 +@property (nonatomic, strong) NSString* initalRoute; + +///dart 入口 +@property (nonatomic, strong) NSString* dartEntryPoint; + +/// dart入口参数 +@property (nonatomic, strong) NSArray* dartEntryPointArgs; + +///FlutterDartProject数据 +@property (nonatomic, strong) FlutterDartProject* dartObject; + +///是否提前预热引擎,如果提前预热引擎,可以减少第一次打开flutter页面的短暂白屏,以及字体大小跳动的现象 +///默认值为YES +@property (nonatomic, assign) BOOL warmUpEngine; + +///创建一个默认的Options对象 ++ (FlutterBoostSetupOptions*)createDefault; + +@end + + +///路由参数配置 +@interface FlutterBoostRouteOptions : NSObject + +///页面在路由表中的名字 +@property(nonatomic, strong) NSString* pageName; + +///参数 +@property(nonatomic, strong) NSDictionary* arguments; + +///参数回传的回调闭包,仅在原生->flutter页面的时候有用 +@property(nonatomic, strong) void(^onPageFinished)(NSDictionary*); + +///open方法完成后的回调,仅在原生->flutter页面的时候有用 +@property(nonatomic, strong) void(^completion)(BOOL); + +///代理内部会使用,原生往flutter open的时候此参数设为nil即可 +@property(nonatomic, strong) NSString* uniqueId; + +///这个页面是否透明 注意:default value = YES +@property(nonatomic,assign) BOOL opaque; +@end diff --git a/flutter_boost_ios/ios/Classes/Options.m b/flutter_boost_ios/ios/Classes/Options.m new file mode 100644 index 000000000..ac27c6e7b --- /dev/null +++ b/flutter_boost_ios/ios/Classes/Options.m @@ -0,0 +1,52 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import "Options.h" + +@implementation FlutterBoostSetupOptions +- (instancetype)init { + self = [super init]; + if (self) { + self.dartEntryPoint = @"main"; + self.initalRoute = @"/"; + self.warmUpEngine = YES; + } + return self; +} + ++ (FlutterBoostSetupOptions*)createDefault { + return [[FlutterBoostSetupOptions alloc] init]; +} +@end + +@implementation FlutterBoostRouteOptions +- (instancetype)init { + self = [super init]; + if (self) { + //设置opaque默认为YES + self.opaque = YES; + } + return self; +} +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterContainer.h b/flutter_boost_ios/ios/Classes/container/FBFlutterContainer.h new file mode 100644 index 000000000..751589b38 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterContainer.h @@ -0,0 +1,33 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import + +@protocol FBFlutterContainer +- (NSString *)name; +- (NSDictionary *)params; +- (NSString *)uniqueId; +- (NSString *)uniqueIDString; +- (BOOL)opaque; +- (void)setName:(NSString *)name uniqueId:(NSString *)uniqueId params:(NSDictionary *)params opaque:(BOOL) opaque; +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.h b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.h new file mode 100644 index 000000000..39148a8a4 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.h @@ -0,0 +1,36 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FBFlutterContainer.h" + +@interface FBFlutterContainerManager : NSObject +- (void)addContainer:(id)container forUniqueId:(NSString *)uniqueId; +- (void)activeContainer:(id)container forUniqueId:(NSString *)uniqueId; +- (void)removeContainerByUniqueId:(NSString *)uniqueId; +- (id)findContainerByUniqueId:(NSString *)uniqueId; +- (id)getTopContainer; +- (BOOL)isTopContainer:(NSString *)uniqueId; +- (NSInteger)containerSize; +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.m b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.m new file mode 100644 index 000000000..b89f68451 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.m @@ -0,0 +1,87 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FBFlutterContainerManager.h" + +@interface FBFlutterContainerManager() +@property (nonatomic, strong) NSMutableDictionary *allContainers; +@property (nonatomic, strong) NSMutableArray *activeContainers; +@end + +@implementation FBFlutterContainerManager +- (instancetype)init { + if (self = [super init]) { + _allContainers = [NSMutableDictionary dictionary]; + _activeContainers = [NSMutableArray new]; + } + + return self; +} + +- (void)addContainer:(id)container + forUniqueId:(NSString *)uniqueId { + self.allContainers[uniqueId] = container; +} + +- (void)activeContainer:(id)container + forUniqueId:(NSString *)uniqueId { + if (uniqueId == nil || container == nil) return; + assert(self.allContainers[uniqueId] != nil); + if ([self.activeContainers containsObject:container]) { + [self.activeContainers removeObject:container]; + } + [self.activeContainers addObject:container]; +} + +- (void)removeContainerByUniqueId:(NSString *)uniqueId { + if (!uniqueId) return; + id container = self.allContainers[uniqueId]; + [self.allContainers removeObjectForKey:uniqueId]; + [self.activeContainers removeObject:container]; +} + +- (id)findContainerByUniqueId:(NSString *)uniqueId { + return self.allContainers[uniqueId]; +} + +- (id)getTopContainer { + if (self.activeContainers.count) { + return self.activeContainers.lastObject; + } + return nil; +} + +- (BOOL)isTopContainer:(NSString *)uniqueId { + id top = [self getTopContainer]; + if (top != nil && [top.uniqueIDString isEqualToString:uniqueId]) { + return YES; + } + return NO; +} + +- (NSInteger)containerSize { + return self.allContainers.count; +} +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.h b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.h new file mode 100644 index 000000000..60e538343 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.h @@ -0,0 +1,41 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import +#import "FBFlutterContainer.h" + +@interface FBFlutterViewContainer : FlutterViewController +@property (nonatomic,copy,readwrite) NSString *name; +@property (nonatomic, strong) NSNumber *disablePopGesture; +@property (nonatomic, strong) NSNumber *enableLeftPanBackGesture; + +- (instancetype)init; +- (void)surfaceUpdated:(BOOL)appeared; +- (void)updateViewportMetrics; +- (void)detachFlutterEngineIfNeeded; +- (void)notifyWillDealloc; +@end + + diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.m b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.m new file mode 100644 index 000000000..d87d06c99 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.m @@ -0,0 +1,332 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FBFlutterViewContainer.h" +#import "FlutterBoost.h" +#import "FBLifecycle.h" +#import +#import + +#define ENGINE [[FlutterBoost instance] engine] +#define FB_PLUGIN [FlutterBoostPlugin getPlugin: [[FlutterBoost instance] engine]] + +#define weakify(var) ext_keywordify __weak typeof(var) O2OWeak_##var = var; +#define strongify(var) ext_keywordify \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +__strong typeof(var) var = O2OWeak_##var; \ +_Pragma("clang diagnostic pop") +#if DEBUG +# define ext_keywordify autoreleasepool {} +#else +# define ext_keywordify try {} @catch (...) {} +#endif + +@interface FlutterViewController (bridgeToviewDidDisappear) +- (void)flushOngoingTouches; +- (void)bridge_viewDidDisappear:(BOOL)animated; +- (void)bridge_viewWillAppear:(BOOL)animated; +- (void)surfaceUpdated:(BOOL)appeared; +- (void)updateViewportMetrics; +@end + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincomplete-implementation" +@implementation FlutterViewController (bridgeToviewDidDisappear) +- (void)bridge_viewDidDisappear:(BOOL)animated { + [self flushOngoingTouches]; + [super viewDidDisappear:animated]; +} + +- (void)bridge_viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; +} +@end +#pragma pop + +@interface FBFlutterViewContainer () +@property (nonatomic,strong,readwrite) NSDictionary *params; +@property (nonatomic,copy) NSString *uniqueId; +@property (nonatomic, copy) NSString *flbNibName; +@property (nonatomic, strong) NSBundle *flbNibBundle; +@property (nonatomic, assign) BOOL opaque; +@property (nonatomic, strong) FBVoidCallback removeEventCallback; +@property (nonatomic, strong) UIScreenEdgePanGestureRecognizer* leftEdgeGesture; +@end + +@implementation FBFlutterViewContainer +- (instancetype)init { + ENGINE.viewController = nil; + if (self = [super initWithEngine:ENGINE + nibName:_flbNibName + bundle:_flbNibBundle]) { + // NOTES:在present页面时,默认是全屏,如此可以触发底层VC的页面事件。否则不会触发而导致异常 + self.modalPresentationStyle = UIModalPresentationFullScreen; + [self _setup]; + } + return self; +} + +- (instancetype)initWithProject:(FlutterDartProject*)projectOrNil + nibName:(NSString*)nibNameOrNil + bundle:(NSBundle*)nibBundleOrNil { + ENGINE.viewController = nil; + if (self = [super initWithProject:projectOrNil + nibName:nibNameOrNil + bundle:nibBundleOrNil]) { + [self _setup]; + } + return self; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-designated-initializers" +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder: aDecoder]) { + NSAssert(NO, @"unsupported init method!"); + [self _setup]; + } + return self; +} +#pragma pop + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil + bundle:(NSBundle *)nibBundleOrNil { + _flbNibName = nibNameOrNil; + _flbNibBundle = nibBundleOrNil; + ENGINE.viewController = nil; + return [self init]; +} + +- (void)setName:(NSString *)name + uniqueId:(NSString *)uniqueId + params:(NSDictionary *)params + opaque:(BOOL) opaque { + if (!_name && name) { + _name = name; + _params = params; + _opaque = opaque; + + // 这里如果是不透明的情况,才将viewOpaque 设为false, + // 并且才将modalStyle设为UIModalPresentationOverFullScreen + // 因为UIModalPresentationOverFullScreen模式下,下面的vc重新显示的时候不会 + // 调用viewAppear相关生命周期,所以需要手动调用beginAppearanceTransition相关方法来触发 + if (!_opaque) { + self.viewOpaque = opaque; + self.modalPresentationStyle = UIModalPresentationOverFullScreen; + } + if (uniqueId != nil) { + _uniqueId = uniqueId; + } + } + + [FB_PLUGIN containerCreated:self]; + + // 设置这个container对应的从flutter过来的事件监听 + [self setupEventListeningFromFlutter]; +} + +/// 设置这个container对应的从flutter过来的事件监听 +- (void)setupEventListeningFromFlutter { + @weakify(self) + // 为这个容器注册监听,监听内部的flutterPage往这个容器发的事件 + self.removeEventCallback = [FlutterBoost.instance addEventListener:^(NSString *name, NSDictionary *arguments) { + @strongify(self) + //事件名 + NSString *event = arguments[@"event"]; + + //事件参数 + NSDictionary *args = arguments[@"args"]; + + if ([event isEqualToString:@"enablePopGesture"]) { + // 多page情况下的侧滑动态禁用和启用事件 + NSNumber *enableNum = args[@"enable"]; + BOOL enable = [enableNum boolValue]; + self.navigationController.interactivePopGestureRecognizer.enabled = enable; + } + } forName:self.uniqueId]; +} + +- (NSString *)uniqueIDString { + return self.uniqueId; +} + +- (void)_setup { + self.uniqueId = [[NSUUID UUID] UUIDString]; +} + +- (void)didMoveToParentViewController:(UIViewController *)parent { + if (!parent) { + //当VC被移出parent时,就通知flutter层销毁page + [self detachFlutterEngineIfNeeded]; + [self notifyWillDealloc]; + } + [super didMoveToParentViewController:parent]; +} + +- (void)dismissViewControllerAnimated:(BOOL)flag + completion:(void (^)(void))completion { + [super dismissViewControllerAnimated:flag + completion:^() { + if (completion) { + completion(); + } + //当VC被dismiss时,就通知flutter层销毁page + [self detachFlutterEngineIfNeeded]; + [self notifyWillDealloc]; + }]; +} + +- (void)dealloc { + if (self.removeEventCallback != nil) { + self.removeEventCallback(); + } + [NSNotificationCenter.defaultCenter removeObserver:self]; + _leftEdgeGesture.delegate = nil; +} + +- (void)notifyWillDealloc { + [FB_PLUGIN containerDestroyed:self]; +} + +- (void)viewDidLoad { + // Ensure current view controller attach to Flutter engine + [self attatchFlutterEngine]; + + [super viewDidLoad]; + //只有在不透明情况下,才设置背景颜色,否则不设置颜色(也就是默认透明) + if (self.opaque) { + self.view.backgroundColor = UIColor.whiteColor; + } + + if (self.enableLeftPanBackGesture) { + _leftEdgeGesture = [[UIScreenEdgePanGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleLeftEdgeGesture:)]; + _leftEdgeGesture.edges = UIRectEdgeLeft; + _leftEdgeGesture.delegate = self; + [self.view addGestureRecognizer:_leftEdgeGesture]; + } +} + +- (void)handleLeftEdgeGesture:(UIScreenEdgePanGestureRecognizer *)gesture { + if (UIGestureRecognizerStateEnded == gesture.state) { + [FB_PLUGIN onBackSwipe]; + } +} + +#pragma mark - ScreenShots +- (BOOL)isFlutterViewAttatched { + return ENGINE.viewController.view.superview == self.view; +} + +- (void)attatchFlutterEngine { + if (ENGINE.viewController != self){ + ENGINE.viewController = self; + } +} + +- (void)detachFlutterEngineIfNeeded { + if (self.engine.viewController == self) { + // need to call [surfaceUpdated:NO] to detach the view controller's ref from + // interal engine platformViewController,or dealloc will not be called after controller close. + // detail:https://github.com/flutter/engine/blob/07e2520d5d8f837da439317adab4ecd7bff2f72d/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L529 + [self surfaceUpdated:NO]; + + if (ENGINE.viewController != nil) { + ENGINE.viewController = nil; + } + } +} + +- (void)surfaceUpdated:(BOOL)appeared { + if (self.engine && self.engine.viewController == self) { + [super surfaceUpdated:appeared]; + } +} + +- (void)updateViewportMetrics { + if (self.engine && self.engine.viewController == self) { + [super updateViewportMetrics]; + } +} + +#pragma mark - Life circle methods + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; +} + +- (void)viewWillAppear:(BOOL)animated { + [FB_PLUGIN containerWillAppear:self]; + + // For new page we should attach flutter view in view will appear + // for better performance. + [self attatchFlutterEngine]; + + [super bridge_viewWillAppear:animated]; + [self.view setNeedsLayout];//TODO:通过param来设定 +} + +- (void)viewDidAppear:(BOOL)animated { + //Ensure flutter view is attached. + [self attatchFlutterEngine]; + + // 根据淘宝特价版日志证明,即使在UIViewController的viewDidAppear下,application也可能在inactive模式,此时如果提交渲染会导致GPU后台渲染而crash + // 参考:https://github.com/flutter/flutter/issues/57973 + // https://github.com/flutter/engine/pull/18742 + if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive){ + //NOTES:务必在show之后再update,否则有闪烁; 或导致侧滑返回时上一个页面会和top页面内容一样 + [self surfaceUpdated:YES]; + } + [super viewDidAppear:animated]; + + // Enable or disable pop gesture + // note: if disablePopGesture is nil, do nothing + if (self.disablePopGesture) { + self.navigationController.interactivePopGestureRecognizer.enabled = ![self.disablePopGesture boolValue]; + } + [FB_PLUGIN containerAppeared:self]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [[[UIApplication sharedApplication] keyWindow] endEditing:YES]; + [super viewWillDisappear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super bridge_viewDidDisappear:animated]; + [FB_PLUGIN containerDisappeared:self]; +} + +- (void)installSplashScreenViewIfNecessary { + //Do nothing. +} + +- (BOOL)loadDefaultSplashScreenView { + return YES; +} +@end + diff --git a/flutter_boost_ios/ios/Classes/container/FBLifecycle.h b/flutter_boost_ios/ios/Classes/container/FBLifecycle.h new file mode 100644 index 000000000..a99d42653 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBLifecycle.h @@ -0,0 +1,28 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +@interface FBLifecycle : NSObject ++ (void)pause; ++ (void)resume; +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBLifecycle.m b/flutter_boost_ios/ios/Classes/container/FBLifecycle.m new file mode 100644 index 000000000..a0c155eac --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBLifecycle.m @@ -0,0 +1,44 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import +#import "FBLifecycle.h" +#import "FlutterBoost.h" +#import "FBFlutterContainer.h" + +#define ENGINE [[FlutterBoost instance] engine] + +@implementation FBLifecycle ++ (void)pause { + [[FlutterBoost instance]sendEventToFlutterWith:@"app_lifecycle_changed_key" + arguments:@{@"lifecycleState":@4}]; + if (ENGINE.viewController != nil){ + ENGINE.viewController = nil; + } +} + ++ (void)resume { + [[FlutterBoost instance]sendEventToFlutterWith:@"app_lifecycle_changed_key" + arguments:@{@"lifecycleState":@1}]; +} +@end diff --git a/flutter_boost_ios/ios/Classes/messages.h b/flutter_boost_ios/ios/Classes/messages.h new file mode 100644 index 000000000..57b96e238 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/messages.h @@ -0,0 +1,86 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +@class FBCommonParams; +@class FBStackInfo; +@class FBFlutterContainer; +@class FBFlutterPage; + +@interface FBCommonParams : NSObject ++ (instancetype)makeWithOpaque:(nullable NSNumber *)opaque + key:(nullable NSString *)key + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments; +@property(nonatomic, strong, nullable) NSNumber * opaque; +@property(nonatomic, copy, nullable) NSString * key; +@property(nonatomic, copy, nullable) NSString * pageName; +@property(nonatomic, copy, nullable) NSString * uniqueId; +@property(nonatomic, strong, nullable) NSDictionary * arguments; +@end + +@interface FBStackInfo : NSObject ++ (instancetype)makeWithIds:(nullable NSArray *)ids + containers:(nullable NSDictionary *)containers; +@property(nonatomic, strong, nullable) NSArray * ids; +@property(nonatomic, strong, nullable) NSDictionary * containers; +@end + +@interface FBFlutterContainer : NSObject ++ (instancetype)makeWithPages:(nullable NSArray *)pages; +@property(nonatomic, strong, nullable) NSArray * pages; +@end + +@interface FBFlutterPage : NSObject ++ (instancetype)makeWithWithContainer:(nullable NSNumber *)withContainer + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments; +@property(nonatomic, strong, nullable) NSNumber * withContainer; +@property(nonatomic, copy, nullable) NSString * pageName; +@property(nonatomic, copy, nullable) NSString * uniqueId; +@property(nonatomic, strong, nullable) NSDictionary * arguments; +@end + +/// The codec used by FBNativeRouterApi. +NSObject *FBNativeRouterApiGetCodec(void); + +@protocol FBNativeRouterApi +- (void)pushNativeRouteParam:(FBCommonParams *)param error:(FlutterError *_Nullable *_Nonnull)error; +- (void)pushFlutterRouteParam:(FBCommonParams *)param error:(FlutterError *_Nullable *_Nonnull)error; +- (void)popRouteParam:(FBCommonParams *)param completion:(void(^)(FlutterError *_Nullable))completion; +/// @return `nil` only when `error != nil`. +- (nullable FBStackInfo *)getStackFromHostWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)saveStackToHostStack:(FBStackInfo *)stack error:(FlutterError *_Nullable *_Nonnull)error; +- (void)sendEventToNativeParams:(FBCommonParams *)params error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FBNativeRouterApiSetup(id binaryMessenger, NSObject *_Nullable api); + +/// The codec used by FBFlutterRouterApi. +NSObject *FBFlutterRouterApiGetCodec(void); + +@interface FBFlutterRouterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)pushRouteParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)popRouteParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)removeRouteParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onForegroundParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onBackgroundParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onNativeResultParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onContainerShowParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onContainerHideParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)sendEventToFlutterParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onBackPressedWithCompletion:(void(^)(NSError *_Nullable))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/flutter_boost_ios/ios/Classes/messages.m b/flutter_boost_ios/ios/Classes/messages.m new file mode 100644 index 000000000..b78863a07 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/messages.m @@ -0,0 +1,526 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code": (error.code ?: [NSNull null]), + @"message": (error.message ?: [NSNull null]), + @"details": (error.details ?: [NSNull null]), + }; + } + return @{ + @"result": (result ?: [NSNull null]), + @"error": errorDict, + }; +} +static id GetNullableObject(NSDictionary* dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + + +@interface FBCommonParams () ++ (FBCommonParams *)fromMap:(NSDictionary *)dict; ++ (nullable FBCommonParams *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FBStackInfo () ++ (FBStackInfo *)fromMap:(NSDictionary *)dict; ++ (nullable FBStackInfo *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FBFlutterContainer () ++ (FBFlutterContainer *)fromMap:(NSDictionary *)dict; ++ (nullable FBFlutterContainer *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FBFlutterPage () ++ (FBFlutterPage *)fromMap:(NSDictionary *)dict; ++ (nullable FBFlutterPage *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FBCommonParams ++ (instancetype)makeWithOpaque:(nullable NSNumber *)opaque + key:(nullable NSString *)key + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments { + FBCommonParams* pigeonResult = [[FBCommonParams alloc] init]; + pigeonResult.opaque = opaque; + pigeonResult.key = key; + pigeonResult.pageName = pageName; + pigeonResult.uniqueId = uniqueId; + pigeonResult.arguments = arguments; + return pigeonResult; +} ++ (FBCommonParams *)fromMap:(NSDictionary *)dict { + FBCommonParams *pigeonResult = [[FBCommonParams alloc] init]; + pigeonResult.opaque = GetNullableObject(dict, @"opaque"); + pigeonResult.key = GetNullableObject(dict, @"key"); + pigeonResult.pageName = GetNullableObject(dict, @"pageName"); + pigeonResult.uniqueId = GetNullableObject(dict, @"uniqueId"); + pigeonResult.arguments = GetNullableObject(dict, @"arguments"); + return pigeonResult; +} ++ (nullable FBCommonParams *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBCommonParams fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"opaque" : (self.opaque ?: [NSNull null]), + @"key" : (self.key ?: [NSNull null]), + @"pageName" : (self.pageName ?: [NSNull null]), + @"uniqueId" : (self.uniqueId ?: [NSNull null]), + @"arguments" : (self.arguments ?: [NSNull null]), + }; +} +@end + +@implementation FBStackInfo ++ (instancetype)makeWithIds:(nullable NSArray *)ids + containers:(nullable NSDictionary *)containers { + FBStackInfo* pigeonResult = [[FBStackInfo alloc] init]; + pigeonResult.ids = ids; + pigeonResult.containers = containers; + return pigeonResult; +} ++ (FBStackInfo *)fromMap:(NSDictionary *)dict { + FBStackInfo *pigeonResult = [[FBStackInfo alloc] init]; + pigeonResult.ids = GetNullableObject(dict, @"ids"); + pigeonResult.containers = GetNullableObject(dict, @"containers"); + return pigeonResult; +} ++ (nullable FBStackInfo *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBStackInfo fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"ids" : (self.ids ?: [NSNull null]), + @"containers" : (self.containers ?: [NSNull null]), + }; +} +@end + +@implementation FBFlutterContainer ++ (instancetype)makeWithPages:(nullable NSArray *)pages { + FBFlutterContainer* pigeonResult = [[FBFlutterContainer alloc] init]; + pigeonResult.pages = pages; + return pigeonResult; +} ++ (FBFlutterContainer *)fromMap:(NSDictionary *)dict { + FBFlutterContainer *pigeonResult = [[FBFlutterContainer alloc] init]; + pigeonResult.pages = GetNullableObject(dict, @"pages"); + return pigeonResult; +} ++ (nullable FBFlutterContainer *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBFlutterContainer fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"pages" : (self.pages ?: [NSNull null]), + }; +} +@end + +@implementation FBFlutterPage ++ (instancetype)makeWithWithContainer:(nullable NSNumber *)withContainer + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments { + FBFlutterPage* pigeonResult = [[FBFlutterPage alloc] init]; + pigeonResult.withContainer = withContainer; + pigeonResult.pageName = pageName; + pigeonResult.uniqueId = uniqueId; + pigeonResult.arguments = arguments; + return pigeonResult; +} ++ (FBFlutterPage *)fromMap:(NSDictionary *)dict { + FBFlutterPage *pigeonResult = [[FBFlutterPage alloc] init]; + pigeonResult.withContainer = GetNullableObject(dict, @"withContainer"); + pigeonResult.pageName = GetNullableObject(dict, @"pageName"); + pigeonResult.uniqueId = GetNullableObject(dict, @"uniqueId"); + pigeonResult.arguments = GetNullableObject(dict, @"arguments"); + return pigeonResult; +} ++ (nullable FBFlutterPage *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBFlutterPage fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"withContainer" : (self.withContainer ?: [NSNull null]), + @"pageName" : (self.pageName ?: [NSNull null]), + @"uniqueId" : (self.uniqueId ?: [NSNull null]), + @"arguments" : (self.arguments ?: [NSNull null]), + }; +} +@end + +@interface FBNativeRouterApiCodecReader : FlutterStandardReader +@end +@implementation FBNativeRouterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type +{ + switch (type) { + case 128: + return [FBCommonParams fromMap:[self readValue]]; + + case 129: + return [FBFlutterContainer fromMap:[self readValue]]; + + case 130: + return [FBFlutterPage fromMap:[self readValue]]; + + case 131: + return [FBStackInfo fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + + } +} +@end + +@interface FBNativeRouterApiCodecWriter : FlutterStandardWriter +@end +@implementation FBNativeRouterApiCodecWriter +- (void)writeValue:(id)value +{ + if ([value isKindOfClass:[FBCommonParams class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else + if ([value isKindOfClass:[FBFlutterContainer class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else + if ([value isKindOfClass:[FBFlutterPage class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else + if ([value isKindOfClass:[FBStackInfo class]]) { + [self writeByte:131]; + [self writeValue:[value toMap]]; + } else +{ + [super writeValue:value]; + } +} +@end + +@interface FBNativeRouterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FBNativeRouterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FBNativeRouterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FBNativeRouterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FBNativeRouterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FBNativeRouterApiCodecReaderWriter *readerWriter = [[FBNativeRouterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + + +void FBNativeRouterApiSetup(id binaryMessenger, NSObject *api) { + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.pushNativeRoute" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pushNativeRouteParam:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(pushNativeRouteParam:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_param = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api pushNativeRouteParam:arg_param error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pushFlutterRouteParam:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(pushFlutterRouteParam:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_param = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api pushFlutterRouteParam:arg_param error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.popRoute" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(popRouteParam:completion:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(popRouteParam:completion:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_param = GetNullableObjectAtIndex(args, 0); + [api popRouteParam:arg_param completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.getStackFromHost" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getStackFromHostWithError:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(getStackFromHostWithError:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + FBStackInfo *output = [api getStackFromHostWithError:&error]; + callback(wrapResult(output, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.saveStackToHost" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(saveStackToHostStack:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(saveStackToHostStack:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBStackInfo *arg_stack = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api saveStackToHostStack:arg_stack error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.sendEventToNative" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(sendEventToNativeParams:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(sendEventToNativeParams:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_params = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api sendEventToNativeParams:arg_params error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } +} +@interface FBFlutterRouterApiCodecReader : FlutterStandardReader +@end +@implementation FBFlutterRouterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type +{ + switch (type) { + case 128: + return [FBCommonParams fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + + } +} +@end + +@interface FBFlutterRouterApiCodecWriter : FlutterStandardWriter +@end +@implementation FBFlutterRouterApiCodecWriter +- (void)writeValue:(id)value +{ + if ([value isKindOfClass:[FBCommonParams class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else +{ + [super writeValue:value]; + } +} +@end + +@interface FBFlutterRouterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FBFlutterRouterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FBFlutterRouterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FBFlutterRouterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FBFlutterRouterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FBFlutterRouterApiCodecReaderWriter *readerWriter = [[FBFlutterRouterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + + +@interface FBFlutterRouterApi () +@property (nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FBFlutterRouterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)pushRouteParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.pushRoute" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)popRouteParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.popRoute" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)removeRouteParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.removeRoute" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onForegroundParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onForeground" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onBackgroundParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onBackground" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onNativeResultParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onNativeResult" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onContainerShowParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onContainerShow" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onContainerHideParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onContainerHide" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)sendEventToFlutterParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onBackPressedWithCompletion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onBackPressed" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:nil reply:^(id reply) { + completion(nil); + }]; +} +@end diff --git a/flutter_boost_ios/ios/flutter_boost_ios.podspec b/flutter_boost_ios/ios/flutter_boost_ios.podspec new file mode 100755 index 000000000..803ee0fac --- /dev/null +++ b/flutter_boost_ios/ios/flutter_boost_ios.podspec @@ -0,0 +1,36 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'flutter_boost_ios' + s.version = '5.0.2' + s.summary = 'iOS implementation of the flutter_boost plugin.' + s.description = <<-DESC +iOS implementation of the flutter_boost plugin. + DESC + s.homepage = 'https://github.com/alibaba/flutter_boost' + s.license = { :file => '../LICENSE' } + s.author = { 'Alibaba Xianyu' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*.{h,m,mm}' + + s.public_header_files = + 'Classes/FlutterBoost.h', + 'Classes/FlutterBoostDelegate.h', + 'Classes/FlutterBoostPlugin.h', + 'Classes/container/FBFlutterViewContainer.h', + 'Classes/container/FBFlutterContainer.h', + 'Classes/Options.h', + 'Classes/messages.h' + + + s.dependency 'Flutter' + s.libraries = 'c++' + s.pod_target_xcconfig = { + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11', + 'CLANG_CXX_LIBRARY' => 'libc++' + } + + s.ios.deployment_target = '8.0' +end + diff --git a/flutter_boost_ios/lib/flutter_boost_ios.dart b/flutter_boost_ios/lib/flutter_boost_ios.dart new file mode 100644 index 000000000..82bd4dfbb --- /dev/null +++ b/flutter_boost_ios/lib/flutter_boost_ios.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +library flutter_boost_ios; + +// This package does not export any Dart code to the app-facing package. +// It only provides the iOS platform implementation. diff --git a/flutter_boost_ios/pubspec.yaml b/flutter_boost_ios/pubspec.yaml new file mode 100644 index 000000000..5b3b8b3bf --- /dev/null +++ b/flutter_boost_ios/pubspec.yaml @@ -0,0 +1,30 @@ +# Copyright (c) 2019 Alibaba Group. All rights reserved. +# Use of this source code is governed by a MIT license that can be +# found in the LICENSE file. + +name: flutter_boost_ios +description: iOS implementation of the flutter_boost plugin. +version: 5.0.2 +homepage: https://github.com/alibaba/flutter_boost + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + flutter_boost_platform_interface: + path: ../flutter_boost_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + plugin: + implements: flutter_boost + platforms: + ios: + pluginClass: FlutterBoostPlugin diff --git a/flutter_boost_platform_interface/.gitignore b/flutter_boost_platform_interface/.gitignore new file mode 100644 index 000000000..6d11bab8b --- /dev/null +++ b/flutter_boost_platform_interface/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +pubspec.lock diff --git a/flutter_boost_platform_interface/CHANGELOG.md b/flutter_boost_platform_interface/CHANGELOG.md new file mode 100644 index 000000000..c20e3bff3 --- /dev/null +++ b/flutter_boost_platform_interface/CHANGELOG.md @@ -0,0 +1,4 @@ +## 5.0.2 + +* Initial release of the platform interface for flutter_boost. +* Extracted from `flutter_boost` as part of federated plugin migration. diff --git a/flutter_boost_platform_interface/LICENSE b/flutter_boost_platform_interface/LICENSE new file mode 100755 index 000000000..88ed1cb37 --- /dev/null +++ b/flutter_boost_platform_interface/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alibaba Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_boost_platform_interface/README.md b/flutter_boost_platform_interface/README.md new file mode 100644 index 000000000..8ac755e90 --- /dev/null +++ b/flutter_boost_platform_interface/README.md @@ -0,0 +1,13 @@ +# flutter_boost_platform_interface + +A common platform interface for the `flutter_boost` plugin. + +This interface allows platform-specific implementations of the `flutter_boost` plugin, as well as the plugin itself, to ensure they are supporting the same interface. + +## Usage + +To implement a new platform-specific implementation of `flutter_boost`, extend `FlutterBoostPlatform` with an implementation that performs the platform-specific behavior. + +## Note + +This package is not intended for direct use by end users. For the app-facing plugin, see [flutter_boost](https://pub.dev/packages/flutter_boost). diff --git a/flutter_boost_platform_interface/analysis_options.yaml b/flutter_boost_platform_interface/analysis_options.yaml new file mode 100644 index 000000000..8d81c200c --- /dev/null +++ b/flutter_boost_platform_interface/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false diff --git a/flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart b/flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart new file mode 100644 index 000000000..4f36fa1a6 --- /dev/null +++ b/flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +library flutter_boost_platform_interface; + +export 'src/flutter_boost_platform_interface.dart'; +export 'src/method_channel_flutter_boost.dart'; +export 'src/messages.dart'; diff --git a/flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart b/flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart new file mode 100644 index 000000000..d3a09b138 --- /dev/null +++ b/flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'messages.dart'; +import 'method_channel_flutter_boost.dart'; + +/// The interface that implementations of flutter_boost must implement. +/// +/// Platform implementations should extend this class rather than implement it as `flutter_boost` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [FlutterBoostPlatform] methods. +abstract class FlutterBoostPlatform extends PlatformInterface { + /// Constructs a FlutterBoostPlatform. + FlutterBoostPlatform() : super(token: _token); + + static final Object _token = Object(); + + static FlutterBoostPlatform _instance = MethodChannelFlutterBoost(); + + /// The default instance of [FlutterBoostPlatform] to use. + /// + /// Defaults to [MethodChannelFlutterBoost]. + static FlutterBoostPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterBoostPlatform] when + /// they register themselves. + static set instance(FlutterBoostPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Returns a [NativeRouterApi] instance for communicating with native side. + NativeRouterApi getNativeRouterApi(); + + /// Sets up the [FlutterRouterApi] for receiving messages from native side. + void setupFlutterRouterApi(FlutterRouterApi? api); +} diff --git a/flutter_boost_platform_interface/lib/src/messages.dart b/flutter_boost_platform_interface/lib/src/messages.dart new file mode 100644 index 000000000..2d8fd9594 --- /dev/null +++ b/flutter_boost_platform_interface/lib/src/messages.dart @@ -0,0 +1,519 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +class CommonParams { + CommonParams({ + this.opaque, + this.key, + this.pageName, + this.uniqueId, + this.arguments, + }); + + bool? opaque; + String? key; + String? pageName; + String? uniqueId; + Map? arguments; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['opaque'] = opaque; + pigeonMap['key'] = key; + pigeonMap['pageName'] = pageName; + pigeonMap['uniqueId'] = uniqueId; + pigeonMap['arguments'] = arguments; + return pigeonMap; + } + + static CommonParams decode(Object message) { + final Map pigeonMap = message as Map; + return CommonParams( + opaque: pigeonMap['opaque'] as bool?, + key: pigeonMap['key'] as String?, + pageName: pigeonMap['pageName'] as String?, + uniqueId: pigeonMap['uniqueId'] as String?, + arguments: (pigeonMap['arguments'] as Map?)?.cast(), + ); + } +} + +class StackInfo { + StackInfo({ + this.ids, + this.containers, + }); + + List? ids; + Map? containers; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['ids'] = ids; + pigeonMap['containers'] = containers; + return pigeonMap; + } + + static StackInfo decode(Object message) { + final Map pigeonMap = message as Map; + return StackInfo( + ids: (pigeonMap['ids'] as List?)?.cast(), + containers: (pigeonMap['containers'] as Map?)?.cast(), + ); + } +} + +class FlutterContainer { + FlutterContainer({ + this.pages, + }); + + List? pages; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['pages'] = pages; + return pigeonMap; + } + + static FlutterContainer decode(Object message) { + final Map pigeonMap = message as Map; + return FlutterContainer( + pages: (pigeonMap['pages'] as List?)?.cast(), + ); + } +} + +class FlutterPage { + FlutterPage({ + this.withContainer, + this.pageName, + this.uniqueId, + this.arguments, + }); + + bool? withContainer; + String? pageName; + String? uniqueId; + Map? arguments; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['withContainer'] = withContainer; + pigeonMap['pageName'] = pageName; + pigeonMap['uniqueId'] = uniqueId; + pigeonMap['arguments'] = arguments; + return pigeonMap; + } + + static FlutterPage decode(Object message) { + final Map pigeonMap = message as Map; + return FlutterPage( + withContainer: pigeonMap['withContainer'] as bool?, + pageName: pigeonMap['pageName'] as String?, + uniqueId: pigeonMap['uniqueId'] as String?, + arguments: (pigeonMap['arguments'] as Map?)?.cast(), + ); + } +} + +class _NativeRouterApiCodec extends StandardMessageCodec { + const _NativeRouterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CommonParams) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else + if (value is FlutterContainer) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else + if (value is FlutterPage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else + if (value is StackInfo) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CommonParams.decode(readValue(buffer)!); + + case 129: + return FlutterContainer.decode(readValue(buffer)!); + + case 130: + return FlutterPage.decode(readValue(buffer)!); + + case 131: + return StackInfo.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} + +class NativeRouterApi { + /// Constructor for [NativeRouterApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NativeRouterApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _NativeRouterApiCodec(); + + Future pushNativeRoute(CommonParams arg_param) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.pushNativeRoute', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_param]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future pushFlutterRoute(CommonParams arg_param) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_param]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future popRoute(CommonParams arg_param) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.popRoute', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_param]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getStackFromHost() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.getStackFromHost', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as StackInfo?)!; + } + } + + Future saveStackToHost(StackInfo arg_stack) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.saveStackToHost', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_stack]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future sendEventToNative(CommonParams arg_params) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.sendEventToNative', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_params]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _FlutterRouterApiCodec extends StandardMessageCodec { + const _FlutterRouterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CommonParams) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CommonParams.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} +abstract class FlutterRouterApi { + static const MessageCodec codec = _FlutterRouterApiCodec(); + + void pushRoute(CommonParams param); + void popRoute(CommonParams param); + void removeRoute(CommonParams param); + void onForeground(CommonParams param); + void onBackground(CommonParams param); + void onNativeResult(CommonParams param); + void onContainerShow(CommonParams param); + void onContainerHide(CommonParams param); + void sendEventToFlutter(CommonParams param); + void onBackPressed(); + static void setup(FlutterRouterApi? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.pushRoute', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null, expected non-null CommonParams.'); + api.pushRoute(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.popRoute', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null, expected non-null CommonParams.'); + api.popRoute(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.removeRoute', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null, expected non-null CommonParams.'); + api.removeRoute(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onForeground', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null, expected non-null CommonParams.'); + api.onForeground(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onBackground', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null, expected non-null CommonParams.'); + api.onBackground(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onNativeResult', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null, expected non-null CommonParams.'); + api.onNativeResult(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onContainerShow', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null, expected non-null CommonParams.'); + api.onContainerShow(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onContainerHide', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null, expected non-null CommonParams.'); + api.onContainerHide(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null, expected non-null CommonParams.'); + api.sendEventToFlutter(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onBackPressed', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + // ignore message + api.onBackPressed(); + return; + }); + } + } + } +} diff --git a/flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart b/flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart new file mode 100644 index 000000000..f2d93b344 --- /dev/null +++ b/flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import 'messages.dart'; +import 'flutter_boost_platform_interface.dart'; + +/// An implementation of [FlutterBoostPlatform] that uses method channels. +class MethodChannelFlutterBoost extends FlutterBoostPlatform { + NativeRouterApi? _nativeRouterApi; + + @override + NativeRouterApi getNativeRouterApi() { + _nativeRouterApi ??= NativeRouterApi(); + return _nativeRouterApi!; + } + + @override + void setupFlutterRouterApi(FlutterRouterApi? api) { + FlutterRouterApi.setup(api); + } +} diff --git a/flutter_boost_platform_interface/pubspec.yaml b/flutter_boost_platform_interface/pubspec.yaml new file mode 100644 index 000000000..a58b415b5 --- /dev/null +++ b/flutter_boost_platform_interface/pubspec.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2019 Alibaba Group. All rights reserved. +# Use of this source code is governed by a MIT license that can be +# found in the LICENSE file. + +name: flutter_boost_platform_interface +description: A common platform interface for the flutter_boost plugin. +version: 5.0.2 +homepage: https://github.com/alibaba/flutter_boost + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.0 + collection: ^1.16.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 diff --git a/lib/flutter_boost.dart b/lib/flutter_boost.dart index 870d44852..d9f2b2978 100644 --- a/lib/flutter_boost.dart +++ b/lib/flutter_boost.dart @@ -4,6 +4,10 @@ library flutter_boost; +// Export platform interface messages +export 'package:flutter_boost_platform_interface/flutter_boost_platform_interface.dart' + show CommonParams, StackInfo, FlutterContainer, FlutterPage; + export 'src/boost_channel.dart'; export 'src/boost_container.dart'; export 'src/boost_flutter_binding.dart'; diff --git a/lib/src/messages.dart b/lib/src/messages.dart index 2d8fd9594..8749eb85a 100644 --- a/lib/src/messages.dart +++ b/lib/src/messages.dart @@ -1,519 +1,13 @@ // Copyright (c) 2019 Alibaba Group. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v3.2.9), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; - -class CommonParams { - CommonParams({ - this.opaque, - this.key, - this.pageName, - this.uniqueId, - this.arguments, - }); - - bool? opaque; - String? key; - String? pageName; - String? uniqueId; - Map? arguments; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['opaque'] = opaque; - pigeonMap['key'] = key; - pigeonMap['pageName'] = pageName; - pigeonMap['uniqueId'] = uniqueId; - pigeonMap['arguments'] = arguments; - return pigeonMap; - } - - static CommonParams decode(Object message) { - final Map pigeonMap = message as Map; - return CommonParams( - opaque: pigeonMap['opaque'] as bool?, - key: pigeonMap['key'] as String?, - pageName: pigeonMap['pageName'] as String?, - uniqueId: pigeonMap['uniqueId'] as String?, - arguments: (pigeonMap['arguments'] as Map?)?.cast(), - ); - } -} - -class StackInfo { - StackInfo({ - this.ids, - this.containers, - }); - - List? ids; - Map? containers; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['ids'] = ids; - pigeonMap['containers'] = containers; - return pigeonMap; - } - - static StackInfo decode(Object message) { - final Map pigeonMap = message as Map; - return StackInfo( - ids: (pigeonMap['ids'] as List?)?.cast(), - containers: (pigeonMap['containers'] as Map?)?.cast(), - ); - } -} - -class FlutterContainer { - FlutterContainer({ - this.pages, - }); - - List? pages; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['pages'] = pages; - return pigeonMap; - } - - static FlutterContainer decode(Object message) { - final Map pigeonMap = message as Map; - return FlutterContainer( - pages: (pigeonMap['pages'] as List?)?.cast(), - ); - } -} - -class FlutterPage { - FlutterPage({ - this.withContainer, - this.pageName, - this.uniqueId, - this.arguments, - }); - - bool? withContainer; - String? pageName; - String? uniqueId; - Map? arguments; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['withContainer'] = withContainer; - pigeonMap['pageName'] = pageName; - pigeonMap['uniqueId'] = uniqueId; - pigeonMap['arguments'] = arguments; - return pigeonMap; - } - - static FlutterPage decode(Object message) { - final Map pigeonMap = message as Map; - return FlutterPage( - withContainer: pigeonMap['withContainer'] as bool?, - pageName: pigeonMap['pageName'] as String?, - uniqueId: pigeonMap['uniqueId'] as String?, - arguments: (pigeonMap['arguments'] as Map?)?.cast(), - ); - } -} - -class _NativeRouterApiCodec extends StandardMessageCodec { - const _NativeRouterApiCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is CommonParams) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else - if (value is FlutterContainer) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else - if (value is FlutterPage) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else - if (value is StackInfo) { - buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else -{ - super.writeValue(buffer, value); - } - } - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return CommonParams.decode(readValue(buffer)!); - - case 129: - return FlutterContainer.decode(readValue(buffer)!); - - case 130: - return FlutterPage.decode(readValue(buffer)!); - - case 131: - return StackInfo.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - - } - } -} - -class NativeRouterApi { - /// Constructor for [NativeRouterApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - NativeRouterApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _NativeRouterApiCodec(); - - Future pushNativeRoute(CommonParams arg_param) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.pushNativeRoute', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_param]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future pushFlutterRoute(CommonParams arg_param) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_param]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future popRoute(CommonParams arg_param) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.popRoute', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_param]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future getStackFromHost() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.getStackFromHost', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as StackInfo?)!; - } - } - - Future saveStackToHost(StackInfo arg_stack) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.saveStackToHost', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_stack]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future sendEventToNative(CommonParams arg_params) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.sendEventToNative', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_params]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } -} - -class _FlutterRouterApiCodec extends StandardMessageCodec { - const _FlutterRouterApiCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is CommonParams) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else -{ - super.writeValue(buffer, value); - } - } - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return CommonParams.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - - } - } -} -abstract class FlutterRouterApi { - static const MessageCodec codec = _FlutterRouterApiCodec(); - - void pushRoute(CommonParams param); - void popRoute(CommonParams param); - void removeRoute(CommonParams param); - void onForeground(CommonParams param); - void onBackground(CommonParams param); - void onNativeResult(CommonParams param); - void onContainerShow(CommonParams param); - void onContainerHide(CommonParams param); - void sendEventToFlutter(CommonParams param); - void onBackPressed(); - static void setup(FlutterRouterApi? api, {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.pushRoute', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null, expected non-null CommonParams.'); - api.pushRoute(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.popRoute', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null, expected non-null CommonParams.'); - api.popRoute(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.removeRoute', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null, expected non-null CommonParams.'); - api.removeRoute(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onForeground', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null, expected non-null CommonParams.'); - api.onForeground(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onBackground', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null, expected non-null CommonParams.'); - api.onBackground(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onNativeResult', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null, expected non-null CommonParams.'); - api.onNativeResult(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onContainerShow', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null, expected non-null CommonParams.'); - api.onContainerShow(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onContainerHide', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null, expected non-null CommonParams.'); - api.onContainerHide(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null, expected non-null CommonParams.'); - api.sendEventToFlutter(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onBackPressed', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - // ignore message - api.onBackPressed(); - return; - }); - } - } - } -} +// Re-export messages from platform interface +export 'package:flutter_boost_platform_interface/flutter_boost_platform_interface.dart' + show + CommonParams, + StackInfo, + FlutterContainer, + FlutterPage, + NativeRouterApi, + FlutterRouterApi; diff --git a/pubspec.yaml b/pubspec.yaml index 3ff2f2d58..2e04740a7 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,12 @@ dependencies: flutter: sdk: flutter collection: ^1.16.0 + flutter_boost_platform_interface: + path: flutter_boost_platform_interface + flutter_boost_android: + path: flutter_boost_android + flutter_boost_ios: + path: flutter_boost_ios dev_dependencies: flutter_lints: ^2.0.1 @@ -23,7 +29,6 @@ flutter: plugin: platforms: android: - package: com.idlefish.flutterboost - pluginClass: FlutterBoostPlugin + default_package: flutter_boost_android ios: - pluginClass: FlutterBoostPlugin + default_package: flutter_boost_ios From 5a72249fa2a5c07ce4b5d65e781a16a7b0fd26b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:27:26 +0000 Subject: [PATCH 3/5] Complete federated plugin migration verification Co-authored-by: 0xZOne <26625149+0xZOne@users.noreply.github.com> --- _codeql_detected_source_root | 1 + 1 file changed, 1 insertion(+) create mode 120000 _codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 000000000..945c9b46d --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file From 5f69389aeb2fd1b11a15221ce09ccc4586b97051 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:27:40 +0000 Subject: [PATCH 4/5] Remove temporary codeql symlink Co-authored-by: 0xZOne <26625149+0xZOne@users.noreply.github.com> --- _codeql_detected_source_root | 1 - 1 file changed, 1 deletion(-) delete mode 120000 _codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root deleted file mode 120000 index 945c9b46d..000000000 --- a/_codeql_detected_source_root +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file From 669bc8afa6fa609373fe947125c4fa8e4db881b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:29:54 +0000 Subject: [PATCH 5/5] Fix security vulnerability: upgrade fastjson from 1.2.41 to 1.2.83 Co-authored-by: 0xZOne <26625149+0xZOne@users.noreply.github.com> --- FEDERATED_PLUGIN_MIGRATION.md | 17 +++++++++++++++++ android/build.gradle | 2 +- flutter_boost_android/android/build.gradle | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/FEDERATED_PLUGIN_MIGRATION.md b/FEDERATED_PLUGIN_MIGRATION.md index 9ad2940af..471bf4353 100644 --- a/FEDERATED_PLUGIN_MIGRATION.md +++ b/FEDERATED_PLUGIN_MIGRATION.md @@ -284,6 +284,23 @@ flutter_boost/ --- +## 安全修复 + +### 修复 fastjson 安全漏洞 + +**文件**: `android/build.gradle`, `flutter_boost_android/android/build.gradle` + +**修改内容**: +- 将 `com.alibaba:fastjson` 版本从 `1.2.41` 升级到 `1.2.83` + +**原因**: +- 版本 1.2.41 存在多个已知安全漏洞: + - 不安全的反序列化漏洞 (CVE-2022-25845 等) + - 从不受信任的控制范围包含功能的漏洞 +- 版本 1.2.83 已修复这些漏洞 + +--- + ## 验证清单 - [x] 平台接口包结构正确 diff --git a/android/build.gradle b/android/build.gradle index 89e0adc70..b9273f213 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -45,7 +45,7 @@ android { dependencies { compileOnly 'com.google.android.material:material:1.0.0' - compileOnly 'com.alibaba:fastjson:1.2.41' + compileOnly 'com.alibaba:fastjson:1.2.83' } diff --git a/flutter_boost_android/android/build.gradle b/flutter_boost_android/android/build.gradle index 89e0adc70..b9273f213 100755 --- a/flutter_boost_android/android/build.gradle +++ b/flutter_boost_android/android/build.gradle @@ -45,7 +45,7 @@ android { dependencies { compileOnly 'com.google.android.material:material:1.0.0' - compileOnly 'com.alibaba:fastjson:1.2.41' + compileOnly 'com.alibaba:fastjson:1.2.83' }