neoagent 2.3.1-beta.50 → 2.3.1-beta.52

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 (38) hide show
  1. package/flutter_app/android/app/build.gradle.kts +2 -0
  2. package/flutter_app/android/app/src/main/AndroidManifest.xml +24 -0
  3. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +57 -0
  4. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
  5. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
  6. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
  7. package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
  8. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  9. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +262 -0
  10. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +298 -0
  11. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +59 -0
  12. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +185 -0
  13. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +164 -0
  14. package/flutter_app/lib/main.dart +2 -0
  15. package/flutter_app/lib/main_chat.dart +422 -421
  16. package/flutter_app/lib/main_controller.dart +49 -0
  17. package/flutter_app/lib/main_runtime.dart +3 -0
  18. package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
  19. package/flutter_app/lib/src/backend_client.dart +4 -0
  20. package/flutter_app/lib/src/oauth_launcher_io.dart +18 -0
  21. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +4 -0
  22. package/flutter_app/linux/flutter/generated_plugins.cmake +1 -0
  23. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +4 -0
  24. package/flutter_app/pubspec.lock +136 -0
  25. package/flutter_app/pubspec.yaml +4 -0
  26. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +3 -0
  27. package/flutter_app/windows/flutter/generated_plugins.cmake +1 -0
  28. package/package.json +1 -1
  29. package/server/db/database.js +15 -1
  30. package/server/public/assets/AssetManifest.bin +1 -1
  31. package/server/public/assets/AssetManifest.bin.json +1 -1
  32. package/server/public/assets/NOTICES +280 -0
  33. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  34. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  35. package/server/public/flutter_bootstrap.js +1 -1
  36. package/server/public/main.dart.js +79280 -77226
  37. package/server/routes/auth.js +20 -4
  38. package/server/services/ai/tools.js +11 -1
@@ -98,6 +98,8 @@ android {
98
98
 
99
99
  dependencies {
100
100
  implementation("androidx.activity:activity-ktx:1.10.1")
101
+ implementation("androidx.car.app:app:1.7.0")
102
+ implementation("androidx.car.app:app-projected:1.7.0")
101
103
  implementation("androidx.health.connect:connect-client:1.1.0")
102
104
  implementation("androidx.security:security-crypto:1.1.0")
103
105
  implementation("androidx.work:work-runtime-ktx:2.10.1")
@@ -1,6 +1,7 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
2
  xmlns:tools="http://schemas.android.com/tools">
3
3
  <uses-permission android:name="android.permission.INTERNET" />
4
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
4
5
  <uses-permission android:name="android.permission.CAMERA" />
5
6
  <uses-permission android:name="android.permission.RECORD_AUDIO" />
6
7
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -31,6 +32,9 @@
31
32
  android:name="${applicationName}"
32
33
  android:icon="@mipmap/ic_launcher"
33
34
  android:usesCleartextTraffic="true">
35
+ <meta-data
36
+ android:name="com.google.android.gms.car.application"
37
+ android:resource="@xml/automotive_app_desc" />
34
38
  <activity
35
39
  android:name=".MainActivity"
36
40
  android:exported="true"
@@ -95,6 +99,26 @@
95
99
  android:name="androidx.work.impl.foreground.SystemForegroundService"
96
100
  android:foregroundServiceType="dataSync"
97
101
  tools:node="merge" />
102
+ <service
103
+ android:name=".telecom.NeoAgentConnectionService"
104
+ android:exported="true"
105
+ android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
106
+ <intent-filter>
107
+ <action android:name="android.telecom.ConnectionService" />
108
+ </intent-filter>
109
+ </service>
110
+
111
+ <service
112
+ android:name=".auto.NeoAgentCarAppService"
113
+ android:exported="true">
114
+ <intent-filter>
115
+ <action android:name="androidx.car.app.CarAppService" />
116
+ <category android:name="androidx.car.app.category.IOT" />
117
+ </intent-filter>
118
+ <meta-data
119
+ android:name="androidx.car.app.minCarApiLevel"
120
+ android:value="1" />
121
+ </service>
98
122
  <service
99
123
  android:name=".recording.RecordingForegroundService"
100
124
  android:exported="false"
@@ -26,6 +26,14 @@ import com.neoagent.flutter_app.widgets.WidgetSyncScheduler
26
26
  import io.flutter.embedding.android.FlutterFragmentActivity
27
27
  import io.flutter.embedding.engine.FlutterEngine
28
28
  import io.flutter.plugin.common.EventChannel
29
+ import android.telecom.PhoneAccount
30
+ import android.telecom.PhoneAccountHandle
31
+ import android.telecom.TelecomManager
32
+ import android.content.ComponentName
33
+ import android.os.Bundle
34
+ import android.telecom.DisconnectCause
35
+ import android.content.Context
36
+ import com.neoagent.flutter_app.telecom.NeoAgentConnectionService
29
37
  import io.flutter.plugin.common.MethodChannel
30
38
  import io.flutter.plugins.GeneratedPluginRegistrant
31
39
  import kotlinx.coroutines.launch
@@ -50,11 +58,22 @@ class MainActivity : FlutterFragmentActivity() {
50
58
 
51
59
  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
52
60
  super.configureFlutterEngine(flutterEngine)
61
+ io.flutter.embedding.engine.FlutterEngineCache.getInstance().put("main_engine", flutterEngine)
53
62
  GeneratedPluginRegistrant.registerWith(flutterEngine)
54
63
 
55
64
  healthGateway = HealthConnectGateway(this)
56
65
  healthSyncScheduler = HealthSyncScheduler(this)
57
66
  widgetSyncScheduler = WidgetSyncScheduler(this)
67
+
68
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
69
+ val telecomManager = getSystemService(Context.TELECOM_SERVICE) as TelecomManager
70
+ val componentName = ComponentName(this, NeoAgentConnectionService::class.java)
71
+ val phoneAccountHandle = PhoneAccountHandle(componentName, "NeoAgentVoiceId")
72
+ val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "NeoAgent Voice Assistant")
73
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
74
+ .build()
75
+ telecomManager.registerPhoneAccount(phoneAccount)
76
+ }
58
77
  recordingStateStore = RecordingStateStore(this)
59
78
  permissionLauncher = registerForActivityResult(
60
79
  PermissionController.createRequestPermissionResultContract(),
@@ -91,6 +110,44 @@ class MainActivity : FlutterFragmentActivity() {
91
110
  )
92
111
  }
93
112
  }
113
+ MethodChannel(
114
+ flutterEngine.dartExecutor.binaryMessenger,
115
+ "neoagent/telecom",
116
+ ).setMethodCallHandler { call, result ->
117
+ when (call.method) {
118
+ "startCallRouting" -> {
119
+ try {
120
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
121
+ result.error("UNSUPPORTED", "Self-managed calls require Android O or newer", null)
122
+ return@setMethodCallHandler
123
+ }
124
+ val telecomManager = getSystemService(Context.TELECOM_SERVICE) as TelecomManager
125
+ val componentName = ComponentName(this, NeoAgentConnectionService::class.java)
126
+ val phoneAccountHandle = PhoneAccountHandle(componentName, "NeoAgentVoiceId")
127
+ val extras = Bundle().apply {
128
+ putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
129
+ putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, android.telecom.VideoProfile.STATE_AUDIO_ONLY)
130
+ putBoolean("is_flutter_initiated", true)
131
+ }
132
+ telecomManager.placeCall(Uri.parse("tel:NeoAgent"), extras)
133
+ result.success(true)
134
+ } catch (e: SecurityException) {
135
+ result.error("PERMISSION_DENIED", "Missing Manage Own Calls permission", null)
136
+ } catch (e: Exception) {
137
+ result.error("ERROR", e.message, null)
138
+ }
139
+ }
140
+ "stopCallRouting" -> {
141
+ NeoAgentConnectionService.getAndClearCurrentConnection()?.let {
142
+ it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
143
+ it.destroy()
144
+ }
145
+ result.success(true)
146
+ }
147
+ else -> result.notImplemented()
148
+ }
149
+ }
150
+
94
151
  MethodChannel(
95
152
  flutterEngine.dartExecutor.binaryMessenger,
96
153
  "neoagent/health",
@@ -0,0 +1,114 @@
1
+ package com.neoagent.flutter_app.auto
2
+
3
+ import android.content.Intent
4
+ import android.content.Context
5
+ import android.net.Uri
6
+ import android.os.Bundle
7
+ import android.telecom.PhoneAccount
8
+ import android.telecom.PhoneAccountHandle
9
+ import android.telecom.TelecomManager
10
+ import android.content.ComponentName
11
+ import android.content.pm.ApplicationInfo
12
+ import androidx.car.app.CarAppService
13
+ import androidx.car.app.CarContext
14
+ import androidx.car.app.CarToast
15
+ import androidx.car.app.Screen
16
+ import androidx.car.app.Session
17
+ import androidx.car.app.model.Action
18
+ import androidx.car.app.model.CarIcon
19
+ import androidx.car.app.model.GridItem
20
+ import androidx.car.app.model.GridTemplate
21
+ import androidx.car.app.model.ItemList
22
+ import androidx.car.app.model.Template
23
+ import androidx.car.app.validation.HostValidator
24
+ import androidx.core.graphics.drawable.IconCompat
25
+ import com.neoagent.flutter_app.MainActivity
26
+ import com.neoagent.flutter_app.R
27
+ import com.neoagent.flutter_app.widgets.VoiceLaunchWidgetProvider
28
+ import com.neoagent.flutter_app.telecom.NeoAgentConnectionService
29
+
30
+ class NeoAgentCarAppService : CarAppService() {
31
+ override fun onCreateSession(): Session = NeoAgentCarSession()
32
+
33
+ override fun createHostValidator(): HostValidator {
34
+ val debuggable =
35
+ (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
36
+ return if (debuggable) {
37
+ HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
38
+ } else {
39
+ HostValidator.Builder(this)
40
+ .addAllowedHosts(R.array.hosts_allowlist)
41
+ .build()
42
+ }
43
+ }
44
+ }
45
+
46
+ private class NeoAgentCarSession : Session() {
47
+ override fun onCreateScreen(intent: Intent): Screen {
48
+ return NeoAgentCarHomeScreen(carContext)
49
+ }
50
+ }
51
+
52
+ private class NeoAgentCarHomeScreen(carContext: CarContext) : Screen(carContext) {
53
+ override fun onGetTemplate(): Template {
54
+ val voiceItem = GridItem.Builder()
55
+ .setTitle("Voice mode")
56
+ .setText("Tap to talk")
57
+ .setImage(
58
+ CarIcon.Builder(
59
+ IconCompat.createWithResource(carContext, R.mipmap.ic_launcher),
60
+ ).build(),
61
+ GridItem.IMAGE_TYPE_ICON,
62
+ )
63
+ .setOnClickListener { launchVoiceAssistant() }
64
+ .build()
65
+
66
+ val grid = ItemList.Builder().addItem(voiceItem).build()
67
+ val template = GridTemplate.Builder()
68
+ .setTitle("NeoAgent")
69
+ .setHeaderAction(Action.APP_ICON)
70
+ .setSingleList(grid)
71
+
72
+ if (carContext.carAppApiLevel >= 8) {
73
+ template.setItemSize(GridTemplate.ITEM_SIZE_LARGE)
74
+ }
75
+
76
+ return template.build()
77
+ }
78
+
79
+ private fun launchVoiceAssistant() {
80
+ try {
81
+ val telecomManager = carContext.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
82
+ if (telecomManager == null) {
83
+ CarToast.makeText(carContext, "Telecom service unavailable", CarToast.LENGTH_LONG).show()
84
+ return
85
+ }
86
+ val componentName = ComponentName(carContext, NeoAgentConnectionService::class.java)
87
+ val phoneAccountHandle = PhoneAccountHandle(componentName, "NeoAgentVoiceId")
88
+
89
+ val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "NeoAgent Voice Assistant")
90
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
91
+ .build()
92
+
93
+ telecomManager.registerPhoneAccount(phoneAccount)
94
+
95
+ val extras = Bundle().apply {
96
+ putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
97
+ // For proper routing:
98
+ putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, android.telecom.VideoProfile.STATE_AUDIO_ONLY)
99
+ }
100
+
101
+ telecomManager.placeCall(Uri.parse("tel:NeoAgent"), extras)
102
+
103
+ CarToast.makeText(
104
+ carContext,
105
+ "Starting voice mode...",
106
+ CarToast.LENGTH_SHORT,
107
+ ).show()
108
+ } catch (e: SecurityException) {
109
+ CarToast.makeText(carContext, "Missing phone permission", CarToast.LENGTH_LONG).show()
110
+ } catch (e: Exception) {
111
+ CarToast.makeText(carContext, "Could not start call: ${e.message}", CarToast.LENGTH_LONG).show()
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,118 @@
1
+ package com.neoagent.flutter_app.telecom
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import android.telecom.Connection
8
+ import android.telecom.DisconnectCause
9
+ import android.telecom.TelecomManager
10
+ import io.flutter.embedding.engine.FlutterEngine
11
+ import io.flutter.embedding.engine.FlutterEngineCache
12
+ import io.flutter.embedding.engine.dart.DartExecutor
13
+ import io.flutter.plugin.common.MethodChannel
14
+
15
+ class NeoAgentConnection(private val context: Context) : Connection() {
16
+ private var flutterEngine: FlutterEngine? = null
17
+ var isFlutterInitiated: Boolean = false
18
+ private var voiceHeadlessStarted: Boolean = false
19
+
20
+ init {
21
+ audioModeIsVoip = true
22
+ setAddress(Uri.parse("tel:NeoAgent"), TelecomManager.PRESENTATION_ALLOWED)
23
+ setCallerDisplayName("NeoAgent", TelecomManager.PRESENTATION_ALLOWED)
24
+ connectionProperties = PROPERTY_SELF_MANAGED
25
+ connectionCapabilities = CAPABILITY_MUTE or CAPABILITY_HOLD
26
+ }
27
+
28
+ override fun onAnswer() {
29
+ setActive()
30
+ startVoiceAssistantHeadless()
31
+ }
32
+
33
+ override fun onAnswer(videoState: Int) {
34
+ onAnswer()
35
+ }
36
+
37
+ override fun onReject() {
38
+ setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
39
+ destroy()
40
+ cleanup()
41
+ }
42
+
43
+ override fun onAbort() {
44
+ setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
45
+ destroy()
46
+ cleanup()
47
+ }
48
+
49
+ override fun onDisconnect() {
50
+ setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
51
+ destroy()
52
+ cleanup()
53
+ }
54
+
55
+ override fun onStateChanged(state: Int) {
56
+ super.onStateChanged(state)
57
+ if (state == STATE_ACTIVE) {
58
+ startVoiceAssistantHeadless()
59
+ } else if (state == STATE_DISCONNECTED) {
60
+ cleanup()
61
+ }
62
+ }
63
+
64
+ private fun startVoiceAssistantHeadless() {
65
+ if (voiceHeadlessStarted) return
66
+ voiceHeadlessStarted = true
67
+ if (flutterEngine != null) return
68
+
69
+ // Wait a slight moment for audio routing to settle before capturing mic
70
+ Handler(Looper.getMainLooper()).postDelayed({
71
+ // Check if there is already an active engine to avoid duplicating connections
72
+ var existingEngine = FlutterEngineCache.getInstance().get("main_engine")
73
+
74
+ if (existingEngine != null) {
75
+ // If engine exists (app is in background), we can just trigger it via method channel
76
+ flutterEngine = existingEngine
77
+ } else {
78
+ // Spawn headless engine
79
+ flutterEngine = FlutterEngine(context.applicationContext)
80
+ flutterEngine?.dartExecutor?.executeDartEntrypoint(
81
+ DartExecutor.DartEntrypoint.createDefault()
82
+ )
83
+ }
84
+
85
+ if (!isFlutterInitiated) {
86
+ // To ensure the flutter side starts the LiveVoiceCapture session automatically
87
+ // we should ideally notify it through a MethodChannel.
88
+ // But since this is a headless connection, if we just initialize the engine,
89
+ // we need to tell it to start voice mode.
90
+ // We will invoke a method call to Dart.
91
+ MethodChannel(
92
+ flutterEngine!!.dartExecutor.binaryMessenger,
93
+ "neoagent/car_auto"
94
+ ).invokeMethod("startVoiceMode", null)
95
+ isFlutterInitiated = true
96
+ }
97
+
98
+ }, 500)
99
+ }
100
+
101
+ private fun cleanup() {
102
+ if (!isFlutterInitiated) {
103
+ val messenger = flutterEngine?.dartExecutor?.binaryMessenger
104
+ if (messenger != null) {
105
+ MethodChannel(
106
+ messenger,
107
+ "neoagent/car_auto"
108
+ ).invokeMethod("stopVoiceMode", null)
109
+ }
110
+ }
111
+
112
+ NeoAgentConnectionService.getAndClearCurrentConnection()
113
+
114
+ // We only destroy the engine if we created it headless and we want to clean up.
115
+ // For simplicity and reusing existing state, we can leave it running
116
+ // to handle the end-of-call cleanly on the dart side.
117
+ }
118
+ }
@@ -0,0 +1,86 @@
1
+ package com.neoagent.flutter_app.telecom
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.os.Bundle
6
+ import android.telecom.Connection
7
+ import android.telecom.ConnectionService
8
+ import android.telecom.ConnectionRequest
9
+ import android.telecom.PhoneAccountHandle
10
+ import android.telecom.TelecomManager
11
+ import android.telecom.DisconnectCause
12
+
13
+ class NeoAgentConnectionService : ConnectionService() {
14
+ override fun onCreateOutgoingConnection(
15
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
16
+ request: ConnectionRequest?
17
+ ): Connection {
18
+ return createConnection(request, isIncoming = false)
19
+ }
20
+
21
+ override fun onCreateOutgoingConnectionFailed(
22
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
23
+ request: ConnectionRequest?
24
+ ) {
25
+ super.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request)
26
+ }
27
+
28
+ override fun onCreateIncomingConnection(
29
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
30
+ request: ConnectionRequest?
31
+ ): Connection {
32
+ return createConnection(request, isIncoming = true)
33
+ }
34
+
35
+ override fun onCreateIncomingConnectionFailed(
36
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
37
+ request: ConnectionRequest?
38
+ ) {
39
+ super.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount, request)
40
+ }
41
+
42
+ companion object {
43
+ @Volatile
44
+ private var currentConnection: NeoAgentConnection? = null
45
+
46
+ @Synchronized
47
+ fun getAndClearCurrentConnection(): NeoAgentConnection? {
48
+ val conn = currentConnection
49
+ currentConnection = null
50
+ return conn
51
+ }
52
+
53
+ @Synchronized
54
+ fun swapConnection(newConnection: NeoAgentConnection) {
55
+ currentConnection?.let {
56
+ try {
57
+ it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
58
+ it.destroy()
59
+ } catch (e: Exception) {
60
+ // Ignore
61
+ }
62
+ }
63
+ currentConnection = newConnection
64
+ }
65
+ }
66
+
67
+ private fun createConnection(request: ConnectionRequest?, isIncoming: Boolean): Connection {
68
+ val connection = NeoAgentConnection(applicationContext)
69
+ swapConnection(connection)
70
+
71
+ // Custom flag from Flutter to know if it started the call
72
+ val isFlutterInitiated = request?.extras?.getBoolean("is_flutter_initiated") ?: false
73
+ connection.isFlutterInitiated = isFlutterInitiated
74
+
75
+ connection.connectionProperties = Connection.PROPERTY_SELF_MANAGED
76
+ connection.setInitializing()
77
+
78
+ if (isIncoming) {
79
+ connection.setRinging()
80
+ } else {
81
+ connection.setDialing()
82
+ }
83
+
84
+ return connection
85
+ }
86
+ }
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <automotiveApp>
3
+ <uses name="template" />
4
+ </automotiveApp>