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..471bf4353
--- /dev/null
+++ b/FEDERATED_PLUGIN_MIGRATION.md
@@ -0,0 +1,320 @@
+# 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 代码
+
+---
+
+## 安全修复
+
+### 修复 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] 平台接口包结构正确
+- [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/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/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..b9273f213
--- /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.83'
+
+}
+
+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 000000000..135367700
Binary files /dev/null and b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.properties b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 000000000..5c1b6c95b
--- /dev/null
+++ b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/flutter_boost_android/android/gradlew b/flutter_boost_android/android/gradlew
new file mode 100755
index 000000000..cccdd3d51
--- /dev/null
+++ b/flutter_boost_android/android/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ 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 extends FlutterPlugin> pluginClass =
+ (Class extends FlutterPlugin>) 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