[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: zoeim # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n- 'https://payone.wencai.app/s/zoe'\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.dart_tool/\n\n.packages\n.pub/\n\nbuild/\n"
  },
  {
    "path": ".idea/libraries/Dart_SDK.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"Dart SDK\">\n    <CLASSES>\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/async\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/cli\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/collection\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/convert\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/core\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/developer\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/ffi\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/html\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/indexed_db\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/io\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/isolate\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/js\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/js_interop\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/js_interop_unsafe\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/js_util\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/math\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/mirrors\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/svg\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/typed_data\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/web_audio\" />\n      <root url=\"file://$USER_HOME$/sdk/flutter/bin/cache/dart-sdk/lib/web_gl\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/Flutter_Plugins.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"Flutter Plugins\" type=\"FlutterPluginsLibraryType\">\n    <CLASSES>\n      <root url=\"file://$PROJECT_DIR$\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>"
  },
  {
    "path": ".idea/libraries/Flutter_for_Android.xml",
    "content": "<component name=\"libraryTable\">\n  <library name=\"Flutter for Android\">\n    <CLASSES>\n      <root url=\"jar:///Users/johnzoe/projects/flutter/bin/cache/artifacts/engine/android-arm/flutter.jar!/\" />\n    </CLASSES>\n    <JAVADOC />\n    <SOURCES />\n  </library>\n</component>\n"
  },
  {
    "path": ".idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/flutter_floatwing.iml\" filepath=\"$PROJECT_DIR$/flutter_floatwing.iml\" />\n      <module fileurl=\"file://$PROJECT_DIR$/android/flutter_floatwing_android.iml\" filepath=\"$PROJECT_DIR$/android/flutter_floatwing_android.iml\" />\n      <module fileurl=\"file://$PROJECT_DIR$/example/android/flutter_floatwing_example_android.iml\" filepath=\"$PROJECT_DIR$/example/android/flutter_floatwing_example_android.iml\" />\n    </modules>\n  </component>\n</project>\n"
  },
  {
    "path": ".idea/runConfigurations/example_lib_main_dart.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"example/lib/main.dart\" type=\"FlutterRunConfigurationType\" factoryName=\"Flutter\">\n    <option name=\"filePath\" value=\"$PROJECT_DIR$/example/lib/main.dart\" />\n    <method />\n  </configuration>\n</component>"
  },
  {
    "path": ".idea/workspace.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"AutoImportSettings\">\n    <option name=\"autoReloadType\" value=\"SELECTIVE\" />\n  </component>\n  <component name=\"ChangeListManager\">\n    <list default=\"true\" id=\"8206a1b2-2e86-4987-a077-1fa67b40ac5e\" name=\"Changes\" comment=\"\" />\n    <option name=\"SHOW_DIALOG\" value=\"false\" />\n    <option name=\"HIGHLIGHT_CONFLICTS\" value=\"true\" />\n    <option name=\"HIGHLIGHT_NON_ACTIVE_CHANGELIST\" value=\"false\" />\n    <option name=\"LAST_RESOLUTION\" value=\"IGNORE\" />\n  </component>\n  <component name=\"ClangdSettings\">\n    <option name=\"formatViaClangd\" value=\"false\" />\n  </component>\n  <component name=\"FileEditorManager\">\n    <leaf>\n      <file leaf-file-name=\"flutter_floatwing.dart\" pinned=\"false\" current-in-tab=\"true\">\n        <entry file=\"file://$PROJECT_DIR$/lib/flutter_floatwing.dart\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"0\">\n              <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n            </state>\n          </provider>\n        </entry>\n      </file>\n      <file leaf-file-name=\"main.dart\" pinned=\"false\" current-in-tab=\"false\">\n        <entry file=\"file://$PROJECT_DIR$/example/lib/main.dart\">\n          <provider selected=\"true\" editor-type-id=\"text-editor\">\n            <state relative-caret-position=\"0\">\n              <caret line=\"0\" column=\"0\" lean-forward=\"false\" selection-start-line=\"0\" selection-start-column=\"0\" selection-end-line=\"0\" selection-end-column=\"0\" />\n            </state>\n          </provider>\n        </entry>\n      </file>\n    </leaf>\n  </component>\n  <component name=\"ProjectView\">\n    <navigator currentView=\"ProjectPane\" proportions=\"\" version=\"1\" />\n    <panes>\n      <pane id=\"ProjectPane\">\n        <option name=\"show-excluded-files\" value=\"false\" />\n      </pane>\n    </panes>\n  </component>\n  <component name=\"PropertiesComponent\"><![CDATA[{\n  \"keyToString\": {\n    \"dart.analysis.tool.window.visible\": \"false\"\n  }\n}]]></component>\n  <component name=\"TaskManager\">\n    <servers />\n  </component>\n  <component name=\"ToolWindowManager\">\n    <editor active=\"true\" />\n    <layout>\n      <window_info id=\"Project\" active=\"true\" anchor=\"left\" auto_hide=\"false\" internal_type=\"DOCKED\" type=\"DOCKED\" visible=\"true\" show_stripe_button=\"true\" weight=\"0.25\" sideWeight=\"0.5\" order=\"0\" side_tool=\"false\" content_ui=\"combo\" />\n    </layout>\n  </component>\n</project>"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: f7a6a7906be96d2288f5d63a5a54c515a6e987fe\n  channel: unknown\n\nproject_type: plugin\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 0.3.1\n\n- fix: replace deprecated `window` with `PlatformDispatcher.instance.implicitView`\n- fix: remove unnecessary null assertions in provider\n- fix: suppress unused element warnings for reserved future APIs\n- chore: add analysis_options.yaml\n\n## 0.3.0\n\n- fix: resolve screen size 0x0 bug causing incorrect window positioning\n- fix: wait for valid physicalSize before sending system config to Android\n- fix: update cached config when existing config has invalid 0x0 screen size\n- feat: improve assistive touch menu animation with scale from touch ball position\n- chore: upgrade Android Gradle Plugin to 8.5.0\n- chore: upgrade Kotlin to 1.9.22\n- chore: upgrade Gradle to 8.10.2\n- chore: migrate to new Flutter Gradle plugin declarative syntax\n- chore: update compileSdk/targetSdk to 34, minSdk to 21\n- test: add comprehensive unit tests (73 tests)\n- test: add integration tests (13 tests)\n\n## 0.1.1\n\n- fix: fix build error for version of kotlin\n\n## 0.1.0\n\n- feature: basic support for overlay window\n- chore: add exmaples\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2021 wellwell.work, LLC by Zoe\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n# flutter_floatwing\n\n[![Pub Version](https://img.shields.io/pub/v/flutter_floatwing?color=blue&logo=dart)](https://pub.dev/packages/flutter_floatwing)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)\n[![Platform](https://img.shields.io/badge/platform-Android-green.svg)](https://flutter.dev)\n\nA Flutter plugin that makes it easier to create floating/overlay windows for Android with pure Flutter. **Android only**\n\n</div>\n\n---\n\n## ✨ Features\n\n| Feature | Description |\n|---------|-------------|\n| 🎨 **Pure Flutter** | Write your entire overlay window in pure Flutter |\n| 🚀 **Simple** | Start your overlay window with as little as 1 line of code |\n| 📐 **Auto Resize** | Just focus on your Flutter widget size — the Android view resizes automatically |\n| 🪟 **Multi-window** | Create multiple overlay windows with parent-child relationships |\n| 💬 **Communicable** | Main app and overlay windows can communicate seamlessly with each other |\n| 📡 **Event Mechanism** | Subscribe to window lifecycle events and drag actions for flexible control |\n\n*More features are coming...*\n\n## 📸 Previews\n\n| Night Mode | Simple Example | Assistive Touch |\n|:----------:|:--------------:|:---------------:|\n| ![Night mode](./assets/flutter-floatwing-example-1.gif) | ![Simple example](./assets/flutter-floatwing-example-2.gif) | ![Assistive touch](./assets/flutter-floatwing-example-3.gif) |\n\n## 📦 Installation\n\nAdd `flutter_floatwing` to your `pubspec.yaml` file:\n\n```yaml\ndependencies:\n  flutter_floatwing: ^0.2.1\n```\n\nThen install it:\n- **Terminal**: Run `flutter pub get`\n- **Android Studio/IntelliJ**: Click \"Packages get\" in the action ribbon at the top of `pubspec.yaml`\n- **VS Code**: Click \"Get Packages\" on the right side of the action ribbon at the top of `pubspec.yaml`\n\nOr simply run:\n\n```bash\nflutter pub add flutter_floatwing\n```\n\n## 🚀 Quick Start\n\nSince we use Android's system alert window for display, you need to add the permission to `AndroidManifest.xml` first:\n\n```xml\n<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" />\n```\n\nAdd a route for the widget that will be displayed in the overlay window:\n\n```dart\n@override\nWidget build(BuildContext context) {\n  return MaterialApp(\n    debugShowCheckedModeBanner: false,\n    initialRoute: \"/\",\n    routes: {\n      \"/\": (_) => HomePage(),\n      // Add a route as the entry point for your overlay window\n      \"/my-overlay-window\": (_) => MyOverlayWindow(),\n    },\n  );\n}\n```\n\nBefore starting the floating window, check and request permission, then initialize the `flutter_floatwing` plugin in `initState` or a button callback:\n\n```dart\n// Check and request the system alert window permission\nFloatwingPlugin().checkPermission().then((granted) {\n  if (!granted) FloatwingPlugin().openPermissionSetting();\n});\n\n// Initialize the plugin first\nFloatwingPlugin().initialize();\n```\n\nNow create and start your overlay window:\n\n```dart\n// Define window config and start the window\nWindowConfig(route: \"/my-overlay-window\")\n    .to()           // Create a window object\n    .create(start: true);  // Create and start the overlay window\n```\n\n---\n\n**Notes:**\n\n- `route` is one of 3 ways to define an entry point for the overlay window. See the [Entry Point](#-entry-point) section for more details.\n- See the [Usage](#-usage) section for more features.\n\n## 🏗️ Architecture\n\nBefore diving into how `flutter_floatwing` manages windows, here are some key concepts:\n\n- **`id`** is the unique identifier for each window. All operations on a window are based on this `id` — you must provide one before creating a window.\n- The first engine created when opening the main application is called the **main engine** (or **plugin engine**). Engines created by the service are called **window engines**.\n- Different engines run in **different threads** and cannot communicate directly.\n- You can subscribe to events from all windows in the main engine. In a window engine, you can subscribe to events from itself and its child windows, but not from sibling or parent windows.\n- **`share` data** is the only way to communicate between engines. The only restriction is that the data must be serializable — you can share data from anywhere to anywhere.\n\nA floatwing window object consists of a Flutter engine that runs a widget via `runApp` and a view that is added to the Android window manager.\n\n![floatwing window](./assets/flutter-floatwing-window.png)\n\nThe overall view hierarchy looks like this:\n\n![flutter floatwing architecture](./assets/flutter-floatwing-arch.png)\n\n## 📖 Usage\n\nHere's how `flutter_floatwing` creates a new overlay window:\n\n1. Start a background service as the window manager from the main app.\n2. Send a create window request to the service.\n3. In the service, start a Flutter engine with the specified entry point.\n4. Create a new Flutter view and attach it to the Flutter engine.\n5. Add the view to the Android window manager.\n\n### Window & Config\n\n`WindowConfig` contains all configuration options for a window. You can create a window using configuration like this:\n\n```dart\nvoid _createWindow() {\n  var config = WindowConfig();\n  var w = Window(config, id: \"my-window\");\n  w.create();\n}\n```\n\nIf you don't need to register event or data handlers, you can create a window directly from the config:\n\n```dart\nvoid _createWindow() {\n  WindowConfig(id: \"my-window\").create();\n}\n```\n\nNote that if you want to specify a window ID, you must provide it in `WindowConfig`.\n\nIf you want to register handlers, use the `to()` function to convert a config to a window first — this is useful for keeping your code clean:\n\n```dart\nvoid _createWindow() {\n  WindowConfig(id: \"my-window\")\n      .to()\n      .on(EventType.WindowCreated, (w, _) {})\n      .create();\n}\n```\n\n#### Window Lifecycle\n\n- created\n- started\n- paused\n- resumed\n- destroyed\n\n### 🎯 Entry Point\n\nThe entry point determines where the engine starts execution. We support 3 configuration modes:\n\n| Name | Config | How to Use |\n|:-----|:-------|:-----------|\n| `route` | `WindowConfig(route: \"/my-overlay\")` | Add a route for the overlay window in your main routes, then start with: `WindowConfig(route: \"/my-overlay\")` |\n| `static function` | `WindowConfig(callback: myOverlayMain)` | Define a static `void Function()` that calls `runApp` to start a widget, then start with: `WindowConfig(callback: myOverlayMain)` |\n| `entry-point` | `WindowConfig(entry: \"myOverlayMain\")` | Same as static function, but add `@pragma(\"vm:entry-point\")` above the function and use the function name as a string: `WindowConfig(entry: \"myOverlayMain\")` |\n\n#### Example: Using `route`\n\n1. Add a route for your overlay widget in the main application:\n\n```dart\n@override\nWidget build(BuildContext context) {\n  return MaterialApp(\n    debugShowCheckedModeBanner: false,\n    initialRoute: \"/\",\n    routes: {\n      \"/\": (_) => HomePage(),\n      // Add a route as the entry point for your overlay window\n      \"/my-overlay-window\": (_) => MyOverlayWindow(),\n    },\n  );\n}\n```\n\n2. Start the window with `route`:\n\n```dart\nvoid _startWindow() {\n  WindowConfig(route: \"/my-overlay-window\")\n      .to()\n      .create(start: true);\n}\n```\n\n#### Example: Using `static function`\n\n1. Define a static function that calls `runApp`:\n\n```dart\nvoid myOverlayMain() {\n  runApp(MaterialApp(\n    home: AssistivePanel(),\n  ));\n  // Or use the floatwing helper to inject MaterialApp\n  // runApp(AssistivePanel().floatwing(app: true));\n}\n```\n\n2. Start the window with `callback`:\n\n```dart\nvoid _startWindow() {\n  WindowConfig(callback: myOverlayMain)\n      .to()\n      .create(start: true);\n}\n```\n\n#### Example: Using `entry-point`\n\n1. Define a static function that calls `runApp` and add the pragma annotation:\n\n```dart\n@pragma(\"vm:entry-point\")\nvoid myOverlayMain() {\n  runApp(MaterialApp(\n    home: AssistivePanel(),\n  ));\n  // Or use the floatwing helper to inject MaterialApp\n  // runApp(AssistivePanel().floatwing(app: true));\n}\n```\n\n2. Start the window with `entry`:\n\n```dart\nvoid _startWindow() {\n  WindowConfig(entry: \"myOverlayMain\")\n      .to()\n      .create(start: true);\n}\n```\n\n### Wrapping Your Widget\n\nFor simple widgets, no special wrapping is needed. But if you want additional functionality and cleaner code, we provide an injector for your widget.\n\nCurrent features include:\n- Auto-resize the window view\n- Auto-sync and ensure the window\n- Wrap with `MaterialApp`\n- *More features coming...*\n\nPreviously, you would write your overlay main function like this:\n\n```dart\nvoid overlayMain() {\n  runApp(MaterialApp(\n    home: MyOverlayView(),\n  ));\n}\n```\n\nNow you can simplify it to:\n\n```dart\nvoid overlayMain() {\n  runApp(MyOverlayView().floatwing(app: true));\n}\n```\n\nYou can wrap both `Widget` and `WidgetBuilder`. When wrapping a `WidgetBuilder`, you can access the window instance using `Window.of(context)`. For wrapped `Widget`, use `FloatwingPlugin().currentWindow` instead.\n\nTo access the window via `Window.of(context)`, use this pattern:\n\n```dart\nvoid overlayMain() {\n  runApp(((_) => MyOverlayView()).floatwing(app: true).make());\n}\n```\n\n### Accessing Window in Overlay\n\nIn your window engine, you can access the window object in two ways:\n\n- Directly access the plugin's cached field: `FloatwingPlugin().currentWindow`\n- If the widget is wrapped with `.floatwing()`, use `Window.of(context)`\n\n`FloatwingPlugin().currentWindow` returns `null` until initialization is complete.\n\nIf you inject a `WidgetBuilder` with `.floatwing()`, you can access the current window. It will always return a non-null value, unless you enable debug mode with `.floatwing(debug: true)`.\n\nFor example, to get the `id` of the current window:\n\n```dart\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nclass _ExampleViewState extends State<ExampleView> {\n  Window? w;\n\n  @override\n  void initState() {\n    super.initState();\n    SchedulerBinding.instance?.addPostFrameCallback((_) {\n      w = Window.of(context);\n      print(\"My window ID is ${w?.id}\");\n    });\n  }\n}\n```\n\n### Subscribing to Events\n\nYou can subscribe to window events and trigger actions when they fire. Window events are sent to the main engine, the window's own engine, and the parent window engine. This means you can subscribe to window events from the main application, the overlay window itself, or the parent overlay window.\n\nCurrently supported events include window lifecycle and drag actions:\n\n```dart\nenum EventType {\n  WindowCreated,\n  WindowStarted,\n  WindowPaused,\n  WindowResumed,\n  WindowDestroy,\n\n  WindowDragStart,\n  WindowDragging,\n  WindowDragEnd,\n}\n```\n\n*More event types are coming — contributions are welcome!*\n\nFor example, to perform an action when a window starts:\n\n```dart\n@override\nvoid initState() {\n  super.initState();\n\n  SchedulerBinding.instance?.addPostFrameCallback((_) {\n    w = Window.of(context);\n    w?.on(EventType.WindowStarted, (window, _) {\n      print(\"$w has started.\");\n    }).on(EventType.WindowDestroy, (window, data) {\n      // data is a boolean indicating whether the window was force-closed\n      print(\"$w has been destroyed, force: $data\");\n    });\n  });\n}\n```\n\n### Sharing Data with Windows\n\nSharing data is the only way to communicate with windows. Use `window.share(data)` for this purpose.\n\nFor example, to share data from the main application to an overlay window:\n\nFirst, get the target window in the main application (either the one you created or from the `windows` cache by ID):\n\n```dart\nWindow w;\n\nvoid _startWindow() {\n  w = WindowConfig(route: \"/my-overlay-window\").to();\n}\n\nvoid _shareData(dynamic data) {\n  w.share(data).then((value) {\n    // The window can return a value\n  });\n  // Or get the window from cache\n  // FloatwingPlugin().windows[\"default\"]?.share(data);\n}\n```\n\nTo share data with a specific name, add the name parameter: `w.share(data, name: \"name-1\")`.\n\nThen register a data handler in the window to receive the data:\n\n```dart\n@override\nvoid initState() {\n  super.initState();\n\n  SchedulerBinding.instance?.addPostFrameCallback((_) {\n    w = Window.of(context);\n    w?.onData((source, name, data) async {\n      print(\"Received $name data from $source: $data\");\n    });\n  });\n}\n```\n\nThe handler function signature is `Future<dynamic> Function(String? source, String? name, dynamic data)`:\n\n- `source`: Where the data comes from. `null` if from the main application; otherwise, the `id` of the source window.\n- `name`: The data name, useful for sharing data for different purposes.\n- `data`: The actual data received.\n- Return a value if you want to respond.\n\nYou can send data to any window as long as you know its ID — the only restriction is that you cannot send data to yourself. *Note: Sharing data to the main application is not yet implemented.*\n\n**Important: The data you share must be serializable.**\n\n## 📚 API Reference\n\n### FloatwingPlugin\n\n```dart\nFloatwingPlugin()\n  // Permission\n  ..checkPermission()       // Check overlay permission → Future<bool>\n  ..openPermissionSetting() // Open system settings → Future<bool>\n  \n  // Initialization\n  ..initialize()            // Initialize the plugin → Future<bool>\n  \n  // Service Management\n  ..isServiceRunning()      // Check if background service is running → Future<bool>\n  ..startService()          // Start the background service → Future<bool>\n  ..syncWindows()           // Sync windows from service → Future<bool>\n  ..cleanCache()            // Clean cached data → Future<bool>\n  \n  // Window Access\n  ..currentWindow           // Get current window (in overlay) → Window?\n  ..windows                 // Map of all windows by ID → Map<String, Window>\n  ..isWindow                // Check if running in window engine → bool\n```\n\n`FloatwingPlugin` is a singleton class that returns the same instance every time you call the `FloatwingPlugin()` factory method.\n\n### WindowConfig\n\nComplete configuration options for overlay windows:\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `id` | `String` | `\"default\"` | Unique window identifier |\n| `entry` | `String` | `\"main\"` | Entry point function name |\n| `route` | `String?` | `null` | Flutter route for the window |\n| `callback` | `Function?` | `null` | Static function to run (must be static) |\n| `width` | `int?` | `null` | Window width in pixels |\n| `height` | `int?` | `null` | Window height in pixels |\n| `x` | `int?` | `null` | X position on screen |\n| `y` | `int?` | `null` | Y position on screen |\n| `autosize` | `bool?` | `null` | Auto-resize to fit content |\n| `gravity` | `GravityType?` | `null` | Window position alignment |\n| `clickable` | `bool?` | `null` | Allow click-through when `false` |\n| `draggable` | `bool?` | `null` | Enable drag to move |\n| `focusable` | `bool?` | `null` | Allow window to receive focus |\n| `immersion` | `bool?` | `null` | Immersive status bar mode |\n| `visible` | `bool?` | `null` | Initial visibility state |\n\n#### WindowSize Constants\n\n```dart\nWindowSize.MatchParent  // -1: Fill entire screen\nWindowSize.WrapContent  // -2: Fit to content size\n```\n\n#### GravityType Enum\n\n```dart\nGravityType.Center        // Center of screen\nGravityType.CenterTop     // Top center\nGravityType.CenterBottom  // Bottom center\nGravityType.LeftTop       // Top left corner\nGravityType.LeftCenter    // Left center\nGravityType.LeftBottom    // Bottom left corner\nGravityType.RightTop      // Top right corner\nGravityType.RightCenter   // Right center\nGravityType.RightBottom   // Bottom right corner\n```\n\n**Example — Full-screen non-clickable overlay (night mode):**\n\n```dart\nWindowConfig(\n  id: \"night-mode\",\n  route: \"/night\",\n  width: WindowSize.MatchParent,\n  height: WindowSize.MatchParent,\n  clickable: false,  // Touch passes through\n)\n```\n\n**Example — Draggable floating button:**\n\n```dart\nWindowConfig(\n  id: \"float-button\",\n  route: \"/button\",\n  draggable: true,\n  gravity: GravityType.RightBottom,\n)\n```\n\n### Window\n\n| Method | Returns | Description |\n|--------|---------|-------------|\n| `create({start: bool})` | `Future<Window?>` | Create window, optionally start immediately |\n| `start()` | `Future<bool?>` | Start/show the window |\n| `close({force: bool})` | `Future<bool?>` | Close the window |\n| `show({visible: bool})` | `Future<bool?>` | Show or hide the window |\n| `hide()` | `Future<bool?>` | Hide the window (shortcut for `show(visible: false)`) |\n| `update(WindowConfig)` | `Future<bool>` | Update window configuration |\n| `share(data, {name})` | `Future<dynamic>` | Send data to this window |\n| `on(EventType, handler)` | `Window` | Subscribe to events (chainable) |\n| `onData(handler)` | `Window` | Register data receive handler |\n| `launchMainActivity()` | `Future<bool>` | Open main app from overlay |\n| `createChildWindow(...)` | `Future<Window?>` | Create a child window (from overlay only) |\n\n**Static Methods:**\n\n| Method | Returns | Description |\n|--------|---------|-------------|\n| `Window.of(context)` | `Window?` | Get window instance from BuildContext |\n| `Window.sync()` | `Future<Map?>` | Sync window state from Android |\n\n### Child Windows\n\nYou can create nested windows from within an overlay:\n\n```dart\n// In your overlay window widget\nfinal parentWindow = Window.of(context);\n\nparentWindow?.createChildWindow(\n  \"child-popup\",\n  WindowConfig(\n    route: \"/popup\",\n    width: 200,\n    height: 100,\n  ),\n  start: true,\n);\n```\n\n## ❤️ Support\n\nDid you find this plugin useful? Please consider making a donation to help improve it!\n\n## 🔧 Troubleshooting\n\n### Release Mode Error: \"No top-level getter declared\"\n\nIf you see this error in release mode when using `entry-point`:\n\n```\nNoSuchMethodError: No top-level getter 'xxx' declared.\nCould not resolve main entrypoint function.\n```\n\n**Solution**: Make sure your entry point function is:\n\n1. **Defined in `main.dart`** or imported into `main.dart`\n2. **Marked with `@pragma(\"vm:entry-point\")`** to prevent tree-shaking\n\n```dart\n// In main.dart\n@pragma(\"vm:entry-point\")\nvoid myOverlayMain() {\n  runApp(MyOverlayWidget().floatwing(app: true));\n}\n```\n\nIf defined in another file, import it in `main.dart`:\n\n```dart\n// main.dart\nimport 'package:myapp/overlay_entry.dart';  // Contains myOverlayMain\n\nvoid main() {\n  runApp(MyApp());\n}\n\n// Re-export to ensure it's included in the build\nexport 'package:myapp/overlay_entry.dart';\n```\n\n### Buttons Get Stuck Pressed When Dragging\n\nIf buttons inside a draggable overlay widget get stuck in pressed state when pressing and dragging simultaneously:\n\n**Workaround**: Disable dragging while the button is pressed:\n\n```dart\nElevatedButton(\n  style: ButtonStyle(\n    foregroundColor: WidgetStateProperty.resolveWith((states) {\n      if (states.contains(WidgetState.pressed)) {\n        Window.of(context)?.update(WindowConfig(draggable: false));\n        return Colors.blue;\n      } else {\n        Window.of(context)?.update(WindowConfig(draggable: true));\n        return Colors.white;\n      }\n    }),\n  ),\n  onPressed: () { /* ... */ },\n  child: Text(\"Button\"),\n)\n```\n\n### MissingPluginException\n\nIf you see `MissingPluginException(No implementation found for method window.start...)`:\n\n1. **Clean rebuild**: `flutter clean && flutter pub get && flutter run`\n2. **Check permissions**: Ensure `SYSTEM_ALERT_WINDOW` permission is granted\n3. **Update to latest version**: This was fixed in recent updates\n\n## 🤝 Contributing\n\nContributions are always welcome!\n\n- Report bugs or request features via [Issues](https://github.com/jiusanzhou/flutter_floatwing/issues)\n- Submit pull requests\n- Improve documentation\n\n## 📄 License\n\n```\nApache License 2.0\nCopyright (c) 2022 Zoe\n```\n\n<div align=\"center\">\n\n**[⬆ Back to Top](#flutter_floatwing)**\n\n</div>\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\nanalyzer:\n  errors:\n    unused_element: ignore\n    unused_element_parameter: ignore\n"
  },
  {
    "path": "android/.gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n"
  },
  {
    "path": "android/build.gradle",
    "content": "group 'im.zoe.labs.flutter_floatwing'\nversion '1.0-SNAPSHOT'\n\nbuildscript {\n    ext.kotlin_version = '1.9.22'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.5.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nrootProject.allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\napply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\nandroid {\n    namespace 'im.zoe.labs.flutter_floatwing'\n    compileSdk 34\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n        test.java.srcDirs += 'src/test/kotlin'\n    }\n\n    defaultConfig {\n        minSdk 21\n    }\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n}\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.0-bin.zip\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "package android\n\nrootProject.name = 'flutter_floatwing'\n"
  },
  {
    "path": "android/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" />\n</manifest>\n"
  },
  {
    "path": "android/src/main/kotlin/im/zoe/labs/flutter_floatwing/FloatWindow.kt",
    "content": "package im.zoe.labs.flutter_floatwing\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Color\nimport android.graphics.PixelFormat\nimport android.os.Build\nimport android.util.Log\nimport android.view.Gravity\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.WindowManager\nimport android.view.WindowManager.LayoutParams\nimport android.view.WindowManager.LayoutParams.*\nimport io.flutter.embedding.android.FlutterTextureView\nimport io.flutter.embedding.android.FlutterView\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.embedding.engine.FlutterEngineCache\nimport io.flutter.plugin.common.BasicMessageChannel\nimport io.flutter.plugin.common.JSONMessageCodec\nimport io.flutter.plugin.common.MethodCall\nimport io.flutter.plugin.common.MethodChannel\n\n@SuppressLint(\"ClickableViewAccessibility\")\nclass FloatWindow(\n    context: Context,\n    wmr: WindowManager,\n    engKey: String,\n    eng: FlutterEngine,\n    cfg: Config): View.OnTouchListener, MethodChannel.MethodCallHandler,\n    BasicMessageChannel.MessageHandler<Any?> {\n\n    var parent: FloatWindow? = null\n\n    var config = cfg\n\n    var key: String = \"default\"\n\n    var engineKey = engKey\n    var engine = eng\n\n    var wm = wmr\n\n    var subscribedEvents: HashMap<String, Boolean> = HashMap()\n\n    var view: FlutterView = FlutterView(context, FlutterTextureView(context))\n\n    lateinit var layoutParams: LayoutParams\n\n    lateinit var service: FloatwingService\n\n    // method and message channel for window engine call\n    var _channel: MethodChannel = MethodChannel(eng.dartExecutor.binaryMessenger,\n        \"${FloatwingService.METHOD_CHANNEL}/window\").also {\n        it.setMethodCallHandler(this) }\n    var _message: BasicMessageChannel<Any?> = BasicMessageChannel(eng.dartExecutor.binaryMessenger,\n        \"${FloatwingService.MESSAGE_CHANNEL}/window_msg\", JSONMessageCodec.INSTANCE)\n        .also { it.setMessageHandler(this) }\n\n    var _started = false\n\n    fun init(): FloatWindow {\n        layoutParams = config.to()\n\n        config.focusable?.let{\n            view.isFocusable = it\n            view.isFocusableInTouchMode = it\n        }\n\n        view.setBackgroundColor(Color.TRANSPARENT)\n        view.fitsSystemWindows = true\n\n        config.visible?.let{ setVisible(it) }\n\n        view.setOnTouchListener(this)\n\n        // view.attachToFlutterEngine(engine)\n        return this\n    }\n\n    fun destroy(force: Boolean = true): Boolean {\n        Log.i(TAG, \"[window] destroy window: $key force: $force\")\n\n        // remote from manager must be first\n        if (_started) wm.removeView(view)\n\n        view.detachFromFlutterEngine()\n\n\n        // TODO: should we stop the engine for flutter?\n        if (force) {\n            // stop engine and remove from cache\n            FlutterEngineCache.getInstance().remove(engineKey)\n            engine.destroy()\n            service.windows.remove(key)\n            emit(\"destroy\", null)\n        } else {\n            _started = false\n            engine.lifecycleChannel.appIsPaused()\n            emit(\"paused\", null)\n        }\n        return true\n    }\n\n    fun setVisible(visible: Boolean = true): Boolean {\n        Log.d(TAG, \"[window] set window $key => $visible\")\n        emit(\"visible\", visible)\n        view.visibility = if (visible) View.VISIBLE else View.GONE\n        return visible\n    }\n\n    fun update(cfg: Config): Map<String, Any?> {\n        Log.d(TAG, \"[window] update window $key => $cfg\")\n        config = config.update(cfg).also {\n            layoutParams = it.to()\n            if (_started) wm.updateViewLayout(view, layoutParams)\n        }\n        return toMap()\n    }\n\n    fun start(): Boolean {\n        if (_started) {\n            Log.d(TAG, \"[window] window $key already started\")\n            return true\n        }\n\n        _started = true\n        Log.d(TAG, \"[window] start window: $key\")\n\n        engine.lifecycleChannel.appIsResumed()\n\n        // if engine is paused, send re-render message\n        // make sure reuse engine can be re-render\n        emit(\"resumed\")\n\n        view.attachToFlutterEngine(engine)\n\n        wm.addView(view, layoutParams)\n\n        emit(\"started\")\n\n        return true\n    }\n\n    fun shareData(data: Map<*, *>, source: String? = null, result: MethodChannel.Result? = null) {\n        shareData(_channel, data, source, result)\n    }\n\n    fun simpleEmit(msgChannel: BasicMessageChannel<Any?>, name: String, data: Any?=null) {\n        val map = HashMap<String, Any?>()\n        map[\"name\"] = name\n        map[\"id\"] = key // this is special for main engine\n        map[\"data\"] = data\n        msgChannel.send(map)\n    }\n\n    fun emit(name: String, data: Any? = null, prefix: String?=\"window\", pluginNeed: Boolean = true) {\n        val evtName = \"$prefix.$name\"\n        // Log.i(TAG, \"[window] emit event: Window[$key] $name \")\n\n        // check if need to send to my self\n        if (true||subscribedEvents.containsKey(name)||subscribedEvents.containsKey(\"*\")) {\n            // emit to window engine\n            simpleEmit(_message, evtName, data)\n        }\n\n        // plugin\n        // check if we need to fire to plugin\n        if (pluginNeed&&(true||service.subscribedEvents.containsKey(\"*\")||service.subscribedEvents.containsKey(evtName))) {\n            simpleEmit(service._message, evtName, data)\n        }\n\n        // emit parent engine\n        // if fire to parent need have no need to fire to service again\n        if(parent!=null&&parent!=this) {\n            parent!!.simpleEmit(parent!!._message, evtName, data)\n        }\n\n        // _channel.invokeMethod(\"window.$name\", data)\n        // we need to send to man engine\n        // service._channel.invokeMethod(\"window.$name\", key)\n    }\n\n    fun toMap(): Map<String, Any?> {\n        // must not null if success created\n        val map = HashMap<String, Any?>()\n        map[\"id\"] = key\n        map[\"pixelRadio\"] = service.pixelRadio\n        map[\"system\"] = service.systemConfig\n        map[\"config\"] = config.toMap().filter { it.value != null }\n        return map\n    }\n\n    override fun toString(): String {\n        return \"${toMap()}\"\n    }\n\n    // return window from svc.windows by id\n    fun take(id: String): FloatWindow? {\n        return service.windows[id]\n    }\n\n    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {\n        return when (call.method) {\n            // just take current engine's window\n            \"window.sync\" -> {\n                // when flutter is ready should call this to sync the window object.\n                Log.i(TAG, \"[window] window.sync from flutter side: $key\")\n                result.success(toMap())\n            }\n\n            // we need to support call window.* in window engine\n            // but the window engine register as window channel\n            // so we should take the id first and then get window from windows cache\n            // TODO: those code should move to service\n\n            \"window.create_child\" -> {\n                val id = call.argument<String>(\"id\") ?: \"default\"\n                val cfg = call.argument<Map<String, *>>(\"config\")!!\n                val start = call.argument<Boolean>(\"start\") ?: false\n                val config = FloatWindow.Config.from(cfg)\n                Log.d(TAG, \"[service] window.create_child request_id: $id\")\n                return result.success(FloatwingService.createWindow(service.applicationContext, id,\n                        config, start, this))\n            }\n            \"window.close\" -> {\n                val id = call.argument<String?>(\"id\")?:\"<unset>\"\n                Log.d(TAG, \"[window] window.close request_id: $id, my_id: $key\")\n                val force = call.argument(\"force\") ?: false\n                return result.success(take(id)?.destroy(force))\n            }\n            \"window.destroy\" -> {\n                val id = call.argument<String?>(\"id\")?:\"<unset>\"\n                Log.d(TAG, \"[window] window.destroy request_id: $id, my_id: $key\")\n                return result.success(take(id)?.destroy(true))\n            }\n            \"window.start\" -> {\n                val id = call.argument<String?>(\"id\")?:\"<unset>\"\n                Log.d(TAG, \"[window] window.start request_id: $id, my_id: $key\")\n                return result.success(take(id)?.start())\n            }\n            \"window.update\" -> {\n                val id = call.argument<String?>(\"id\")?:\"<unset>\"\n                Log.d(TAG, \"[window] window.update request_id: $id, my_id: $key\")\n                val config = Config.from(call.argument<Map<String, *>>(\"config\")!!)\n                return result.success(take(id)?.update(config))\n            }\n            \"window.show\" -> {\n                val id = call.argument<String?>(\"id\")?:\"<unset>\"\n                Log.d(TAG, \"[window] window.show request_id: $id, my_id: $key\")\n                val visible = call.argument<Boolean>(\"visible\") ?: true\n                return result.success(take(id)?.setVisible(visible))\n            }\n            \"window.launch_main\" -> {\n                Log.d(TAG, \"[window] window.launch_main\")\n                return result.success(service.launchMainActivity())\n            }\n            \"window.lifecycle\" -> {\n\n            }\n            \"event.subscribe\" -> {\n                val id = call.argument<String?>(\"id\")?:\"<unset>\"\n\n            }\n            \"data.share\" -> {\n                // communicate with other window, only 1 - 1 with id\n                val args = call.arguments as Map<*, *>\n                val targetId = call.argument<String?>(\"target\")\n                Log.d(TAG, \"[window] share data from $key with $targetId: $args\")\n                if (targetId == null) {\n                    Log.d(TAG, \"[window] share data with plugin\")\n                    return result.success(shareData(service._channel, args, source=key, result=result))\n                }\n                if (targetId == key) {\n                    Log.d(TAG, \"[window] can't share data with self\")\n                    return result.error(\"no allow\", \"share data from $key to $targetId\", \"\")\n                }\n                val target = service.windows[targetId]\n                    ?: return result.error(\"not found\", \"target window $targetId not exits\", \"\");\n                return target.shareData(args, source=key, result=result)\n            }\n            else -> {\n                result.notImplemented()\n            }\n        }\n    }\n\n    override fun onMessage(msg: Any?, reply: BasicMessageChannel.Reply<Any?>) {\n        // stream message\n    }\n\n    companion object {\n        private const val TAG = \"FloatWindow\"\n\n        fun shareData(channel: MethodChannel, data: Map<*, *>, source: String? = null,\n                      result: MethodChannel.Result? = null): Any? {\n            // id is the data comes from\n            // invoke the method channel\n            val map = HashMap<String, Any?>()\n            map[\"source\"] = source\n            data.forEach { map[it.key as String] = it.value }\n            channel.invokeMethod(\"data.share\", map, result)\n            // how to get data back\n            return null\n        }\n    }\n\n    // window is dragging\n    private var dragging = false\n\n    // start point\n    private var lastX = 0f\n    private var lastY = 0f\n\n    // border around\n    // TODO: support generate around edge\n\n    override fun onTouch(view: View?, event: MotionEvent?): Boolean {\n        // default draggable should be false\n        if (config.draggable != true) return false\n        when (event?.action) {\n            MotionEvent.ACTION_DOWN -> {\n                // touch start\n                dragging = false\n                lastX = event.rawX\n                lastY = event.rawY\n                // TODO: support generate around edge\n            }\n            MotionEvent.ACTION_MOVE -> {\n                // touch move\n                val dx = event.rawX - lastX\n                val dy = event.rawY - lastY\n\n                // ignore too small fist start moving(some time is click)\n                if (!dragging && dx*dx+dy*dy < 25) {\n                    return false\n                }\n\n                // update the last point\n                lastX = event.rawX\n                lastY = event.rawY\n\n                val xx = layoutParams.x + dx.toInt()\n                val yy = layoutParams.y + dy.toInt()\n\n                if (!dragging) {\n                    // first time dragging\n                    emit(\"drag_start\", listOf(xx, yy))\n                }\n\n                dragging = true\n                // update x, y, need to update config so use config to update\n                update(Config().apply {\n                    // calculate with the border\n                    x = xx\n                    y = yy\n                })\n\n                emit(\"dragging\", listOf(xx, yy))\n            }\n            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {\n                // touch end\n                if (dragging) emit(\"drag_end\", listOf(event.rawX, event.rawY))\n                return dragging\n            }\n            else -> {\n                return false\n            }\n        }\n        return false\n    }\n\n    class Config {\n        // this three fields can not be changed\n        // var id: String = \"default\"\n        var entry: String? = null\n        var route: String? = null\n        var callback: Long? = null\n\n        var autosize: Boolean? = null\n\n        var width: Int? = null\n        var height: Int? = null\n        var x: Int? = null\n        var y: Int? = null\n\n        var format: Int? = null\n        var gravity: Int? = null\n        var type: Int? = null\n\n        var clickable: Boolean? = null\n        var draggable: Boolean? = null\n        var focusable: Boolean? = null\n\n        var immersion: Boolean? = null\n\n        var visible: Boolean? = null\n\n\n        // inline fun <reified T: Any?>to(): T {\n        fun to(): LayoutParams {\n            val cfg = this\n            return LayoutParams().apply {\n                // set size\n                width = cfg.width ?: 1 // we must have 1 pixel, let flutter can generate the pixel radio\n                height = cfg.height ?: 1 // we must have 1 pixel, let flutter can generate the pixel radio\n\n                // set position fixed if with (x, y)\n                cfg.x?.let { x = it } // default not set\n                cfg.y?.let { y = it } // default not set\n\n                // format\n                format = cfg.format ?: PixelFormat.TRANSPARENT\n\n                // default start from center\n                gravity = cfg.gravity ?: Gravity.TOP or Gravity.LEFT\n\n                // default flags\n                flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL\n                // if immersion add flag no limit\n                cfg.immersion?.let{ if (it) flags = flags or FLAG_LAYOUT_NO_LIMITS }\n                // default we should be clickable\n                // if not clickable, add flag not touchable\n                cfg.clickable?.let{ if (!it) flags = flags or FLAG_NOT_TOUCHABLE }\n                // default we should be no focusable\n                if (cfg.focusable == null) { cfg.focusable = false }\n                // if not focusable, add no focusable flag\n                cfg.focusable?.let { if (!it) flags = flags or FLAG_NOT_FOCUSABLE }\n\n                // default type is overlay\n                type = cfg.type ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) TYPE_APPLICATION_OVERLAY else TYPE_PHONE\n            }\n        }\n\n        fun toMap(): Map<String, Any?> {\n            val map = HashMap<String, Any?>()\n            map[\"entry\"] = entry\n            map[\"route\"] = route\n            map[\"callback\"] = callback\n\n            map[\"autosize\"] = autosize\n\n            map[\"width\"] = width\n            map[\"height\"] = height\n            map[\"x\"] = x\n            map[\"y\"] = y\n\n            map[\"format\"] = format\n            map[\"gravity\"] = gravity\n            map[\"type\"] = type\n\n            map[\"clickable\"] = clickable\n            map[\"draggable\"] = draggable\n            map[\"focusable\"] = focusable\n\n            map[\"immersion\"] = immersion\n\n            map[\"visible\"] = visible\n\n            return map\n        }\n\n        fun update(cfg: Config): Config {\n            // entry, route, callback shouldn't be updated\n\n            cfg.autosize?.let { autosize = it }\n\n            cfg.width?.let { width = it }\n            cfg.height?.let { height = it }\n            cfg.x?.let { x = it }\n            cfg.y?.let { y = it }\n\n            cfg.format?.let { format = it }\n            cfg.gravity?.let { gravity = it }\n            cfg.type?.let { type = it }\n\n            cfg.clickable?.let{ clickable = it }\n            cfg.draggable?.let { draggable = it }\n            cfg.focusable?.let { focusable = it }\n\n            cfg.immersion?.let { immersion = it }\n\n            cfg.visible?.let { visible = it }\n\n            return this\n        }\n\n        override fun toString(): String {\n            val map = toMap()?.filter { it.value != null }\n            return \"$map\"\n        }\n\n        companion object {\n\n            fun from(data: Map<String, *>): Config {\n                val cfg = Config()\n\n                // (data[\"id\"]?.let { it as String } ?: \"default\").also { cfg.id = it }\n                cfg.entry = data[\"entry\"] as String?\n                cfg.route = data[\"route\"] as String?\n\n                val int_callback = data[\"callback\"] as Number?\n                cfg.callback = int_callback?.toLong()\n\n                cfg.autosize = data[\"autosize\"] as Boolean?\n\n                cfg.width = data[\"width\"] as Int?\n                cfg.height = data[\"height\"] as Int?\n                cfg.x = data[\"x\"] as Int?\n                cfg.y = data[\"y\"] as Int?\n\n                cfg.gravity = data[\"gravity\"] as Int?\n                cfg.format = data[\"format\"] as Int?\n                cfg.type = data[\"type\"] as Int?\n\n                cfg.clickable = data[\"clickable\"] as Boolean?\n                cfg.draggable = data[\"draggable\"] as Boolean?\n                cfg.focusable = data[\"focusable\"] as Boolean?\n\n                cfg.immersion = data[\"immersion\"] as Boolean?\n\n                cfg.visible = data[\"visible\"] as Boolean?\n\n                return cfg\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "android/src/main/kotlin/im/zoe/labs/flutter_floatwing/FloatwingService.kt",
    "content": "package im.zoe.labs.flutter_floatwing\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.Service\nimport android.content.Context\nimport android.content.Intent\nimport android.content.Intent.ACTION_SHUTDOWN\nimport android.os.Build\nimport android.os.IBinder\nimport android.os.PowerManager\nimport android.util.Log\nimport android.view.WindowManager\nimport androidx.core.app.NotificationCompat\nimport im.zoe.labs.flutter_floatwing.Utils.Companion.toMap\nimport io.flutter.FlutterInjector\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.embedding.engine.FlutterEngineCache\nimport io.flutter.embedding.engine.FlutterEngineGroup\nimport io.flutter.embedding.engine.dart.DartExecutor\nimport io.flutter.plugin.common.BasicMessageChannel\nimport io.flutter.plugin.common.JSONMessageCodec\nimport io.flutter.plugin.common.MethodCall\nimport io.flutter.plugin.common.MethodChannel\nimport io.flutter.view.FlutterCallbackInformation\nimport org.json.JSONObject\nimport java.lang.Exception\n\nclass FloatwingService : MethodChannel.MethodCallHandler, BasicMessageChannel.MessageHandler<Any?>, Service() {\n\n    private lateinit var mContext: Context\n    private lateinit var windowManager: WindowManager\n\n    private lateinit var engGroup: FlutterEngineGroup\n\n    lateinit var _channel: MethodChannel\n    lateinit var _message: BasicMessageChannel<Any?>\n\n    var subscribedEvents: HashMap<String, Boolean> = HashMap()\n\n    var pixelRadio = 2.0\n    var systemConfig = emptyMap<String, Any?>()\n\n    // store the window object use the id as key\n    val windows = HashMap<String, FloatWindow>()\n\n    override fun onCreate() {\n        super.onCreate()\n\n        // set the instance\n        instance = this\n\n        mContext = applicationContext\n\n        engGroup = FlutterEngineGroup(mContext)\n\n        Log.i(TAG, \"[service] the background service onCreate\")\n\n        // get the window manager and store\n        (getSystemService(WINDOW_SERVICE) as WindowManager).also { windowManager = it }\n\n        // load pixel from store\n        pixelRadio = mContext.getSharedPreferences(FlutterFloatwingPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)\n            .getFloat(FlutterFloatwingPlugin.PIXEL_RADIO_KEY, 2F).toDouble()\n        Log.d(TAG, \"[service] load the pixel radio: $pixelRadio\")\n\n        // load system config from store\n        try {\n            val str = mContext.getSharedPreferences(FlutterFloatwingPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)\n                .getString(FlutterFloatwingPlugin.SYSTEM_CONFIG_KEY, \"{}\")\n            val map = JSONObject(str)\n            systemConfig = map.toMap()\n        }catch (e: Exception) {\n            e.printStackTrace()\n        }\n\n        // install this method channel for the main engine\n        FlutterEngineCache.getInstance().get(FlutterFloatwingPlugin.FLUTTER_ENGINE_CACHE_KEY)\n            ?.also {\n                Log.d(TAG, \"[service] install the service handler for main engine\")\n                installChannel(it)\n            }\n    }\n\n    override fun onBind(p0: Intent?): IBinder? {\n        return null\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        when (intent?.action) {\n            ACTION_SHUTDOWN -> {\n                (getSystemService(Context.POWER_SERVICE) as PowerManager).run {\n                    newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply {\n                        if (isHeld) release()\n                    }\n                }\n                Log.d(TAG, \"[service] stop the background service!\")\n                stopSelf()\n            }\n            else -> {\n\n            }\n        }\n        return START_STICKY\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n\n        // clean up: remove all views in the window manager\n        windows.forEach {\n            it.value.destroy()\n            Log.d(TAG, \"[service] service destroy: remove the float window ${it.key}\")\n        }\n\n    }\n\n    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {\n        \"service.stop_service\" -> {\n            Log.d(TAG, \"[service] stop the service\")\n            result.success(stopService(Intent(baseContext, this.javaClass)))\n        }\n        \"service.promote\" -> {\n            Log.d(TAG, \"[service] promote service\")\n            result.success(promoteService(call.arguments as Map<*, *>?))\n        }\n        \"service.demote\" -> {\n            Log.d(TAG, \"[service] demote service\")\n            result.success(demoteService())\n        }\n        \"service.create_window\" -> {\n            val id = call.argument<String>(\"id\") ?: \"default\"\n            val cfg = call.argument<Map<String, *>>(\"config\")!!\n            val start = call.argument<Boolean>(\"start\") ?: false\n            Log.d(TAG, \"[service] window.create request_id: $id\")\n            result.success(createWindow(mContext, id, FloatWindow.Config.from(cfg), start, null))\n        }\n        \"window.close\" -> {\n            val id = call.argument<String>(\"id\")!!\n            Log.d(TAG, \"[service] window.close request_id: $id\")\n            val force = call.argument(\"force\") ?: false\n            result.success(windows[id]?.destroy(force))\n        }\n        \"window.start\" -> {\n            val id = call.argument<String>(\"id\") ?: \"default\"\n            Log.d(TAG, \"[service] window.start request_id: $id ${windows[id]}\")\n            result.success(windows[id]?.start())\n        }\n        \"window.show\" -> {\n            val id = call.argument<String>(\"id\")!!\n            val visible = call.argument(\"visible\") ?: true\n            Log.d(TAG, \"[service] window.show request_id: $id\")\n            result.success(windows[id]?.setVisible(visible))\n        }\n        \"window.update\" -> {\n            val id = call.argument<String>(\"id\")!!\n            Log.d(TAG, \"[service] window.update request_id: $id\")\n            val config = FloatWindow.Config.from(call.argument<Map<String, *>>(\"config\")!!)\n            result.success(windows[id]?.update(config))\n        }\n        \"window.sync\" -> {\n            Log.d(TAG, \"[service] fake window.sync\")\n            result.success(null)\n        }\n        \"data.share\" -> {\n            val args = call.arguments as Map<*, *>\n            val targetId = call.argument<String?>(\"target\")\n            Log.d(TAG, \"[service] share data from <plugin> with $targetId: $args\")\n            if (targetId == null) {\n                Log.d(TAG, \"[service] can't share data with self\")\n                result.error(\"no allow\", \"share data from plugin to plugin\", \"\")\n            } else {\n                val target = windows[targetId]\n                if (target != null) {\n                    target.shareData(args, result = result)\n                } else {\n                    result.error(\"not found\", \"target window $targetId not exits\", \"\")\n                }\n            }\n        }\n        else -> {\n            Log.d(TAG, \"[service] unknown method ${call.method}\")\n            result.notImplemented()\n        }\n    }\n\n    override fun onMessage(message: Any?, reply: BasicMessageChannel.Reply<Any?>) {\n        // update the windows from message\n    }\n\n    private fun promoteService(map: Map<*, *>?): Boolean {\n        Log.i(TAG, \"[service] promote service to foreground\")\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {\n            Log.e(TAG, \"[service] promoteToForeground need sdk >= 26\")\n            return false\n        }\n\n        if (map == null) {\n            Log.e(TAG, \"[service] promote service config is null\")\n            return false\n        }\n\n        /*\n        (getSystemService(Context.POWER_SERVICE) as PowerManager).run {\n            newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply {\n                setReferenceCounted(false)\n                acquire()\n            }\n        }\n         */\n\n        val title = map[\"title\"] as String? ?: \"Floatwing Service\"\n        val description = map[\"description\"] as String? ?: \"Floatwing service is running\"\n        val showWhen = map[\"showWhen\"] as Boolean? ?: false\n        val ticker = map[\"ticker\"] as String?\n        val subText = map[\"subText\"] as String?\n\n\n        val channel = NotificationChannel(\"flutter_floatwing\", \"Floatwing Service\", NotificationManager.IMPORTANCE_HIGH)\n        val imageId = resources.getIdentifier(\"ic_launcher\", \"mipmap\", packageName)\n        (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel)\n\n        val notification = NotificationCompat.Builder(this, \"flutter_floatwing\")\n            .setContentTitle(title)\n            .setContentText(description)\n            .setShowWhen(showWhen)\n            .setTicker(ticker)\n            .setSubText(subText)\n            .setSmallIcon(imageId)\n            .setPriority(NotificationCompat.PRIORITY_HIGH)\n            .setCategory(NotificationCompat.CATEGORY_SERVICE)\n            .build()\n\n        startForeground(1, notification)\n        return true\n    }\n\n    private fun demoteService(): Boolean {\n        Log.i(TAG, \"[service] demote service to background\")\n        stopForeground(true)\n        return true\n    }\n\n    fun launchMainActivity(): Boolean {\n        if (mActivityClass == null) {\n            Log.e(TAG, \"[service] the main activity is null, maybe the service start from background\")\n            return false\n        }\n        Log.d(TAG, \"[service] launch the main activity\")\n        val intent = Intent(this, mActivityClass)\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        startActivity(intent)\n        return true\n    }\n\n    private fun createWindow(id: String, config: FloatWindow.Config, start: Boolean = false,\n        p: FloatWindow?): Map<String, Any?>? {\n        // check if id exits\n        if (windows.contains(id)) {\n            Log.e(TAG, \"[service] window with id $id exits\")\n            return null\n        }\n\n        // get flutter engine\n        val fKey = id.flutterKey()\n        val (eng, fromCache) = getFlutterEngine(fKey, config.entry, config.route, config.callback)\n\n        val svc = this\n        return FloatWindow(mContext, windowManager, fKey, eng, config).apply {\n            key = id\n            service = svc\n            parent = p\n            Log.d(TAG, \"[service] set window as handler $METHOD_CHANNEL/window for $eng\")\n        }.init().also {\n            Log.d(TAG, \"[service] created window: $id $config\")\n            it.emit(\"created\", !fromCache)\n            windows[it.key] = it\n            if (start) it.start()\n        }.toMap()\n    }\n\n    // this function is useful when we want to start service automatically\n    private  fun getFlutterEngine(key: String, entryName: String?, route: String?, callback: Long?): Pair<FlutterEngine, Boolean> {\n        // first take from cache\n        var eng = FlutterEngineCache.getInstance().get(key)\n        if (eng != null) {\n            Log.i(TAG, \"[service] use the flutter exits in cache, id: $key\")\n            return Pair(eng, true)\n        }\n\n        Log.d(TAG, \"[service] miss from cache need to create a new flutter engine\")\n\n        // then create a flutter engine\n\n        // ensure initialization\n        // FlutterInjector.instance().flutterLoader().startInitialization(mContext)\n        // FlutterInjector.instance().flutterLoader().ensureInitializationComplete(mContext, arrayOf())\n\n        // first let's use callback to start engine first\n        if (callback!=null&&callback>0L) {\n            Log.i(TAG, \"[service] start flutter engine, id: $key callback: $callback\")\n\n            eng = FlutterEngine(mContext)\n            val info = FlutterCallbackInformation.lookupCallbackInformation(callback)\n            val args = DartExecutor.DartCallback(mContext.assets, FlutterInjector.instance().flutterLoader().findAppBundlePath(), info)\n            // execute the callback function\n            eng.dartExecutor.executeDartCallback(args)\n\n            // store the engine to cache\n            FlutterEngineCache.getInstance().put(key, eng)\n\n            return Pair(eng, false)\n        }\n\n        var entry = entryName\n        if (entry==null) {\n            // try use the main entrypoint\n            entry = \"main\"\n            Log.w(TAG, \"[service] recommend to use a entrypoint\")\n        }\n\n        // check the main and default route\n        if (entry == \"main\" && route == null) {\n            Log.w(TAG, \"[service] use the main entrypoint and default route\")\n        }\n\n        Log.i(TAG, \"[service] start flutter engine, id: $key entrypoint: $entry, route: $route\")\n\n        // make sure the entrypoint exits\n        val entrypoint = DartExecutor.DartEntrypoint(\n            FlutterInjector.instance().flutterLoader().findAppBundlePath(), entry)\n\n        // start the dart executor with special entrypoint\n        eng = engGroup.createAndRunEngine(mContext, entrypoint, route)\n\n        // store the engine to cache\n        FlutterEngineCache.getInstance().put(key, eng)\n\n        return Pair(eng, false)\n    }\n\n    // window engine won't call this, so just window method\n    private fun installChannel(eng: FlutterEngine): Boolean {\n        Log.d(TAG, \"[service] set service as handler $METHOD_CHANNEL/window for $eng\")\n        // set the method and message channel\n        // this must be same as window, because we use the same method to call invoke\n        _channel = MethodChannel(eng.dartExecutor.binaryMessenger,\n            \"$METHOD_CHANNEL/window\").also { it.setMethodCallHandler(this) }\n        _message = BasicMessageChannel(eng.dartExecutor.binaryMessenger,\n            \"$METHOD_CHANNEL/window_msg\", JSONMessageCodec.INSTANCE).also { it.setMessageHandler(this) }\n        return true\n    }\n\n    private fun String.flutterKey(): String {\n        return FLUTTER_ENGINE_KEY + this\n    }\n\n    companion object {\n        @JvmStatic\n        private val TAG = \"FloatwingService\"\n\n        // TODO: improve\n        @SuppressLint(\"StaticFieldLeak\")\n        var mActivity: Activity? = null\n        var mActivityClass: Class<Activity>? = null\n\n        @SuppressLint(\"StaticFieldLeak\")\n        var instance: FloatwingService? = null\n\n        const val WAKELOCK_TAG = \"FloatwingService::WAKE_LOCK\"\n        const val FLUTTER_ENGINE_KEY = \"floatwing_flutter_engine_\"\n        const val METHOD_CHANNEL = \"im.zoe.labs/flutter_floatwing\"\n        const val MESSAGE_CHANNEL = \"im.zoe.labs/flutter_floatwing\"\n\n        fun initialize(): Boolean {\n            Log.i(TAG, \"[service] initialize\")\n            return true\n        }\n\n        fun createWindow(context: Context, id: String, config: FloatWindow.Config,\n                         start: Boolean = false, parent: FloatWindow?): Map<String, Any?>? {\n            Log.i(TAG, \"[service] create a window: $id $config\")\n            // make sure the service started\n            if (!ensureService(context)) return null\n\n            // If instance not ready yet, wait a bit with handler\n            if (instance == null) {\n                Log.i(TAG, \"[service] instance null after ensureService, waiting...\")\n                // Can't block main thread, return null and let caller retry\n                return null\n            }\n\n            // start the window\n            return instance?.createWindow(id, config, start, parent)\n        }\n        \n        fun createWindowAsync(context: Context, id: String, config: FloatWindow.Config,\n                              start: Boolean = false, parent: FloatWindow?,\n                              callback: (Map<String, Any?>?) -> Unit) {\n            Log.i(TAG, \"[service] create a window async: $id\")\n            \n            fun doCreate() {\n                val result = instance?.createWindow(id, config, start, parent)\n                callback(result)\n            }\n            \n            if (instance != null) {\n                doCreate()\n                return\n            }\n            \n            ensureServiceAsync(context) { success ->\n                if (success && instance != null) {\n                    doCreate()\n                } else {\n                    Log.e(TAG, \"[service] failed to ensure service for window creation\")\n                    callback(null)\n                }\n            }\n        }\n\n        // ensure the service is started\n        private fun ensureService(context: Context): Boolean {\n            if (instance != null) return true\n\n            // let's start the service\n\n            // make sure we granted permission\n            if (!FlutterFloatwingPlugin.permissionGiven(context)) {\n                Log.e(TAG, \"[service] don't have permission to create overlay window\")\n                return false\n            }\n\n            // start the service\n            val intent = Intent(context, FloatwingService::class.java)\n            context.startService(intent)\n            \n            // Service.onCreate() runs on main thread, so we can't block here\n            // Return true optimistically - the service will be ready when needed\n            // Since we're on main thread, service onCreate will run after this returns\n            Log.i(TAG, \"[service] service start requested, returning optimistically\")\n            return true\n        }\n        \n        // Async version for callbacks\n        fun ensureServiceAsync(context: Context, callback: (Boolean) -> Unit) {\n            if (instance != null) {\n                callback(true)\n                return\n            }\n            \n            if (!FlutterFloatwingPlugin.permissionGiven(context)) {\n                Log.e(TAG, \"[service] don't have permission to create overlay window\")\n                callback(false)\n                return\n            }\n            \n            val intent = Intent(context, FloatwingService::class.java)\n            context.startService(intent)\n            \n            // Use Handler to check after service has chance to start\n            android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({\n                callback(instance != null)\n            }, 100)\n        }\n\n        fun onActivityAttached(activity: Activity) {\n            Log.i(TAG, \"[service] activity attached\")\n            // maybe instance is null, so set failed\n            if (mActivity != null) {\n                Log.w(TAG, \"[service] main activity already set\")\n                return\n            }\n            mActivity = activity\n            // store the class\n            mActivityClass = mActivity?.javaClass\n        }\n\n        fun installChannel(eng: FlutterEngine): Boolean {\n            Log.i(TAG, \"[service] install the service channel for engine\")\n            return instance?.installChannel(eng) ?: false\n        }\n\n        fun isRunning(context: Context): Boolean {\n            return Utils.getRunningService(context, FloatwingService::class.java) != null\n        }\n\n        fun start(context: Context): Boolean {\n            return ensureService(context)\n        }\n    }\n}"
  },
  {
    "path": "android/src/main/kotlin/im/zoe/labs/flutter_floatwing/FlutterFloatwingPlugin.kt",
    "content": "package im.zoe.labs.flutter_floatwing\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Build\nimport android.provider.Settings\nimport android.util.Log\nimport androidx.annotation.NonNull;\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.embedding.engine.FlutterEngineCache\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.embedding.engine.plugins.activity.ActivityAware\nimport io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding\nimport io.flutter.plugin.common.MethodCall\nimport io.flutter.plugin.common.MethodChannel\nimport io.flutter.plugin.common.MethodChannel.MethodCallHandler\nimport io.flutter.plugin.common.MethodChannel.Result\nimport io.flutter.plugin.common.PluginRegistry\nimport org.json.JSONObject\nimport java.lang.Exception\n\n/** FlutterFloatwingPlugin */\nclass FlutterFloatwingPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginRegistry.ActivityResultListener {\n\n  private lateinit var mContext: Context\n  private lateinit var mActivity: Activity\n  private lateinit var channel : MethodChannel\n  private lateinit var engine: FlutterEngine\n  private lateinit var waitPermissionResult: Result\n\n  private var serviceChannelInstalled = false\n  private var isMain = false\n\n  override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {\n    mContext = binding.applicationContext\n\n    // should window's engine install method channel?\n    channel = MethodChannel(binding.binaryMessenger, CHANNEL_NAME)\n    channel.setMethodCallHandler(this)\n\n    // how to known i'm a window engine not the main one?\n    // if contains engine already, means we are coming from window\n    // TODO: take first engine as main, but if service auto start the window\n    // this will cause error\n    if (FlutterEngineCache.getInstance().contains(FLUTTER_ENGINE_CACHE_KEY)) {\n      Log.d(TAG, \"[plugin] on attached to window engine\")\n    } else {\n      // update the main flag\n      isMain = true\n      // store the flutter engine @only main\n      engine = binding.flutterEngine\n      FlutterEngineCache.getInstance().put(FLUTTER_ENGINE_CACHE_KEY, engine)\n      // should install service handler for every engine? @only main\n      // window has already set in this own logic\n      serviceChannelInstalled = FloatwingService.installChannel(engine)\n        .also { r -> if (!r) {\n          MethodChannel(engine.dartExecutor.binaryMessenger,\n            \"${FloatwingService.METHOD_CHANNEL}/window\").also { it.setMethodCallHandler(this) }\n        } }\n\n      Log.d(TAG, \"[plugin] on attached to main engine\")\n    }\n  }\n\n  private fun saveSystemConfig(data: Map<*, *>?): Boolean {\n    if (data == null) return false\n    \n    val newScreen = data[\"screen\"] as? Map<*, *>\n    val newWidth = (newScreen?.get(\"width\") as? Number)?.toInt() ?: 0\n    val newHeight = (newScreen?.get(\"height\") as? Number)?.toInt() ?: 0\n    val newConfigValid = newWidth > 0 && newHeight > 0\n    \n    val old = mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)\n      .getString(SYSTEM_CONFIG_KEY, null)\n    \n    if (old != null) {\n      try {\n        val oldJson = JSONObject(old)\n        val oldScreen = oldJson.optJSONObject(\"screen\")\n        val oldWidth = oldScreen?.optInt(\"width\", 0) ?: 0\n        val oldHeight = oldScreen?.optInt(\"height\", 0) ?: 0\n        val oldConfigValid = oldWidth > 0 && oldHeight > 0\n        \n        if (oldConfigValid) {\n          Log.d(TAG, \"[plugin] system config already exists with valid screen: $old\")\n          return false\n        }\n        \n        if (!newConfigValid) {\n          Log.d(TAG, \"[plugin] both old and new config have invalid screen size, skipping update\")\n          return false\n        }\n        \n        Log.d(TAG, \"[plugin] updating system config: old has 0x0 screen, new has ${newWidth}x${newHeight}\")\n      } catch (e: Exception) {\n        Log.e(TAG, \"[plugin] error parsing old system config: ${e.message}\")\n      }\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    FloatwingService.instance?.systemConfig = data as Map<String, Any?>\n\n    return try {\n      val str = JSONObject(data).toString()\n      // json encode map to string\n      // try to save\n      mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()\n        .putString(SYSTEM_CONFIG_KEY, str)\n        .apply()\n      true\n    } catch (e: Exception) {\n      e.printStackTrace()\n      false\n    }\n  }\n\n  private fun savePixelRadio(pixelRadio: Double): Boolean {\n    val old = mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)\n      .getFloat(PIXEL_RADIO_KEY, 0F)\n    if (old > 1F) {\n      Log.d(TAG, \"[plugin] pixel radio already exits\")\n      return false\n    }\n\n    FloatwingService.instance?.pixelRadio = pixelRadio\n\n    // we need to save pixel radio\n    Log.d(TAG, \"[plugin] pixel radio need to be saved\")\n    mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()\n      .putFloat(PIXEL_RADIO_KEY, pixelRadio.toFloat())\n      .apply()\n    return true\n  }\n\n  private fun cleanCache(): Boolean {\n    // delete all of cache files\n    Log.w(TAG, \"[plugin] will delete all of contents\")\n    mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()\n      .clear().apply()\n    return true\n  }\n\n  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {\n    when (call.method) {\n      \"plugin.initialize\" -> {\n        // the main engine should call initialize?\n        // but the sub engine don't\n        val pixelRadio = call.argument(\"pixelRadio\") ?: 1.0\n        val systemConfig = call.argument<Map<*, *>?>(\"system\") as Map<*, *>\n\n        val map = HashMap<String, Any?>()\n        map[\"permission_grated\"] = permissionGiven(mContext)\n        map[\"service_running\"] = FloatwingService.isRunning(mContext)\n        map[\"windows\"] = FloatwingService.instance?.windows?.map { it.value.toMap() }\n\n        map[\"pixel_radio_updated\"] = savePixelRadio(pixelRadio)\n        map[\"system_config_updated\"] = saveSystemConfig(systemConfig)\n\n        return result.success(map)\n      }\n      \"plugin.has_permission\" -> {\n        return result.success(permissionGiven(mContext))\n      }\n      \"plugin.open_permission_setting\" -> {\n        return result.success(requestPermissions())\n      }\n      \"plugin.grant_permission\" -> {\n        return grantPermission(result)\n      }\n      // remove\n      \"plugin.create_window\" -> {\n        val id = call.argument<String>(\"id\") ?: \"default\"\n        val cfg = call.argument<Map<String, *>>(\"config\")!!\n        val start = call.argument<Boolean>(\"start\") ?: false\n        val config = FloatWindow.Config.from(cfg)\n        \n        // Use async version to avoid main thread blocking\n        FloatwingService.createWindowAsync(mContext, id, config, start, null) { windowResult ->\n          result.success(windowResult)\n        }\n        return\n      }\n      \"plugin.is_service_running\" -> {\n        return result.success(FloatwingService.isRunning(mContext))\n      }\n      \"plugin.start_service\" -> {\n        return result.success(FloatwingService.isRunning(mContext)\n          .or(FloatwingService.start(mContext)))\n      }\n      \"plugin.clean_cache\" -> {\n        return result.success(cleanCache())\n      }\n      \"plugin.sync_windows\" -> {\n        return result.success(FloatwingService.instance?.windows?.map { it.value.toMap() })\n      }\n      \"window.sync\" -> {\n        Log.d(TAG, \"[plugin] fake window.sync\")\n        return result.success(null)\n      }\n      else -> {\n        Log.d(TAG, \"[plugin] method ${call.method} not implement\")\n        result.notImplemented()\n      }\n    }\n  }\n\n  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {\n    channel.setMethodCallHandler(null)\n  }\n\n  override fun onAttachedToActivity(binding: ActivityPluginBinding) {\n    mActivity = binding.activity\n\n    // TODO: notify the window to show and return the result?\n\n    Log.d(TAG, \"[plugin] on attached to activity\")\n\n    // how to known are the main\n     FloatwingService.onActivityAttached(mActivity)\n  }\n\n  override fun onDetachedFromActivityForConfigChanges() {\n\n  }\n\n  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {\n    mActivity = binding.activity\n  }\n\n  override fun onDetachedFromActivity() {\n\n  }\n\n  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {\n    if (requestCode == ALERT_WINDOW_PERMISSION) {\n      waitPermissionResult.success(permissionGiven(mContext))\n      return true\n    }\n    return false\n  }\n\n  private fun requestPermissions(): Boolean {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n      mActivity.startActivityForResult(Intent(\n        Settings.ACTION_MANAGE_OVERLAY_PERMISSION,\n        Uri.parse(\"package:${mContext.packageName}\")\n      ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), ALERT_WINDOW_PERMISSION)\n      return true\n    }\n    return false\n  }\n\n  private fun grantPermission(result: Result) {\n    waitPermissionResult = result\n    requestPermissions()\n  }\n\n  companion object {\n    private const val TAG = \"FloatwingPlugin\"\n    private const val CHANNEL_NAME = \"im.zoe.labs/flutter_floatwing/method\"\n    private const val ALERT_WINDOW_PERMISSION = 1248\n\n    const val FLUTTER_ENGINE_CACHE_KEY = \"flutter_engine_main\"\n    const val SHARED_PREFERENCES_KEY = \"flutter_floatwing_cache\"\n    const val CALLBACK_KEY = \"callback_key\"\n    const val PIXEL_RADIO_KEY = \"pixel_radio\"\n    const val SYSTEM_CONFIG_KEY = \"system_config\"\n\n    fun permissionGiven(context: Context): Boolean {\n      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n        return Settings.canDrawOverlays(context)\n      }\n      return false\n    }\n  }\n}\n"
  },
  {
    "path": "android/src/main/kotlin/im/zoe/labs/flutter_floatwing/Utils.kt",
    "content": "package im.zoe.labs.flutter_floatwing\n\nimport android.app.ActivityManager\nimport android.content.Context\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.math.BigInteger\nimport java.security.MessageDigest\n\nclass Utils {\n    companion object {\n        fun getRunningService(context: Context, serviceClass: Class<*>): ActivityManager.RunningServiceInfo? {\n            val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager?\n            for (service in manager!!.getRunningServices(Int.MAX_VALUE)) {\n                if (serviceClass.name == service.service.className) {\n                    return service\n                }\n            }\n\n            return null\n        }\n\n        fun md5(input:String): String {\n            val md = MessageDigest.getInstance(\"MD5\")\n            return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')\n        }\n\n        fun genKey(vararg items: Any?): String {\n            return Utils.md5(items.joinToString(separator=\"-\"){ \"$it\" }).slice(IntRange(0, 12))\n        }\n\n        fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith {\n            when (val value = this[it])\n            {\n                is JSONArray ->\n                {\n                    val map = (0 until value.length()).associate { Pair(it.toString(), value[it]) }\n                    JSONObject(map).toMap().values.toList()\n                }\n                is JSONObject -> value.toMap()\n                JSONObject.NULL -> null\n                else            -> value\n            }\n        }\n    }\n}"
  },
  {
    "path": "example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Exceptions to above rules.\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n"
  },
  {
    "path": "example/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: f7a6a7906be96d2288f5d63a5a54c515a6e987fe\n  channel: unknown\n\nproject_type: app\n"
  },
  {
    "path": "example/README.md",
    "content": "# flutter_floatwing Example\n\nThis example demonstrates the core features of `flutter_floatwing` plugin.\n\n## Demos Included\n\n| Demo | Description |\n|------|-------------|\n| **Normal** | Basic draggable floating window |\n| **Assistive Touch** | iOS-style assistive touch button |\n| **Night Mode** | Full-screen non-clickable overlay filter |\n\n## Running the Example\n\n### Prerequisites\n\n- Flutter SDK (>=1.20.0)\n- Android device or emulator (API 21+)\n- USB debugging enabled\n\n### Steps\n\n```bash\n# Clone the repository\ngit clone https://github.com/jiusanzhou/flutter_floatwing.git\ncd flutter_floatwing/example\n\n# Install dependencies\nflutter pub get\n\n# Run on connected device\nflutter run\n```\n\n### Grant Permission\n\nWhen you first run the app, you'll need to grant the **\"Display over other apps\"** permission:\n\n1. Click \"Start\" button in the app\n2. System will redirect to permission settings\n3. Enable the permission for the example app\n4. Return to the app\n\n## Project Structure\n\n```\nexample/\n├── lib/\n│   ├── main.dart              # App entry & home page\n│   └── views/\n│       ├── normal.dart        # Basic floating window\n│       ├── assistive_touch.dart  # Assistive touch demo\n│       └── night.dart         # Night mode overlay\n└── android/\n    └── app/src/main/\n        └── AndroidManifest.xml  # Permission declaration\n```\n\n## Key Code Examples\n\n### Creating Multiple Windows\n\n```dart\nvar _configs = [\n  WindowConfig(id: \"normal\", route: \"/normal\", draggable: true),\n  WindowConfig(id: \"assistive\", route: \"/assistive\", draggable: true),\n  WindowConfig(\n    id: \"night\",\n    route: \"/night\",\n    width: WindowSize.MatchParent,\n    height: WindowSize.MatchParent,\n    clickable: false,  // Touch passes through\n  ),\n];\n```\n\n### Using Route-based Entry Points\n\n```dart\n// Register routes with .floatwing() wrapper\nMap<String, Widget Function(BuildContext)> _routes = {\n  \"/\": (_) => HomePage(),\n  \"/normal\": (_) => NormalView().floatwing(),\n  \"/assistive\": (_) => AssistiveTouch().floatwing(),\n  \"/night\": (_) => NightView().floatwing(),\n};\n```\n\n## Troubleshooting\n\n**Window not appearing?**\n- Ensure permission is granted\n- Check if service is running: `FloatwingPlugin().isServiceRunning()`\n\n**MissingPluginException?**\n- Clean rebuild: `flutter clean && flutter pub get && flutter run`\n\n**Debug mode issues?**\n- Use route-based entry points for easier debugging\n- Navigate to the route directly to test your overlay widget\n"
  },
  {
    "path": "example/android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n"
  },
  {
    "path": "example/android/app/build.gradle",
    "content": "plugins {\n    id \"com.android.application\"\n    id \"kotlin-android\"\n    id \"dev.flutter.flutter-gradle-plugin\"\n}\n\ndef localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\nandroid {\n    namespace 'im.zoe.labs.flutter_floatwing_example'\n    compileSdk 34\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n        test.java.srcDirs += 'src/test/kotlin'\n    }\n\n    defaultConfig {\n        applicationId \"im.zoe.labs.flutter_floatwing_example\"\n        minSdkVersion flutter.minSdkVersion\n        targetSdk 34\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n    }\n\n    buildTypes {\n        release {\n            signingConfig signingConfigs.debug\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.22\"\n}\n"
  },
  {
    "path": "example/android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "example/android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" />\n\n    <!-- io.flutter.app.FlutterApplication is an android.app.Application that\n         calls FlutterMain.startInitialization(this); in its onCreate method.\n         In most cases you can leave this as-is, but you if you want to provide\n         additional functionality it is fine to subclass or reimplement\n         FlutterApplication and put your custom class here. -->\n    <application\n        android:label=\"flutter_floatwing_example\"\n        android:icon=\"@mipmap/ic_launcher\">\n        <activity\n            android:name=\"im.zoe.labs.flutter_floatwing_example.MainActivity\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\"\n            android:exported=\"true\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data android:name=\"io.flutter.embedding.android.NormalTheme\" android:resource=\"@style/NormalTheme\" />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n\n        <service android:name=\"im.zoe.labs.flutter_floatwing.FloatwingService\"\n            android:label=\"Floatwing Service\"/>\n\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "example/android/app/src/main/kotlin/im/zoe/labs/flutter_floatwing_example/MainActivity.kt",
    "content": "package im.zoe.labs.flutter_floatwing_example\n\nimport android.os.Bundle\nimport android.os.PersistableBundle\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n}\n"
  },
  {
    "path": "example/android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "example/android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "example/android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "example/android/build.gradle",
    "content": "allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = \"../build\"\n\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\nsubprojects {\n    project.evaluationDependsOn(\":app\")\n}\n\ntasks.register(\"clean\", Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "example/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.10.2-all.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "example/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\nandroid.defaults.buildfeatures.buildconfig=true\nandroid.nonTransitiveRClass=false\nandroid.nonFinalResIds=false\n"
  },
  {
    "path": "example/android/settings.gradle",
    "content": "pluginManagement {\n    def flutterSdkPath = {\n        def properties = new Properties()\n        file(\"local.properties\").withInputStream { properties.load(it) }\n        def flutterSdkPath = properties.getProperty(\"flutter.sdk\")\n        assert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\n        return flutterSdkPath\n    }()\n\n    includeBuild(\"$flutterSdkPath/packages/flutter_tools/gradle\")\n\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\nplugins {\n    id \"dev.flutter.flutter-plugin-loader\" version \"1.0.0\"\n    id \"com.android.application\" version \"8.5.0\" apply false\n    id \"org.jetbrains.kotlin.android\" version \"1.9.22\" apply false\n}\n\ninclude \":app\"\n"
  },
  {
    "path": "example/lib/main.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\nimport 'package:flutter_floatwing_example/views/assistive_touch.dart';\nimport 'package:flutter_floatwing_example/views/night.dart';\nimport 'package:flutter_floatwing_example/views/normal.dart';\n\nvoid main() {\n  runApp(MyApp());\n}\n\n@pragma(\"vm:entry-point\")\nvoid floatwing() {\n  runApp(((_) => NonrmalView()).floatwing().make());\n}\n\nvoid floatwing2(Window w) {\n  runApp(MaterialApp(\n    // floatwing on widget can't use Window.of(context)\n    // to access window instance\n    // should use FloatwingPlugin().currentWindow\n    home: NonrmalView().floatwing(),\n  ));\n}\n\nclass MyApp extends StatefulWidget {\n  @override\n  _MyAppState createState() => _MyAppState();\n}\n\nclass _MyAppState extends State<MyApp> {\n  var _configs = [\n    WindowConfig(\n      id: \"normal\",\n      // entry: \"floatwing\",\n      route: \"/normal\",\n      draggable: true,\n    ),\n    WindowConfig(\n      id: \"assitive_touch\",\n      // entry: \"floatwing\",\n      route: \"/assitive_touch\",\n      draggable: true,\n    ),\n    WindowConfig(\n      id: \"night\",\n      // entry: \"floatwing\",\n      route: \"/night\",\n      width: WindowSize.MatchParent, height: WindowSize.MatchParent,\n      clickable: false,\n    )\n  ];\n\n  Map<String, WidgetBuilder> _builders = {\n    \"normal\": (_) => NonrmalView(),\n    \"assitive_touch\": (_) => AssistiveTouch(),\n    \"night\": (_) => NightView(),\n  };\n\n  Map<String, Widget Function(BuildContext)> _routes = {};\n\n  @override\n  void initState() {\n    super.initState();\n\n    _routes[\"/\"] = (_) => HomePage(configs: _configs);\n\n    _configs.forEach((c) => {\n          if (c.route != null && _builders[c.id] != null)\n            {_routes[c.route!] = _builders[c.id]!.floatwing(debug: false)}\n        });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      debugShowCheckedModeBanner: false,\n      initialRoute: \"/\",\n      routes: _routes,\n    );\n  }\n}\n\nclass HomePage extends StatefulWidget {\n  final List<WindowConfig> configs;\n  const HomePage({Key? key, required this.configs}) : super(key: key);\n\n  @override\n  State<HomePage> createState() => _HomePageState();\n}\n\nclass _HomePageState extends State<HomePage> {\n  @override\n  void initState() {\n    super.initState();\n\n    widget.configs.forEach((c) => _windows.add(c.to()));\n\n    FloatwingPlugin().initialize();\n\n    initAsyncState();\n  }\n\n  List<Window> _windows = [];\n\n  Map<Window, bool> _readys = {};\n\n  bool _ready = false;\n\n  initAsyncState() async {\n    var p1 = await FloatwingPlugin().checkPermission();\n    var p2 = await FloatwingPlugin().isServiceRunning();\n\n    // get permission first\n    if (!p1) {\n      FloatwingPlugin().openPermissionSetting();\n      return;\n    }\n\n    // start service\n    if (!p2) {\n      FloatwingPlugin().startService();\n    }\n\n    _createWindows();\n\n    setState(() {\n      _ready = true;\n    });\n  }\n\n  _createWindows() async {\n    await FloatwingPlugin().isServiceRunning().then((v) async {\n      if (!v)\n        await FloatwingPlugin().startService().then((_) {\n          print(\"start the backgroud service success.\");\n        });\n    });\n\n    _windows.forEach((w) {\n      var _w = FloatwingPlugin().windows[w.id];\n      if (null != _w) {\n        // replace w with _w\n        _readys[w] = true;\n        return;\n      }\n      w.on(EventType.WindowCreated, (window, data) {\n        _readys[window] = true;\n        setState(() {});\n      }).create();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Floatwing example app'),\n      ),\n      body: _ready\n          ? ListView(\n              children: _windows.map((e) => _item(e)).toList(),\n            )\n          : Center(\n              child: ElevatedButton(\n                  onPressed: () {\n                    initAsyncState();\n                  },\n                  child: Text(\"Start\")),\n            ),\n    );\n  }\n\n  _debug(Window w) {\n    Navigator.of(context).pushNamed(w.config!.route!);\n  }\n\n  Widget _item(Window w) {\n    return Card(\n      margin: EdgeInsets.all(10),\n      child: Padding(\n          padding: EdgeInsets.all(10),\n          child: Column(\n            children: [\n              Text(w.id,\n                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),\n              SizedBox(height: 10),\n              Container(\n                width: double.infinity,\n                padding: EdgeInsets.all(5),\n                decoration: BoxDecoration(\n                    color: Color.fromARGB(255, 214, 213, 213),\n                    borderRadius: BorderRadius.all(Radius.circular(4))),\n                child: Text(w.config?.toString() ?? \"\"),\n              ),\n              SizedBox(height: 10),\n              Row(\n                mainAxisAlignment: MainAxisAlignment.end,\n                children: [\n                  TextButton(\n                    onPressed: (_readys[w] == true) ? () => w.start() : null,\n                    child: Text(\"Open\"),\n                  ),\n                  TextButton(\n                      onPressed:\n                          w.config?.route != null ? () => _debug(w) : null,\n                      child: Text(\"Debug\")),\n                  TextButton(\n                    onPressed: (_readys[w] == true)\n                        ? () => {w.close(), w.share(\"close\")}\n                        : null,\n                    child: Text(\"Close\", style: TextStyle(color: Colors.red)),\n                  ),\n                ],\n              )\n            ],\n          )),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/views/assistive_touch.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nclass AssistiveTouch extends StatefulWidget {\n  const AssistiveTouch({Key? key}) : super(key: key);\n\n  @override\n  State<AssistiveTouch> createState() => _AssistiveTouchState();\n}\n\n@pragma(\"vm:entry-point\")\nvoid _pannelMain() {\n  runApp(((_) => AssistivePannel()).floatwing(app: true).make());\n}\n\nclass _AssistiveTouchState extends State<AssistiveTouch> {\n  /// The state of the touch state\n  bool expend = false;\n  bool pannelReady = false;\n  Window? pannelWindow;\n  Window? touchWindow;\n\n  @override\n  void initState() {\n    super.initState();\n\n    initAsyncState();\n\n    SchedulerBinding.instance?.addPostFrameCallback((_) {\n      touchWindow = Window.of(context);\n      touchWindow?.on(EventType.WindowStarted, (window, data) {\n        print(\"touch window start ...\");\n        expend = false;\n        setState(() {});\n      });\n    });\n  }\n\n  void initAsyncState() async {\n    // create the pannel window\n    pannelWindow = WindowConfig(\n      id: \"assistive_pannel\",\n      callback: _pannelMain,\n      width: WindowSize.MatchParent,\n      height: WindowSize.MatchParent,\n      autosize: false,\n    ).to();\n\n    pannelWindow?.create();\n    // we can't subscribe the events from other windows\n    // that means pannelWindow's events can't be fired to here.\n    // This is a feature, make sure window only care about events\n    // from self. If we want to communicate with the other windows,\n    // we can use the data communicatting method.\n    pannelWindow?.on(EventType.WindowCreated, (window, data) {\n      pannelReady = true;\n      setState(() {});\n    }).on(EventType.WindowPaused, (window, data) {\n      // open the assitive_touch\n      touchWindow?.start();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AssistiveButton(onTap: _onTap);\n  }\n\n  void _onTap() {\n    var w = Window.of(context);\n    var pixelRatio = w?.pixelRadio ?? 3.0;\n    var x = (w?.config?.x ?? 0) / pixelRatio;\n    var y = (w?.config?.y ?? 0) / pixelRatio;\n    pannelWindow?.share([x, y]);\n    pannelWindow?.start();\n    setState(() {\n      expend = true;\n    });\n  }\n}\n\n@immutable\nclass AssistiveButton extends StatefulWidget {\n  const AssistiveButton({\n    Key? key,\n    this.child = const _DefaultChild(),\n    this.visible = true,\n    this.shouldStickToSide = true,\n    this.margin = const EdgeInsets.all(8.0),\n    this.initialOffset = Offset.infinite,\n    this.onTap,\n    this.animatedBuilder,\n  }) : super(key: key);\n\n  /// The widget below this widget in the tree.\n  final Widget child;\n\n  /// Switches between showing the [child] or hiding it.\n  final bool visible;\n\n  /// Whether it sticks to the side.\n  final bool shouldStickToSide;\n\n  /// Empty space to surround the [child].\n  final EdgeInsets margin;\n\n  final Offset initialOffset;\n\n  /// A tap with a primary button has occurred.\n  final VoidCallback? onTap;\n\n  /// Custom animated builder.\n  final Widget Function(\n    BuildContext context,\n    Widget child,\n    bool visible,\n  )? animatedBuilder;\n\n  @override\n  _AssistiveButtonState createState() => _AssistiveButtonState();\n}\n\nclass _AssistiveButtonState extends State<AssistiveButton>\n    with TickerProviderStateMixin {\n  bool isInitialized = false;\n  late Offset offset = widget.initialOffset;\n  late Offset largerOffset = offset;\n  Size size = Size.zero;\n  bool isDragging = false;\n  bool isIdle = true;\n  Timer? timer;\n  late final AnimationController _scaleAnimationController =\n      AnimationController(\n    duration: const Duration(milliseconds: 200),\n    vsync: this,\n  )..addListener(() {\n          setState(() {});\n        });\n  late final Animation<double> _scaleAnimation = CurvedAnimation(\n    parent: _scaleAnimationController,\n    curve: Curves.easeInOut,\n  );\n  Timer? scaleTimer;\n\n  Window? window;\n\n  @override\n  void initState() {\n    super.initState();\n    scaleTimer = Timer.periodic(const Duration(milliseconds: 60), (_) {\n      if (mounted == false) {\n        return;\n      }\n\n      if (widget.visible) {\n        _scaleAnimationController.forward();\n      } else {\n        _scaleAnimationController.reverse();\n      }\n    });\n    FocusManager.instance.addListener(listener);\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    if (isInitialized == false) {\n      isInitialized = true;\n      _setOffset(offset);\n    }\n  }\n\n  @override\n  void dispose() {\n    timer?.cancel();\n    scaleTimer?.cancel();\n    _scaleAnimationController.dispose();\n    FocusManager.instance.removeListener(listener);\n    super.dispose();\n  }\n\n  void listener() {\n    Timer(const Duration(milliseconds: 200), () {\n      if (mounted == false) return;\n      largerOffset = Offset(\n        max(largerOffset.dx, offset.dx),\n        max(largerOffset.dy, offset.dy),\n      );\n\n      _setOffset(largerOffset, false);\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var child = widget.child;\n\n    if (window == null) {\n      window = Window.of(context);\n      window?.on(EventType.WindowDragStart, (window, data) => _onDragStart());\n      window?.on(EventType.WindowDragging, (window, data) {\n        var p = data as List<dynamic>;\n        _onDragUpdate(p[0], p[1]);\n      });\n      window?.on(EventType.WindowDragEnd, (window, windowdata) => _onDragEnd());\n    }\n\n    child = GestureDetector(\n      onTap: _onTap,\n      child: child,\n    );\n\n    child = widget.animatedBuilder != null\n        ? widget.animatedBuilder!(context, child, widget.visible)\n        : ScaleTransition(\n            scale: _scaleAnimation,\n            child: AnimatedOpacity(\n              opacity: isIdle ? .3 : 1,\n              duration: const Duration(milliseconds: 300),\n              child: child,\n            ),\n          );\n\n    return child;\n  }\n\n  void _onTap() async {\n    if (widget.onTap != null) {\n      setState(() {\n        isIdle = false;\n      });\n      _scheduleIdle();\n      widget.onTap!();\n    }\n  }\n\n  void _onDragStart() {\n    setState(() {\n      isDragging = true;\n      isIdle = false;\n    });\n    timer?.cancel();\n  }\n\n  Offset _old = Offset.zero;\n  void _onDragUpdate(int x, int y) {\n    _old = Offset(x.toDouble(), y.toDouble());\n    _setOffset(_old);\n  }\n\n  void _onDragEnd() {\n    setState(() {\n      isDragging = false;\n    });\n    _scheduleIdle();\n\n    _setOffset(offset);\n  }\n\n  void _scheduleIdle() {\n    timer?.cancel();\n    timer = Timer(const Duration(seconds: 2), () {\n      if (isDragging == false) {\n        setState(() {\n          isIdle = true;\n        });\n      }\n    });\n  }\n\n  void _updatePosition() {\n    window?.update(WindowConfig(\n      x: offset.dx.toInt(),\n      y: offset.dy.toInt(),\n    ));\n  }\n\n  /// TODO: this function should depend on the gravity to calcute the position\n  void _setOffset(Offset offset, [bool shouldUpdateLargerOffset = true]) {\n    if (shouldUpdateLargerOffset) {\n      largerOffset = offset;\n    }\n\n    if (isDragging) {\n      this.offset = offset;\n      return;\n    }\n\n    final screenSize =\n        window?.system?.screenSize ?? MediaQuery.of(context).size;\n    final screenPadding = MediaQuery.of(context).padding;\n    final viewInsets = MediaQuery.of(context).viewInsets;\n    final left = screenPadding.left + viewInsets.left + widget.margin.left;\n    final top = screenPadding.top + viewInsets.top + widget.margin.top;\n    final right = screenSize.width -\n        screenPadding.right -\n        viewInsets.right -\n        widget.margin.right -\n        size.width;\n    final bottom = screenSize.height -\n        screenPadding.bottom -\n        viewInsets.bottom -\n        widget.margin.bottom -\n        size.height;\n\n    final halfWidth = (right - left) / 2;\n\n    if (widget.shouldStickToSide) {\n      final normalizedTop = max(min(offset.dy, bottom), top);\n      final normalizedLeft = max(\n        min(\n          normalizedTop == bottom || normalizedTop == top\n              ? offset.dx\n              : offset.dx < halfWidth\n                  ? left\n                  : right,\n          right,\n        ),\n        left,\n      );\n      this.offset = Offset(normalizedLeft, normalizedTop);\n    } else {\n      final normalizedTop = max(min(offset.dy, bottom), top);\n      final normalizedLeft = max(min(offset.dx, right), left);\n      this.offset = Offset(normalizedLeft, normalizedTop);\n    }\n    _updatePosition();\n  }\n\n  // Offset _applyGravity(Offset o) {\n  //   return window?.config?.gravity.apply(o) ?? o;\n  // }\n}\n\nclass AssistivePannel extends StatefulWidget {\n  const AssistivePannel({\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<AssistivePannel> createState() => _AssistivePannelState();\n}\n\nclass _AssistivePannelState extends State<AssistivePannel>\n    with SingleTickerProviderStateMixin {\n  late AnimationController _animationController = AnimationController(\n    duration: _duration,\n    vsync: this,\n  );\n\n  Duration _duration = Duration(milliseconds: 250);\n\n  Window? window;\n\n  @override\n  void dispose() {\n    _animationController.dispose();\n    super.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    SchedulerBinding.instance?.addPostFrameCallback((_) {\n      window = Window.of(context);\n      window?.on(EventType.WindowStarted, (window, data) {\n        print(\"pannel just start ...\");\n        setState(() {\n          _show = true;\n        });\n      }).onData((source, name, data) async {\n        double x = (data[0] as num).toDouble();\n        double y = (data[1] as num).toDouble();\n        _updatePostion(x, y);\n        return;\n      });\n    });\n  }\n\n  double _touchX = 0.0;\n  double _touchY = 0.0;\n  double _touchSize = 56.0;\n\n  _updatePostion(double x, double y) {\n    setState(() {\n      _touchX = x;\n      _touchY = y;\n    });\n  }\n\n  var factor = 0.8;\n\n  double screenWidth = 0.0;\n  double screenHeight = 0.0;\n  double size = 0.0;\n\n  @override\n  Widget build(BuildContext context) {\n    screenWidth = MediaQuery.of(context).size.width;\n    screenHeight = MediaQuery.of(context).size.height;\n    size = screenWidth * factor;\n\n    final touchCenterX = _touchX + _touchSize / 2;\n    final touchCenterY = _touchY + _touchSize / 2;\n\n    final expandedLeft = (screenWidth - size) / 2;\n    final expandedTop =\n        max(20.0, min(touchCenterY - size / 2, screenHeight - size - 20));\n\n    final panelCenterX = expandedLeft + size / 2;\n    final panelCenterY = expandedTop + size / 2;\n\n    final alignX = (touchCenterX - panelCenterX) / (size / 2);\n    final alignY = (touchCenterY - panelCenterY) / (size / 2);\n\n    return GestureDetector(\n      onTap: _onTap,\n      child: Container(\n        color: Colors.transparent,\n        child: Stack(\n          children: [\n            Positioned(\n              left: expandedLeft,\n              top: expandedTop,\n              width: size,\n              height: size,\n              child: GestureDetector(\n                onTap: () => null,\n                child: AnimatedScale(\n                  scale: _show ? 1.0 : 0.0,\n                  alignment: Alignment(\n                      alignX.clamp(-1.0, 1.0), alignY.clamp(-1.0, 1.0)),\n                  duration: _duration,\n                  curve: Curves.easeOutCubic,\n                  child: Container(\n                    decoration: BoxDecoration(\n                      borderRadius: BorderRadius.all(Radius.circular(16)),\n                      color: Color.fromARGB(255, 25, 24, 24),\n                    ),\n                    child: Stack(\n                      children: [],\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  bool _show = false;\n\n  _onTap() {\n    setState(() {\n      _show = false;\n    });\n    Timer(_duration, () => window?.close());\n  }\n}\n\nclass _DefaultChild extends StatelessWidget {\n  const _DefaultChild({\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 56,\n      width: 56,\n      alignment: Alignment.center,\n      decoration: BoxDecoration(\n        color: Colors.grey[900],\n        borderRadius: const BorderRadius.all(Radius.circular(28)),\n      ),\n      child: Container(\n        height: 40,\n        width: 40,\n        alignment: Alignment.center,\n        decoration: BoxDecoration(\n          color: Colors.grey[400]!.withOpacity(.6),\n          borderRadius: const BorderRadius.all(Radius.circular(28)),\n        ),\n        child: Container(\n          height: 32,\n          width: 32,\n          alignment: Alignment.center,\n          decoration: BoxDecoration(\n            color: Colors.grey[300]!.withOpacity(.6),\n            borderRadius: const BorderRadius.all(Radius.circular(28)),\n          ),\n          child: Container(\n            height: 24,\n            width: 24,\n            alignment: Alignment.center,\n            decoration: const BoxDecoration(\n              color: Colors.white,\n              borderRadius: BorderRadius.all(Radius.circular(28)),\n            ),\n            child: const SizedBox.expand(),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/views/night.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nclass NightView extends StatefulWidget {\n  const NightView({Key? key}) : super(key: key);\n\n  @override\n  State<NightView> createState() => _NightViewState();\n}\n\nclass _NightViewState extends State<NightView> {\n  Color color = Color.fromARGB(255, 192, 200, 41).withOpacity(0.20);\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  Window? w;\n\n  var _show = true;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: _show ? MediaQuery.of(context).size.height : 0,\n      width: _show ? MediaQuery.of(context).size.width : 0,\n      color: color,\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/views/normal.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nclass NonrmalView extends StatefulWidget {\n  const NonrmalView({Key? key}) : super(key: key);\n\n  @override\n  State<NonrmalView> createState() => _NonrmalViewState();\n}\n\nclass _NonrmalViewState extends State<NonrmalView> {\n  bool _expend = false;\n  double _size = 150;\n\n  @override\n  void initState() {\n    super.initState();\n\n    SchedulerBinding.instance?.addPostFrameCallback((_) {\n      w = Window.of(context);\n      w?.on(EventType.WindowDragStart, (window, data) {\n        if (mounted) setState(() => {dragging = true});\n      }).on(EventType.WindowDragEnd, (window, data) {\n        if (mounted) setState(() => {dragging = false});\n      });\n    });\n  }\n\n  Window? w;\n  bool dragging = false;\n\n  _changeSize() {\n    _expend = !_expend;\n    _size = _expend ? 250 : 150;\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Container(\n        width: _size,\n        height: _size,\n        color: dragging ? Colors.yellowAccent : null,\n        child: Card(\n            child: Stack(\n          children: [\n            Center(\n                child: ElevatedButton(\n                    onPressed: () {\n                      w?.launchMainActivity();\n                    },\n                    child: Text(\"Start Activity\"))),\n            Positioned(\n                right: 5, top: 5, child: Icon(Icons.drag_handle_rounded)),\n            Positioned(\n                right: 5,\n                bottom: 5,\n                child: RotationTransition(\n                    turns: AlwaysStoppedAnimation(-45 / 360),\n                    child: InkWell(\n                        onTap: _changeSize,\n                        child: Icon(Icons.unfold_more_rounded))))\n          ],\n        )),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/pubspec.yaml",
    "content": "name: flutter_floatwing_example\ndescription: Demonstrates how to use the flutter_floatwing plugin.\n\n# The following line prevents the package from being accidentally published to\n# pub.dev using `pub publish`. This is preferred for private packages.\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\n\nenvironment:\n  sdk: '>=3.0.0 <4.0.0'\n\ndependencies:\n  flutter:\n    sdk: flutter\n\n  flutter_floatwing:\n    # When depending on this package from a real application you should use:\n    #   flutter_floatwing: ^x.y.z\n    # See https://dart.dev/tools/pub/dependencies#version-constraints\n    # The example app is bundled with the plugin so we use a path dependency on\n    # the parent directory to use the current plugin's version. \n    path: ../\n\n  # The following adds the Cupertino Icons font to your application.\n  # Use with the CupertinoIcons class for iOS style icons.\n  cupertino_icons: ^1.0.8\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\n# The following section is specific to Flutter.\nflutter:\n\n  # The following line ensures that the Material Icons font is\n  # included with your application, so that you can use the icons in\n  # the material Icons class.\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  # assets:\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware.\n\n  # For details regarding adding assets from package dependencies, see\n  # https://flutter.dev/assets-and-images/#from-packages\n\n  # To add custom fonts to your application, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n  #\n  # For details regarding fonts from package dependencies,\n  # see https://flutter.dev/custom-fonts/#from-packages\n"
  },
  {
    "path": "example/test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility that Flutter provides. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'package:flutter_floatwing_example/main.dart';\n\nvoid main() {\n  testWidgets('Verify Platform version', (WidgetTester tester) async {\n    // Build our app and trigger a frame.\n    await tester.pumpWidget(MyApp());\n  });\n}\n"
  },
  {
    "path": "flutter_floatwing.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/lib\" isTestSource=\"false\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/.dart_tool\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/.idea\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/.pub\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/example/.pub\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/example/build\" />\n    </content>\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n    <orderEntry type=\"library\" name=\"Dart Packages\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Dart SDK\" level=\"project\" />\n    <orderEntry type=\"library\" name=\"Flutter Plugins\" level=\"project\" />\n  </component>\n</module>"
  },
  {
    "path": "integration_test/floatwing_integration_test.dart",
    "content": "// Integration tests for flutter_floatwing plugin\n//\n// These tests simulate end-to-end workflows by mocking the native layer.\n// Since flutter_floatwing requires Android-specific features (SYSTEM_ALERT_WINDOW),\n// true integration testing requires running on an actual Android device.\n//\n// These tests verify the Dart layer integration works correctly.\n\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('End-to-end workflow tests', () {\n    const MethodChannel methodChannel =\n        MethodChannel('im.zoe.labs/flutter_floatwing/method');\n    const MethodChannel windowChannel =\n        MethodChannel('im.zoe.labs/flutter_floatwing/window');\n\n    // Track method calls for verification\n    final List<String> methodCalls = [];\n\n    setUp(() {\n      methodCalls.clear();\n\n      // Mock main plugin channel\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel,\n              (MethodCall methodCall) async {\n        methodCalls.add(methodCall.method);\n\n        switch (methodCall.method) {\n          case 'plugin.has_permission':\n            return true;\n          case 'plugin.open_permission_setting':\n            return true;\n          case 'plugin.is_service_running':\n            return false; // Start as not running\n          case 'plugin.start_service':\n            return true;\n          case 'plugin.initialize':\n            return {\n              'permission_grated': true,\n              'service_running': true,\n              'windows': [],\n            };\n          case 'plugin.sync_windows':\n            return [];\n          case 'plugin.create_window':\n            final id = methodCall.arguments['id'] ?? 'default';\n            final config = methodCall.arguments['config'];\n            return {\n              'id': id,\n              'pixelRadio': 2.0,\n              'config': config,\n            };\n          default:\n            return null;\n        }\n      });\n\n      // Mock window channel\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(windowChannel,\n              (MethodCall methodCall) async {\n        methodCalls.add(methodCall.method);\n\n        switch (methodCall.method) {\n          case 'window.start':\n            return true;\n          case 'window.show':\n            return true;\n          case 'window.close':\n            return true;\n          case 'window.update':\n            return {\n              'id': methodCall.arguments['id'],\n              'config': methodCall.arguments['config'],\n            };\n          case 'data.share':\n            return {'received': true};\n          default:\n            return null;\n        }\n      });\n    });\n\n    tearDown(() {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel, null);\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(windowChannel, null);\n    });\n\n    test('Complete window creation workflow', () async {\n      final plugin = FloatwingPlugin();\n\n      // Step 1: Check permission\n      final hasPermission = await plugin.checkPermission();\n      expect(hasPermission, isTrue);\n      expect(methodCalls, contains('plugin.has_permission'));\n\n      // Step 2: Create window config\n      final config = WindowConfig(\n        id: 'test-overlay',\n        route: '/overlay',\n        width: 200,\n        height: 100,\n        draggable: true,\n        gravity: GravityType.RightBottom,\n      );\n\n      // Step 3: Create and start window\n      final window =\n          await plugin.createWindow('test-overlay', config, start: true);\n\n      expect(window, isNotNull);\n      expect(window?.id, equals('test-overlay'));\n      expect(methodCalls, contains('plugin.create_window'));\n    });\n\n    test('Window lifecycle workflow', () async {\n      final plugin = FloatwingPlugin();\n\n      // Create window\n      final config = WindowConfig(route: '/test');\n      final window = await plugin.createWindow('lifecycle-test', config);\n      expect(window, isNotNull);\n\n      // Start window\n      final started = await window?.start();\n      expect(started, isTrue);\n      expect(methodCalls, contains('window.start'));\n\n      // Show/hide window\n      final shown = await window?.show(visible: true);\n      expect(shown, isTrue);\n      expect(methodCalls, contains('window.show'));\n\n      final hidden = await window?.hide();\n      expect(hidden, isTrue);\n\n      // Close window\n      final closed = await window?.close();\n      expect(closed, isTrue);\n      expect(methodCalls, contains('window.close'));\n    });\n\n    test('Window update workflow', () async {\n      final plugin = FloatwingPlugin();\n\n      // Create window\n      final config = WindowConfig(\n        route: '/update-test',\n        width: 100,\n        height: 100,\n      );\n      final window = await plugin.createWindow('update-test', config);\n      expect(window, isNotNull);\n\n      // Update window position\n      final updateResult = await window?.update(WindowConfig(\n        x: 50,\n        y: 100,\n        gravity: GravityType.Center,\n      ));\n      expect(updateResult, isTrue);\n      expect(methodCalls, contains('window.update'));\n    });\n\n    test('Data sharing workflow', () async {\n      final plugin = FloatwingPlugin();\n\n      // Create window\n      final config = WindowConfig(route: '/share-test');\n      final window = await plugin.createWindow('share-test', config);\n      expect(window, isNotNull);\n\n      // Share data with window\n      final shareResult = await window?.share(\n        {'message': 'Hello from main app!'},\n        name: 'greeting',\n      );\n      expect(shareResult, isNotNull);\n      expect(methodCalls, contains('data.share'));\n    });\n\n    test('Event registration workflow', () async {\n      final plugin = FloatwingPlugin();\n\n      // Create window\n      final config = WindowConfig(route: '/event-test');\n      final window = await plugin.createWindow('event-test', config);\n      expect(window, isNotNull);\n\n      // Register event handlers\n      window?.on(EventType.WindowCreated, (w, data) {\n        // Handle created\n      }).on(EventType.WindowStarted, (w, data) {\n        // Handle started\n      }).on(EventType.WindowDestroy, (w, data) {\n        // Handle destroy\n      }).on(EventType.WindowDragging, (w, data) {\n        // Handle dragging\n      });\n\n      // Verify chaining works\n      expect(window, isNotNull);\n    });\n\n    test('WindowConfig to Window workflow using to()', () async {\n      // Using the fluent API\n      final window = WindowConfig(\n        id: 'fluent-test',\n        route: '/fluent',\n        width: 150,\n        height: 150,\n        draggable: true,\n        clickable: true,\n      ).to();\n\n      expect(window, isNotNull);\n      expect(window.id, equals('fluent-test'));\n      expect(window.config?.route, equals('/fluent'));\n      expect(window.config?.width, equals(150));\n      expect(window.config?.draggable, isTrue);\n\n      // Register events before creating\n      window\n          .on(EventType.WindowCreated, (w, data) {})\n          .on(EventType.WindowStarted, (w, data) {});\n\n      // Now create - this would actually create the window\n      final created = await window.create(start: true);\n      expect(created, isNotNull);\n    });\n\n    test('Multiple windows workflow', () async {\n      final plugin = FloatwingPlugin();\n\n      // Clear any existing windows\n      plugin.windows.clear();\n\n      // Create multiple windows\n      final window1 = await plugin.createWindow(\n        'window-1',\n        WindowConfig(route: '/window1'),\n      );\n      final window2 = await plugin.createWindow(\n        'window-2',\n        WindowConfig(route: '/window2'),\n      );\n      final window3 = await plugin.createWindow(\n        'window-3',\n        WindowConfig(route: '/window3'),\n      );\n\n      expect(window1, isNotNull);\n      expect(window2, isNotNull);\n      expect(window3, isNotNull);\n\n      // Verify all windows are cached\n      expect(plugin.windows.length, equals(3));\n      expect(plugin.windows.containsKey('window-1'), isTrue);\n      expect(plugin.windows.containsKey('window-2'), isTrue);\n      expect(plugin.windows.containsKey('window-3'), isTrue);\n\n      // Close windows\n      await window1?.close();\n      await window2?.close();\n      await window3?.close();\n    });\n  });\n\n  group('Error handling integration tests', () {\n    const MethodChannel methodChannel =\n        MethodChannel('im.zoe.labs/flutter_floatwing/method');\n\n    test('Should handle permission denied gracefully', () async {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel,\n              (MethodCall methodCall) async {\n        if (methodCall.method == 'plugin.has_permission') {\n          return false;\n        }\n        return null;\n      });\n\n      final plugin = FloatwingPlugin();\n      final hasPermission = await plugin.checkPermission();\n\n      expect(hasPermission, isFalse);\n\n      // Cleanup\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel, null);\n    });\n\n    test('Should throw when creating window without permission', () async {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel,\n              (MethodCall methodCall) async {\n        if (methodCall.method == 'plugin.has_permission') {\n          return false;\n        }\n        return null;\n      });\n\n      final plugin = FloatwingPlugin();\n      final config = WindowConfig(route: '/test');\n\n      expect(\n        () => plugin.createWindow('no-permission', config),\n        throwsException,\n      );\n\n      // Cleanup\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel, null);\n    });\n  });\n\n  group('Configuration combinations', () {\n    test('Full-screen overlay config (night mode style)', () {\n      final config = WindowConfig(\n        id: 'night-mode',\n        route: '/night',\n        width: WindowSize.MatchParent,\n        height: WindowSize.MatchParent,\n        clickable: false, // Touch passes through\n        focusable: false,\n      );\n\n      expect(config.width, equals(WindowSize.MatchParent));\n      expect(config.height, equals(WindowSize.MatchParent));\n      expect(config.clickable, isFalse);\n      expect(config.focusable, isFalse);\n    });\n\n    test('Floating button config (assistive touch style)', () {\n      final config = WindowConfig(\n        id: 'float-button',\n        route: '/button',\n        width: 56,\n        height: 56,\n        draggable: true,\n        gravity: GravityType.RightBottom,\n        autosize: true,\n      );\n\n      expect(config.width, equals(56));\n      expect(config.height, equals(56));\n      expect(config.draggable, isTrue);\n      expect(config.gravity, equals(GravityType.RightBottom));\n      expect(config.autosize, isTrue);\n    });\n\n    test('Popup config', () {\n      final config = WindowConfig(\n        id: 'popup',\n        route: '/popup',\n        width: 300,\n        height: 200,\n        gravity: GravityType.Center,\n        clickable: true,\n        focusable: true,\n        draggable: false,\n      );\n\n      expect(config.gravity, equals(GravityType.Center));\n      expect(config.clickable, isTrue);\n      expect(config.focusable, isTrue);\n      expect(config.draggable, isFalse);\n    });\n\n    test('All gravity types should be distinct', () {\n      final gravities = [\n        GravityType.Center,\n        GravityType.CenterTop,\n        GravityType.CenterBottom,\n        GravityType.LeftTop,\n        GravityType.LeftCenter,\n        GravityType.LeftBottom,\n        GravityType.RightTop,\n        GravityType.RightCenter,\n        GravityType.RightBottom,\n      ];\n\n      // All should have unique int values\n      final intValues = gravities.map((g) => g.toInt()).toSet();\n      expect(intValues.length, equals(gravities.length));\n    });\n  });\n}\n"
  },
  {
    "path": "lib/flutter_floatwing.dart",
    "content": "export 'src/plugin.dart';\nexport 'src/window.dart';\nexport 'src/provider.dart';\nexport 'src/event.dart';\nexport 'src/utils.dart';\nexport 'src/constants.dart';\n"
  },
  {
    "path": "lib/src/constants.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// window size\n///\nclass WindowSize {\n  static const int MatchParent = -1;\n  static const int WrapContent = -2;\n}\n\nenum GravityType {\n  Center,\n  CenterTop,\n  CenterBottom,\n  LeftTop,\n  LeftCenter,\n  LeftBottom,\n  RightTop,\n  RightCenter,\n  RightBottom,\n\n  Unknown,\n}\n\nextension GravityTypeConverter on GravityType {\n  // ignore: slash_for_doc_comments\n  /**\n    public static final int AXIS_CLIP = 8;\n    public static final int AXIS_PULL_AFTER = 4;\n    public static final int AXIS_PULL_BEFORE = 2;\n    public static final int AXIS_SPECIFIED = 1;\n    public static final int AXIS_X_SHIFT = 0;\n    public static final int AXIS_Y_SHIFT = 4;\n    public static final int BOTTOM = 80;\n    public static final int CENTER = 17;\n    public static final int CENTER_HORIZONTAL = 1;\n    public static final int CENTER_VERTICAL = 16;\n    public static final int CLIP_HORIZONTAL = 8;\n    public static final int CLIP_VERTICAL = 128;\n    public static final int DISPLAY_CLIP_HORIZONTAL = 16777216;\n    public static final int DISPLAY_CLIP_VERTICAL = 268435456;\n    public static final int END = 8388613;\n    public static final int FILL = 119;\n    public static final int FILL_HORIZONTAL = 7;\n    public static final int FILL_VERTICAL = 112;\n    public static final int HORIZONTAL_GRAVITY_MASK = 7;\n    public static final int LEFT = 3;\n    public static final int NO_GRAVITY = 0;\n    public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = 8388615;\n    public static final int RELATIVE_LAYOUT_DIRECTION = 8388608;\n    public static final int RIGHT = 5;\n    public static final int START = 8388611;\n    public static final int TOP = 48;\n    public static final int VERTICAL_GRAVITY_MASK = 112;\n   */\n\n  // 0001 0001\n  static const Center = 17;\n  // 0011 0000\n  static const Top = 48;\n  // 0101 0000\n  static const Bottom = 80;\n  // 0000 0011\n  static const Left = 3;\n  // 0000 0101\n  static const Right = 5;\n\n  static final _values = {\n    GravityType.Center: Center,\n    GravityType.CenterTop: Top | Center,\n    GravityType.CenterBottom: Bottom | Center,\n    GravityType.LeftTop: Top | Left,\n    GravityType.LeftCenter: Center | Left,\n    GravityType.LeftBottom: Bottom | Left,\n    GravityType.RightTop: Top | Right,\n    GravityType.RightCenter: Center | Right,\n    GravityType.RightBottom: Bottom | Right,\n  };\n\n  int? toInt() {\n    return _values[this];\n  }\n\n  GravityType? fromInt(int? v) {\n    if (v == null) return null;\n    var r = _values.keys\n        .firstWhere((e) => _values[e] == v, orElse: () => GravityType.Unknown);\n    return r == GravityType.Unknown ? null : r;\n  }\n\n  /// convert offset in topleft to others\n  Offset apply(\n    Offset o, {\n    required double width,\n    required double height,\n  }) {\n    var v = this.toInt();\n    if (v == null) return o;\n\n    var dx = o.dx;\n    var dy = o.dy;\n\n    // Reserved for future gravity calculation\n    // var halfWidth = width / 2;\n    // var halfHeight = height / 2;\n\n    // calcute the x: & 0000 1111 = 15\n    // 3 1 5 => -1 0 1 => 0 1 2\n    // dx += ((v&15) / 2) * halfWidth;\n    // if (v&15 == 1) {\n    //   dx += halfWidth;\n    // } else if (v&15 == 2) {\n    //   dx += width;\n    // }\n\n    // // calcute the y: & 1111 0000 = 240\n    // // 48 16 80 => 0 1 2\n    // dy += ((v&240) / 2) * halfHeight;\n\n    return Offset(dx, dy);\n  }\n}\n"
  },
  {
    "path": "lib/src/event.dart",
    "content": "import 'dart:developer';\n\nimport 'package:flutter/services.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\ntypedef WindowListener = dynamic Function(Window window, dynamic data);\n\n/// events name\nenum EventType {\n  WindowCreated,\n  WindowStarted,\n  WindowPaused,\n  WindowResumed,\n  WindowDestroy,\n\n  WindowDragStart,\n  WindowDragging,\n  WindowDragEnd,\n}\n\nextension _EventType on EventType {\n  static final _names = {\n    EventType.WindowCreated: \"window.created\",\n    EventType.WindowStarted: \"window.started\",\n    EventType.WindowPaused: \"window.paused\",\n    EventType.WindowResumed: \"window.resumed\",\n    EventType.WindowDestroy: \"window.destroy\",\n    EventType.WindowDragStart: \"window.drag_start\",\n    EventType.WindowDragging: \"window.dragging\",\n    EventType.WindowDragEnd: \"window.drag_end\",\n  };\n\n  /// Parse event type from string (reserved for future use)\n  // ignore: unused_element\n  static EventType? fromString(String v) {\n    try {\n      return EventType.values.firstWhere((e) => e.name == v);\n    } catch (_) {\n      return null;\n    }\n  }\n\n  String get name => _names[this]!;\n}\n\n/// Event is a common event\nclass Event {\n  /// id is window id\n  String? id;\n\n  /// name is the event name\n  String? name;\n\n  /// data is the payload for event\n  dynamic data;\n\n  Event({\n    this.id,\n    this.name,\n    this.data,\n  });\n\n  factory Event.fromMap(Map<dynamic, dynamic> map) {\n    return Event(id: map[\"id\"], name: map[\"name\"], data: map[\"data\"]);\n  }\n}\n\n// final SendPort? _send = IsolateNameServer.lookupPortByName(SEND_PORT_NAME);\nclass EventManager {\n  EventManager._(this._msgChannel) {\n    // set just for window, so window have no need to do this\n    _msgChannel.setMessageHandler((msg) {\n      var map = msg as Map<dynamic, dynamic>?;\n      if (map == null) {\n        log(\"[event] unsupported message, we except a map\");\n      }\n      var evt = Event.fromMap(map!);\n      var rs = sink(evt);\n      log(\"[event] handled event: ${evt.name}, handlers: ${rs.length}\");\n      return Future.value(null);\n    });\n  }\n\n  // event listenders\n  // because enum from string O(n), so just use string\n  // Map<String, Map<Window, List<WindowListener>>> _listeners = {};\n  // w.id -> type -> w -> [cb]\n  Map<String, Map<String, Map<Window, List<WindowListener>>>> _listeners = {};\n\n  Map<String, List<Window>> _windows = {};\n\n  BasicMessageChannel _msgChannel;\n\n  // make sure one channel must only have one instance\n  static final Map<String, EventManager> _instances = {};\n\n  factory EventManager(\n    BasicMessageChannel _msgChannel, {\n    Window? window,\n  }) {\n    if (_instances[_msgChannel.name] == null) {\n      _instances[_msgChannel.name] = EventManager._(_msgChannel);\n    }\n\n    var current = _instances[_msgChannel.name]!;\n\n    // store the window which create the event manager\n    if (window != null) {\n      if (current._windows[window.id] == null) current._windows[window.id] = [];\n      current._windows[window.id]!.add(window);\n    }\n\n    // make sure one message channel only one event manager\n    return current;\n  }\n\n  List<dynamic> sink(Event evt) {\n    var res = [];\n    // w.id -> type -> w -> [cb]\n\n    // get windows\n    var ws = (_listeners[evt.id] ?? {})[evt.name] ?? {};\n    ws.forEach((w, cbs) {\n      (cbs).forEach((c) {\n        res.add(c(w, evt.data));\n      });\n    });\n    return res;\n  }\n\n  EventManager on(Window window, EventType type, WindowListener callback) {\n    var key = type.name;\n    log(\"[event] register listener $key for $window\");\n    // w.id -> w -> type -> [cb]\n    if (_listeners[window.id] == null) _listeners[window.id] = {};\n    if (_listeners[window.id]![key] == null) _listeners[window.id]![key] = {};\n    if (_listeners[window.id]![key]![window] == null)\n      _listeners[window.id]![key]![window] = [];\n    if (!_listeners[window.id]![key]![window]!.contains(callback))\n      _listeners[window.id]![key]![window]!.add(callback);\n    return this;\n  }\n\n  @override\n  String toString() {\n    return \"EventManager@${super.hashCode}\";\n  }\n}\n"
  },
  {
    "path": "lib/src/plugin.dart",
    "content": "import 'dart:async';\nimport 'dart:developer';\nimport 'dart:ui';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nclass FloatwingPlugin {\n  FloatwingPlugin._() {\n    WidgetsFlutterBinding.ensureInitialized();\n\n    // make sure this only be called once\n    // what happens when multiple window instances\n    // are created and register event handlers?\n    // Window().on(): id -> [Window, Window]\n    // _eventManager = EventManager(_msgChannel);\n\n    // _bgChannel.setMethodCallHandler((call) {\n    //   var id = call.arguments as String;\n    //   // if we are window egine, should call main engine\n    //   FloatwingPlugin().windows[id]?.eventManager?.sink(call.method, call.arguments);\n    //   switch (call.method) {\n\n    //   }\n    //   return Future.value(null);\n    // });\n  }\n\n  static const String channelID = \"im.zoe.labs/flutter_floatwing\";\n\n  static final MethodChannel _channel = MethodChannel('$channelID/method');\n\n  // Reserved for future background communication\n  // ignore: unused_field\n  static final MethodChannel _bgChannel = MethodChannel('$channelID/bg_method');\n\n  // Reserved for future message-based communication\n  // ignore: unused_field\n  static final BasicMessageChannel _msgChannel =\n      BasicMessageChannel('$channelID/bg_message', JSONMessageCodec());\n\n  static final FloatwingPlugin _instance = FloatwingPlugin._();\n\n  /// event manager\n  // EventManager? _eventManager;\n\n  /// flag for inited\n  bool _inited = false;\n\n  /// permission granted already (updated by initialize)\n  // ignore: unused_field\n  bool? _permissionGranted;\n\n  /// service running already (updated by initialize)\n  // ignore: unused_field\n  bool? _serviceRunning;\n\n  /// _windows for the main engine to manage the windows started\n  /// items added by start function\n  Map<String, Window> _windows = {};\n\n  /// reutrn all windows only works for main engine\n  Map<String, Window> get windows =>\n      _windows; // _windows.entries.map<Window>((e) => e.value).toList();\n\n  /// _window for the sub window engine to manage it's self\n  /// setted after window's engine start and initital call\n  Window? _window;\n\n  /// return current window for window's engine\n  Window? get currentWindow => _window;\n\n  /// i'm window engine, default is the main engine\n  /// if we sync success, we set to true.\n  bool get isWindow => _isWindow;\n  bool _isWindow = false;\n\n  factory FloatwingPlugin() {\n    return _instance;\n  }\n\n  FloatwingPlugin get instance {\n    return _instance;\n  }\n\n  /// sync make the plugin to sync windows from services\n  Future<bool> syncWindows() async {\n    var _ws = await _channel.invokeListMethod(\"plugin.sync_windows\");\n    _ws?.forEach((e) {\n      var w = Window.fromMap(e);\n      _windows[w.id] = w;\n    });\n    return true;\n  }\n\n  Future<SystemConfig> _getValidSystemConfig() async {\n    var config = SystemConfig();\n    if (config.screenWidth != null &&\n        config.screenWidth! > 0 &&\n        config.screenHeight != null &&\n        config.screenHeight! > 0) {\n      return config;\n    }\n\n    final completer = Completer<SystemConfig>();\n    void checkMetrics(Duration _) {\n      final view = PlatformDispatcher.instance.implicitView;\n      final size = view?.physicalSize ?? Size.zero;\n      if (size.width > 0 && size.height > 0) {\n        completer.complete(SystemConfig());\n      } else {\n        WidgetsBinding.instance.addPostFrameCallback(checkMetrics);\n      }\n    }\n\n    WidgetsBinding.instance.addPostFrameCallback(checkMetrics);\n    return completer.future.timeout(\n      const Duration(seconds: 5),\n      onTimeout: () => config,\n    );\n  }\n\n  Future<bool> initialize() async {\n    if (_inited) return false;\n    _inited = true;\n\n    final systemConfig = await _getValidSystemConfig();\n    final view = PlatformDispatcher.instance.implicitView;\n\n    var map = await _channel.invokeMapMethod(\"plugin.initialize\", {\n      \"pixelRadio\": view?.devicePixelRatio ?? 1.0,\n      \"system\": systemConfig.toMap(),\n    });\n\n    log(\"[plugin] initialize result: $map\");\n\n    _serviceRunning = map?[\"service_running\"];\n    _permissionGranted = map?[\"permission_grated\"];\n\n    var ws = map?[\"windows\"] as List<dynamic>?;\n    ws?.forEach((e) {\n      var w = Window.fromMap(e);\n      _windows[w.id] = w;\n    });\n\n    log(\"[plugin] there are ${_windows.length} windows already started\");\n\n    return true;\n  }\n\n  Future<bool> checkPermission() async {\n    return await _channel.invokeMethod(\"plugin.has_permission\");\n  }\n\n  Future<bool> openPermissionSetting() async {\n    return await _channel.invokeMethod(\"plugin.open_permission_setting\");\n  }\n\n  Future<bool> isServiceRunning() async {\n    return await _channel.invokeMethod(\"plugin.is_service_running\");\n  }\n\n  Future<bool> startService() async {\n    return await _channel.invokeMethod(\"plugin.start_service\");\n  }\n\n  Future<bool> cleanCache() async {\n    return await _channel.invokeMethod(\"plugin.clean_cache\");\n  }\n\n  /// create window to create a window\n  Future<Window?> createWindow(\n    String? id,\n    WindowConfig config, {\n    bool start = false, // start immediately if true\n    Window? window,\n  }) async {\n    var w = isWindow\n        ? await currentWindow?.createChildWindow(id, config,\n            start: start, window: window)\n        : await internalCreateWindow(id, config,\n            start: start, window: window, channel: _channel);\n    if (w == null) return null;\n    // store current window for window engine\n    // for window engine use, update the current window\n    // if we use create_window first?\n    // _window = w; // we should don't use create_window first!!!\n    // store the window to cache\n    _windows[w.id] = w;\n    return w;\n  }\n\n  // create window object for main engine\n  Future<Window?> internalCreateWindow(\n    String? id,\n    WindowConfig config, {\n    bool start = false, // start immediately if true\n    Window? window,\n    required MethodChannel channel,\n    String name = \"plugin.create_window\",\n  }) async {\n    // check permission first\n    if (!await checkPermission()) {\n      throw Exception(\"no permission to create window\");\n    }\n\n    // store the window first\n    // window.id can't be updated\n    // for main engine use\n    // if (window != null) _windows[window.id] = window;\n    var updates = await channel.invokeMapMethod(name, {\n      \"id\": id,\n      \"config\": config.toMap(),\n      \"start\": start,\n    });\n    // if window is not created, new one\n    return updates == null ? null : (window ?? Window()).applyMap(updates);\n  }\n\n  /// ensure window make sure the window object sync from android\n  /// call this as soon at posible when engine start\n  /// you should only call this in the window engine\n  /// if only main as entry point, it's ok to call this\n  /// and return nothing\n  // only window engine call this\n  // make sure window engine return only one window from every where\n  Future<Window?> ensureWindow() async {\n    // window object don't have sync method, we must do at here\n    // assert if you are in main engine should call this\n    var map = await Window.sync();\n    log(\"[window] sync window object from android: $map\");\n    if (map == null) return null;\n    // store current window if needed\n    // use the static window first\n    // so sync will return only one instance of window\n    // improve this logic\n    // means first time call sync, just create a new window\n    if (_window == null) _window = Window();\n    _window!.applyMap(map);\n    _isWindow = true;\n    return _window;\n  }\n\n  /// `on` register event handlers for all windows\n  /// or we can use stream mode\n  FloatwingPlugin on(EventType type, WindowListener callback) {\n    // TODO:\n    return this;\n  }\n}\n"
  },
  {
    "path": "lib/src/provider.dart",
    "content": "import 'dart:developer';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\n// typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child);\n// typedef WidgetBuilder = Widget Function(BuildContext context);\n\nclass FloatwingProvider extends InheritedWidget {\n  final Window? window;\n  final Widget child;\n\n  FloatwingProvider({\n    Key? key,\n    required this.child,\n    required this.window,\n  }) : super(key: key, child: child);\n\n  @override\n  bool updateShouldNotify(FloatwingProvider oldWidget) {\n    return true;\n  }\n}\n\nclass FloatwingContainer extends StatefulWidget {\n  final Widget? child;\n  final WidgetBuilder? builder;\n  final bool debug;\n  final bool app;\n\n  const FloatwingContainer({\n    Key? key,\n    this.child,\n    this.builder,\n    this.debug = false,\n    this.app = false,\n  })  : assert(child != null || builder != null),\n        super(key: key);\n\n  @override\n  State<FloatwingContainer> createState() => _FloatwingContainerState();\n}\n\nclass _FloatwingContainerState extends State<FloatwingContainer> {\n  Window? _window = FloatwingPlugin().currentWindow;\n\n  var _ignorePointer = false;\n  var _autosize = true;\n\n  @override\n  void initState() {\n    super.initState();\n    initSyncState();\n  }\n\n  initSyncState() async {\n    // send started message to service\n    // this make sure ui already\n    if (_window == null) {\n      log(\"[provider] have not sync window at init, need to do at here\");\n      await FloatwingPlugin().ensureWindow().then((w) => _window = w);\n    }\n    // init window from engine and save, only call this int here\n    // sync a window from engine\n    _changed();\n    _window?.on(EventType.WindowResumed, (w, _) => _changed());\n  }\n\n  Widget _empty = Container();\n\n  @override\n  Widget build(BuildContext context) {\n    // make sure window is ready?\n    if (!widget.debug && _window == null) return _empty;\n    // in production, make sure builder when window is ready\n    return Builder(builder: widget.builder ?? (_) => widget.child!)\n        ._provider(_window)\n        ._autosize(enabled: _autosize, onChange: _onSizeChanged)\n        ._material(color: Colors.transparent)\n        ._pointerless(_ignorePointer)\n        ._app(enabled: widget.app, debug: widget.debug);\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    // TODO: remove event listener\n    // w.un(\"resumed\").un(\"\")\n  }\n\n  _changed() async {\n    // clickable == !ignorePointer\n    _ignorePointer = !(_window?.config?.clickable ?? true);\n    _autosize = _window?.config?.autosize ?? true;\n    // update the flutter ui\n    if (mounted) setState(() {});\n  }\n\n  _onSizeChanged(Size size) {\n    var radio = _window?.pixelRadio ?? 1;\n    _window?.update(WindowConfig(\n      width: (size.width * radio).toInt(),\n      height: (size.height * radio).toInt(),\n    ));\n  }\n}\n\nclass _MeasuredSized extends StatefulWidget {\n  const _MeasuredSized({\n    Key? key,\n    required this.onChange,\n    required this.child,\n    this.delay = 0,\n  }) : super(key: key);\n\n  final Widget child;\n\n  final int delay;\n\n  final void Function(Size size)? onChange;\n\n  @override\n  _MeasuredSizedState createState() => _MeasuredSizedState();\n}\n\nclass _MeasuredSizedState extends State<_MeasuredSized> {\n  @override\n  void initState() {\n    SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.onChange == null) return widget.child;\n    SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);\n    return UnconstrainedBox(\n      child: Container(\n        key: widgetKey,\n        child: NotificationListener<SizeChangedLayoutNotification>(\n          onNotification: (_) {\n            SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);\n            return true;\n          },\n          child: SizeChangedLayoutNotifier(child: widget.child),\n        ),\n      ),\n    );\n  }\n\n  final widgetKey = GlobalKey();\n  Size? oldSize;\n\n  void postFrameCallback(Duration _) async {\n    final ctx = widgetKey.currentContext;\n    if (ctx == null) return;\n\n    if (widget.delay > 0) {\n      await Future<void>.delayed(Duration(milliseconds: widget.delay));\n    }\n    if (mounted == false) return;\n\n    final newSize = ctx.size;\n    if (newSize == null || newSize == Size.zero) return;\n    // if (oldSize == newSize) return;\n    oldSize = newSize;\n    widget.onChange!(newSize);\n  }\n}\n\ntypedef DragCallback = void Function(Offset offset);\n\nclass _DragAnchor extends StatefulWidget {\n  final Widget child;\n  // TODO:\n  // final bool horizontal;\n  // final bool vertical;\n\n  // final DragCallback? onDragStart;\n  // final DragCallback? onDragUpdate;\n  // final DragCallback? onDragEnd;\n\n  const _DragAnchor({\n    Key? key,\n    required this.child,\n\n    // this.horizontal = true,\n    // this.vertical = true,\n\n    // this.onDragStart,\n    // this.onDragUpdate,\n    // this.onDragEnd,\n  }) : super(key: key);\n\n  @override\n  State<_DragAnchor> createState() => _DragAnchorState();\n}\n\nclass _DragAnchorState extends State<_DragAnchor> {\n  @override\n  Widget build(BuildContext context) {\n    // return Draggable();\n    return GestureDetector(\n      onTapDown: _enableDrag,\n      onTapUp: _disableDrag2,\n      onTapCancel: _disableDrag,\n      child: widget.child,\n    );\n  }\n\n  _enableDrag(_) {\n    // enabe drag\n    Window.of(context)?.update(WindowConfig(\n      draggable: true,\n    ));\n  }\n\n  _disableDrag() {\n    // disable drag\n    Window.of(context)?.update(WindowConfig(\n      draggable: false,\n    ));\n  }\n\n  _disableDrag2(_) {\n    _disableDrag();\n  }\n}\n\nclass _ResizeAnchor extends StatefulWidget {\n  final Widget child;\n\n  // Reserved for future resize direction support\n  // ignore: unused_element\n  final bool horizontal;\n  // ignore: unused_element\n  final bool vertical;\n\n  const _ResizeAnchor({\n    Key? key,\n    required this.child,\n    this.horizontal = true,\n    this.vertical = true,\n  }) : super(key: key);\n\n  @override\n  State<_ResizeAnchor> createState() => __ResizeAnchorState();\n}\n\nclass __ResizeAnchorState extends State<_ResizeAnchor> {\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onScaleStart: (v) {\n        print(\"=======> scale start $v\");\n      },\n      onScaleUpdate: (v) {\n        print(\"=======> scale update $v\");\n      },\n      onScaleEnd: (v) {\n        print(\"=======> scale end $v\");\n      },\n      child: widget.child,\n    );\n  }\n}\n\nextension WidgetProviderExtension on Widget {\n  /// Export floatwing extension function to inject for root widget\n  Widget floatwing({\n    bool debug = false,\n    bool app = false,\n  }) {\n    return FloatwingContainer(child: this, debug: debug, app: app);\n  }\n\n  /// Export draggable extension function to inject for child widget\n  // Widget draggable({\n  //   bool enabled = true,\n  // }) {\n  //   return enabled?_DragAnchor(child: this):this;\n  // }\n\n  /// Export resizable extension function to inject for child\n  // Widget resizable({\n  //   bool enabled = true,\n  // }) {\n  //   return enabled?_ResizeAnchor(child: this):this;\n  // }\n\n  Widget _provider(Window? window) {\n    return FloatwingProvider(child: this, window: window);\n  }\n\n  Widget _autosize({\n    bool enabled = false,\n    void Function(Size)? onChange,\n    int delay = 0,\n  }) {\n    return !enabled\n        ? this\n        : _MeasuredSized(child: this, delay: delay, onChange: onChange);\n  }\n\n  Widget _pointerless([bool ignoring = false]) {\n    return IgnorePointer(child: this, ignoring: ignoring);\n  }\n\n  Widget _material({\n    bool enabled = false,\n    Color? color,\n  }) {\n    return !enabled ? this : Material(color: color, child: this);\n  }\n\n  Widget _app({\n    bool enabled = false,\n    bool debug = false,\n  }) {\n    return !enabled\n        ? this\n        : MaterialApp(debugShowCheckedModeBanner: debug, home: this);\n  }\n}\n\nextension WidgetBuilderProviderExtension on WidgetBuilder {\n  WidgetBuilder floatwing({\n    bool debug = false,\n    bool app = false,\n  }) {\n    return (_) => FloatwingContainer(\n          builder: this,\n          debug: debug,\n          app: app,\n        );\n  }\n\n  Widget make() {\n    return Builder(builder: this);\n  }\n}\n"
  },
  {
    "path": "lib/src/utils.dart",
    "content": "import 'dart:ui';\n\nclass SystemConfig {\n  int? pixelRadio;\n  int? screenWidth;\n  int? screenHeight;\n\n  Size? screenSize;\n\n  SystemConfig._({\n    this.pixelRadio,\n    this.screenWidth,\n    this.screenHeight,\n  }) {\n    var w = screenWidth?.toDouble();\n    var h = screenHeight?.toDouble();\n    if (w != null && h != null) screenSize = Size(w, h);\n  }\n\n  Map<dynamic, dynamic> toMap() {\n    return {\n      \"pixelRadio\": pixelRadio,\n      \"screen\": {\n        \"height\": screenHeight,\n        \"width\": screenWidth,\n      },\n    };\n  }\n\n  @override\n  String toString() {\n    return \"${toMap()} $screenSize\";\n  }\n\n  factory SystemConfig() {\n    final view = PlatformDispatcher.instance.implicitView;\n    return SystemConfig._(\n      pixelRadio: view?.devicePixelRatio.toInt() ?? 1,\n      screenHeight: view?.physicalSize.height.toInt() ?? 0,\n      screenWidth: view?.physicalSize.width.toInt() ?? 0,\n    );\n  }\n\n  factory SystemConfig.fromMap(Map<dynamic, dynamic> map) {\n    var screen = map[\"screen\"] ?? {};\n    return SystemConfig._(\n      pixelRadio: map[\"pixelRadio\"],\n      screenHeight: screen[\"height\"],\n      screenWidth: screen[\"width\"],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/src/window.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'dart:convert';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\ntypedef OnDataHanlder = Future<dynamic> Function(\n    String? source, String? name, dynamic data);\n\nclass Window {\n  String id = \"default\";\n  WindowConfig? config;\n\n  double? pixelRadio;\n  SystemConfig? system;\n  OnDataHanlder? _onDataHandler;\n\n  late EventManager _eventManager;\n\n  Window({this.id = \"default\", this.config}) {\n    _eventManager = EventManager(_message, window: this);\n\n    // share data use the call\n    _channel.setMethodCallHandler((call) {\n      switch (call.method) {\n        case \"data.share\":\n          {\n            var map = call.arguments as Map<dynamic, dynamic>;\n            // source, name, data\n            // if not provided, should not call this\n            return _onDataHandler?.call(\n                    map[\"source\"], map[\"name\"], map[\"data\"]) ??\n                Future.value(null);\n          }\n      }\n      return Future.value(null);\n    });\n  }\n\n  static final MethodChannel _channel =\n      MethodChannel('${FloatwingPlugin.channelID}/window');\n  static final BasicMessageChannel _message = BasicMessageChannel(\n      '${FloatwingPlugin.channelID}/window_msg', JSONMessageCodec());\n\n  factory Window.fromMap(Map<dynamic, dynamic>? map) {\n    return Window().applyMap(map);\n  }\n\n  @override\n  String toString() {\n    return \"Window[$id]@${super.hashCode}, ${_eventManager.toString()}, config: $config\";\n  }\n\n  Window applyMap(Map<dynamic, dynamic>? map) {\n    // apply the map to config and object\n    if (map == null) return this;\n    id = map[\"id\"];\n    pixelRadio = map[\"pixelRadio\"] ?? 1.0;\n    system = SystemConfig.fromMap(map[\"system\"] ?? {});\n    config = WindowConfig.fromMap(map[\"config\"]);\n    return this;\n  }\n\n  /// `of` extact window object window from context\n  /// The data from the closest instance of this class that encloses the given\n  /// context.\n  static Window? of(BuildContext context) {\n    return context\n        .dependOnInheritedWidgetOfExactType<FloatwingProvider>()\n        ?.window;\n  }\n\n  Future<bool?> hide() {\n    return show(visible: false);\n    // return FloatwingPlugin().showWindow(id, false);\n  }\n\n  Future<bool?> close({bool force = false}) async {\n    // return await FloatwingPlugin().closeWindow(id, force: force);\n    return await _channel.invokeMethod(\"window.close\", {\n      \"id\": id,\n      \"force\": force,\n    }).then((v) {\n      // remove the window from plugin\n      FloatwingPlugin().windows.remove(id);\n      return v;\n    });\n  }\n\n  Future<Window?> create({bool start = false}) async {\n    // // create the engine first\n    return await FloatwingPlugin()\n        .createWindow(this.id, this.config!, start: start, window: this);\n  }\n\n  /// create child window\n  /// just method shoudld only called in window engine\n  Future<Window?> createChildWindow(\n    String? id,\n    WindowConfig config, {\n    bool start = false, // start immediately if true\n    Window? window,\n  }) async {\n    return FloatwingPlugin().internalCreateWindow(id, config,\n        start: start,\n        window: window,\n        channel: _channel,\n        name: \"window.create_child\");\n  }\n\n  Future<bool?> start() async {\n    assert(config != null, \"config can't be null\");\n    return await _channel.invokeMethod(\"window.start\", {\n      \"id\": id,\n    });\n    // return await FloatwingPlugin().startWindow(id);\n  }\n\n  Future<bool> update(WindowConfig cfg) async {\n    // update window with config, config con't update with id, entry, route\n    var size = config?.size;\n    if (size != null && size < Size.zero) {\n      // special case, should updated\n      cfg.width = null;\n      cfg.height = null;\n    }\n    var updates = await _channel.invokeMapMethod(\"window.update\", {\n      \"id\": id,\n      // don't set pixelRadio\n      \"config\": cfg.toMap(),\n    });\n    // var updates = await FloatwingPlugin().updateWindow(id, cfg);\n    // update the plugin store\n    applyMap(updates);\n    return true;\n  }\n\n  Future<bool?> show({bool visible = true}) async {\n    config?.visible = visible;\n    return await _channel.invokeMethod(\"window.show\", {\n      \"id\": id,\n      \"visible\": visible,\n    }).then((v) {\n      // update the plugin store\n      if (v) FloatwingPlugin().windows[id]?.config?.visible = visible;\n      return v;\n    });\n  }\n\n  /// share data with current window\n  /// send data use current window id as target id\n  /// and get value return\n  Future<dynamic> share(\n    dynamic data, {\n    String name = \"default\",\n  }) async {\n    var map = {};\n    map[\"target\"] = id;\n    map[\"data\"] = data;\n    map[\"name\"] = name;\n    // make sure data is serialized\n    return await _channel.invokeMethod(\"data.share\", map);\n  }\n\n  /// launch main activity\n  Future<bool> launchMainActivity() async {\n    return await _channel.invokeMethod(\"window.launch_main\");\n  }\n\n  /// on data to receive data from other shared\n  /// maybe same like event handler\n  /// but one window in engine can only have one data handler\n  /// to make sure data not be comsumed multiple times.\n  Window onData(OnDataHanlder handler) {\n    assert(_onDataHandler == null, \"onData can only called once\");\n    _onDataHandler = handler;\n    return this;\n  }\n\n  // sync window object from android service\n  // only window engine call this\n  // if we manage other windows in some window engine\n  // this will not works, we must improve it\n  static Future<Map<dynamic, dynamic>?> sync() async {\n    return await _channel.invokeMapMethod(\"window.sync\");\n  }\n\n  /// on register callback to listener\n  Window on(EventType type, WindowListener callback) {\n    _eventManager.on(this, type, callback);\n    return this;\n  }\n\n  Map<String, dynamic> toMap() {\n    var map = Map<String, dynamic>();\n    map[\"id\"] = id;\n    map[\"pixelRadio\"] = pixelRadio;\n    map[\"config\"] = config?.toMap();\n    return map;\n  }\n}\n\nclass WindowConfig {\n  String? id;\n\n  String? entry;\n  String? route;\n  Function? callback; // use callback to start engine\n\n  bool? autosize;\n\n  int? width;\n  int? height;\n  int? x;\n  int? y;\n\n  int? format;\n  GravityType? gravity;\n  int? type;\n\n  bool? clickable;\n  bool? draggable;\n  bool? focusable;\n\n  /// immersion status bar\n  bool? immersion;\n\n  bool? visible;\n\n  /// we need this for update, so must wihtout default value\n  WindowConfig({\n    this.id = \"default\",\n    this.entry = \"main\",\n    this.route,\n    this.callback,\n    this.autosize,\n    this.width,\n    this.height,\n    this.x,\n    this.y,\n    this.format,\n    this.gravity,\n    this.type,\n    this.clickable,\n    this.draggable,\n    this.focusable,\n    this.immersion,\n    this.visible,\n  }) : assert(\n            callback == null ||\n                PluginUtilities.getCallbackHandle(callback) != null,\n            \"callback is not a static function\");\n\n  factory WindowConfig.fromMap(Map<dynamic, dynamic> map) {\n    var _cb;\n    if (map[\"callback\"] != null)\n      _cb = PluginUtilities.getCallbackFromHandle(\n          CallbackHandle.fromRawHandle(map[\"callback\"]));\n    return WindowConfig(\n      // id: map[\"id\"],\n      entry: map[\"entry\"],\n      route: map[\"route\"],\n      callback: _cb, // get the callback from id\n\n      autosize: map[\"autosize\"],\n\n      width: map[\"width\"],\n      height: map[\"height\"],\n      x: map[\"x\"],\n      y: map[\"y\"],\n\n      format: map[\"format\"],\n      gravity: GravityType.Unknown.fromInt(map[\"gravity\"]),\n      type: map[\"type\"],\n\n      clickable: map[\"clickable\"],\n      draggable: map[\"draggable\"],\n      focusable: map[\"focusable\"],\n\n      immersion: map[\"immersion\"],\n\n      visible: map[\"visible\"],\n    );\n  }\n\n  Map<String, dynamic> toMap() {\n    var map = Map<String, dynamic>();\n    // map[\"id\"] = id;\n    map[\"entry\"] = entry;\n    map[\"route\"] = route;\n    // find the callback id from callback function\n    map[\"callback\"] = callback != null\n        ? PluginUtilities.getCallbackHandle(callback!)?.toRawHandle()\n        : null;\n\n    map[\"autosize\"] = autosize;\n\n    map[\"width\"] = width;\n    map[\"height\"] = height;\n    map[\"x\"] = x;\n    map[\"y\"] = y;\n\n    map[\"format\"] = format;\n    map[\"gravity\"] = gravity?.toInt();\n    map[\"type\"] = type;\n\n    map[\"clickable\"] = clickable;\n    map[\"draggable\"] = draggable;\n    map[\"focusable\"] = focusable;\n\n    map[\"immersion\"] = immersion;\n\n    map[\"visible\"] = visible;\n\n    return map;\n  }\n\n  // return a window frm config\n  Window to() {\n    // will lose window instance\n    return Window(id: this.id ?? \"default\", config: this);\n  }\n\n  Future<Window?> create({\n    String? id = \"default\",\n    bool start = false,\n  }) async {\n    assert(!(entry == \"main\" && route == null));\n    return await FloatwingPlugin().createWindow(id, this, start: start);\n  }\n\n  Size get size => Size((width ?? 0).toDouble(), (height ?? 0).toDouble());\n\n  @override\n  String toString() {\n    var map = this.toMap();\n    map.removeWhere((key, value) => value == null);\n    return json.encode(map).toString();\n  }\n}\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: flutter_floatwing\ndescription: A Flutter plugin that makes it easier to make floating/overlay window for Android with pure Flutter.\nversion: 0.3.1\nhomepage: https://github.com/jiusanzhou/flutter_floatwing\n\nenvironment:\n  sdk: '>=3.0.0 <4.0.0'\n  flutter: \">=1.20.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\n# The following section is specific to Flutter.\nflutter:\n  # This section identifies this Flutter project as a plugin project.\n  # The 'pluginClass' and Android 'package' identifiers should not ordinarily\n  # be modified. They are used by the tooling to maintain consistency when\n  # adding or updating assets for this project.\n  plugin:\n    platforms:\n      android:\n        package: im.zoe.labs.flutter_floatwing\n        pluginClass: FlutterFloatwingPlugin\n\n  # To add assets to your plugin package, add an assets section, like this:\n  # assets:\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n  #\n  # For details regarding assets in packages, see\n  # https://flutter.dev/assets-and-images/#from-packages\n  #\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware.\n\n  # To add custom fonts to your plugin package, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n  #\n  # For details regarding fonts in packages, see\n  # https://flutter.dev/custom-fonts/#from-packages\n"
  },
  {
    "path": "test/event_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('EventType', () {\n    test('should have all expected event types', () {\n      expect(EventType.values, contains(EventType.WindowCreated));\n      expect(EventType.values, contains(EventType.WindowStarted));\n      expect(EventType.values, contains(EventType.WindowPaused));\n      expect(EventType.values, contains(EventType.WindowResumed));\n      expect(EventType.values, contains(EventType.WindowDestroy));\n      expect(EventType.values, contains(EventType.WindowDragStart));\n      expect(EventType.values, contains(EventType.WindowDragging));\n      expect(EventType.values, contains(EventType.WindowDragEnd));\n    });\n\n    test('should have correct number of event types', () {\n      expect(EventType.values.length, equals(8));\n    });\n  });\n\n  group('Event', () {\n    test('should create with all parameters', () {\n      final event = Event(\n        id: 'window-1',\n        name: 'window.created',\n        data: {'key': 'value'},\n      );\n\n      expect(event.id, equals('window-1'));\n      expect(event.name, equals('window.created'));\n      expect(event.data, isA<Map>());\n      expect(event.data['key'], equals('value'));\n    });\n\n    test('should create with null parameters', () {\n      final event = Event();\n\n      expect(event.id, isNull);\n      expect(event.name, isNull);\n      expect(event.data, isNull);\n    });\n\n    test('should create from map correctly', () {\n      final map = {\n        'id': 'window-2',\n        'name': 'window.started',\n        'data': {'position': 'center'},\n      };\n\n      final event = Event.fromMap(map);\n\n      expect(event.id, equals('window-2'));\n      expect(event.name, equals('window.started'));\n      expect(event.data, isA<Map>());\n      expect(event.data['position'], equals('center'));\n    });\n\n    test('should handle missing fields in fromMap', () {\n      final map = <dynamic, dynamic>{\n        'id': 'window-3',\n      };\n\n      final event = Event.fromMap(map);\n\n      expect(event.id, equals('window-3'));\n      expect(event.name, isNull);\n      expect(event.data, isNull);\n    });\n\n    test('should handle dynamic data types', () {\n      final eventWithString = Event(data: 'string data');\n      expect(eventWithString.data, equals('string data'));\n\n      final eventWithInt = Event(data: 42);\n      expect(eventWithInt.data, equals(42));\n\n      final eventWithList = Event(data: [1, 2, 3]);\n      expect(eventWithList.data, equals([1, 2, 3]));\n\n      final eventWithBool = Event(data: true);\n      expect(eventWithBool.data, isTrue);\n    });\n  });\n\n  group('Event name mapping', () {\n    // Test that event names are correctly mapped (based on the _EventType extension)\n    test('WindowCreated should map to window.created', () {\n      // We can't directly test the private extension, but we can verify\n      // the event types exist and are usable\n      expect(EventType.WindowCreated, isNotNull);\n    });\n\n    test('WindowDragStart should map to window.drag_start', () {\n      expect(EventType.WindowDragStart, isNotNull);\n    });\n\n    test('all event types should be distinct', () {\n      final types = EventType.values.toSet();\n      expect(types.length, equals(EventType.values.length));\n    });\n  });\n\n  group('Window event registration', () {\n    test('should allow registering multiple event handlers', () {\n      final window = Window(id: 'test-window');\n      int handlerCount = 0;\n\n      window.on(EventType.WindowCreated, (w, data) {\n        handlerCount++;\n      }).on(EventType.WindowStarted, (w, data) {\n        handlerCount++;\n      }).on(EventType.WindowDestroy, (w, data) {\n        handlerCount++;\n      });\n\n      // The handlers are registered but not called yet\n      expect(handlerCount, equals(0));\n    });\n\n    test('on() should return window for chaining', () {\n      final window = Window(id: 'test-window');\n\n      final result = window.on(EventType.WindowCreated, (w, data) {});\n\n      expect(result, same(window));\n    });\n\n    test('should handle all event types registration', () {\n      final window = Window(id: 'test-window');\n\n      // Register handlers for all event types\n      for (final eventType in EventType.values) {\n        window.on(eventType, (w, data) {});\n      }\n\n      // If we get here without errors, all event types are registrable\n      expect(true, isTrue);\n    });\n  });\n\n  group('WindowListener typedef', () {\n    test('should accept correct function signature', () {\n      // WindowListener = dynamic Function(Window window, dynamic data)\n      WindowListener listener = (Window w, dynamic data) {\n        return 'handled';\n      };\n\n      final window = Window(id: 'test');\n      final result = listener(window, {'event': 'data'});\n\n      expect(result, equals('handled'));\n    });\n\n    test('should work with async handlers', () async {\n      WindowListener asyncListener = (Window w, dynamic data) async {\n        await Future.delayed(Duration(milliseconds: 10));\n        return 'async handled';\n      };\n\n      final window = Window(id: 'test');\n      final result = await asyncListener(window, null);\n\n      expect(result, equals('async handled'));\n    });\n\n    test('should allow void return', () {\n      WindowListener voidListener = (Window w, dynamic data) {\n        // No return\n      };\n\n      final window = Window(id: 'test');\n      final result = voidListener(window, null);\n\n      expect(result, isNull);\n    });\n  });\n}\n"
  },
  {
    "path": "test/flutter_floatwing_test.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nvoid main() {\n  const MethodChannel channel = MethodChannel('flutter_floatwing');\n\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(() {\n    channel.setMockMethodCallHandler((MethodCall methodCall) async {\n      return '42';\n    });\n  });\n\n  tearDown(() {\n    channel.setMockMethodCallHandler(null);\n  });\n\n  test('getPlatformVersion', () async {\n    // expect(await FlutterFloatwing.platformVersion, '42');\n  });\n}\n"
  },
  {
    "path": "test/plugin_test.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FloatwingPlugin', () {\n    const MethodChannel methodChannel =\n        MethodChannel('im.zoe.labs/flutter_floatwing/method');\n\n    setUp(() {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel,\n              (MethodCall methodCall) async {\n        switch (methodCall.method) {\n          case 'plugin.has_permission':\n            return true;\n          case 'plugin.open_permission_setting':\n            return true;\n          case 'plugin.is_service_running':\n            return true;\n          case 'plugin.start_service':\n            return true;\n          case 'plugin.clean_cache':\n            return true;\n          case 'plugin.initialize':\n            return {\n              'permission_grated': true,\n              'service_running': true,\n              'windows': [],\n            };\n          case 'plugin.sync_windows':\n            return [\n              {\n                'id': 'window-1',\n                'config': {'entry': 'main', 'route': '/test'},\n              },\n              {\n                'id': 'window-2',\n                'config': {'entry': 'main', 'route': '/test2'},\n              },\n            ];\n          case 'plugin.create_window':\n            return {\n              'id': methodCall.arguments['id'] ?? 'default',\n              'config': methodCall.arguments['config'],\n            };\n          default:\n            return null;\n        }\n      });\n    });\n\n    tearDown(() {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel, null);\n    });\n\n    test('should be a singleton', () {\n      final plugin1 = FloatwingPlugin();\n      final plugin2 = FloatwingPlugin();\n\n      expect(identical(plugin1, plugin2), isTrue);\n    });\n\n    test('instance getter should return same instance', () {\n      final plugin = FloatwingPlugin();\n\n      expect(identical(plugin, plugin.instance), isTrue);\n    });\n\n    test('checkPermission should return true', () async {\n      final result = await FloatwingPlugin().checkPermission();\n\n      expect(result, isTrue);\n    });\n\n    test('openPermissionSetting should return true', () async {\n      final result = await FloatwingPlugin().openPermissionSetting();\n\n      expect(result, isTrue);\n    });\n\n    test('isServiceRunning should return true', () async {\n      final result = await FloatwingPlugin().isServiceRunning();\n\n      expect(result, isTrue);\n    });\n\n    test('startService should return true', () async {\n      final result = await FloatwingPlugin().startService();\n\n      expect(result, isTrue);\n    });\n\n    test('cleanCache should return true', () async {\n      final result = await FloatwingPlugin().cleanCache();\n\n      expect(result, isTrue);\n    });\n\n    test('syncWindows should populate windows map', () async {\n      // Clear any existing state\n      FloatwingPlugin().windows.clear();\n\n      final result = await FloatwingPlugin().syncWindows();\n\n      expect(result, isTrue);\n      expect(FloatwingPlugin().windows.length, equals(2));\n      expect(FloatwingPlugin().windows.containsKey('window-1'), isTrue);\n      expect(FloatwingPlugin().windows.containsKey('window-2'), isTrue);\n    });\n\n    test('windows should return map of windows', () {\n      final windows = FloatwingPlugin().windows;\n\n      expect(windows, isA<Map<String, Window>>());\n    });\n\n    test('currentWindow should be null initially for main engine', () {\n      // In main engine, currentWindow should be null until ensureWindow is called\n      // Since we're testing as main engine, this is expected behavior\n      expect(FloatwingPlugin().currentWindow, isNull);\n    });\n\n    test('isWindow should be false for main engine', () {\n      // isWindow is false until ensureWindow succeeds with valid data\n      // For main engine tests, this should be false\n      expect(FloatwingPlugin().isWindow, isFalse);\n    });\n\n    test('createWindow should create and cache window', () async {\n      final config = WindowConfig(route: '/new-window');\n\n      final window = await FloatwingPlugin().createWindow('new-window', config);\n\n      expect(window, isNotNull);\n      expect(window?.id, equals('new-window'));\n      expect(FloatwingPlugin().windows.containsKey('new-window'), isTrue);\n    });\n\n    test('createWindow with start=true should create started window', () async {\n      final config = WindowConfig(route: '/started-window');\n\n      final window = await FloatwingPlugin()\n          .createWindow('started-window', config, start: true);\n\n      expect(window, isNotNull);\n      expect(window?.id, equals('started-window'));\n    });\n\n    test('on should return plugin for chaining', () {\n      final plugin = FloatwingPlugin();\n\n      final result = plugin.on(EventType.WindowCreated, (window, data) {});\n\n      expect(result, same(plugin));\n    });\n  });\n\n  group('FloatwingPlugin channel constants', () {\n    test('channelID should be correct', () {\n      expect(\n          FloatwingPlugin.channelID, equals('im.zoe.labs/flutter_floatwing'));\n    });\n  });\n\n  group('FloatwingPlugin permission flow', () {\n    const MethodChannel methodChannel =\n        MethodChannel('im.zoe.labs/flutter_floatwing/method');\n\n    test('should handle permission denied', () async {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel,\n              (MethodCall methodCall) async {\n        if (methodCall.method == 'plugin.has_permission') {\n          return false;\n        }\n        return null;\n      });\n\n      final result = await FloatwingPlugin().checkPermission();\n\n      expect(result, isFalse);\n\n      // Cleanup\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel, null);\n    });\n\n    test('createWindow should throw when permission denied', () async {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel,\n              (MethodCall methodCall) async {\n        if (methodCall.method == 'plugin.has_permission') {\n          return false;\n        }\n        return null;\n      });\n\n      final config = WindowConfig(route: '/test');\n\n      expect(\n        () => FloatwingPlugin().createWindow('test', config),\n        throwsException,\n      );\n\n      // Cleanup\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(methodChannel, null);\n    });\n  });\n}\n"
  },
  {
    "path": "test/utils_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:flutter_floatwing/src/utils.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('SystemConfig', () {\n    test('should create from map correctly', () {\n      final map = {\n        'pixelRadio': 2,\n        'screen': {\n          'width': 1080,\n          'height': 1920,\n        },\n      };\n\n      final config = SystemConfig.fromMap(map);\n\n      expect(config.pixelRadio, equals(2));\n      expect(config.screenWidth, equals(1080));\n      expect(config.screenHeight, equals(1920));\n      expect(config.screenSize, isNotNull);\n      expect(config.screenSize?.width, equals(1080.0));\n      expect(config.screenSize?.height, equals(1920.0));\n    });\n\n    test('should handle missing screen data', () {\n      final map = {\n        'pixelRadio': 3,\n      };\n\n      final config = SystemConfig.fromMap(map);\n\n      expect(config.pixelRadio, equals(3));\n      expect(config.screenWidth, isNull);\n      expect(config.screenHeight, isNull);\n      expect(config.screenSize, isNull);\n    });\n\n    test('should handle empty map', () {\n      final config = SystemConfig.fromMap({});\n\n      expect(config.pixelRadio, isNull);\n      expect(config.screenWidth, isNull);\n      expect(config.screenHeight, isNull);\n      expect(config.screenSize, isNull);\n    });\n\n    test('should convert to map correctly', () {\n      final map = {\n        'pixelRadio': 2,\n        'screen': {\n          'width': 1080,\n          'height': 1920,\n        },\n      };\n\n      final config = SystemConfig.fromMap(map);\n      final result = config.toMap();\n\n      expect(result['pixelRadio'], equals(2));\n      expect(result['screen'], isA<Map>());\n      expect(result['screen']['width'], equals(1080));\n      expect(result['screen']['height'], equals(1920));\n    });\n\n    test('toString should return map and size representation', () {\n      final map = {\n        'pixelRadio': 2,\n        'screen': {\n          'width': 1080,\n          'height': 1920,\n        },\n      };\n\n      final config = SystemConfig.fromMap(map);\n      final str = config.toString();\n\n      expect(str, contains('pixelRadio'));\n      expect(str, contains('1080'));\n      expect(str, contains('1920'));\n    });\n\n    test('should create Size object only when both dimensions are present', () {\n      // Both dimensions present\n      final config1 = SystemConfig.fromMap({\n        'screen': {'width': 100, 'height': 200},\n      });\n      expect(config1.screenSize, isNotNull);\n\n      // Only width present\n      final config2 = SystemConfig.fromMap({\n        'screen': {'width': 100},\n      });\n      expect(config2.screenSize, isNull);\n\n      // Only height present\n      final config3 = SystemConfig.fromMap({\n        'screen': {'height': 200},\n      });\n      expect(config3.screenSize, isNull);\n\n      // Neither present\n      final config4 = SystemConfig.fromMap({\n        'screen': {},\n      });\n      expect(config4.screenSize, isNull);\n    });\n\n    test('should preserve exact values through toMap', () {\n      final originalMap = {\n        'pixelRadio': 3,\n        'screen': {\n          'width': 2560,\n          'height': 1440,\n        },\n      };\n\n      final config = SystemConfig.fromMap(originalMap);\n      final resultMap = config.toMap();\n\n      expect(resultMap['pixelRadio'], equals(originalMap['pixelRadio']));\n      final originalScreen = originalMap['screen'] as Map<String, dynamic>;\n      final resultScreen = resultMap['screen'] as Map<dynamic, dynamic>;\n      expect(\n        resultScreen['width'],\n        equals(originalScreen['width']),\n      );\n      expect(\n        resultScreen['height'],\n        equals(originalScreen['height']),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/window_config_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('WindowConfig', () {\n    test('should create with default values', () {\n      final config = WindowConfig();\n\n      expect(config.id, equals('default'));\n      expect(config.entry, equals('main'));\n      expect(config.route, isNull);\n      expect(config.callback, isNull);\n      expect(config.width, isNull);\n      expect(config.height, isNull);\n      expect(config.x, isNull);\n      expect(config.y, isNull);\n      expect(config.autosize, isNull);\n      expect(config.gravity, isNull);\n      expect(config.clickable, isNull);\n      expect(config.draggable, isNull);\n      expect(config.focusable, isNull);\n      expect(config.immersion, isNull);\n      expect(config.visible, isNull);\n    });\n\n    test('should create with custom values', () {\n      final config = WindowConfig(\n        id: 'test-window',\n        entry: 'customEntry',\n        route: '/test',\n        width: 200,\n        height: 300,\n        x: 10,\n        y: 20,\n        autosize: true,\n        gravity: GravityType.Center,\n        clickable: true,\n        draggable: true,\n        focusable: false,\n        immersion: true,\n        visible: true,\n      );\n\n      expect(config.id, equals('test-window'));\n      expect(config.entry, equals('customEntry'));\n      expect(config.route, equals('/test'));\n      expect(config.width, equals(200));\n      expect(config.height, equals(300));\n      expect(config.x, equals(10));\n      expect(config.y, equals(20));\n      expect(config.autosize, isTrue);\n      expect(config.gravity, equals(GravityType.Center));\n      expect(config.clickable, isTrue);\n      expect(config.draggable, isTrue);\n      expect(config.focusable, isFalse);\n      expect(config.immersion, isTrue);\n      expect(config.visible, isTrue);\n    });\n\n    test('should convert to map correctly', () {\n      final config = WindowConfig(\n        id: 'test-window',\n        route: '/test',\n        width: 200,\n        height: 300,\n        draggable: true,\n      );\n\n      final map = config.toMap();\n\n      expect(map['entry'], equals('main'));\n      expect(map['route'], equals('/test'));\n      expect(map['width'], equals(200));\n      expect(map['height'], equals(300));\n      expect(map['draggable'], isTrue);\n    });\n\n    test('should create from map correctly', () {\n      final map = {\n        'entry': 'customEntry',\n        'route': '/test',\n        'width': 200,\n        'height': 300,\n        'x': 10,\n        'y': 20,\n        'autosize': true,\n        'clickable': true,\n        'draggable': true,\n        'focusable': false,\n        'immersion': true,\n        'visible': true,\n      };\n\n      final config = WindowConfig.fromMap(map);\n\n      expect(config.entry, equals('customEntry'));\n      expect(config.route, equals('/test'));\n      expect(config.width, equals(200));\n      expect(config.height, equals(300));\n      expect(config.x, equals(10));\n      expect(config.y, equals(20));\n      expect(config.autosize, isTrue);\n      expect(config.clickable, isTrue);\n      expect(config.draggable, isTrue);\n      expect(config.focusable, isFalse);\n      expect(config.immersion, isTrue);\n      expect(config.visible, isTrue);\n    });\n\n    test('should return correct size', () {\n      final config = WindowConfig(width: 200, height: 300);\n\n      expect(config.size.width, equals(200.0));\n      expect(config.size.height, equals(300.0));\n    });\n\n    test('should return zero size when dimensions are null', () {\n      final config = WindowConfig();\n\n      expect(config.size.width, equals(0.0));\n      expect(config.size.height, equals(0.0));\n    });\n\n    test('should convert to Window using to()', () {\n      final config = WindowConfig(id: 'test-window', route: '/test');\n      final window = config.to();\n\n      expect(window, isA<Window>());\n      expect(window.id, equals('test-window'));\n      expect(window.config, equals(config));\n    });\n\n    test('toString should return JSON representation', () {\n      final config = WindowConfig(\n        route: '/test',\n        width: 200,\n        draggable: true,\n      );\n\n      final str = config.toString();\n\n      expect(str, contains('route'));\n      expect(str, contains('/test'));\n      expect(str, contains('width'));\n      expect(str, contains('200'));\n      expect(str, contains('draggable'));\n      expect(str, contains('true'));\n    });\n  });\n\n  group('WindowSize', () {\n    test('should have correct constant values', () {\n      expect(WindowSize.MatchParent, equals(-1));\n      expect(WindowSize.WrapContent, equals(-2));\n    });\n  });\n\n  group('GravityType', () {\n    test('should convert to int correctly', () {\n      expect(GravityType.Center.toInt(), isNotNull);\n      expect(GravityType.LeftTop.toInt(), isNotNull);\n      expect(GravityType.RightBottom.toInt(), isNotNull);\n    });\n\n    test('should convert from int correctly', () {\n      final centerInt = GravityType.Center.toInt();\n      final result = GravityType.Unknown.fromInt(centerInt);\n\n      expect(result, equals(GravityType.Center));\n    });\n\n    test('should return null for unknown int', () {\n      final result = GravityType.Unknown.fromInt(999);\n\n      expect(result, isNull);\n    });\n\n    test('should return null for null int', () {\n      final result = GravityType.Unknown.fromInt(null);\n\n      expect(result, isNull);\n    });\n\n    test('all gravity types should have valid int values', () {\n      for (final gravity in GravityType.values) {\n        if (gravity != GravityType.Unknown) {\n          expect(gravity.toInt(), isNotNull,\n              reason: '$gravity should have a valid int value');\n        }\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "test/window_test.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:flutter_floatwing/flutter_floatwing.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Window', () {\n    test('should create with default id', () {\n      final window = Window();\n\n      expect(window.id, equals('default'));\n      expect(window.config, isNull);\n      expect(window.pixelRadio, isNull);\n      expect(window.system, isNull);\n    });\n\n    test('should create with custom id and config', () {\n      final config = WindowConfig(\n        route: '/test',\n        width: 200,\n        height: 300,\n      );\n      final window = Window(id: 'test-window', config: config);\n\n      expect(window.id, equals('test-window'));\n      expect(window.config, equals(config));\n      expect(window.config?.route, equals('/test'));\n      expect(window.config?.width, equals(200));\n      expect(window.config?.height, equals(300));\n    });\n\n    test('should create from map correctly', () {\n      final map = {\n        'id': 'map-window',\n        'pixelRadio': 2.0,\n        'system': {\n          'pixelRadio': 2,\n          'screen': {'width': 1080, 'height': 1920},\n        },\n        'config': {\n          'entry': 'main',\n          'route': '/test',\n          'width': 200,\n          'height': 300,\n          'draggable': true,\n        },\n      };\n\n      final window = Window.fromMap(map);\n\n      expect(window.id, equals('map-window'));\n      expect(window.pixelRadio, equals(2.0));\n      expect(window.system, isNotNull);\n      expect(window.system?.screenWidth, equals(1080));\n      expect(window.system?.screenHeight, equals(1920));\n      expect(window.config?.route, equals('/test'));\n      expect(window.config?.width, equals(200));\n      expect(window.config?.height, equals(300));\n      expect(window.config?.draggable, isTrue);\n    });\n\n    test('should apply map to existing window', () {\n      final window = Window(id: 'original');\n      final map = {\n        'id': 'updated',\n        'pixelRadio': 3.0,\n        'config': {\n          'entry': 'custom',\n          'width': 400,\n        },\n      };\n\n      window.applyMap(map);\n\n      expect(window.id, equals('updated'));\n      expect(window.pixelRadio, equals(3.0));\n      expect(window.config?.entry, equals('custom'));\n      expect(window.config?.width, equals(400));\n    });\n\n    test('should handle null map in applyMap', () {\n      final window = Window(id: 'test');\n      final result = window.applyMap(null);\n\n      expect(result.id, equals('test'));\n    });\n\n    test('should convert to map correctly', () {\n      final config = WindowConfig(\n        route: '/test',\n        width: 200,\n        height: 300,\n      );\n      final window = Window(id: 'test-window', config: config);\n      window.pixelRadio = 2.5;\n\n      final map = window.toMap();\n\n      expect(map['id'], equals('test-window'));\n      expect(map['pixelRadio'], equals(2.5));\n      expect(map['config'], isA<Map>());\n      expect(map['config']['route'], equals('/test'));\n    });\n\n    test('toString should contain window id', () {\n      final window = Window(id: 'my-window');\n\n      final str = window.toString();\n\n      expect(str, contains('Window[my-window]'));\n    });\n\n    test('should register onData handler', () {\n      final window = Window(id: 'test');\n      bool handlerCalled = false;\n\n      window.onData((source, name, data) async {\n        handlerCalled = true;\n        return null;\n      });\n\n      // Handler registered, but we can't easily test it without a real channel\n      expect(window, isNotNull);\n    });\n\n    test(\n        'should register event handler with on() and return window for chaining',\n        () {\n      final window = Window(id: 'test');\n\n      final result = window\n          .on(EventType.WindowCreated, (w, data) {})\n          .on(EventType.WindowStarted, (w, data) {});\n\n      expect(result, equals(window));\n    });\n  });\n\n  group('Window MethodChannel operations', () {\n    const MethodChannel windowChannel =\n        MethodChannel('im.zoe.labs/flutter_floatwing/window');\n\n    setUp(() {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(windowChannel,\n              (MethodCall methodCall) async {\n        switch (methodCall.method) {\n          case 'window.show':\n            return true;\n          case 'window.close':\n            return true;\n          case 'window.start':\n            return true;\n          case 'window.update':\n            return {\n              'id': methodCall.arguments['id'],\n              'config': methodCall.arguments['config'],\n            };\n          case 'data.share':\n            return 'shared';\n          case 'window.launch_main':\n            return true;\n          case 'window.sync':\n            return {\n              'id': 'synced-window',\n              'pixelRadio': 2.0,\n              'config': {'entry': 'main', 'route': '/synced'},\n            };\n          default:\n            return null;\n        }\n      });\n    });\n\n    tearDown(() {\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n          .setMockMethodCallHandler(windowChannel, null);\n    });\n\n    test('hide should call show with visible=false', () async {\n      final config = WindowConfig(visible: true);\n      final window = Window(id: 'test', config: config);\n\n      final result = await window.hide();\n\n      expect(result, isTrue);\n      expect(window.config?.visible, isFalse);\n    });\n\n    test('show should update visibility', () async {\n      final config = WindowConfig(visible: false);\n      final window = Window(id: 'test', config: config);\n\n      final result = await window.show(visible: true);\n\n      expect(result, isTrue);\n    });\n\n    test('close should return true', () async {\n      final window = Window(id: 'test');\n\n      final result = await window.close();\n\n      expect(result, isTrue);\n    });\n\n    test('close with force should work', () async {\n      final window = Window(id: 'test');\n\n      final result = await window.close(force: true);\n\n      expect(result, isTrue);\n    });\n\n    test('start should return true', () async {\n      final config = WindowConfig(route: '/test');\n      final window = Window(id: 'test', config: config);\n\n      final result = await window.start();\n\n      expect(result, isTrue);\n    });\n\n    test('share should send data and return response', () async {\n      final window = Window(id: 'test');\n\n      final result = await window.share({'key': 'value'}, name: 'test-data');\n\n      expect(result, equals('shared'));\n    });\n\n    test('launchMainActivity should return true', () async {\n      final window = Window(id: 'test');\n\n      final result = await window.launchMainActivity();\n\n      expect(result, isTrue);\n    });\n\n    test('Window.sync should return map', () async {\n      final result = await Window.sync();\n\n      expect(result, isNotNull);\n      expect(result?['id'], equals('synced-window'));\n      expect(result?['config']['route'], equals('/synced'));\n    });\n\n    test('update should apply new config', () async {\n      final config = WindowConfig(width: 100, height: 100);\n      final window = Window(id: 'test', config: config);\n\n      final result = await window.update(WindowConfig(width: 200, height: 200));\n\n      expect(result, isTrue);\n    });\n  });\n}\n"
  }
]