Skip to content

Commit a9be814

Browse files
committed
add digital assistant config and fix watch app launching
1 parent da4c2d4 commit a9be814

File tree

9 files changed

+347
-2
lines changed

9 files changed

+347
-2
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
<uses-permission android:name="android.permission.CALL_PHONE" />
3636
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
3737
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
38+
39+
<!-- Voice Interaction Service permissions (for default assistant role) -->
40+
<uses-permission android:name="android.permission.BIND_VOICE_INTERACTION" />
3841

3942
<!-- Ensure compatibility with different Android versions -->
4043
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
@@ -75,6 +78,64 @@
7578
<data android:scheme="agixt" android:host="callback" />
7679
</intent-filter>
7780
</activity>
81+
82+
<!-- Assistant Activity - handles digital assistant intents -->
83+
<activity
84+
android:name=".AssistActivity"
85+
android:exported="true"
86+
android:excludeFromRecents="true"
87+
android:launchMode="singleTask"
88+
android:taskAffinity=""
89+
android:theme="@android:style/Theme.Translucent.NoTitleBar">
90+
<!-- Handle being set as the default digital assistant -->
91+
<intent-filter>
92+
<action android:name="android.intent.action.ASSIST" />
93+
<category android:name="android.intent.category.DEFAULT" />
94+
</intent-filter>
95+
<!-- Handle voice assistant activation -->
96+
<intent-filter>
97+
<action android:name="android.intent.action.VOICE_COMMAND" />
98+
<category android:name="android.intent.category.DEFAULT" />
99+
</intent-filter>
100+
<!-- Handle long-press on search key -->
101+
<intent-filter>
102+
<action android:name="android.intent.action.SEARCH_LONG_PRESS" />
103+
<category android:name="android.intent.category.DEFAULT" />
104+
</intent-filter>
105+
<!-- Android 4.1+ voice assist -->
106+
<intent-filter>
107+
<action android:name="android.intent.action.VOICE_ASSIST" />
108+
<category android:name="android.intent.category.DEFAULT" />
109+
</intent-filter>
110+
<!-- Metadata for assistant capabilities -->
111+
<meta-data
112+
android:name="com.android.systemui.action_assist_icon"
113+
android:resource="@mipmap/ic_launcher" />
114+
</activity>
115+
116+
<!-- Voice Interaction Service - enables AGiXT as default assistant -->
117+
<service
118+
android:name=".AGiXTVoiceInteractionService"
119+
android:exported="true"
120+
android:permission="android.permission.BIND_VOICE_INTERACTION">
121+
<meta-data
122+
android:name="android.voice_interaction"
123+
android:resource="@xml/voice_interaction_service" />
124+
<intent-filter>
125+
<action android:name="android.service.voice.VoiceInteractionService" />
126+
</intent-filter>
127+
</service>
128+
129+
<!-- Voice Interaction Session Service -->
130+
<service
131+
android:name=".AGiXTVoiceInteractionSessionService"
132+
android:exported="true"
133+
android:permission="android.permission.BIND_VOICE_INTERACTION">
134+
<intent-filter>
135+
<action android:name="android.service.voice.VoiceInteractionSessionService" />
136+
</intent-filter>
137+
</service>
138+
78139
<!-- Don't delete the meta-data below.
79140
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
80141
<meta-data
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package dev.agixt.agixt
2+
3+
import android.os.Bundle
4+
import android.service.voice.VoiceInteractionService
5+
import android.service.voice.VoiceInteractionSession
6+
import android.service.voice.VoiceInteractionSessionService
7+
import android.util.Log
8+
import android.content.Intent
9+
10+
/**
11+
* VoiceInteractionService implementation for AGiXT.
12+
* This service enables AGiXT to be selected as the default digital assistant.
13+
*
14+
* When enabled as the default assistant, this service handles:
15+
* - Long-press home button
16+
* - "Hey Google" replacement (if configured)
17+
* - Assistant hardware button (on some devices)
18+
* - Swipe from corner gestures (Android 10+)
19+
*/
20+
class AGiXTVoiceInteractionService : VoiceInteractionService() {
21+
22+
companion object {
23+
private const val TAG = "AGiXTVoiceService"
24+
}
25+
26+
override fun onCreate() {
27+
super.onCreate()
28+
Log.d(TAG, "AGiXT Voice Interaction Service created")
29+
}
30+
31+
override fun onReady() {
32+
super.onReady()
33+
Log.d(TAG, "AGiXT Voice Interaction Service ready")
34+
}
35+
36+
override fun onShutdown() {
37+
Log.d(TAG, "AGiXT Voice Interaction Service shutdown")
38+
super.onShutdown()
39+
}
40+
}
41+
42+
/**
43+
* Session service that creates voice interaction sessions.
44+
*/
45+
class AGiXTVoiceInteractionSessionService : VoiceInteractionSessionService() {
46+
47+
companion object {
48+
private const val TAG = "AGiXTVoiceSession"
49+
}
50+
51+
override fun onNewSession(args: Bundle?): VoiceInteractionSession {
52+
Log.d(TAG, "Creating new voice interaction session")
53+
return AGiXTVoiceInteractionSession(this)
54+
}
55+
}
56+
57+
/**
58+
* The actual voice interaction session that handles user interactions.
59+
*/
60+
class AGiXTVoiceInteractionSession(context: android.content.Context) : VoiceInteractionSession(context) {
61+
62+
companion object {
63+
private const val TAG = "AGiXTSession"
64+
}
65+
66+
override fun onShow(args: Bundle?, showFlags: Int) {
67+
super.onShow(args, showFlags)
68+
Log.d(TAG, "Voice interaction session shown, flags: $showFlags")
69+
70+
// Launch the main app with voice input mode
71+
val intent = Intent(context, MainActivity::class.java).apply {
72+
putExtra("start_voice_input", true)
73+
putExtra("voice_mode", true)
74+
putExtra("from_assistant", true)
75+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
76+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
77+
Intent.FLAG_ACTIVITY_SINGLE_TOP
78+
}
79+
context.startActivity(intent)
80+
81+
// Hide the session UI since we're using our own app UI
82+
hide()
83+
}
84+
85+
override fun onHide() {
86+
Log.d(TAG, "Voice interaction session hidden")
87+
super.onHide()
88+
}
89+
90+
override fun onHandleAssist(state: AssistState) {
91+
super.onHandleAssist(state)
92+
Log.d(TAG, "Handling assist request")
93+
94+
// Launch MainActivity with assist mode
95+
val intent = Intent(context, MainActivity::class.java).apply {
96+
putExtra("start_voice_input", true)
97+
putExtra("voice_mode", true)
98+
putExtra("from_assistant", true)
99+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
100+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
101+
Intent.FLAG_ACTIVITY_SINGLE_TOP
102+
}
103+
context.startActivity(intent)
104+
hide()
105+
}
106+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package dev.agixt.agixt
2+
3+
import android.app.Activity
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import android.util.Log
7+
8+
/**
9+
* Activity that handles digital assistant intents.
10+
* This activity is launched when:
11+
* - User selects AGiXT as the default digital assistant
12+
* - User long-presses the home button
13+
* - User triggers the assistant via voice ("Hey Google" replacement)
14+
* - User triggers assist from search key
15+
*/
16+
class AssistActivity : Activity() {
17+
18+
companion object {
19+
private const val TAG = "AssistActivity"
20+
}
21+
22+
override fun onCreate(savedInstanceState: Bundle?) {
23+
super.onCreate(savedInstanceState)
24+
25+
Log.d(TAG, "AssistActivity launched with action: ${intent?.action}")
26+
27+
// Handle the assistant intent
28+
handleAssistIntent(intent)
29+
}
30+
31+
override fun onNewIntent(intent: Intent?) {
32+
super.onNewIntent(intent)
33+
intent?.let { handleAssistIntent(it) }
34+
}
35+
36+
private fun handleAssistIntent(intent: Intent) {
37+
val action = intent.action
38+
Log.d(TAG, "Handling assist intent: $action")
39+
40+
// Create intent to launch MainActivity with voice input flag
41+
val mainIntent = Intent(this, MainActivity::class.java).apply {
42+
// Preserve the original action for MainActivity to handle
43+
putExtra("assist_action", action)
44+
putExtra("start_voice_input", true)
45+
46+
// Pass along any query text if available
47+
intent.getStringExtra(Intent.EXTRA_ASSIST_CONTEXT)?.let {
48+
putExtra("assist_context", it)
49+
}
50+
51+
// Pass the referrer if available
52+
intent.getStringExtra(Intent.EXTRA_REFERRER)?.let {
53+
putExtra("assist_referrer", it)
54+
}
55+
56+
// Handle voice-specific intents
57+
when (action) {
58+
Intent.ACTION_VOICE_COMMAND,
59+
Intent.ACTION_ASSIST,
60+
"android.intent.action.VOICE_ASSIST" -> {
61+
putExtra("voice_mode", true)
62+
}
63+
Intent.ACTION_SEARCH_LONG_PRESS -> {
64+
putExtra("from_long_press", true)
65+
putExtra("voice_mode", true)
66+
}
67+
}
68+
69+
// Clear task flags to ensure clean launch
70+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
71+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
72+
Intent.FLAG_ACTIVITY_SINGLE_TOP
73+
}
74+
75+
startActivity(mainIntent)
76+
77+
// Finish this activity so it doesn't stay in the back stack
78+
finish()
79+
}
80+
}

android/app/src/main/kotlin/dev/agixt/agixt/MainActivity.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class MainActivity: FlutterActivity() {
3232
private var methodChannelInitialized = false
3333
private var pendingToken: String? = null
3434
private var pendingVoiceInput: Pair<String, String?>? = null
35+
private var pendingAssistantLaunch = false
3536

3637
// Voice & Watch handlers
3738
private var wakeWordHandler: WakeWordHandler? = null
@@ -79,6 +80,18 @@ class MainActivity: FlutterActivity() {
7980
}
8081
}
8182
}
83+
// Handle assistant/voice mode intents
84+
else if (it.getBooleanExtra("start_voice_input", false) ||
85+
it.getBooleanExtra("voice_mode", false) ||
86+
it.getBooleanExtra("from_assistant", false)) {
87+
Log.d(TAG, "Assistant mode activated from intent")
88+
if (methodChannelInitialized) {
89+
triggerVoiceInput()
90+
} else {
91+
// Store flag to trigger voice input once initialized
92+
pendingAssistantLaunch = true
93+
}
94+
}
8295
// Handle voice input from WearableMessageService
8396
else if (WearableMessageService.ACTION_VOICE_INPUT == it.action) {
8497
val text = it.getStringExtra(WearableMessageService.EXTRA_TEXT)
@@ -96,6 +109,13 @@ class MainActivity: FlutterActivity() {
96109
}
97110
}
98111
}
112+
113+
private fun triggerVoiceInput() {
114+
// Send message to Flutter to start voice input
115+
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
116+
MethodChannel(messenger, CHANNEL).invokeMethod("startVoiceInput", null)
117+
}
118+
}
99119

100120
private fun sendTokenToFlutter(token: String) {
101121
val binaryMessenger = flutterEngine?.dartExecutor?.binaryMessenger
@@ -243,6 +263,13 @@ class MainActivity: FlutterActivity() {
243263
pendingVoiceInput = null
244264
}
245265

266+
// Check if we need to trigger voice input from assistant launch
267+
if (pendingAssistantLaunch) {
268+
Log.d(TAG, "Triggering pending assistant voice input")
269+
triggerVoiceInput()
270+
pendingAssistantLaunch = false
271+
}
272+
246273
// Setup the new channel for button events
247274
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BUTTON_EVENTS_CHANNEL).setMethodCallHandler { call, result ->
248275
// Currently no methods expected from Flutter on this channel, but handler is needed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Voice Interaction Service configuration for AGiXT default assistant -->
3+
<voice-interaction-service xmlns:android="http://schemas.android.com/apk/res/android"
4+
android:sessionService="dev.agixt.agixt.AGiXTVoiceInteractionSessionService"
5+
android:recognitionService="dev.agixt.agixt.AGiXTVoiceInteractionService"
6+
android:settingsActivity="dev.agixt.agixt.MainActivity"
7+
android:supportsAssist="true"
8+
android:supportsLaunchVoiceAssistFromKeyguard="true"
9+
android:supportsLocalInteraction="true" />

android/wear/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939
<action android:name="android.intent.action.VOICE_COMMAND" />
4040
<category android:name="android.intent.category.DEFAULT" />
4141
</intent-filter>
42+
<!-- Support being set as default assistant on Wear OS -->
43+
<intent-filter>
44+
<action android:name="android.intent.action.ASSIST" />
45+
<category android:name="android.intent.category.DEFAULT" />
46+
</intent-filter>
47+
<intent-filter>
48+
<action android:name="android.intent.action.VOICE_ASSIST" />
49+
<category android:name="android.intent.category.DEFAULT" />
50+
</intent-filter>
4251
</activity>
4352

4453
<!-- Data Layer Listener Service (receives messages from phone) -->

lib/main.dart

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,34 @@ class _AGiXTAppState extends State<AGiXTApp> {
517517
} catch (e) {
518518
debugPrint('Error setting up OAuth method channel: $e');
519519
}
520+
521+
// Set up method channel for assistant/voice input triggers from native
522+
try {
523+
const assistantChannel = MethodChannel('dev.agixt.agixt/channel');
524+
assistantChannel.setMethodCallHandler((call) async {
525+
try {
526+
if (call.method == 'startVoiceInput') {
527+
debugPrint('Assistant trigger received from native - starting voice input');
528+
// Navigate to home page and trigger voice input
529+
final navigator = AGiXTApp.navigatorKey.currentState;
530+
if (navigator != null) {
531+
// Navigate to home with voice input flag
532+
navigator.pushNamedAndRemoveUntil(
533+
'/home',
534+
(route) => false,
535+
arguments: {'forceNewChat': true, 'startVoiceInput': true},
536+
);
537+
}
538+
}
539+
return null;
540+
} catch (e) {
541+
debugPrint('Error handling assistant method call: $e');
542+
return null;
543+
}
544+
});
545+
} catch (e) {
546+
debugPrint('Error setting up assistant method channel: $e');
547+
}
520548
} catch (e) {
521549
debugPrint('Error initializing deep link handling: $e');
522550
}
@@ -545,7 +573,8 @@ class _AGiXTAppState extends State<AGiXTApp> {
545573
final args = ModalRoute.of(context)?.settings.arguments
546574
as Map<String, dynamic>?;
547575
final forceNewChat = args?['forceNewChat'] as bool? ?? false;
548-
return HomePage(forceNewChat: forceNewChat);
576+
final startVoiceInput = args?['startVoiceInput'] as bool? ?? false;
577+
return HomePage(forceNewChat: forceNewChat, startVoiceInput: startVoiceInput);
549578
},
550579
'/login': (context) => const LoginScreen(),
551580
'/profile': (context) => const ProfileScreen(),

0 commit comments

Comments
 (0)