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.
- package/flutter_app/android/app/build.gradle.kts +2 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +24 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
- package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +262 -0
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +298 -0
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +59 -0
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +185 -0
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +164 -0
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_chat.dart +422 -421
- package/flutter_app/lib/main_controller.dart +49 -0
- package/flutter_app/lib/main_runtime.dart +3 -0
- package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
- package/flutter_app/lib/src/backend_client.dart +4 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +18 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +4 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +1 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +4 -0
- package/flutter_app/pubspec.lock +136 -0
- package/flutter_app/pubspec.yaml +4 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +3 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +1 -0
- package/package.json +1 -1
- package/server/db/database.js +15 -1
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/NOTICES +280 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +79280 -77226
- package/server/routes/auth.js +20 -4
- 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
|
+
}
|
|
Binary file
|