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,280 @@
1
+ package com.neoagent.flutter_app.health
2
+
3
+ import android.content.Context
4
+ import androidx.health.connect.client.HealthConnectClient
5
+ import androidx.health.connect.client.HealthConnectFeatures
6
+ import androidx.health.connect.client.permission.HealthPermission
7
+ import androidx.health.connect.client.records.ExerciseSessionRecord
8
+ import androidx.health.connect.client.records.HeartRateRecord
9
+ import androidx.health.connect.client.records.Record
10
+ import androidx.health.connect.client.records.SleepSessionRecord
11
+ import androidx.health.connect.client.records.StepsRecord
12
+ import androidx.health.connect.client.records.WeightRecord
13
+ import androidx.health.connect.client.records.metadata.Device
14
+ import androidx.health.connect.client.request.ReadRecordsRequest
15
+ import androidx.health.connect.client.time.TimeRangeFilter
16
+ import org.json.JSONArray
17
+ import org.json.JSONObject
18
+ import java.time.Duration
19
+ import java.time.Instant
20
+
21
+ class HealthConnectGateway(
22
+ private val context: Context,
23
+ ) {
24
+
25
+ fun getSdkStatus(): Int {
26
+ return HealthConnectClient.getSdkStatus(context, PROVIDER_PACKAGE_NAME)
27
+ }
28
+
29
+ fun isAvailable(): Boolean = getSdkStatus() == HealthConnectClient.SDK_AVAILABLE
30
+
31
+ fun getClientOrNull(): HealthConnectClient? {
32
+ if (!isAvailable()) return null
33
+ return HealthConnectClient.getOrCreate(context)
34
+ }
35
+
36
+ suspend fun getRequestedPermissions(client: HealthConnectClient): Set<String> {
37
+ val permissions = mutableSetOf(
38
+ HealthPermission.getReadPermission(StepsRecord::class),
39
+ HealthPermission.getReadPermission(HeartRateRecord::class),
40
+ HealthPermission.getReadPermission(SleepSessionRecord::class),
41
+ HealthPermission.getReadPermission(ExerciseSessionRecord::class),
42
+ HealthPermission.getReadPermission(WeightRecord::class),
43
+ )
44
+
45
+ if (
46
+ client.features.getFeatureStatus(
47
+ HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND,
48
+ ) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE
49
+ ) {
50
+ permissions += HealthPermission.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
51
+ }
52
+
53
+ return permissions
54
+ }
55
+
56
+ suspend fun collectBatch(
57
+ client: HealthConnectClient,
58
+ windowStart: Instant,
59
+ windowEnd: Instant,
60
+ ): HealthSyncBatchPayload {
61
+ val timeRange = TimeRangeFilter.between(windowStart, windowEnd)
62
+
63
+ val steps = readAllRecords<StepsRecord>(client, timeRange)
64
+ val heartRates = readAllRecords<HeartRateRecord>(client, timeRange)
65
+ val sleepSessions = readAllRecords<SleepSessionRecord>(client, timeRange)
66
+ val exerciseSessions = readAllRecords<ExerciseSessionRecord>(client, timeRange)
67
+ val weights = readAllRecords<WeightRecord>(client, timeRange)
68
+
69
+ val records = buildList {
70
+ steps.forEach { add(it.toPayload()) }
71
+ heartRates.forEach { add(it.toPayload()) }
72
+ sleepSessions.forEach { add(it.toPayload()) }
73
+ exerciseSessions.forEach { add(it.toPayload()) }
74
+ weights.forEach { add(it.toPayload()) }
75
+ }
76
+
77
+ val totalSteps = steps.sumOf { it.count }
78
+ val allHeartRateSamples = heartRates.flatMap { it.samples }
79
+ val sleepMinutes = sleepSessions.sumOf { Duration.between(it.startTime, it.endTime).toMinutes() }
80
+ val exerciseMinutes = exerciseSessions.sumOf { Duration.between(it.startTime, it.endTime).toMinutes() }
81
+ val latestWeightKg = weights.maxByOrNull { it.time }?.weight?.inKilograms
82
+
83
+ val summary = JSONObject().apply {
84
+ put("stepsTotal", totalSteps)
85
+ put("heartRateRecordCount", heartRates.size)
86
+ put("heartRateSampleCount", allHeartRateSamples.size)
87
+ put("heartRateAvgBpm", if (allHeartRateSamples.isEmpty()) JSONObject.NULL else allHeartRateSamples.map { it.beatsPerMinute }.average())
88
+ put("sleepSessionCount", sleepSessions.size)
89
+ put("sleepMinutes", sleepMinutes)
90
+ put("exerciseSessionCount", exerciseSessions.size)
91
+ put("exerciseMinutes", exerciseMinutes)
92
+ put("weightRecordCount", weights.size)
93
+ put("latestWeightKg", latestWeightKg ?: JSONObject.NULL)
94
+ }
95
+
96
+ return HealthSyncBatchPayload(
97
+ source = SOURCE_NAME,
98
+ provider = PROVIDER_PACKAGE_NAME,
99
+ windowStart = windowStart.toString(),
100
+ windowEnd = windowEnd.toString(),
101
+ summary = summary,
102
+ records = records,
103
+ )
104
+ }
105
+
106
+ private suspend inline fun <reified T : Record> readAllRecords(
107
+ client: HealthConnectClient,
108
+ timeRange: TimeRangeFilter,
109
+ ): List<T> {
110
+ val records = mutableListOf<T>()
111
+ var pageToken: String? = null
112
+
113
+ do {
114
+ val response = client.readRecords(
115
+ ReadRecordsRequest<T>(
116
+ timeRangeFilter = timeRange,
117
+ ascendingOrder = true,
118
+ pageSize = PAGE_SIZE,
119
+ pageToken = pageToken,
120
+ ),
121
+ )
122
+ records += response.records
123
+ pageToken = response.pageToken
124
+ } while (pageToken != null)
125
+
126
+ return records
127
+ }
128
+
129
+ private fun StepsRecord.toPayload(): HealthSyncRecordPayload {
130
+ return HealthSyncRecordPayload(
131
+ metricType = "steps",
132
+ recordId = metadata.id,
133
+ startTime = startTime.toString(),
134
+ endTime = endTime.toString(),
135
+ recordedAt = endTime.toString(),
136
+ numericValue = count.toDouble(),
137
+ textValue = null,
138
+ unit = "steps",
139
+ sourceAppId = metadata.dataOrigin.packageName.takeIf { it.isNotBlank() },
140
+ sourceDevice = metadata.device.toLabel(),
141
+ lastModifiedTime = metadata.lastModifiedTime.toString(),
142
+ payload = JSONObject().apply {
143
+ put("count", count)
144
+ },
145
+ )
146
+ }
147
+
148
+ private fun HeartRateRecord.toPayload(): HealthSyncRecordPayload {
149
+ val min = samples.minOfOrNull { it.beatsPerMinute }
150
+ val max = samples.maxOfOrNull { it.beatsPerMinute }
151
+ val avg = samples.map { it.beatsPerMinute }.average().takeIf { !it.isNaN() }
152
+
153
+ return HealthSyncRecordPayload(
154
+ metricType = "heart_rate",
155
+ recordId = metadata.id,
156
+ startTime = startTime.toString(),
157
+ endTime = endTime.toString(),
158
+ recordedAt = endTime.toString(),
159
+ numericValue = avg,
160
+ textValue = "${samples.size} samples",
161
+ unit = "bpm",
162
+ sourceAppId = metadata.dataOrigin.packageName.takeIf { it.isNotBlank() },
163
+ sourceDevice = metadata.device.toLabel(),
164
+ lastModifiedTime = metadata.lastModifiedTime.toString(),
165
+ payload = JSONObject().apply {
166
+ put("sampleCount", samples.size)
167
+ put("minBpm", min ?: JSONObject.NULL)
168
+ put("maxBpm", max ?: JSONObject.NULL)
169
+ put("avgBpm", avg ?: JSONObject.NULL)
170
+ put(
171
+ "samples",
172
+ JSONArray().apply {
173
+ samples.forEach { sample ->
174
+ put(
175
+ JSONObject().apply {
176
+ put("time", sample.time.toString())
177
+ put("beatsPerMinute", sample.beatsPerMinute)
178
+ },
179
+ )
180
+ }
181
+ },
182
+ )
183
+ },
184
+ )
185
+ }
186
+
187
+ private fun SleepSessionRecord.toPayload(): HealthSyncRecordPayload {
188
+ val durationMinutes = Duration.between(startTime, endTime).toMinutes()
189
+ return HealthSyncRecordPayload(
190
+ metricType = "sleep_session",
191
+ recordId = metadata.id,
192
+ startTime = startTime.toString(),
193
+ endTime = endTime.toString(),
194
+ recordedAt = endTime.toString(),
195
+ numericValue = durationMinutes.toDouble(),
196
+ textValue = title,
197
+ unit = "minutes",
198
+ sourceAppId = metadata.dataOrigin.packageName.takeIf { it.isNotBlank() },
199
+ sourceDevice = metadata.device.toLabel(),
200
+ lastModifiedTime = metadata.lastModifiedTime.toString(),
201
+ payload = JSONObject().apply {
202
+ put("title", title ?: JSONObject.NULL)
203
+ put("notes", notes ?: JSONObject.NULL)
204
+ put("stageCount", stages.size)
205
+ put(
206
+ "stages",
207
+ JSONArray().apply {
208
+ stages.forEach { stage ->
209
+ put(
210
+ JSONObject().apply {
211
+ put("startTime", stage.startTime.toString())
212
+ put("endTime", stage.endTime.toString())
213
+ put("stage", stage.stage)
214
+ },
215
+ )
216
+ }
217
+ },
218
+ )
219
+ },
220
+ )
221
+ }
222
+
223
+ private fun ExerciseSessionRecord.toPayload(): HealthSyncRecordPayload {
224
+ val durationMinutes = Duration.between(startTime, endTime).toMinutes()
225
+ return HealthSyncRecordPayload(
226
+ metricType = "exercise_session",
227
+ recordId = metadata.id,
228
+ startTime = startTime.toString(),
229
+ endTime = endTime.toString(),
230
+ recordedAt = endTime.toString(),
231
+ numericValue = durationMinutes.toDouble(),
232
+ textValue = title ?: "exercise:$exerciseType",
233
+ unit = "minutes",
234
+ sourceAppId = metadata.dataOrigin.packageName.takeIf { it.isNotBlank() },
235
+ sourceDevice = metadata.device.toLabel(),
236
+ lastModifiedTime = metadata.lastModifiedTime.toString(),
237
+ payload = JSONObject().apply {
238
+ put("exerciseType", exerciseType)
239
+ put("title", title ?: JSONObject.NULL)
240
+ put("notes", notes ?: JSONObject.NULL)
241
+ },
242
+ )
243
+ }
244
+
245
+ private fun WeightRecord.toPayload(): HealthSyncRecordPayload {
246
+ val kilograms = weight.inKilograms
247
+ return HealthSyncRecordPayload(
248
+ metricType = "weight",
249
+ recordId = metadata.id,
250
+ startTime = time.toString(),
251
+ endTime = null,
252
+ recordedAt = time.toString(),
253
+ numericValue = kilograms,
254
+ textValue = null,
255
+ unit = "kg",
256
+ sourceAppId = metadata.dataOrigin.packageName.takeIf { it.isNotBlank() },
257
+ sourceDevice = metadata.device.toLabel(),
258
+ lastModifiedTime = metadata.lastModifiedTime.toString(),
259
+ payload = JSONObject().apply {
260
+ put("kilograms", kilograms)
261
+ },
262
+ )
263
+ }
264
+
265
+ private fun Device?.toLabel(): String? {
266
+ if (this == null) return null
267
+ return when {
268
+ !manufacturer.isNullOrBlank() && !model.isNullOrBlank() -> "$manufacturer $model"
269
+ !manufacturer.isNullOrBlank() -> manufacturer
270
+ !model.isNullOrBlank() -> model
271
+ else -> type.toString()
272
+ }
273
+ }
274
+
275
+ companion object {
276
+ private const val PAGE_SIZE = 500
277
+ private const val PROVIDER_PACKAGE_NAME = "com.google.android.apps.healthdata"
278
+ private const val SOURCE_NAME = "android-health-connect"
279
+ }
280
+ }
@@ -0,0 +1,113 @@
1
+ package com.neoagent.flutter_app.health
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.PendingIntent
6
+ import android.content.Context
7
+ import android.content.Intent
8
+ import android.content.pm.ServiceInfo
9
+ import android.os.Build
10
+ import androidx.core.app.NotificationCompat
11
+ import androidx.core.app.NotificationManagerCompat
12
+ import androidx.work.ForegroundInfo
13
+ import com.neoagent.flutter_app.MainActivity
14
+ import com.neoagent.flutter_app.R
15
+
16
+ object HealthSyncNotifications {
17
+ private const val ACTIVE_CHANNEL_ID = "neoagent_health_sync_active"
18
+ private const val ALERT_CHANNEL_ID = "neoagent_health_sync_alerts"
19
+ private const val ACTIVE_NOTIFICATION_ID = 4201
20
+ private const val ALERT_NOTIFICATION_ID = 4202
21
+
22
+ fun register(context: Context) {
23
+ val manager =
24
+ context.getSystemService(NotificationManager::class.java) ?: return
25
+
26
+ manager.createNotificationChannel(
27
+ NotificationChannel(
28
+ ACTIVE_CHANNEL_ID,
29
+ "NeoAgent Health Sync",
30
+ NotificationManager.IMPORTANCE_MIN,
31
+ ).apply {
32
+ description =
33
+ "Low-key notification shown while NeoAgent health sync is actively running."
34
+ setShowBadge(false)
35
+ },
36
+ )
37
+
38
+ manager.createNotificationChannel(
39
+ NotificationChannel(
40
+ ALERT_CHANNEL_ID,
41
+ "NeoAgent Health Alerts",
42
+ NotificationManager.IMPORTANCE_LOW,
43
+ ).apply {
44
+ description =
45
+ "Shown when background health sync needs attention."
46
+ setShowBadge(false)
47
+ enableVibration(false)
48
+ setSound(null, null)
49
+ },
50
+ )
51
+ }
52
+
53
+ fun foregroundInfo(
54
+ context: Context,
55
+ message: String,
56
+ ): ForegroundInfo {
57
+ val notification =
58
+ NotificationCompat.Builder(context, ACTIVE_CHANNEL_ID)
59
+ .setSmallIcon(R.mipmap.ic_launcher)
60
+ .setContentTitle("NeoAgent health sync")
61
+ .setContentText(message)
62
+ .setOngoing(true)
63
+ .setSilent(true)
64
+ .setOnlyAlertOnce(true)
65
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
66
+ .setPriority(NotificationCompat.PRIORITY_MIN)
67
+ .setContentIntent(buildLaunchIntent(context))
68
+ .build()
69
+
70
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
71
+ ForegroundInfo(
72
+ ACTIVE_NOTIFICATION_ID,
73
+ notification,
74
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
75
+ )
76
+ } else {
77
+ ForegroundInfo(ACTIVE_NOTIFICATION_ID, notification)
78
+ }
79
+ }
80
+
81
+ fun showFailure(context: Context, message: String) {
82
+ NotificationManagerCompat.from(context).notify(
83
+ ALERT_NOTIFICATION_ID,
84
+ NotificationCompat.Builder(context, ALERT_CHANNEL_ID)
85
+ .setSmallIcon(android.R.drawable.stat_sys_warning)
86
+ .setContentTitle("Health sync needs attention")
87
+ .setContentText(message)
88
+ .setStyle(NotificationCompat.BigTextStyle().bigText(message))
89
+ .setAutoCancel(true)
90
+ .setOnlyAlertOnce(true)
91
+ .setSilent(true)
92
+ .setPriority(NotificationCompat.PRIORITY_LOW)
93
+ .setContentIntent(buildLaunchIntent(context))
94
+ .build(),
95
+ )
96
+ }
97
+
98
+ fun clearFailure(context: Context) {
99
+ NotificationManagerCompat.from(context).cancel(ALERT_NOTIFICATION_ID)
100
+ }
101
+
102
+ private fun buildLaunchIntent(context: Context): PendingIntent {
103
+ val intent = Intent(context, MainActivity::class.java).apply {
104
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
105
+ }
106
+ return PendingIntent.getActivity(
107
+ context,
108
+ 0,
109
+ intent,
110
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
111
+ )
112
+ }
113
+ }
@@ -0,0 +1,57 @@
1
+ package com.neoagent.flutter_app.health
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+
6
+ data class HealthSyncRecordPayload(
7
+ val metricType: String,
8
+ val recordId: String,
9
+ val startTime: String?,
10
+ val endTime: String?,
11
+ val recordedAt: String?,
12
+ val numericValue: Double?,
13
+ val textValue: String?,
14
+ val unit: String?,
15
+ val sourceAppId: String?,
16
+ val sourceDevice: String?,
17
+ val lastModifiedTime: String?,
18
+ val payload: JSONObject,
19
+ ) {
20
+ fun toJson(): JSONObject = JSONObject().apply {
21
+ put("metricType", metricType)
22
+ put("recordId", recordId)
23
+ put("startTime", startTime)
24
+ put("endTime", endTime)
25
+ put("recordedAt", recordedAt)
26
+ put("numericValue", numericValue)
27
+ put("textValue", textValue)
28
+ put("unit", unit)
29
+ put("sourceAppId", sourceAppId)
30
+ put("sourceDevice", sourceDevice)
31
+ put("lastModifiedTime", lastModifiedTime)
32
+ put("payload", payload)
33
+ }
34
+ }
35
+
36
+ data class HealthSyncBatchPayload(
37
+ val source: String,
38
+ val provider: String,
39
+ val windowStart: String,
40
+ val windowEnd: String,
41
+ val summary: JSONObject,
42
+ val records: List<HealthSyncRecordPayload>,
43
+ ) {
44
+ fun toJson(): JSONObject = JSONObject().apply {
45
+ put("source", source)
46
+ put("provider", provider)
47
+ put("windowStart", windowStart)
48
+ put("windowEnd", windowEnd)
49
+ put("summary", summary)
50
+ put(
51
+ "records",
52
+ JSONArray().apply {
53
+ records.forEach { put(it.toJson()) }
54
+ },
55
+ )
56
+ }
57
+ }
@@ -0,0 +1,78 @@
1
+ package com.neoagent.flutter_app.health
2
+
3
+ import android.content.Context
4
+ import androidx.work.Constraints
5
+ import androidx.work.ExistingPeriodicWorkPolicy
6
+ import androidx.work.ExistingWorkPolicy
7
+ import androidx.work.NetworkType
8
+ import androidx.work.OneTimeWorkRequestBuilder
9
+ import androidx.work.PeriodicWorkRequestBuilder
10
+ import androidx.work.WorkManager
11
+ import java.util.concurrent.TimeUnit
12
+
13
+ internal object HealthSyncPrefs {
14
+ const val PREFS_NAME = "neoagent_health_sync"
15
+ const val KEY_ENABLED = "enabled"
16
+ const val KEY_BACKEND_URL = "backend_url"
17
+ const val KEY_SESSION_COOKIE = "session_cookie"
18
+ const val KEY_LAST_SUCCESS_AT = "last_success_at"
19
+ const val KEY_CONSECUTIVE_FAILURES = "consecutive_failures"
20
+ const val KEY_LAST_ERROR = "last_error"
21
+
22
+ fun read(context: Context) =
23
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
24
+ }
25
+
26
+ class HealthSyncScheduler(private val context: Context) {
27
+
28
+ fun configure(
29
+ enabled: Boolean,
30
+ backendUrl: String,
31
+ sessionCookie: String,
32
+ ) {
33
+ HealthSyncNotifications.register(context)
34
+ HealthSyncPrefs.read(context)
35
+ .edit()
36
+ .putBoolean(HealthSyncPrefs.KEY_ENABLED, enabled)
37
+ .putString(HealthSyncPrefs.KEY_BACKEND_URL, backendUrl.trim())
38
+ .putString(HealthSyncPrefs.KEY_SESSION_COOKIE, sessionCookie.trim())
39
+ .apply()
40
+
41
+ val workManager = WorkManager.getInstance(context)
42
+ if (!enabled) {
43
+ HealthSyncNotifications.clearFailure(context)
44
+ workManager.cancelUniqueWork(UNIQUE_PERIODIC_WORK)
45
+ workManager.cancelUniqueWork(UNIQUE_IMMEDIATE_WORK)
46
+ return
47
+ }
48
+
49
+ val constraints = Constraints.Builder()
50
+ .setRequiredNetworkType(NetworkType.CONNECTED)
51
+ .build()
52
+
53
+ val periodicRequest =
54
+ PeriodicWorkRequestBuilder<HealthSyncWorker>(15, TimeUnit.MINUTES)
55
+ .setConstraints(constraints)
56
+ .build()
57
+
58
+ val immediateRequest = OneTimeWorkRequestBuilder<HealthSyncWorker>()
59
+ .setConstraints(constraints)
60
+ .build()
61
+
62
+ workManager.enqueueUniquePeriodicWork(
63
+ UNIQUE_PERIODIC_WORK,
64
+ ExistingPeriodicWorkPolicy.UPDATE,
65
+ periodicRequest,
66
+ )
67
+ workManager.enqueueUniqueWork(
68
+ UNIQUE_IMMEDIATE_WORK,
69
+ ExistingWorkPolicy.REPLACE,
70
+ immediateRequest,
71
+ )
72
+ }
73
+
74
+ companion object {
75
+ private const val UNIQUE_PERIODIC_WORK = "neoagent_health_periodic_sync"
76
+ private const val UNIQUE_IMMEDIATE_WORK = "neoagent_health_immediate_sync"
77
+ }
78
+ }