neoagent 2.3.1-beta.20 → 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 (215) hide show
  1. package/flutter_app/.metadata +42 -0
  2. package/flutter_app/README.md +21 -0
  3. package/flutter_app/analysis_options.yaml +32 -0
  4. package/flutter_app/android/app/build.gradle.kts +109 -0
  5. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  6. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  7. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  8. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  9. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  10. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  11. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  25. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  26. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  27. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  28. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  29. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  30. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  31. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  32. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  33. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  34. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  35. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  36. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  37. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  38. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  39. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  40. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  41. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  42. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  43. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  44. package/flutter_app/android/build.gradle.kts +24 -0
  45. package/flutter_app/android/ci-release.keystore +0 -0
  46. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  47. package/flutter_app/android/gradle.properties +3 -0
  48. package/flutter_app/android/key.properties +4 -0
  49. package/flutter_app/android/settings.gradle.kts +26 -0
  50. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  51. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  52. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  53. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  54. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  55. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  57. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  58. package/flutter_app/lib/features/location/location_service.dart +119 -0
  59. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  60. package/flutter_app/lib/main.dart +23057 -0
  61. package/flutter_app/lib/main_app_shell.dart +1682 -0
  62. package/flutter_app/lib/main_integrations.dart +931 -0
  63. package/flutter_app/lib/main_launcher.dart +959 -0
  64. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  65. package/flutter_app/lib/main_models.dart +3473 -0
  66. package/flutter_app/lib/main_shared.dart +2861 -0
  67. package/flutter_app/lib/main_theme.dart +204 -0
  68. package/flutter_app/lib/main_voice_assistant.dart +831 -0
  69. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  70. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  71. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  72. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  73. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  74. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  75. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  76. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  77. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  78. package/flutter_app/lib/src/backend_client.dart +1833 -0
  79. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  80. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  81. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  82. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  83. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  84. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  85. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  86. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  87. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  88. package/flutter_app/lib/src/health_bridge.dart +136 -0
  89. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  90. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  91. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  92. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  93. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  94. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  95. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  96. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  97. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  98. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  99. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  100. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  101. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  102. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  103. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  104. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  105. package/flutter_app/lib/src/theme/palette.dart +81 -0
  106. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  107. package/flutter_app/linux/CMakeLists.txt +128 -0
  108. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  109. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  110. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  111. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  112. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  113. package/flutter_app/linux/runner/main.cc +6 -0
  114. package/flutter_app/linux/runner/my_application.cc +144 -0
  115. package/flutter_app/linux/runner/my_application.h +18 -0
  116. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  117. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  118. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  119. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  120. package/flutter_app/macos/Podfile +42 -0
  121. package/flutter_app/macos/Podfile.lock +87 -0
  122. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  123. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  124. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  125. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  126. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  127. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  128. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  129. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  130. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  131. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  132. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  133. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  134. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  135. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  136. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  137. package/flutter_app/macos/Runner/Info.plist +36 -0
  138. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  139. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  140. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  141. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  142. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  143. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  144. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  145. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  146. package/flutter_app/patch_strings.py +12 -0
  147. package/flutter_app/pubspec.lock +1088 -0
  148. package/flutter_app/pubspec.yaml +53 -0
  149. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  150. package/flutter_app/test/recording_payloads_test.dart +53 -0
  151. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  152. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  153. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  154. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  155. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  156. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  157. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  158. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  159. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  160. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  161. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  162. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  163. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  164. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  165. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  166. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  167. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  168. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  169. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  170. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  171. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  172. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  173. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  174. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  175. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  176. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  177. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  178. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  179. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  180. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  181. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  182. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  183. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  184. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  185. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  186. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  187. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  188. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  189. package/flutter_app/web/favicon.png +0 -0
  190. package/flutter_app/web/favicon.svg +12 -0
  191. package/flutter_app/web/icons/Icon-192.png +0 -0
  192. package/flutter_app/web/icons/Icon-512.png +0 -0
  193. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  194. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  195. package/flutter_app/web/index.html +39 -0
  196. package/flutter_app/web/manifest.json +35 -0
  197. package/flutter_app/windows/CMakeLists.txt +108 -0
  198. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  199. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  200. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  201. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  202. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  203. package/flutter_app/windows/runner/Runner.rc +121 -0
  204. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  205. package/flutter_app/windows/runner/flutter_window.h +37 -0
  206. package/flutter_app/windows/runner/main.cpp +53 -0
  207. package/flutter_app/windows/runner/resource.h +16 -0
  208. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  209. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  210. package/flutter_app/windows/runner/utils.cpp +65 -0
  211. package/flutter_app/windows/runner/utils.h +19 -0
  212. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  213. package/flutter_app/windows/runner/win32_window.h +102 -0
  214. package/package.json +2 -1
  215. package/server/public/flutter_bootstrap.js +1 -1
@@ -0,0 +1,194 @@
1
+ package com.neoagent.flutter_app.widgets
2
+
3
+ import android.content.Context
4
+ import org.json.JSONArray
5
+ import org.json.JSONObject
6
+
7
+ data class CachedAiWidgetTask(
8
+ val id: String,
9
+ val name: String,
10
+ val triggerSummary: String,
11
+ )
12
+
13
+ data class CachedAiWidget(
14
+ val id: String,
15
+ val name: String,
16
+ val template: String,
17
+ val layoutVariant: String,
18
+ val refreshCron: String,
19
+ val enabled: Boolean,
20
+ val lastError: String?,
21
+ val latestSnapshot: JSONObject?,
22
+ val tasks: List<CachedAiWidgetTask>,
23
+ )
24
+
25
+ internal object AiWidgetPrefs {
26
+ const val PREFS_NAME = "neoagent_ai_widgets"
27
+ const val KEY_ENABLED = "enabled"
28
+ const val KEY_BACKEND_URL = "backend_url"
29
+ const val KEY_SESSION_COOKIE = "session_cookie"
30
+ const val KEY_CACHED_WIDGETS = "cached_widgets"
31
+ const val KEY_LAST_SYNC_AT = "last_sync_at"
32
+ const val KEY_LAST_ERROR = "last_error"
33
+ const val KEY_PENDING_OPEN_WIDGET_ID = "pending_open_widget_id"
34
+ const val KEY_BINDING_PREFIX = "binding_"
35
+ const val KEY_EXPANDED_PREFIX = "expanded_"
36
+
37
+ fun read(context: Context) =
38
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
39
+ }
40
+
41
+ class AiWidgetStore(private val context: Context) {
42
+
43
+ private val pendingOpenLock = Any()
44
+
45
+ fun saveConfig(
46
+ enabled: Boolean,
47
+ backendUrl: String,
48
+ sessionCookie: String,
49
+ ) {
50
+ AiWidgetPrefs.read(context)
51
+ .edit()
52
+ .putBoolean(AiWidgetPrefs.KEY_ENABLED, enabled)
53
+ .putString(AiWidgetPrefs.KEY_BACKEND_URL, backendUrl.trim())
54
+ .putString(AiWidgetPrefs.KEY_SESSION_COOKIE, sessionCookie.trim())
55
+ .apply()
56
+ }
57
+
58
+ fun isEnabled(): Boolean =
59
+ AiWidgetPrefs.read(context).getBoolean(AiWidgetPrefs.KEY_ENABLED, false)
60
+
61
+ fun backendUrl(): String =
62
+ AiWidgetPrefs.read(context)
63
+ .getString(AiWidgetPrefs.KEY_BACKEND_URL, "")
64
+ ?.trim()
65
+ .orEmpty()
66
+
67
+ fun sessionCookie(): String =
68
+ AiWidgetPrefs.read(context)
69
+ .getString(AiWidgetPrefs.KEY_SESSION_COOKIE, "")
70
+ ?.trim()
71
+ .orEmpty()
72
+
73
+ fun saveCachedWidgets(rawJson: String) {
74
+ AiWidgetPrefs.read(context)
75
+ .edit()
76
+ .putString(AiWidgetPrefs.KEY_CACHED_WIDGETS, rawJson)
77
+ .putString(AiWidgetPrefs.KEY_LAST_SYNC_AT, java.time.Instant.now().toString())
78
+ .remove(AiWidgetPrefs.KEY_LAST_ERROR)
79
+ .apply()
80
+ }
81
+
82
+ fun setLastError(message: String) {
83
+ AiWidgetPrefs.read(context)
84
+ .edit()
85
+ .putString(AiWidgetPrefs.KEY_LAST_ERROR, message)
86
+ .apply()
87
+ }
88
+
89
+ fun cachedWidgets(): List<CachedAiWidget> {
90
+ val raw =
91
+ AiWidgetPrefs.read(context).getString(AiWidgetPrefs.KEY_CACHED_WIDGETS, null)
92
+ ?: return emptyList()
93
+ return try {
94
+ val array = JSONArray(raw)
95
+ buildList {
96
+ for (index in 0 until array.length()) {
97
+ val item = array.optJSONObject(index) ?: continue
98
+ add(
99
+ CachedAiWidget(
100
+ id = item.optString("id").trim(),
101
+ name = item.optString("name").trim(),
102
+ template = item.optString("template", "summary").trim(),
103
+ layoutVariant =
104
+ item.optString("layoutVariant", item.optString("layout_variant"))
105
+ .trim(),
106
+ refreshCron =
107
+ item.optString("refreshCron", item.optString("refresh_cron"))
108
+ .trim(),
109
+ enabled = item.optBoolean("enabled", true),
110
+ lastError =
111
+ item.optString("lastError")
112
+ .trim()
113
+ .ifEmpty { item.optString("last_error").trim() }
114
+ .ifEmpty { "" }
115
+ .let { if (it.isBlank()) null else it },
116
+ latestSnapshot = item.optJSONObject("latestSnapshot"),
117
+ tasks = buildList {
118
+ val tasksArray = item.optJSONArray("tasks")
119
+ if (tasksArray != null) {
120
+ for (t in 0 until tasksArray.length()) {
121
+ val tItem = tasksArray.optJSONObject(t) ?: continue
122
+ add(
123
+ CachedAiWidgetTask(
124
+ id = tItem.optString("id"),
125
+ name = tItem.optString("name", "Task"),
126
+ triggerSummary = tItem.optString("triggerSummary"),
127
+ )
128
+ )
129
+ }
130
+ }
131
+ },
132
+ ),
133
+ )
134
+ }
135
+ }
136
+ } catch (_: Exception) {
137
+ emptyList()
138
+ }
139
+ }
140
+
141
+ fun findWidget(widgetId: String): CachedAiWidget? =
142
+ cachedWidgets().firstOrNull { it.id == widgetId }
143
+
144
+ fun bindAppWidget(appWidgetId: Int, widgetId: String) {
145
+ AiWidgetPrefs.read(context)
146
+ .edit()
147
+ .putString("${AiWidgetPrefs.KEY_BINDING_PREFIX}$appWidgetId", widgetId)
148
+ .apply()
149
+ }
150
+
151
+ fun widgetIdForAppWidget(appWidgetId: Int): String? =
152
+ AiWidgetPrefs.read(context)
153
+ .getString("${AiWidgetPrefs.KEY_BINDING_PREFIX}$appWidgetId", null)
154
+ ?.trim()
155
+ ?.let { if (it.isBlank()) null else it }
156
+
157
+ fun clearBinding(appWidgetId: Int) {
158
+ AiWidgetPrefs.read(context)
159
+ .edit()
160
+ .remove("${AiWidgetPrefs.KEY_BINDING_PREFIX}$appWidgetId")
161
+ .apply()
162
+ }
163
+
164
+ fun setPendingOpenWidgetId(widgetId: String?) {
165
+ AiWidgetPrefs.read(context)
166
+ .edit()
167
+ .putString(AiWidgetPrefs.KEY_PENDING_OPEN_WIDGET_ID, widgetId?.trim())
168
+ .apply()
169
+ }
170
+
171
+ fun consumePendingOpenWidgetId(): String? {
172
+ synchronized(pendingOpenLock) {
173
+ val prefs = AiWidgetPrefs.read(context)
174
+ val widgetId =
175
+ prefs.getString(AiWidgetPrefs.KEY_PENDING_OPEN_WIDGET_ID, null)
176
+ ?.trim()
177
+ ?.let { if (it.isBlank()) null else it }
178
+ if (widgetId != null) {
179
+ prefs.edit().remove(AiWidgetPrefs.KEY_PENDING_OPEN_WIDGET_ID).commit()
180
+ }
181
+ return widgetId
182
+ }
183
+ }
184
+
185
+ fun isTasksExpanded(appWidgetId: Int): Boolean =
186
+ AiWidgetPrefs.read(context).getBoolean("${AiWidgetPrefs.KEY_EXPANDED_PREFIX}$appWidgetId", false)
187
+
188
+ fun toggleTasksExpanded(appWidgetId: Int) {
189
+ val prefs = AiWidgetPrefs.read(context)
190
+ val key = "${AiWidgetPrefs.KEY_EXPANDED_PREFIX}$appWidgetId"
191
+ val current = prefs.getBoolean(key, false)
192
+ prefs.edit().putBoolean(key, !current).apply()
193
+ }
194
+ }
@@ -0,0 +1,67 @@
1
+ package com.neoagent.flutter_app.widgets
2
+
3
+ import android.app.PendingIntent
4
+ import android.appwidget.AppWidgetManager
5
+ import android.appwidget.AppWidgetProvider
6
+ import android.content.ComponentName
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.widget.RemoteViews
10
+ import com.neoagent.flutter_app.MainActivity
11
+ import com.neoagent.flutter_app.R
12
+
13
+ class VoiceLaunchWidgetProvider : AppWidgetProvider() {
14
+
15
+ override fun onUpdate(
16
+ context: Context,
17
+ appWidgetManager: AppWidgetManager,
18
+ appWidgetIds: IntArray,
19
+ ) {
20
+ refreshAll(context, appWidgetManager, appWidgetIds)
21
+ }
22
+
23
+ companion object {
24
+ const val ACTION_OPEN_VOICE_ASSISTANT =
25
+ "com.neoagent.flutter_app.widgets.OPEN_VOICE_ASSISTANT"
26
+ const val OPEN_TARGET_VOICE_ASSISTANT = "voice_assistant"
27
+
28
+ fun refreshAll(context: Context) {
29
+ val manager = AppWidgetManager.getInstance(context)
30
+ val componentName = ComponentName(context, VoiceLaunchWidgetProvider::class.java)
31
+ val ids = manager.getAppWidgetIds(componentName)
32
+ refreshAll(context, manager, ids)
33
+ }
34
+
35
+ fun refreshAll(
36
+ context: Context,
37
+ manager: AppWidgetManager,
38
+ appWidgetIds: IntArray,
39
+ ) {
40
+ appWidgetIds.forEach { appWidgetId ->
41
+ manager.updateAppWidget(appWidgetId, buildRemoteViews(context, appWidgetId))
42
+ }
43
+ }
44
+
45
+ private fun buildRemoteViews(context: Context, appWidgetId: Int): RemoteViews {
46
+ val views = RemoteViews(context.packageName, R.layout.neoagent_voice_widget)
47
+ val intent =
48
+ Intent(context, MainActivity::class.java).apply {
49
+ action = ACTION_OPEN_VOICE_ASSISTANT
50
+ flags =
51
+ Intent.FLAG_ACTIVITY_NEW_TASK or
52
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
53
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
54
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
55
+ }
56
+ val pendingIntent =
57
+ PendingIntent.getActivity(
58
+ context,
59
+ appWidgetId,
60
+ intent,
61
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
62
+ )
63
+ views.setOnClickPendingIntent(R.id.voice_widget_root, pendingIntent)
64
+ return views
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,228 @@
1
+ package com.neoagent.flutter_app.widgets
2
+
3
+ import android.app.Activity
4
+ import android.appwidget.AppWidgetManager
5
+ import android.content.Intent
6
+ import android.graphics.Color
7
+ import android.graphics.Typeface
8
+ import android.graphics.drawable.GradientDrawable
9
+ import android.os.Bundle
10
+ import android.util.TypedValue
11
+ import android.view.View
12
+ import android.view.ViewGroup
13
+ import android.widget.LinearLayout
14
+ import android.widget.ScrollView
15
+ import android.widget.TextView
16
+ import com.neoagent.flutter_app.R
17
+
18
+ class WidgetConfigActivity : Activity() {
19
+
20
+ override fun onCreate(savedInstanceState: Bundle?) {
21
+ super.onCreate(savedInstanceState)
22
+
23
+ val appWidgetId =
24
+ intent?.extras?.getInt(
25
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
26
+ AppWidgetManager.INVALID_APPWIDGET_ID,
27
+ ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
28
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
29
+ finish()
30
+ return
31
+ }
32
+
33
+ setResult(RESULT_CANCELED)
34
+
35
+ val store = AiWidgetStore(this)
36
+ val widgets = store.cachedWidgets().filter { it.id.isNotBlank() }
37
+
38
+ val root =
39
+ ScrollView(this).apply {
40
+ setBackgroundColor(Color.parseColor("#0D1118"))
41
+ isFillViewport = true
42
+ }
43
+ val content =
44
+ LinearLayout(this).apply {
45
+ orientation = LinearLayout.VERTICAL
46
+ setPadding(dp(20), dp(28), dp(20), dp(24))
47
+ }
48
+ root.addView(
49
+ content,
50
+ ViewGroup.LayoutParams(
51
+ ViewGroup.LayoutParams.MATCH_PARENT,
52
+ ViewGroup.LayoutParams.WRAP_CONTENT,
53
+ ),
54
+ )
55
+
56
+ content.addView(
57
+ textView(
58
+ text = getString(R.string.widget_config_title),
59
+ sizeSp = 28f,
60
+ color = Color.WHITE,
61
+ style = Typeface.BOLD,
62
+ ),
63
+ )
64
+ content.addView(spacer(8))
65
+ content.addView(
66
+ textView(
67
+ text = "Choose the widget you want to place on your home screen.",
68
+ sizeSp = 15f,
69
+ color = Color.parseColor("#A7B2C5"),
70
+ ),
71
+ )
72
+ content.addView(spacer(22))
73
+
74
+ if (widgets.isEmpty()) {
75
+ content.addView(
76
+ buildCard(
77
+ title = "No widgets available yet",
78
+ subtitle = getString(R.string.widget_config_empty_state),
79
+ meta = "Open NeoAgent and refresh widgets first",
80
+ ) {},
81
+ )
82
+ setContentView(root)
83
+ return
84
+ }
85
+
86
+ widgets.forEachIndexed { index, widget ->
87
+ val cadence =
88
+ if (widget.enabled) formatCadence(widget.refreshCron) else "Paused"
89
+ val subtitle =
90
+ widget.latestSnapshot?.optString("subtitle")
91
+ ?.trim()
92
+ ?.takeIf { it.isNotEmpty() && !it.equals("null", ignoreCase = true) }
93
+ ?: if (widget.latestSnapshot == null) {
94
+ "Waiting for first update"
95
+ } else {
96
+ "Ready on home screen"
97
+ }
98
+ val card =
99
+ buildCard(
100
+ title = displayName(widget.name),
101
+ subtitle = subtitle,
102
+ meta = cadence,
103
+ ) {
104
+ store.bindAppWidget(appWidgetId, widget.id)
105
+ AiHomeWidgetProvider.refreshAll(this)
106
+ val resultIntent =
107
+ Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
108
+ setResult(RESULT_OK, resultIntent)
109
+ finish()
110
+ }
111
+ content.addView(card)
112
+ if (index < widgets.lastIndex) {
113
+ content.addView(spacer(14))
114
+ }
115
+ }
116
+
117
+ setContentView(root)
118
+ }
119
+
120
+ private fun buildCard(
121
+ title: String,
122
+ subtitle: String,
123
+ meta: String,
124
+ onClick: () -> Unit,
125
+ ): View {
126
+ val card =
127
+ LinearLayout(this).apply {
128
+ orientation = LinearLayout.VERTICAL
129
+ setPadding(dp(18), dp(18), dp(18), dp(18))
130
+ background =
131
+ GradientDrawable().apply {
132
+ cornerRadius = dp(24).toFloat()
133
+ setColor(Color.parseColor("#171E29"))
134
+ setStroke(dp(1), Color.parseColor("#22FFFFFF"))
135
+ }
136
+ isClickable = true
137
+ isFocusable = true
138
+ setOnClickListener { onClick() }
139
+ }
140
+ card.addView(
141
+ textView(
142
+ text = title,
143
+ sizeSp = 20f,
144
+ color = Color.WHITE,
145
+ style = Typeface.BOLD,
146
+ ),
147
+ )
148
+ card.addView(spacer(8))
149
+ card.addView(
150
+ textView(
151
+ text = subtitle,
152
+ sizeSp = 14f,
153
+ color = Color.parseColor("#C7D0E0"),
154
+ ),
155
+ )
156
+ card.addView(spacer(14))
157
+ card.addView(
158
+ textView(
159
+ text = meta,
160
+ sizeSp = 13f,
161
+ color = Color.parseColor("#8FB8FF"),
162
+ style = Typeface.BOLD,
163
+ ),
164
+ )
165
+ return card
166
+ }
167
+
168
+ private fun textView(
169
+ text: String,
170
+ sizeSp: Float,
171
+ color: Int,
172
+ style: Int = Typeface.NORMAL,
173
+ ): TextView {
174
+ return TextView(this).apply {
175
+ this.text = text
176
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, sizeSp)
177
+ setTextColor(color)
178
+ setTypeface(typeface, style)
179
+ }
180
+ }
181
+
182
+ private fun spacer(heightDp: Int): View =
183
+ View(this).apply {
184
+ layoutParams =
185
+ LinearLayout.LayoutParams(
186
+ ViewGroup.LayoutParams.MATCH_PARENT,
187
+ dp(heightDp),
188
+ )
189
+ }
190
+
191
+ private fun formatCadence(refreshCron: String): String {
192
+ val normalized = refreshCron.trim()
193
+ if (normalized == "0 * * * *") {
194
+ return "Updates hourly"
195
+ }
196
+ val hours = Regex("\\*/(\\d+)").find(normalized)?.groupValues?.getOrNull(1)?.toIntOrNull()
197
+ if (hours != null && hours > 1) {
198
+ return "Every $hours hours"
199
+ }
200
+ return "Refreshes automatically"
201
+ }
202
+
203
+ private fun displayName(raw: String): String {
204
+ val normalized =
205
+ raw.trim()
206
+ .replace(Regex("[_-]+"), " ")
207
+ .replace(Regex("\\s+"), " ")
208
+ if (normalized.isBlank()) {
209
+ return "AI Widget"
210
+ }
211
+ return normalized.split(" ")
212
+ .filter { it.isNotBlank() }
213
+ .joinToString(" ") { part ->
214
+ if (part.length <= 2 && part.uppercase() == part) {
215
+ part
216
+ } else {
217
+ part.substring(0, 1).uppercase() + part.substring(1)
218
+ }
219
+ }
220
+ }
221
+
222
+ private fun dp(value: Int): Int =
223
+ TypedValue.applyDimension(
224
+ TypedValue.COMPLEX_UNIT_DIP,
225
+ value.toFloat(),
226
+ resources.displayMetrics,
227
+ ).toInt()
228
+ }
@@ -0,0 +1,72 @@
1
+ package com.neoagent.flutter_app.widgets
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
+ class WidgetSyncScheduler(private val context: Context) {
14
+
15
+ fun configure(
16
+ enabled: Boolean,
17
+ backendUrl: String,
18
+ sessionCookie: String,
19
+ ) {
20
+ val store = AiWidgetStore(context)
21
+ store.saveConfig(
22
+ enabled = enabled,
23
+ backendUrl = backendUrl,
24
+ sessionCookie = sessionCookie,
25
+ )
26
+
27
+ val workManager = WorkManager.getInstance(context)
28
+ if (!enabled) {
29
+ workManager.cancelUniqueWork(UNIQUE_PERIODIC_WORK)
30
+ workManager.cancelUniqueWork(UNIQUE_IMMEDIATE_WORK)
31
+ return
32
+ }
33
+
34
+ val constraints = Constraints.Builder()
35
+ .setRequiredNetworkType(NetworkType.CONNECTED)
36
+ .build()
37
+
38
+ val periodicRequest =
39
+ PeriodicWorkRequestBuilder<WidgetSyncWorker>(1, TimeUnit.HOURS)
40
+ .setConstraints(constraints)
41
+ .build()
42
+
43
+ workManager.enqueueUniquePeriodicWork(
44
+ UNIQUE_PERIODIC_WORK,
45
+ ExistingPeriodicWorkPolicy.UPDATE,
46
+ periodicRequest,
47
+ )
48
+ }
49
+
50
+ fun syncNow() {
51
+ if (!AiWidgetStore(context).isEnabled()) {
52
+ return
53
+ }
54
+ val constraints = Constraints.Builder()
55
+ .setRequiredNetworkType(NetworkType.CONNECTED)
56
+ .build()
57
+ val request =
58
+ OneTimeWorkRequestBuilder<WidgetSyncWorker>()
59
+ .setConstraints(constraints)
60
+ .build()
61
+ WorkManager.getInstance(context).enqueueUniqueWork(
62
+ UNIQUE_IMMEDIATE_WORK,
63
+ ExistingWorkPolicy.REPLACE,
64
+ request,
65
+ )
66
+ }
67
+
68
+ companion object {
69
+ private const val UNIQUE_PERIODIC_WORK = "neoagent_widgets_periodic_sync"
70
+ private const val UNIQUE_IMMEDIATE_WORK = "neoagent_widgets_immediate_sync"
71
+ }
72
+ }