neoagent 2.3.1-beta.2 → 2.3.1-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/.env.example +39 -0
  2. package/README.md +2 -0
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +13 -5
  5. package/docs/integrations.md +4 -1
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +23057 -0
  66. package/flutter_app/lib/main_app_shell.dart +1682 -0
  67. package/flutter_app/lib/main_integrations.dart +931 -0
  68. package/flutter_app/lib/main_launcher.dart +959 -0
  69. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  70. package/flutter_app/lib/main_models.dart +3473 -0
  71. package/flutter_app/lib/main_shared.dart +2861 -0
  72. package/flutter_app/lib/main_theme.dart +204 -0
  73. package/flutter_app/lib/main_voice_assistant.dart +831 -0
  74. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  75. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  76. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  77. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  78. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  79. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  80. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  81. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  82. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  83. package/flutter_app/lib/src/backend_client.dart +1833 -0
  84. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  85. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  86. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  87. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  88. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  89. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  90. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  91. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  92. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  93. package/flutter_app/lib/src/health_bridge.dart +136 -0
  94. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  95. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  96. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  97. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  98. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  99. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  100. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  101. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  102. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  103. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  104. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  105. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  106. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  107. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  108. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  109. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  110. package/flutter_app/lib/src/theme/palette.dart +81 -0
  111. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  112. package/flutter_app/linux/CMakeLists.txt +128 -0
  113. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  114. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  115. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  116. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  117. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  118. package/flutter_app/linux/runner/main.cc +6 -0
  119. package/flutter_app/linux/runner/my_application.cc +144 -0
  120. package/flutter_app/linux/runner/my_application.h +18 -0
  121. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  122. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  123. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  124. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  125. package/flutter_app/macos/Podfile +42 -0
  126. package/flutter_app/macos/Podfile.lock +87 -0
  127. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  128. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  129. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  130. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  131. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  132. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  133. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  134. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  135. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  136. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  137. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  138. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  139. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  140. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  141. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  142. package/flutter_app/macos/Runner/Info.plist +36 -0
  143. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  144. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  145. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  146. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  147. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  148. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  149. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  150. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  151. package/flutter_app/patch_strings.py +12 -0
  152. package/flutter_app/pubspec.lock +1088 -0
  153. package/flutter_app/pubspec.yaml +53 -0
  154. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  155. package/flutter_app/test/recording_payloads_test.dart +53 -0
  156. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  157. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  158. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  159. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  160. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  161. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  162. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  163. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  164. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  165. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  166. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  167. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  168. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  169. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  170. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  171. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  172. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  173. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  174. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  175. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  176. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  177. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  178. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  179. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  180. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  181. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  182. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  183. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  184. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  185. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  186. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  187. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  188. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  189. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  190. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  191. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  192. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  193. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  194. package/flutter_app/web/favicon.png +0 -0
  195. package/flutter_app/web/favicon.svg +12 -0
  196. package/flutter_app/web/icons/Icon-192.png +0 -0
  197. package/flutter_app/web/icons/Icon-512.png +0 -0
  198. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  199. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  200. package/flutter_app/web/index.html +39 -0
  201. package/flutter_app/web/manifest.json +35 -0
  202. package/flutter_app/windows/CMakeLists.txt +108 -0
  203. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  204. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  205. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  206. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  207. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  208. package/flutter_app/windows/runner/Runner.rc +121 -0
  209. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  210. package/flutter_app/windows/runner/flutter_window.h +37 -0
  211. package/flutter_app/windows/runner/main.cpp +53 -0
  212. package/flutter_app/windows/runner/resource.h +16 -0
  213. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  214. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  215. package/flutter_app/windows/runner/utils.cpp +65 -0
  216. package/flutter_app/windows/runner/utils.h +19 -0
  217. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  218. package/flutter_app/windows/runner/win32_window.h +102 -0
  219. package/lib/manager.js +231 -7
  220. package/package.json +3 -1
  221. package/server/db/database.js +68 -0
  222. package/server/http/middleware.js +50 -0
  223. package/server/http/routes.js +3 -1
  224. package/server/index.js +1 -0
  225. package/server/public/.last_build_id +1 -1
  226. package/server/public/assets/NOTICES +61 -0
  227. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  228. package/server/public/flutter_bootstrap.js +1 -1
  229. package/server/public/main.dart.js +65262 -64422
  230. package/server/routes/integrations.js +86 -0
  231. package/server/routes/memory.js +11 -2
  232. package/server/routes/screenHistory.js +46 -0
  233. package/server/routes/triggers.js +81 -0
  234. package/server/services/ai/models.js +30 -0
  235. package/server/services/ai/providers/githubCopilot.js +97 -0
  236. package/server/services/ai/providers/openai.js +2 -1
  237. package/server/services/ai/providers/openaiCodex.js +31 -0
  238. package/server/services/ai/settings.js +20 -0
  239. package/server/services/ai/systemPrompt.js +1 -1
  240. package/server/services/ai/tools.js +35 -6
  241. package/server/services/browser/controller.js +47 -3
  242. package/server/services/desktop/screenRecorder.js +172 -0
  243. package/server/services/integrations/env.js +5 -0
  244. package/server/services/integrations/github/common.js +106 -0
  245. package/server/services/integrations/github/provider.js +499 -0
  246. package/server/services/integrations/github/repos.js +1124 -0
  247. package/server/services/integrations/home_assistant/provider.js +306 -26
  248. package/server/services/integrations/manager.js +63 -7
  249. package/server/services/integrations/oauth_provider.js +13 -6
  250. package/server/services/integrations/provider_config_store.js +76 -0
  251. package/server/services/integrations/registry.js +4 -0
  252. package/server/services/integrations/trello/provider.js +744 -0
  253. package/server/services/integrations/whatsapp/provider.js +6 -2
  254. package/server/services/manager.js +22 -0
  255. package/server/services/memory/manager.js +39 -2
  256. package/server/services/skills/base_catalog.js +33 -0
  257. package/server/services/tasks/adapters/index.js +1 -0
  258. package/server/services/tasks/adapters/manual.js +12 -0
  259. package/server/services/tasks/runtime.js +1 -1
  260. package/server/services/voice/openaiClient.js +4 -1
  261. package/server/services/voice/providers.js +2 -1
  262. package/server/services/widgets/service.js +49 -4
  263. package/server/utils/local_secrets.js +56 -0
  264. package/server/utils/logger.js +37 -9
@@ -0,0 +1,747 @@
1
+ package com.neoagent.flutter_app
2
+
3
+ import android.content.Intent
4
+ import android.content.ActivityNotFoundException
5
+ import android.content.pm.PackageManager
6
+ import android.media.AudioManager
7
+ import android.net.Uri
8
+ import android.os.BatteryManager
9
+ import android.os.Build
10
+ import android.provider.Settings
11
+ import android.view.KeyEvent
12
+ import androidx.activity.result.ActivityResultLauncher
13
+ import androidx.activity.result.contract.ActivityResultContracts
14
+ import androidx.core.content.ContextCompat
15
+ import androidx.core.content.FileProvider
16
+ import androidx.health.connect.client.PermissionController
17
+ import androidx.lifecycle.lifecycleScope
18
+ import com.neoagent.flutter_app.health.HealthConnectGateway
19
+ import com.neoagent.flutter_app.health.HealthSyncScheduler
20
+ import com.neoagent.flutter_app.recording.RecordingForegroundService
21
+ import com.neoagent.flutter_app.recording.RecordingStateStore
22
+ import com.neoagent.flutter_app.widgets.AiHomeWidgetProvider
23
+ import com.neoagent.flutter_app.widgets.AiWidgetStore
24
+ import com.neoagent.flutter_app.widgets.VoiceLaunchWidgetProvider
25
+ import com.neoagent.flutter_app.widgets.WidgetSyncScheduler
26
+ import io.flutter.embedding.android.FlutterFragmentActivity
27
+ import io.flutter.embedding.engine.FlutterEngine
28
+ import io.flutter.plugin.common.EventChannel
29
+ import io.flutter.plugin.common.MethodChannel
30
+ import io.flutter.plugins.GeneratedPluginRegistrant
31
+ import kotlinx.coroutines.launch
32
+ import java.io.File
33
+ import java.time.Instant
34
+
35
+ class MainActivity : FlutterFragmentActivity() {
36
+
37
+ private lateinit var healthGateway: HealthConnectGateway
38
+ private lateinit var healthSyncScheduler: HealthSyncScheduler
39
+ private lateinit var widgetSyncScheduler: WidgetSyncScheduler
40
+ private lateinit var recordingStateStore: RecordingStateStore
41
+ private lateinit var permissionLauncher: ActivityResultLauncher<Set<String>>
42
+ private lateinit var microphonePermissionLauncher: ActivityResultLauncher<String>
43
+ private var pendingPermissionResult: MethodChannel.Result? = null
44
+ private var pendingRecordingResult: MethodChannel.Result? = null
45
+ private var pendingRecordingArgs: Map<*, *>? = null
46
+ private var launcherButtonSink: EventChannel.EventSink? = null
47
+ private var widgetEventSink: EventChannel.EventSink? = null
48
+ private var appLaunchEventSink: EventChannel.EventSink? = null
49
+ private var pendingAppLaunchAction: String? = null
50
+
51
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
52
+ super.configureFlutterEngine(flutterEngine)
53
+ GeneratedPluginRegistrant.registerWith(flutterEngine)
54
+
55
+ healthGateway = HealthConnectGateway(this)
56
+ healthSyncScheduler = HealthSyncScheduler(this)
57
+ widgetSyncScheduler = WidgetSyncScheduler(this)
58
+ recordingStateStore = RecordingStateStore(this)
59
+ permissionLauncher = registerForActivityResult(
60
+ PermissionController.createRequestPermissionResultContract(),
61
+ ) {
62
+ val pending = pendingPermissionResult
63
+ pendingPermissionResult = null
64
+ lifecycleScope.launch {
65
+ pending?.success(buildStatusMap())
66
+ }
67
+ }
68
+ microphonePermissionLauncher = registerForActivityResult(
69
+ ActivityResultContracts.RequestPermission(),
70
+ ) { granted ->
71
+ val pending = pendingRecordingResult
72
+ val args = pendingRecordingArgs
73
+ pendingRecordingResult = null
74
+ pendingRecordingArgs = null
75
+ if (!granted) {
76
+ pending?.error(
77
+ "recording_permission_denied",
78
+ "Microphone permission is required for background recording.",
79
+ null,
80
+ )
81
+ return@registerForActivityResult
82
+ }
83
+ try {
84
+ startRecordingService(args)
85
+ pending?.success(recordingStateStore.statusMap())
86
+ } catch (err: Exception) {
87
+ pending?.error(
88
+ "recording_start_failed",
89
+ err.message ?: err.javaClass.simpleName,
90
+ null,
91
+ )
92
+ }
93
+ }
94
+ MethodChannel(
95
+ flutterEngine.dartExecutor.binaryMessenger,
96
+ "neoagent/health",
97
+ ).setMethodCallHandler { call, result ->
98
+ when (call.method) {
99
+ "status" -> lifecycleScope.launch {
100
+ result.success(buildStatusMap())
101
+ }
102
+
103
+ "requestPermissions" -> lifecycleScope.launch {
104
+ val client = healthGateway.getClientOrNull()
105
+ if (client == null) {
106
+ result.error(
107
+ "health_unavailable",
108
+ "Health Connect is unavailable on this device.",
109
+ null,
110
+ )
111
+ return@launch
112
+ }
113
+
114
+ pendingPermissionResult = result
115
+ permissionLauncher.launch(healthGateway.getRequestedPermissions(client))
116
+ }
117
+
118
+ "collectBatch" -> lifecycleScope.launch {
119
+ try {
120
+ val client = healthGateway.getClientOrNull()
121
+ if (client == null) {
122
+ result.error(
123
+ "health_unavailable",
124
+ "Health Connect is unavailable on this device.",
125
+ null,
126
+ )
127
+ return@launch
128
+ }
129
+
130
+ val required = healthGateway.getRequestedPermissions(client)
131
+ val granted = client.permissionController.getGrantedPermissions()
132
+ if (!granted.containsAll(required)) {
133
+ result.error(
134
+ "health_permissions",
135
+ "Grant Health Connect permissions before syncing.",
136
+ null,
137
+ )
138
+ return@launch
139
+ }
140
+
141
+ val args = call.arguments as? Map<*, *>
142
+ val windowStartRaw = args?.get("windowStart")?.toString()
143
+ val windowEndRaw = args?.get("windowEnd")?.toString()
144
+ if (windowStartRaw.isNullOrBlank() || windowEndRaw.isNullOrBlank()) {
145
+ result.error(
146
+ "health_sync_window",
147
+ "windowStart and windowEnd are required.",
148
+ null,
149
+ )
150
+ return@launch
151
+ }
152
+ val windowStart =
153
+ try {
154
+ Instant.parse(windowStartRaw)
155
+ } catch (_: Exception) {
156
+ result.error(
157
+ "health_sync_window",
158
+ "windowStart must be an ISO-8601 timestamp.",
159
+ null,
160
+ )
161
+ return@launch
162
+ }
163
+ val windowEnd =
164
+ try {
165
+ Instant.parse(windowEndRaw)
166
+ } catch (_: Exception) {
167
+ result.error(
168
+ "health_sync_window",
169
+ "windowEnd must be an ISO-8601 timestamp.",
170
+ null,
171
+ )
172
+ return@launch
173
+ }
174
+ val payload = healthGateway.collectBatch(client, windowStart, windowEnd)
175
+ result.success(payload.toJson().toString())
176
+ } catch (err: Exception) {
177
+ result.error(
178
+ "health_sync_failed",
179
+ err.message ?: err.javaClass.simpleName,
180
+ null,
181
+ )
182
+ }
183
+ }
184
+
185
+ "configureBackgroundSync" -> {
186
+ val args = call.arguments as? Map<*, *>
187
+ val enabled = args?.get("enabled") == true
188
+ val backendUrl = args?.get("backendUrl")?.toString().orEmpty()
189
+ val sessionCookie = args?.get("sessionCookie")?.toString().orEmpty()
190
+ healthSyncScheduler.configure(
191
+ enabled = enabled,
192
+ backendUrl = backendUrl,
193
+ sessionCookie = sessionCookie,
194
+ )
195
+ result.success(null)
196
+ }
197
+
198
+ else -> result.notImplemented()
199
+ }
200
+ }
201
+
202
+ MethodChannel(
203
+ flutterEngine.dartExecutor.binaryMessenger,
204
+ "neoagent/recordings",
205
+ ).setMethodCallHandler { call, result ->
206
+ when (call.method) {
207
+ "status" -> result.success(recordingStateStore.statusMap())
208
+
209
+ "startBackgroundRecording" -> {
210
+ try {
211
+ val args = call.arguments as? Map<*, *>
212
+ if (ContextCompat.checkSelfPermission(
213
+ this,
214
+ android.Manifest.permission.RECORD_AUDIO,
215
+ ) == android.content.pm.PackageManager.PERMISSION_GRANTED
216
+ ) {
217
+ startRecordingService(args)
218
+ result.success(recordingStateStore.statusMap())
219
+ } else {
220
+ pendingRecordingResult = result
221
+ pendingRecordingArgs = args
222
+ microphonePermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO)
223
+ }
224
+ } catch (err: Exception) {
225
+ result.error(
226
+ "recording_start_failed",
227
+ err.message ?: err.javaClass.simpleName,
228
+ null,
229
+ )
230
+ }
231
+ }
232
+
233
+ "pauseBackgroundRecording" -> {
234
+ startService(RecordingForegroundService.buildPauseIntent(this))
235
+ result.success(recordingStateStore.statusMap())
236
+ }
237
+
238
+ "resumeBackgroundRecording" -> {
239
+ startService(RecordingForegroundService.buildResumeIntent(this))
240
+ result.success(recordingStateStore.statusMap())
241
+ }
242
+
243
+ "stopBackgroundRecording" -> {
244
+ startService(RecordingForegroundService.buildStopIntent(this))
245
+ result.success(recordingStateStore.statusMap())
246
+ }
247
+
248
+ else -> result.notImplemented()
249
+ }
250
+ }
251
+
252
+ MethodChannel(
253
+ flutterEngine.dartExecutor.binaryMessenger,
254
+ "neoagent/app_update",
255
+ ).setMethodCallHandler { call, result ->
256
+ when (call.method) {
257
+ "canRequestPackageInstalls" -> {
258
+ result.success(canRequestPackageInstalls())
259
+ }
260
+
261
+ "openInstallUnknownAppsSettings" -> {
262
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
263
+ result.error(
264
+ "app_update_unknown_sources_unsupported",
265
+ "Install unknown apps settings require Android 8.0 or newer.",
266
+ null,
267
+ )
268
+ return@setMethodCallHandler
269
+ }
270
+ try {
271
+ val intent = Intent(
272
+ Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
273
+ Uri.parse("package:$packageName"),
274
+ ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
275
+ startActivity(intent)
276
+ result.success(null)
277
+ } catch (err: ActivityNotFoundException) {
278
+ result.error(
279
+ "app_update_unknown_sources_failed",
280
+ err.message ?: err.javaClass.simpleName,
281
+ null,
282
+ )
283
+ } catch (err: Exception) {
284
+ result.error(
285
+ "app_update_unknown_sources_failed",
286
+ err.message ?: err.javaClass.simpleName,
287
+ null,
288
+ )
289
+ }
290
+ }
291
+
292
+ "installApk" -> {
293
+ try {
294
+ val args = call.arguments as? Map<*, *>
295
+ val apkPath = args?.get("apkPath")?.toString().orEmpty()
296
+ if (apkPath.isBlank()) {
297
+ result.error(
298
+ "app_update_invalid_args",
299
+ "apkPath is required.",
300
+ null,
301
+ )
302
+ return@setMethodCallHandler
303
+ }
304
+ result.success(launchApkInstaller(apkPath))
305
+ } catch (err: Exception) {
306
+ result.error(
307
+ "app_update_install_failed",
308
+ err.message ?: err.javaClass.simpleName,
309
+ null,
310
+ )
311
+ }
312
+ }
313
+
314
+ else -> result.notImplemented()
315
+ }
316
+ }
317
+
318
+ MethodChannel(
319
+ flutterEngine.dartExecutor.binaryMessenger,
320
+ "neoagent/launcher_device",
321
+ ).setMethodCallHandler { call, result ->
322
+ when (call.method) {
323
+ "getVolumeState" -> {
324
+ val audioManager = getSystemService(AudioManager::class.java)
325
+ if (audioManager == null) {
326
+ result.error(
327
+ "launcher_audio_unavailable",
328
+ "Audio manager is unavailable on this device.",
329
+ null,
330
+ )
331
+ return@setMethodCallHandler
332
+ }
333
+ result.success(buildVolumeState(audioManager))
334
+ }
335
+
336
+ "setVolume" -> {
337
+ val audioManager = getSystemService(AudioManager::class.java)
338
+ if (audioManager == null) {
339
+ result.error(
340
+ "launcher_audio_unavailable",
341
+ "Audio manager is unavailable on this device.",
342
+ null,
343
+ )
344
+ return@setMethodCallHandler
345
+ }
346
+ val args = call.arguments as? Map<*, *>
347
+ val target = (args?.get("value") as? Number)?.toInt()
348
+ if (target == null) {
349
+ result.error(
350
+ "launcher_volume_invalid_args",
351
+ "value is required.",
352
+ null,
353
+ )
354
+ return@setMethodCallHandler
355
+ }
356
+ val minVolume =
357
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
358
+ audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
359
+ } else {
360
+ 0
361
+ }
362
+ val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
363
+ audioManager.setStreamVolume(
364
+ AudioManager.STREAM_MUSIC,
365
+ target.coerceIn(minVolume, maxVolume),
366
+ 0,
367
+ )
368
+ result.success(buildVolumeState(audioManager))
369
+ }
370
+
371
+ "adjustVolume" -> {
372
+ val audioManager = getSystemService(AudioManager::class.java)
373
+ if (audioManager == null) {
374
+ result.error(
375
+ "launcher_audio_unavailable",
376
+ "Audio manager is unavailable on this device.",
377
+ null,
378
+ )
379
+ return@setMethodCallHandler
380
+ }
381
+ val args = call.arguments as? Map<*, *>
382
+ val delta = (args?.get("delta") as? Number)?.toInt() ?: 0
383
+ val minVolume =
384
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
385
+ audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
386
+ } else {
387
+ 0
388
+ }
389
+ val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
390
+ val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
391
+ audioManager.setStreamVolume(
392
+ AudioManager.STREAM_MUSIC,
393
+ (currentVolume + delta).coerceIn(minVolume, maxVolume),
394
+ 0,
395
+ )
396
+ result.success(buildVolumeState(audioManager))
397
+ }
398
+
399
+ "openWifiSettings" -> {
400
+ try {
401
+ val intent = Intent(Settings.ACTION_WIFI_SETTINGS).apply {
402
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
403
+ }
404
+ startActivity(intent)
405
+ result.success(null)
406
+ } catch (err: Exception) {
407
+ result.error(
408
+ "launcher_wifi_settings_failed",
409
+ err.message ?: err.javaClass.simpleName,
410
+ null,
411
+ )
412
+ }
413
+ }
414
+
415
+ "openDateSettings", "openTimeSettings" -> {
416
+ try {
417
+ val intent = Intent(Settings.ACTION_DATE_SETTINGS)
418
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
419
+ if (intent.resolveActivity(packageManager) != null) {
420
+ startActivity(intent)
421
+ } else {
422
+ startActivity(
423
+ Intent(Settings.ACTION_SETTINGS)
424
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
425
+ )
426
+ }
427
+ result.success(null)
428
+ } catch (err: Exception) {
429
+ result.error(
430
+ "launcher_date_settings_failed",
431
+ err.message ?: err.javaClass.simpleName,
432
+ null,
433
+ )
434
+ }
435
+ }
436
+
437
+ "openSystemSettings" -> {
438
+ try {
439
+ startActivity(
440
+ Intent(Settings.ACTION_SETTINGS)
441
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
442
+ )
443
+ result.success(null)
444
+ } catch (err: Exception) {
445
+ result.error(
446
+ "launcher_settings_failed",
447
+ err.message ?: err.javaClass.simpleName,
448
+ null,
449
+ )
450
+ }
451
+ }
452
+
453
+ "getDeviceStatus", "getBatteryState" -> {
454
+ result.success(buildDeviceStatusMap())
455
+ }
456
+
457
+ "getAppMode" -> result.success(currentAppMode())
458
+
459
+ else -> result.notImplemented()
460
+ }
461
+ }
462
+
463
+ MethodChannel(
464
+ flutterEngine.dartExecutor.binaryMessenger,
465
+ "neoagent/widgets",
466
+ ).setMethodCallHandler { call, result ->
467
+ when (call.method) {
468
+ "configureHomeWidgets" -> {
469
+ val args = call.arguments as? Map<*, *>
470
+ widgetSyncScheduler.configure(
471
+ enabled = args?.get("enabled") == true,
472
+ backendUrl = args?.get("backendUrl")?.toString().orEmpty(),
473
+ sessionCookie = args?.get("sessionCookie")?.toString().orEmpty(),
474
+ )
475
+ result.success(null)
476
+ }
477
+
478
+ "syncHomeWidgetsNow" -> {
479
+ widgetSyncScheduler.syncNow()
480
+ result.success(null)
481
+ }
482
+
483
+ else -> result.notImplemented()
484
+ }
485
+ }
486
+
487
+ EventChannel(
488
+ flutterEngine.dartExecutor.binaryMessenger,
489
+ "neoagent/launcher_buttons",
490
+ ).setStreamHandler(
491
+ object : EventChannel.StreamHandler {
492
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
493
+ launcherButtonSink = events
494
+ }
495
+
496
+ override fun onCancel(arguments: Any?) {
497
+ launcherButtonSink = null
498
+ }
499
+ },
500
+ )
501
+
502
+ EventChannel(
503
+ flutterEngine.dartExecutor.binaryMessenger,
504
+ "neoagent/widgets/events",
505
+ ).setStreamHandler(
506
+ object : EventChannel.StreamHandler {
507
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
508
+ widgetEventSink = events
509
+ emitPendingWidgetIntent()
510
+ }
511
+
512
+ override fun onCancel(arguments: Any?) {
513
+ widgetEventSink = null
514
+ }
515
+ },
516
+ )
517
+
518
+ EventChannel(
519
+ flutterEngine.dartExecutor.binaryMessenger,
520
+ "neoagent/app_launch/events",
521
+ ).setStreamHandler(
522
+ object : EventChannel.StreamHandler {
523
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
524
+ appLaunchEventSink = events
525
+ emitPendingAppLaunchIntent()
526
+ }
527
+
528
+ override fun onCancel(arguments: Any?) {
529
+ appLaunchEventSink = null
530
+ }
531
+ },
532
+ )
533
+
534
+ captureWidgetIntent(intent)
535
+ captureAppLaunchIntent(intent)
536
+ }
537
+
538
+ override fun onNewIntent(intent: Intent) {
539
+ super.onNewIntent(intent)
540
+ setIntent(intent)
541
+ captureWidgetIntent(intent)
542
+ captureAppLaunchIntent(intent)
543
+ }
544
+
545
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
546
+ emitLauncherButtonEvent(event, "down")
547
+ return super.onKeyDown(keyCode, event)
548
+ }
549
+
550
+ override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
551
+ emitLauncherButtonEvent(event, "up")
552
+ return super.onKeyUp(keyCode, event)
553
+ }
554
+
555
+ private fun startRecordingService(args: Map<*, *>?) {
556
+ val backendUrl = args?.get("backendUrl")?.toString().orEmpty()
557
+ val sessionCookie = args?.get("sessionCookie")?.toString().orEmpty()
558
+ val sessionId = args?.get("sessionId")?.toString().orEmpty()
559
+ val intent = RecordingForegroundService.buildStartIntent(
560
+ this,
561
+ backendUrl = backendUrl,
562
+ sessionCookie = sessionCookie,
563
+ sessionId = sessionId,
564
+ )
565
+ ContextCompat.startForegroundService(this, intent)
566
+ }
567
+
568
+ private fun currentAppMode(): String {
569
+ return try {
570
+ val appInfo = packageManager.getApplicationInfo(
571
+ packageName,
572
+ PackageManager.GET_META_DATA,
573
+ )
574
+ appInfo.metaData?.getString("com.neoagent.APP_MODE")?.trim().orEmpty()
575
+ .ifBlank { "standard" }
576
+ } catch (_: Exception) {
577
+ "standard"
578
+ }
579
+ }
580
+
581
+ private fun canRequestPackageInstalls(): Boolean {
582
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
583
+ packageManager.canRequestPackageInstalls()
584
+ } else {
585
+ true
586
+ }
587
+ }
588
+
589
+ private fun launchApkInstaller(apkPath: String): Boolean {
590
+ val apkFile = File(apkPath)
591
+ if (!apkFile.exists() || !apkFile.isFile) {
592
+ throw IllegalArgumentException("APK not found: $apkPath")
593
+ }
594
+ val contentUri = FileProvider.getUriForFile(
595
+ this,
596
+ "$packageName.fileprovider",
597
+ apkFile,
598
+ )
599
+ val installIntent = Intent(Intent.ACTION_VIEW).apply {
600
+ setDataAndType(contentUri, "application/vnd.android.package-archive")
601
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
602
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
603
+ putExtra(Intent.EXTRA_RETURN_RESULT, false)
604
+ }
605
+ if (installIntent.resolveActivity(packageManager) == null) {
606
+ throw IllegalStateException("No Android package installer is available.")
607
+ }
608
+ startActivity(installIntent)
609
+ return true
610
+ }
611
+
612
+ private fun shouldEmitLauncherButtonEvent(keyCode: Int): Boolean {
613
+ if (currentAppMode() != "launcher") {
614
+ return false
615
+ }
616
+ return keyCode != KeyEvent.KEYCODE_BACK &&
617
+ keyCode != KeyEvent.KEYCODE_HOME &&
618
+ keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
619
+ keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
620
+ keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
621
+ keyCode != KeyEvent.KEYCODE_APP_SWITCH &&
622
+ keyCode != KeyEvent.KEYCODE_MENU
623
+ }
624
+
625
+ private fun emitLauncherButtonEvent(event: KeyEvent, action: String) {
626
+ if (!shouldEmitLauncherButtonEvent(event.keyCode)) {
627
+ return
628
+ }
629
+ launcherButtonSink?.success(
630
+ mapOf(
631
+ "keyCode" to event.keyCode,
632
+ "scanCode" to event.scanCode,
633
+ "repeatCount" to event.repeatCount,
634
+ "action" to action,
635
+ "eventTimeMs" to event.eventTime,
636
+ ),
637
+ )
638
+ }
639
+
640
+ private fun captureWidgetIntent(intent: Intent?) {
641
+ if (intent?.action != AiHomeWidgetProvider.ACTION_OPEN_WIDGET) {
642
+ return
643
+ }
644
+ val widgetId =
645
+ intent.getStringExtra(AiHomeWidgetProvider.EXTRA_WIDGET_ID)?.trim().orEmpty()
646
+ if (widgetId.isBlank()) {
647
+ return
648
+ }
649
+ AiWidgetStore(this).setPendingOpenWidgetId(widgetId)
650
+ emitPendingWidgetIntent()
651
+ }
652
+
653
+ private fun emitPendingWidgetIntent() {
654
+ val sink = widgetEventSink ?: return
655
+ val widgetId = AiWidgetStore(this).consumePendingOpenWidgetId() ?: return
656
+ sink.success(mapOf("widgetId" to widgetId))
657
+ }
658
+
659
+ private fun captureAppLaunchIntent(intent: Intent?) {
660
+ if (intent?.action != VoiceLaunchWidgetProvider.ACTION_OPEN_VOICE_ASSISTANT) {
661
+ return
662
+ }
663
+ pendingAppLaunchAction = VoiceLaunchWidgetProvider.OPEN_TARGET_VOICE_ASSISTANT
664
+ emitPendingAppLaunchIntent()
665
+ }
666
+
667
+ private fun emitPendingAppLaunchIntent() {
668
+ val sink = appLaunchEventSink ?: return
669
+ val action = pendingAppLaunchAction ?: return
670
+ pendingAppLaunchAction = null
671
+ sink.success(mapOf("action" to action))
672
+ }
673
+
674
+ private fun buildVolumeState(audioManager: AudioManager): Map<String, Any> {
675
+ val minVolume =
676
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
677
+ audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
678
+ } else {
679
+ 0
680
+ }
681
+ return mapOf(
682
+ "current" to audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
683
+ "max" to audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC),
684
+ "min" to minVolume,
685
+ "muted" to audioManager.isStreamMute(AudioManager.STREAM_MUSIC),
686
+ )
687
+ }
688
+
689
+ private fun buildDeviceStatusMap(): Map<String, Any?> {
690
+ val batteryStateIntent = registerReceiver(
691
+ null,
692
+ android.content.IntentFilter(Intent.ACTION_BATTERY_CHANGED),
693
+ )
694
+ val batteryLevel = batteryStateIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
695
+ val batteryScale = batteryStateIntent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
696
+ val batteryPercentFromIntent =
697
+ if (batteryLevel >= 0 && batteryScale > 0) {
698
+ ((batteryLevel * 100f) / batteryScale).toInt().coerceIn(0, 100)
699
+ } else {
700
+ null
701
+ }
702
+ val batteryManager = getSystemService(BatteryManager::class.java)
703
+ val batteryPercentFromManager = batteryManager
704
+ ?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
705
+ ?.takeIf { it in 0..100 }
706
+ val batteryPercent = batteryPercentFromIntent ?: batteryPercentFromManager
707
+ val batteryStatus = batteryStateIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
708
+ val plugged = batteryStateIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) ?: 0
709
+ val charging =
710
+ batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING ||
711
+ batteryStatus == BatteryManager.BATTERY_STATUS_FULL ||
712
+ plugged != 0
713
+ return mapOf(
714
+ "batteryPercent" to batteryPercent,
715
+ "charging" to charging,
716
+ )
717
+ }
718
+
719
+ private suspend fun buildStatusMap(): Map<String, Any?> {
720
+ val available = healthGateway.isAvailable()
721
+ val client = healthGateway.getClientOrNull()
722
+ val required = if (client != null) {
723
+ healthGateway.getRequestedPermissions(client).toList()
724
+ } else {
725
+ emptyList()
726
+ }
727
+ val granted = if (client != null) {
728
+ client.permissionController.getGrantedPermissions().toList()
729
+ } else {
730
+ emptyList()
731
+ }
732
+
733
+ val message = when {
734
+ !available -> "Health Connect is unavailable on this device."
735
+ !granted.containsAll(required) -> "Permissions are required for sync."
736
+ else -> "Health sync is ready."
737
+ }
738
+
739
+ return mapOf(
740
+ "available" to available,
741
+ "permissionsGranted" to granted.containsAll(required),
742
+ "requiredPermissions" to required,
743
+ "grantedPermissions" to granted,
744
+ "message" to message,
745
+ )
746
+ }
747
+ }