21 files changed
+1253
-10
android/app/src/main/AndroidManifest.xml
+4
@@ -43,6 +43,10 @@
43
<action android:name="android.intent.action.MAIN"/>
44
<category android:name="android.intent.category.LAUNCHER"/>
45
</intent-filter>
46
+ <intent-filter>
47
+ <action android:name="android.intent.action.ASSIST" />
48
+ <category android:name="android.intent.category.DEFAULT" />
49
+ </intent-filter>
50
</activity>
51
<!-- Don't delete the meta-data below.
52
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
android/app/src/main/kotlin/pro/momin/localmind/MainActivity.kt
+164
@@ -1,8 +1,16 @@
1
package pro.momin.localmind
2
3
import androidx.annotation.NonNull
4
+import android.app.Activity
5
import android.app.ActivityManager
6
+import android.app.role.RoleManager
7
+import android.content.ActivityNotFoundException
8
import android.content.Context
9
+import android.content.Intent
10
+import android.net.Uri
11
+import android.os.Build
12
+import android.os.Bundle
13
+import android.provider.Settings
14
import com.ryanheise.audioservice.AudioServiceActivity
15
import io.flutter.embedding.engine.FlutterEngine
16
import io.flutter.plugin.common.MethodChannel
@@ -10,6 +18,23 @@ import io.flutter.plugin.common.MethodChannel
18
class MainActivity : AudioServiceActivity() {
19
private val CHANNEL = "localmind/chat_background"
20
private val MEMORY_CHANNEL = "localmind/device_memory"
21
+ private val ASSISTANT_CHANNEL = "localmind/android_assistant"
22
+
23
+ private var assistantChannel: MethodChannel? = null
24
+ private var pendingAssistantInvocation = false
25
+ private var pendingRoleRequest: MethodChannel.Result? = null
26
+
27
+ override fun onCreate(savedInstanceState: Bundle?) {
28
+ super.onCreate(savedInstanceState)
29
+ captureAssistantInvocation(intent)
30
+ }
31
+
32
+ override fun onNewIntent(intent: Intent) {
33
+ super.onNewIntent(intent)
34
+ setIntent(intent)
35
+ captureAssistantInvocation(intent)
36
+ deliverAssistantInvocation()
37
+ }
38
39
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
40
super.configureFlutterEngine(flutterEngine)
@@ -45,5 +70,144 @@ class MainActivity : AudioServiceActivity() {
70
else -> result.notImplemented()
71
}
72
}
73
+
74
+ assistantChannel = MethodChannel(
75
+ flutterEngine.dartExecutor.binaryMessenger,
76
+ ASSISTANT_CHANNEL
77
+ ).also { channel ->
78
+ channel.setMethodCallHandler { call, result ->
79
+ when (call.method) {
80
+ "consumePendingInvocation" -> {
81
+ val wasPending = pendingAssistantInvocation
82
+ pendingAssistantInvocation = false
83
+ result.success(wasPending)
84
+ }
85
+ "getAssistantStatus" -> result.success(getAssistantStatus())
86
+ "requestAssistantRole" -> requestAssistantRole(result)
87
+ "openAssistantSettings" -> {
88
+ if (openAssistantSettings()) {
89
+ result.success(null)
90
+ } else {
91
+ result.error(
92
+ "assistant_settings_unavailable",
93
+ "Android assistant settings are unavailable.",
94
+ null
95
+ )
96
+ }
97
+ }
98
+ else -> result.notImplemented()
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
105
+ super.onActivityResult(requestCode, resultCode, data)
106
+ if (requestCode != ASSISTANT_ROLE_REQUEST_CODE) return
107
+
108
+ val result = pendingRoleRequest ?: return
109
+ pendingRoleRequest = null
110
+ result.success(
111
+ resultCode == Activity.RESULT_OK || getAssistantStatus() == "active"
112
+ )
113
+ }
114
+
115
+ private fun captureAssistantInvocation(intent: Intent?) {
116
+ if (intent?.action == Intent.ACTION_ASSIST) {
117
+ pendingAssistantInvocation = true
118
+ }
119
+ }
120
+
121
+ private fun deliverAssistantInvocation() {
122
+ if (!pendingAssistantInvocation) return
123
+ val channel = assistantChannel ?: return
124
+
125
+ channel.invokeMethod("assistantInvoked", null, object : MethodChannel.Result {
126
+ override fun success(result: Any?) {
127
+ pendingAssistantInvocation = false
128
+ }
129
+
130
+ override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) = Unit
131
+
132
+ override fun notImplemented() = Unit
133
+ })
134
+ }
135
+
136
+ private fun getAssistantStatus(): String {
137
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return "manual"
138
+
139
+ val roleManager = getSystemService(RoleManager::class.java)
140
+ if (!roleManager.isRoleAvailable(RoleManager.ROLE_ASSISTANT)) {
141
+ return "unsupported"
142
+ }
143
+ return if (roleManager.isRoleHeld(RoleManager.ROLE_ASSISTANT)) {
144
+ "active"
145
+ } else {
146
+ "available"
147
+ }
148
+ }
149
+
150
+ private fun requestAssistantRole(result: MethodChannel.Result) {
151
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
152
+ if (openAssistantSettings()) {
153
+ result.success(false)
154
+ } else {
155
+ result.error(
156
+ "assistant_settings_unavailable",
157
+ "Android assistant settings are unavailable.",
158
+ null
159
+ )
160
+ }
161
+ return
162
+ }
163
+
164
+ val roleManager = getSystemService(RoleManager::class.java)
165
+ if (!roleManager.isRoleAvailable(RoleManager.ROLE_ASSISTANT)) {
166
+ result.success(false)
167
+ return
168
+ }
169
+ if (roleManager.isRoleHeld(RoleManager.ROLE_ASSISTANT)) {
170
+ result.success(true)
171
+ return
172
+ }
173
+ if (pendingRoleRequest != null) {
174
+ result.error(
175
+ "assistant_role_request_active",
176
+ "An assistant role request is already active.",
177
+ null
178
+ )
179
+ return
180
+ }
181
+
182
+ pendingRoleRequest = result
183
+ startActivityForResult(
184
+ roleManager.createRequestRoleIntent(RoleManager.ROLE_ASSISTANT),
185
+ ASSISTANT_ROLE_REQUEST_CODE
186
+ )
187
+ }
188
+
189
+ private fun openAssistantSettings(): Boolean {
190
+ val candidates = listOf(
191
+ Intent(Settings.ACTION_VOICE_INPUT_SETTINGS),
192
+ Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS),
193
+ Intent(
194
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
195
+ Uri.parse("package:$packageName")
196
+ )
197
+ )
198
+
199
+ for (candidate in candidates) {
200
+ try {
201
+ startActivity(candidate)
202
+ return true
203
+ } catch (_: ActivityNotFoundException) {
204
+ // Try the next settings destination.
205
+ }
206
+ }
207
+ return false
208
+ }
209
+
210
+ companion object {
211
+ private const val ASSISTANT_ROLE_REQUEST_CODE = 4101
212
}
213
}
lib/app.dart
+2
-1
@@ -10,6 +10,7 @@ import 'core/providers/app_providers.dart';
10
import 'core/routes/app_routes.dart';
11
import 'core/theme/app_theme.dart';
12
import 'core/utils/locale_utils.dart';
13
+import 'core/widgets/android_assistant_invocation_host.dart';
14
import 'features/chat/providers/chat_providers.dart';
15
import 'features/conversations/providers/conversation_providers.dart' as conv;
16
import 'features/chat/views/chat_screen.dart';
@@ -100,7 +101,7 @@ final routerProvider = Provider<GoRouter>((ref) {
101
ShellRoute(
102
navigatorKey: _shellNavigatorKey,
103
builder: (context, state, child) {
103
- return AppShell(child: child);
104
+ return AndroidAssistantInvocationHost(child: AppShell(child: child));
105
},
106
routes: [
107
GoRoute(
lib/core/services/android_assistant_service.dart
new
+104
@@ -0,0 +1,104 @@
1
+import 'dart:async';
2
+
3
+import 'package:flutter/foundation.dart';
4
+import 'package:flutter/services.dart';
5
+import 'package:flutter_riverpod/flutter_riverpod.dart';
6
+
7
+import '../logger/app_logger.dart';
8
+
9
+enum AndroidAssistantStatus { unsupported, available, active, manual, unknown }
10
+
11
+class AndroidAssistantService {
12
+ AndroidAssistantService({MethodChannel? channel, bool? supportedPlatform})
13
+ : _channel = channel ?? const MethodChannel(_channelName),
14
+ _supportedPlatformOverride = supportedPlatform;
15
+
16
+ static const _channelName = 'localmind/android_assistant';
17
+
18
+ final MethodChannel _channel;
19
+ final bool? _supportedPlatformOverride;
20
+ final StreamController<void> _invocations =
21
+ StreamController<void>.broadcast();
22
+
23
+ bool _initialized = false;
24
+ bool _disposed = false;
25
+
26
+ bool get isSupportedPlatform =>
27
+ _supportedPlatformOverride ??
28
+ (!kIsWeb && defaultTargetPlatform == TargetPlatform.android);
29
+
30
+ Stream<void> get invocations => _invocations.stream;
31
+
32
+ Future<void> initialize() async {
33
+ if (!isSupportedPlatform || _initialized || _disposed) return;
34
+ _initialized = true;
35
+
36
+ _channel.setMethodCallHandler(_handleNativeCall);
37
+
38
+ try {
39
+ final pending =
40
+ await _channel.invokeMethod<bool>('consumePendingInvocation') ??
41
+ false;
42
+ if (pending && !_disposed) {
43
+ _invocations.add(null);
44
+ }
45
+ } on PlatformException catch (error) {
46
+ Log.error('Android assistant initialization failed: $error');
47
+ }
48
+ }
49
+
50
+ Future<AndroidAssistantStatus> getStatus() async {
51
+ if (!isSupportedPlatform) return AndroidAssistantStatus.unsupported;
52
+
53
+ try {
54
+ final value = await _channel.invokeMethod<String>('getAssistantStatus');
55
+ return switch (value) {
56
+ 'active' => AndroidAssistantStatus.active,
57
+ 'available' => AndroidAssistantStatus.available,
58
+ 'manual' => AndroidAssistantStatus.manual,
59
+ 'unsupported' => AndroidAssistantStatus.unsupported,
60
+ _ => AndroidAssistantStatus.unknown,
61
+ };
62
+ } on PlatformException catch (error) {
63
+ Log.error('Unable to read Android assistant status: $error');
64
+ return AndroidAssistantStatus.unknown;
65
+ }
66
+ }
67
+
68
+ Future<bool> requestRole() async {
69
+ if (!isSupportedPlatform) return false;
70
+ return await _channel.invokeMethod<bool>('requestAssistantRole') ?? false;
71
+ }
72
+
73
+ Future<void> openSettings() async {
74
+ if (!isSupportedPlatform) return;
75
+ await _channel.invokeMethod<void>('openAssistantSettings');
76
+ }
77
+
78
+ Future<dynamic> _handleNativeCall(MethodCall call) async {
79
+ if (call.method != 'assistantInvoked') {
80
+ throw MissingPluginException(
81
+ 'Unknown Android assistant call: ${call.method}',
82
+ );
83
+ }
84
+
85
+ if (!_disposed) {
86
+ _invocations.add(null);
87
+ }
88
+ }
89
+
90
+ Future<void> dispose() async {
91
+ if (_disposed) return;
92
+ _disposed = true;
93
+ _channel.setMethodCallHandler(null);
94
+ await _invocations.close();
95
+ }
96
+}
97
+
98
+final androidAssistantServiceProvider = Provider<AndroidAssistantService>((
99
+ ref,
100
+) {
101
+ final service = AndroidAssistantService();
102
+ ref.onDispose(service.dispose);
103
+ return service;
104
+});
lib/core/widgets/android_assistant_invocation_host.dart
new
+59
@@ -0,0 +1,59 @@
1
+import 'dart:async';
2
+
3
+import 'package:flutter/material.dart';
4
+import 'package:flutter_riverpod/flutter_riverpod.dart';
5
+
6
+import '../../features/voice_mode/providers/voice_mode_provider.dart';
7
+import '../../features/voice_mode/views/voice_mode_overlay.dart';
8
+import '../services/android_assistant_service.dart';
9
+
10
+class AndroidAssistantInvocationHost extends ConsumerStatefulWidget {
11
+ const AndroidAssistantInvocationHost({required this.child, super.key});
12
+
13
+ final Widget child;
14
+
15
+ @override
16
+ ConsumerState<AndroidAssistantInvocationHost> createState() =>
17
+ _AndroidAssistantInvocationHostState();
18
+}
19
+
20
+class _AndroidAssistantInvocationHostState
21
+ extends ConsumerState<AndroidAssistantInvocationHost> {
22
+ StreamSubscription<void>? _subscription;
23
+ bool _isShowingVoiceMode = false;
24
+
25
+ @override
26
+ void initState() {
27
+ super.initState();
28
+ final service = ref.read(androidAssistantServiceProvider);
29
+ if (!service.isSupportedPlatform) return;
30
+
31
+ _subscription = service.invocations.listen((_) {
32
+ unawaited(_showVoiceMode());
33
+ });
34
+ WidgetsBinding.instance.addPostFrameCallback((_) {
35
+ unawaited(service.initialize());
36
+ });
37
+ }
38
+
39
+ Future<void> _showVoiceMode() async {
40
+ if (!mounted || _isShowingVoiceMode) return;
41
+ if (ref.read(voiceModeProvider).isActive) return;
42
+
43
+ _isShowingVoiceMode = true;
44
+ try {
45
+ await VoiceModeOverlay.show(context);
46
+ } finally {
47
+ _isShowingVoiceMode = false;
48
+ }
49
+ }
50
+
51
+ @override
52
+ void dispose() {
53
+ unawaited(_subscription?.cancel());
54
+ super.dispose();
55
+ }
56
+
57
+ @override
58
+ Widget build(BuildContext context) => widget.child;
59
+}
lib/features/settings/views/settings_screen.dart
+167
-8
@@ -1,6 +1,7 @@
1
import 'package:cue/cue.dart';
2
import 'package:hugeicons/hugeicons.dart';
3
import 'package:flutter/material.dart';
4
+import 'package:flutter/services.dart';
5
import 'package:flutter_riverpod/flutter_riverpod.dart';
6
import 'package:go_router/go_router.dart';
7
import 'package:shadcn_ui/shadcn_ui.dart';
@@ -11,6 +12,7 @@ import 'package:localmind/l10n/app_localizations.dart';
12
import '../../../core/models/enums.dart';
13
import '../../../core/providers/app_providers.dart';
14
import '../../../core/routes/app_routes.dart';
15
+import '../../../core/services/android_assistant_service.dart';
16
import '../../../core/services/app_haptics.dart';
17
import '../../../core/services/crash_report_service.dart';
18
import '../../../core/theme/app_theme.dart';
@@ -34,6 +36,7 @@ class SettingsViews extends ConsumerWidget {
36
final currentTheme = ref.watch(themeModeProvider);
37
final systemBottomInset = bottomSystemInset(context);
38
final packageInfo = ref.watch(packageInfoProvider);
39
+ final assistantService = ref.watch(androidAssistantServiceProvider);
40
final servers = (ref.watch(serversProvider).value ?? [])
41
.map((server) => (server.id, server.name))
42
.toList();
@@ -43,19 +46,18 @@ class SettingsViews extends ConsumerWidget {
46
47
// The default model dropdown lists models from the server used as the
48
// default (the configured default server, falling back to the first one).
46
- final defaultServerId = settings.defaultServerId ??
49
+ final defaultServerId =
50
+ settings.defaultServerId ??
51
(servers.isNotEmpty ? servers.first.$1 : null);
52
final availableModels = defaultServerId != null
53
? (ref.watch(availableModelsProvider(defaultServerId)).value ??
50
- const <dynamic>[])
51
- .cast<ModelInfo>()
54
+ const <dynamic>[])
55
+ .cast<ModelInfo>()
56
: <ModelInfo>[];
53
- final models = availableModels
54
- .map((m) => (m.id, m.displayName))
55
- .toList();
57
+ final models = availableModels.map((m) => (m.id, m.displayName)).toList();
58
final isDefaultModelForServer =
59
settings.defaultModelServerId != null &&
58
- settings.defaultModelServerId == defaultServerId;
60
+ settings.defaultModelServerId == defaultServerId;
61
62
final isDark = Theme.of(context).brightness == Brightness.dark;
63
final topPadding = MediaQuery.of(context).padding.top;
@@ -211,6 +213,15 @@ class SettingsViews extends ConsumerWidget {
213
],
214
);
215
216
+ final assistantCard = assistantService.isSupportedPlatform
217
+ ? _SettingsSectionCard(
218
+ title: l10n.settings_android_assistant,
219
+ icon: HugeIcons.strokeRoundedVoice,
220
+ accent: const Color(0xFF6366F1),
221
+ children: const [_AndroidAssistantSetting()],
222
+ )
223
+ : null;
224
+
225
final behaviorCard = _SettingsSectionCard(
226
title: l10n.settings_behavior,
227
icon: HugeIcons.strokeRoundedSlidersHorizontal,
@@ -424,7 +435,10 @@ class SettingsViews extends ConsumerWidget {
435
items: models,
436
onChanged: (value) => ref
437
.read(settingsProvider.notifier)
427
- .setDefaultModel(serverId: defaultServerId, modelId: value),
438
+ .setDefaultModel(
439
+ serverId: defaultServerId,
440
+ modelId: value,
441
+ ),
442
icon: HugeIcons.strokeRoundedSmartPhone01,
443
),
444
const SizedBox(height: 8),
@@ -565,6 +579,7 @@ class SettingsViews extends ConsumerWidget {
579
child: _SettingsSectionColumn(
580
children: [
581
ttsCard,
582
+ ?assistantCard,
583
onDeviceCard,
584
dataCard,
585
aboutCard,
@@ -578,6 +593,7 @@ class SettingsViews extends ConsumerWidget {
593
children: [
594
appearanceCard,
595
ttsCard,
596
+ ?assistantCard,
597
behaviorCard,
598
onDeviceCard,
599
defaultsCard,
@@ -616,6 +632,149 @@ class _SettingsSectionColumn extends StatelessWidget {
632
}
633
}
634
635
+class _AndroidAssistantSetting extends ConsumerStatefulWidget {
636
+ const _AndroidAssistantSetting();
637
+
638
+ @override
639
+ ConsumerState<_AndroidAssistantSetting> createState() =>
640
+ _AndroidAssistantSettingState();
641
+}
642
+
643
+class _AndroidAssistantSettingState
644
+ extends ConsumerState<_AndroidAssistantSetting>
645
+ with WidgetsBindingObserver {
646
+ AndroidAssistantStatus? _status;
647
+ bool _busy = false;
648
+
649
+ @override
650
+ void initState() {
651
+ super.initState();
652
+ WidgetsBinding.instance.addObserver(this);
653
+ _refreshStatus();
654
+ }
655
+
656
+ @override
657
+ void didChangeAppLifecycleState(AppLifecycleState state) {
658
+ if (state == AppLifecycleState.resumed) {
659
+ _refreshStatus();
660
+ }
661
+ }
662
+
663
+ Future<void> _refreshStatus() async {
664
+ final status = await ref.read(androidAssistantServiceProvider).getStatus();
665
+ if (mounted) setState(() => _status = status);
666
+ }
667
+
668
+ Future<void> _handleAction() async {
669
+ if (_busy) return;
670
+ setState(() => _busy = true);
671
+
672
+ final service = ref.read(androidAssistantServiceProvider);
673
+ try {
674
+ if (_status == AndroidAssistantStatus.available) {
675
+ await service.requestRole();
676
+ } else {
677
+ await service.openSettings();
678
+ }
679
+ await _refreshStatus();
680
+ } on PlatformException {
681
+ if (mounted) {
682
+ ScaffoldMessenger.of(context).showSnackBar(
683
+ SnackBar(
684
+ content: Text(AppLocalizations.of(context)!.assistant_error),
685
+ ),
686
+ );
687
+ }
688
+ } finally {
689
+ if (mounted) setState(() => _busy = false);
690
+ }
691
+ }
692
+
693
+ @override
694
+ void dispose() {
695
+ WidgetsBinding.instance.removeObserver(this);
696
+ super.dispose();
697
+ }
698
+
699
+ @override
700
+ Widget build(BuildContext context) {
701
+ final l10n = AppLocalizations.of(context)!;
702
+ final theme = Theme.of(context);
703
+ final status = _status ?? AndroidAssistantStatus.unknown;
704
+ final statusLabel = switch (status) {
705
+ AndroidAssistantStatus.active => l10n.assistant_status_active,
706
+ AndroidAssistantStatus.available => l10n.assistant_status_available,
707
+ AndroidAssistantStatus.manual => l10n.assistant_status_manual,
708
+ AndroidAssistantStatus.unsupported => l10n.assistant_status_unsupported,
709
+ AndroidAssistantStatus.unknown => l10n.assistant_status_checking,
710
+ };
711
+ final actionLabel = status == AndroidAssistantStatus.available
712
+ ? l10n.assistant_set_default
713
+ : l10n.assistant_open_settings;
714
+
715
+ return _SettingPanel(
716
+ child: Column(
717
+ crossAxisAlignment: CrossAxisAlignment.start,
718
+ children: [
719
+ Row(
720
+ children: [
721
+ Expanded(
722
+ child: Text(
723
+ l10n.assistant_default_title,
724
+ style: theme.textTheme.titleSmall?.copyWith(
725
+ fontWeight: FontWeight.w700,
726
+ ),
727
+ ),
728
+ ),
729
+ Container(
730
+ padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
731
+ decoration: BoxDecoration(
732
+ color: theme.colorScheme.primary.withValues(alpha: 0.09),
733
+ borderRadius: BorderRadius.circular(999),
734
+ ),
735
+ child: Text(
736
+ statusLabel,
737
+ style: theme.textTheme.labelSmall?.copyWith(
738
+ color: theme.colorScheme.primary,
739
+ fontWeight: FontWeight.w700,
740
+ ),
741
+ ),
742
+ ),
743
+ ],
744
+ ),
745
+ const SizedBox(height: 4),
746
+ Text(
747
+ l10n.assistant_default_description,
748
+ style: theme.textTheme.bodySmall?.copyWith(
749
+ color: theme.colorScheme.onSurfaceVariant,
750
+ ),
751
+ ),
752
+ const SizedBox(height: 12),
753
+ ShadButton.outline(
754
+ onPressed: _busy || status == AndroidAssistantStatus.unsupported
755
+ ? null
756
+ : _handleAction,
757
+ width: double.infinity,
758
+ leading: _busy
759
+ ? const SizedBox.square(
760
+ dimension: 16,
761
+ child: CircularProgressIndicator(strokeWidth: 2),
762
+ )
763
+ : const HugeIcon(
764
+ icon: HugeIcons.strokeRoundedSmartPhone01,
765
+ size: 16,
766
+ ),
767
+ child: Align(
768
+ alignment: Alignment.centerLeft,
769
+ child: Text(actionLabel),
770
+ ),
771
+ ),
772
+ ],
773
+ ),
774
+ );
775
+ }
776
+}
777
+
778
class _SettingsHero extends StatelessWidget {
779
const _SettingsHero({required this.settings, required this.themeType});
780
lib/l10n/app_en.arb
+44
@@ -740,6 +740,50 @@
740
"@settings_tts": {
741
"description": "Settings section header for TTS"
742
},
743
+ "settings_android_assistant": "Android Assistant",
744
+ "@settings_android_assistant": {
745
+ "description": "Settings section header for Android default assistant integration"
746
+ },
747
+ "assistant_default_title": "Use LocalMind as your assistant",
748
+ "@assistant_default_title": {
749
+ "description": "Title for the Android default assistant setting"
750
+ },
751
+ "assistant_default_description": "Launch voice mode with Android's assistant gesture or power-button shortcut.",
752
+ "@assistant_default_description": {
753
+ "description": "Description for the Android default assistant setting"
754
+ },
755
+ "assistant_status_active": "Active",
756
+ "@assistant_status_active": {
757
+ "description": "Status shown when LocalMind is the default Android assistant"
758
+ },
759
+ "assistant_status_available": "Not active",
760
+ "@assistant_status_available": {
761
+ "description": "Status shown when LocalMind can request the Android assistant role"
762
+ },
763
+ "assistant_status_manual": "Check settings",
764
+ "@assistant_status_manual": {
765
+ "description": "Status shown when assistant selection must be checked in Android settings"
766
+ },
767
+ "assistant_status_unsupported": "Unavailable",
768
+ "@assistant_status_unsupported": {
769
+ "description": "Status shown when the Android assistant role is unavailable"
770
+ },
771
+ "assistant_status_checking": "Checking…",
772
+ "@assistant_status_checking": {
773
+ "description": "Status shown while checking the Android assistant role"
774
+ },
775
+ "assistant_set_default": "Set as default assistant",
776
+ "@assistant_set_default": {
777
+ "description": "Button that requests the Android default assistant role"
778
+ },
779
+ "assistant_open_settings": "Open Android assistant settings",
780
+ "@assistant_open_settings": {
781
+ "description": "Button that opens Android assistant settings"
782
+ },
783
+ "assistant_error": "Could not open Android assistant settings.",
784
+ "@assistant_error": {
785
+ "description": "Error shown when Android assistant settings cannot be opened"
786
+ },
787
"settings_behavior": "Behavior",
788
"@settings_behavior": {
789
"description": "Settings section header for behavior"
lib/l10n/app_localizations.dart
+66
@@ -1077,6 +1077,72 @@ abstract class AppLocalizations {
1077
/// **'Text-to-Speech'**
1078
String get settings_tts;
1079
1080
+ /// Settings section header for Android default assistant integration
1081
+ ///
1082
+ /// In en, this message translates to:
1083
+ /// **'Android Assistant'**
1084
+ String get settings_android_assistant;
1085
+
1086
+ /// Title for the Android default assistant setting
1087
+ ///
1088
+ /// In en, this message translates to:
1089
+ /// **'Use LocalMind as your assistant'**
1090
+ String get assistant_default_title;
1091
+
1092
+ /// Description for the Android default assistant setting
1093
+ ///
1094
+ /// In en, this message translates to:
1095
+ /// **'Launch voice mode with Android\'s assistant gesture or power-button shortcut.'**
1096
+ String get assistant_default_description;
1097
+
1098
+ /// Status shown when LocalMind is the default Android assistant
1099
+ ///
1100
+ /// In en, this message translates to:
1101
+ /// **'Active'**
1102
+ String get assistant_status_active;
1103
+
1104
+ /// Status shown when LocalMind can request the Android assistant role
1105
+ ///
1106
+ /// In en, this message translates to:
1107
+ /// **'Not active'**
1108
+ String get assistant_status_available;
1109
+
1110
+ /// Status shown when assistant selection must be checked in Android settings
1111
+ ///
1112
+ /// In en, this message translates to:
1113
+ /// **'Check settings'**
1114
+ String get assistant_status_manual;
1115
+
1116
+ /// Status shown when the Android assistant role is unavailable
1117
+ ///
1118
+ /// In en, this message translates to:
1119
+ /// **'Unavailable'**
1120
+ String get assistant_status_unsupported;
1121
+
1122
+ /// Status shown while checking the Android assistant role
1123
+ ///
1124
+ /// In en, this message translates to:
1125
+ /// **'Checking…'**
1126
+ String get assistant_status_checking;
1127
+
1128
+ /// Button that requests the Android default assistant role
1129
+ ///
1130
+ /// In en, this message translates to:
1131
+ /// **'Set as default assistant'**
1132
+ String get assistant_set_default;
1133
+
1134
+ /// Button that opens Android assistant settings
1135
+ ///
1136
+ /// In en, this message translates to:
1137
+ /// **'Open Android assistant settings'**
1138
+ String get assistant_open_settings;
1139
+
1140
+ /// Error shown when Android assistant settings cannot be opened
1141
+ ///
1142
+ /// In en, this message translates to:
1143
+ /// **'Could not open Android assistant settings.'**
1144
+ String get assistant_error;
1145
+
1146
/// Settings section header for behavior
1147
///
1148
/// In en, this message translates to:
lib/l10n/app_localizations_ar.dart
+34
@@ -535,6 +535,40 @@ class AppLocalizationsAr extends AppLocalizations {
535
@override
536
String get settings_tts => 'تحويل النص إلى كلام';
537
538
+ @override
539
+ String get settings_android_assistant => 'Android Assistant';
540
+
541
+ @override
542
+ String get assistant_default_title => 'Use LocalMind as your assistant';
543
+
544
+ @override
545
+ String get assistant_default_description =>
546
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
547
+
548
+ @override
549
+ String get assistant_status_active => 'Active';
550
+
551
+ @override
552
+ String get assistant_status_available => 'Not active';
553
+
554
+ @override
555
+ String get assistant_status_manual => 'Check settings';
556
+
557
+ @override
558
+ String get assistant_status_unsupported => 'Unavailable';
559
+
560
+ @override
561
+ String get assistant_status_checking => 'Checking…';
562
+
563
+ @override
564
+ String get assistant_set_default => 'Set as default assistant';
565
+
566
+ @override
567
+ String get assistant_open_settings => 'Open Android assistant settings';
568
+
569
+ @override
570
+ String get assistant_error => 'Could not open Android assistant settings.';
571
+
572
@override
573
String get settings_behavior => 'السلوك';
574
lib/l10n/app_localizations_bn.dart
+34
@@ -536,6 +536,40 @@ class AppLocalizationsBn extends AppLocalizations {
536
@override
537
String get settings_tts => 'টেক্সট-টু-স্পিচ';
538
539
+ @override
540
+ String get settings_android_assistant => 'Android Assistant';
541
+
542
+ @override
543
+ String get assistant_default_title => 'Use LocalMind as your assistant';
544
+
545
+ @override
546
+ String get assistant_default_description =>
547
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
548
+
549
+ @override
550
+ String get assistant_status_active => 'Active';
551
+
552
+ @override
553
+ String get assistant_status_available => 'Not active';
554
+
555
+ @override
556
+ String get assistant_status_manual => 'Check settings';
557
+
558
+ @override
559
+ String get assistant_status_unsupported => 'Unavailable';
560
+
561
+ @override
562
+ String get assistant_status_checking => 'Checking…';
563
+
564
+ @override
565
+ String get assistant_set_default => 'Set as default assistant';
566
+
567
+ @override
568
+ String get assistant_open_settings => 'Open Android assistant settings';
569
+
570
+ @override
571
+ String get assistant_error => 'Could not open Android assistant settings.';
572
+
573
@override
574
String get settings_behavior => 'আচরণ';
575
lib/l10n/app_localizations_en.dart
+34
@@ -532,6 +532,40 @@ class AppLocalizationsEn extends AppLocalizations {
532
@override
533
String get settings_tts => 'Text-to-Speech';
534
535
+ @override
536
+ String get settings_android_assistant => 'Android Assistant';
537
+
538
+ @override
539
+ String get assistant_default_title => 'Use LocalMind as your assistant';
540
+
541
+ @override
542
+ String get assistant_default_description =>
543
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
544
+
545
+ @override
546
+ String get assistant_status_active => 'Active';
547
+
548
+ @override
549
+ String get assistant_status_available => 'Not active';
550
+
551
+ @override
552
+ String get assistant_status_manual => 'Check settings';
553
+
554
+ @override
555
+ String get assistant_status_unsupported => 'Unavailable';
556
+
557
+ @override
558
+ String get assistant_status_checking => 'Checking…';
559
+
560
+ @override
561
+ String get assistant_set_default => 'Set as default assistant';
562
+
563
+ @override
564
+ String get assistant_open_settings => 'Open Android assistant settings';
565
+
566
+ @override
567
+ String get assistant_error => 'Could not open Android assistant settings.';
568
+
569
@override
570
String get settings_behavior => 'Behavior';
571
lib/l10n/app_localizations_es.dart
+34
@@ -536,6 +536,40 @@ class AppLocalizationsEs extends AppLocalizations {
536
@override
537
String get settings_tts => 'Texto a voz';
538
539
+ @override
540
+ String get settings_android_assistant => 'Android Assistant';
541
+
542
+ @override
543
+ String get assistant_default_title => 'Use LocalMind as your assistant';
544
+
545
+ @override
546
+ String get assistant_default_description =>
547
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
548
+
549
+ @override
550
+ String get assistant_status_active => 'Active';
551
+
552
+ @override
553
+ String get assistant_status_available => 'Not active';
554
+
555
+ @override
556
+ String get assistant_status_manual => 'Check settings';
557
+
558
+ @override
559
+ String get assistant_status_unsupported => 'Unavailable';
560
+
561
+ @override
562
+ String get assistant_status_checking => 'Checking…';
563
+
564
+ @override
565
+ String get assistant_set_default => 'Set as default assistant';
566
+
567
+ @override
568
+ String get assistant_open_settings => 'Open Android assistant settings';
569
+
570
+ @override
571
+ String get assistant_error => 'Could not open Android assistant settings.';
572
+
573
@override
574
String get settings_behavior => 'Comportamiento';
575
lib/l10n/app_localizations_fr.dart
+34
@@ -534,6 +534,40 @@ class AppLocalizationsFr extends AppLocalizations {
534
@override
535
String get settings_tts => 'Text-to-Speech';
536
537
+ @override
538
+ String get settings_android_assistant => 'Android Assistant';
539
+
540
+ @override
541
+ String get assistant_default_title => 'Use LocalMind as your assistant';
542
+
543
+ @override
544
+ String get assistant_default_description =>
545
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
546
+
547
+ @override
548
+ String get assistant_status_active => 'Active';
549
+
550
+ @override
551
+ String get assistant_status_available => 'Not active';
552
+
553
+ @override
554
+ String get assistant_status_manual => 'Check settings';
555
+
556
+ @override
557
+ String get assistant_status_unsupported => 'Unavailable';
558
+
559
+ @override
560
+ String get assistant_status_checking => 'Checking…';
561
+
562
+ @override
563
+ String get assistant_set_default => 'Set as default assistant';
564
+
565
+ @override
566
+ String get assistant_open_settings => 'Open Android assistant settings';
567
+
568
+ @override
569
+ String get assistant_error => 'Could not open Android assistant settings.';
570
+
571
@override
572
String get settings_behavior => 'Behavior';
573
lib/l10n/app_localizations_hi.dart
+34
@@ -537,6 +537,40 @@ class AppLocalizationsHi extends AppLocalizations {
537
@override
538
String get settings_tts => 'टेक्स्ट-टू-स्पीच';
539
540
+ @override
541
+ String get settings_android_assistant => 'Android Assistant';
542
+
543
+ @override
544
+ String get assistant_default_title => 'Use LocalMind as your assistant';
545
+
546
+ @override
547
+ String get assistant_default_description =>
548
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
549
+
550
+ @override
551
+ String get assistant_status_active => 'Active';
552
+
553
+ @override
554
+ String get assistant_status_available => 'Not active';
555
+
556
+ @override
557
+ String get assistant_status_manual => 'Check settings';
558
+
559
+ @override
560
+ String get assistant_status_unsupported => 'Unavailable';
561
+
562
+ @override
563
+ String get assistant_status_checking => 'Checking…';
564
+
565
+ @override
566
+ String get assistant_set_default => 'Set as default assistant';
567
+
568
+ @override
569
+ String get assistant_open_settings => 'Open Android assistant settings';
570
+
571
+ @override
572
+ String get assistant_error => 'Could not open Android assistant settings.';
573
+
574
@override
575
String get settings_behavior => 'व्यवहार';
576
lib/l10n/app_localizations_it.dart
+34
@@ -538,6 +538,40 @@ class AppLocalizationsIt extends AppLocalizations {
538
@override
539
String get settings_tts => 'Sintesi vocale';
540
541
+ @override
542
+ String get settings_android_assistant => 'Android Assistant';
543
+
544
+ @override
545
+ String get assistant_default_title => 'Use LocalMind as your assistant';
546
+
547
+ @override
548
+ String get assistant_default_description =>
549
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
550
+
551
+ @override
552
+ String get assistant_status_active => 'Active';
553
+
554
+ @override
555
+ String get assistant_status_available => 'Not active';
556
+
557
+ @override
558
+ String get assistant_status_manual => 'Check settings';
559
+
560
+ @override
561
+ String get assistant_status_unsupported => 'Unavailable';
562
+
563
+ @override
564
+ String get assistant_status_checking => 'Checking…';
565
+
566
+ @override
567
+ String get assistant_set_default => 'Set as default assistant';
568
+
569
+ @override
570
+ String get assistant_open_settings => 'Open Android assistant settings';
571
+
572
+ @override
573
+ String get assistant_error => 'Could not open Android assistant settings.';
574
+
575
@override
576
String get settings_behavior => 'Comportamento';
577
lib/l10n/app_localizations_ja.dart
+34
@@ -527,6 +527,40 @@ class AppLocalizationsJa extends AppLocalizations {
527
@override
528
String get settings_tts => '音声合成';
529
530
+ @override
531
+ String get settings_android_assistant => 'Android Assistant';
532
+
533
+ @override
534
+ String get assistant_default_title => 'Use LocalMind as your assistant';
535
+
536
+ @override
537
+ String get assistant_default_description =>
538
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
539
+
540
+ @override
541
+ String get assistant_status_active => 'Active';
542
+
543
+ @override
544
+ String get assistant_status_available => 'Not active';
545
+
546
+ @override
547
+ String get assistant_status_manual => 'Check settings';
548
+
549
+ @override
550
+ String get assistant_status_unsupported => 'Unavailable';
551
+
552
+ @override
553
+ String get assistant_status_checking => 'Checking…';
554
+
555
+ @override
556
+ String get assistant_set_default => 'Set as default assistant';
557
+
558
+ @override
559
+ String get assistant_open_settings => 'Open Android assistant settings';
560
+
561
+ @override
562
+ String get assistant_error => 'Could not open Android assistant settings.';
563
+
564
@override
565
String get settings_behavior => '動作';
566
lib/l10n/app_localizations_ru.dart
+34
@@ -536,6 +536,40 @@ class AppLocalizationsRu extends AppLocalizations {
536
@override
537
String get settings_tts => 'Преобразование текста в речь';
538
539
+ @override
540
+ String get settings_android_assistant => 'Android Assistant';
541
+
542
+ @override
543
+ String get assistant_default_title => 'Use LocalMind as your assistant';
544
+
545
+ @override
546
+ String get assistant_default_description =>
547
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
548
+
549
+ @override
550
+ String get assistant_status_active => 'Active';
551
+
552
+ @override
553
+ String get assistant_status_available => 'Not active';
554
+
555
+ @override
556
+ String get assistant_status_manual => 'Check settings';
557
+
558
+ @override
559
+ String get assistant_status_unsupported => 'Unavailable';
560
+
561
+ @override
562
+ String get assistant_status_checking => 'Checking…';
563
+
564
+ @override
565
+ String get assistant_set_default => 'Set as default assistant';
566
+
567
+ @override
568
+ String get assistant_open_settings => 'Open Android assistant settings';
569
+
570
+ @override
571
+ String get assistant_error => 'Could not open Android assistant settings.';
572
+
573
@override
574
String get settings_behavior => 'Поведение';
575
lib/l10n/app_localizations_tr.dart
+34
@@ -536,6 +536,40 @@ class AppLocalizationsTr extends AppLocalizations {
536
@override
537
String get settings_tts => 'Metin Okuma';
538
539
+ @override
540
+ String get settings_android_assistant => 'Android Assistant';
541
+
542
+ @override
543
+ String get assistant_default_title => 'Use LocalMind as your assistant';
544
+
545
+ @override
546
+ String get assistant_default_description =>
547
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
548
+
549
+ @override
550
+ String get assistant_status_active => 'Active';
551
+
552
+ @override
553
+ String get assistant_status_available => 'Not active';
554
+
555
+ @override
556
+ String get assistant_status_manual => 'Check settings';
557
+
558
+ @override
559
+ String get assistant_status_unsupported => 'Unavailable';
560
+
561
+ @override
562
+ String get assistant_status_checking => 'Checking…';
563
+
564
+ @override
565
+ String get assistant_set_default => 'Set as default assistant';
566
+
567
+ @override
568
+ String get assistant_open_settings => 'Open Android assistant settings';
569
+
570
+ @override
571
+ String get assistant_error => 'Could not open Android assistant settings.';
572
+
573
@override
574
String get settings_behavior => 'Davranış';
575
lib/l10n/app_localizations_zh.dart
+34
@@ -523,6 +523,40 @@ class AppLocalizationsZh extends AppLocalizations {
523
@override
524
String get settings_tts => '语音合成 (TTS)';
525
526
+ @override
527
+ String get settings_android_assistant => 'Android Assistant';
528
+
529
+ @override
530
+ String get assistant_default_title => 'Use LocalMind as your assistant';
531
+
532
+ @override
533
+ String get assistant_default_description =>
534
+ 'Launch voice mode with Android\'s assistant gesture or power-button shortcut.';
535
+
536
+ @override
537
+ String get assistant_status_active => 'Active';
538
+
539
+ @override
540
+ String get assistant_status_available => 'Not active';
541
+
542
+ @override
543
+ String get assistant_status_manual => 'Check settings';
544
+
545
+ @override
546
+ String get assistant_status_unsupported => 'Unavailable';
547
+
548
+ @override
549
+ String get assistant_status_checking => 'Checking…';
550
+
551
+ @override
552
+ String get assistant_set_default => 'Set as default assistant';
553
+
554
+ @override
555
+ String get assistant_open_settings => 'Open Android assistant settings';
556
+
557
+ @override
558
+ String get assistant_error => 'Could not open Android assistant settings.';
559
+
560
@override
561
String get settings_behavior => '行为';
562
lib/l10n/untranslated.txt
+155
-1
@@ -1 +1,155 @@
1
-{}
\ No newline at end of file
1
+{
2
+ "ar": [
3
+ "settings_android_assistant",
4
+ "assistant_default_title",
5
+ "assistant_default_description",
6
+ "assistant_status_active",
7
+ "assistant_status_available",
8
+ "assistant_status_manual",
9
+ "assistant_status_unsupported",
10
+ "assistant_status_checking",
11
+ "assistant_set_default",
12
+ "assistant_open_settings",
13
+ "assistant_error"
14
+ ],
15
+
16
+ "bn": [
17
+ "settings_android_assistant",
18
+ "assistant_default_title",
19
+ "assistant_default_description",
20
+ "assistant_status_active",
21
+ "assistant_status_available",
22
+ "assistant_status_manual",
23
+ "assistant_status_unsupported",
24
+ "assistant_status_checking",
25
+ "assistant_set_default",
26
+ "assistant_open_settings",
27
+ "assistant_error"
28
+ ],
29
+
30
+ "es": [
31
+ "settings_android_assistant",
32
+ "assistant_default_title",
33
+ "assistant_default_description",
34
+ "assistant_status_active",
35
+ "assistant_status_available",
36
+ "assistant_status_manual",
37
+ "assistant_status_unsupported",
38
+ "assistant_status_checking",
39
+ "assistant_set_default",
40
+ "assistant_open_settings",
41
+ "assistant_error"
42
+ ],
43
+
44
+ "fr": [
45
+ "settings_android_assistant",
46
+ "assistant_default_title",
47
+ "assistant_default_description",
48
+ "assistant_status_active",
49
+ "assistant_status_available",
50
+ "assistant_status_manual",
51
+ "assistant_status_unsupported",
52
+ "assistant_status_checking",
53
+ "assistant_set_default",
54
+ "assistant_open_settings",
55
+ "assistant_error"
56
+ ],
57
+
58
+ "hi": [
59
+ "settings_android_assistant",
60
+ "assistant_default_title",
61
+ "assistant_default_description",
62
+ "assistant_status_active",
63
+ "assistant_status_available",
64
+ "assistant_status_manual",
65
+ "assistant_status_unsupported",
66
+ "assistant_status_checking",
67
+ "assistant_set_default",
68
+ "assistant_open_settings",
69
+ "assistant_error"
70
+ ],
71
+
72
+ "it": [
73
+ "settings_android_assistant",
74
+ "assistant_default_title",
75
+ "assistant_default_description",
76
+ "assistant_status_active",
77
+ "assistant_status_available",
78
+ "assistant_status_manual",
79
+ "assistant_status_unsupported",
80
+ "assistant_status_checking",
81
+ "assistant_set_default",
82
+ "assistant_open_settings",
83
+ "assistant_error"
84
+ ],
85
+
86
+ "ja": [
87
+ "settings_android_assistant",
88
+ "assistant_default_title",
89
+ "assistant_default_description",
90
+ "assistant_status_active",
91
+ "assistant_status_available",
92
+ "assistant_status_manual",
93
+ "assistant_status_unsupported",
94
+ "assistant_status_checking",
95
+ "assistant_set_default",
96
+ "assistant_open_settings",
97
+ "assistant_error"
98
+ ],
99
+
100
+ "ru": [
101
+ "settings_android_assistant",
102
+ "assistant_default_title",
103
+ "assistant_default_description",
104
+ "assistant_status_active",
105
+ "assistant_status_available",
106
+ "assistant_status_manual",
107
+ "assistant_status_unsupported",
108
+ "assistant_status_checking",
109
+ "assistant_set_default",
110
+ "assistant_open_settings",
111
+ "assistant_error"
112
+ ],
113
+
114
+ "tr": [
115
+ "settings_android_assistant",
116
+ "assistant_default_title",
117
+ "assistant_default_description",
118
+ "assistant_status_active",
119
+ "assistant_status_available",
120
+ "assistant_status_manual",
121
+ "assistant_status_unsupported",
122
+ "assistant_status_checking",
123
+ "assistant_set_default",
124
+ "assistant_open_settings",
125
+ "assistant_error"
126
+ ],
127
+
128
+ "zh": [
129
+ "settings_android_assistant",
130
+ "assistant_default_title",
131
+ "assistant_default_description",
132
+ "assistant_status_active",
133
+ "assistant_status_available",
134
+ "assistant_status_manual",
135
+ "assistant_status_unsupported",
136
+ "assistant_status_checking",
137
+ "assistant_set_default",
138
+ "assistant_open_settings",
139
+ "assistant_error"
140
+ ],
141
+
142
+ "zh_TW": [
143
+ "settings_android_assistant",
144
+ "assistant_default_title",
145
+ "assistant_default_description",
146
+ "assistant_status_active",
147
+ "assistant_status_available",
148
+ "assistant_status_manual",
149
+ "assistant_status_unsupported",
150
+ "assistant_status_checking",
151
+ "assistant_set_default",
152
+ "assistant_open_settings",
153
+ "assistant_error"
154
+ ]
155
+}
test/core/services/android_assistant_service_test.dart
new
+114
@@ -0,0 +1,114 @@
1
+import 'dart:async';
2
+
3
+import 'package:flutter/services.dart';
4
+import 'package:flutter_test/flutter_test.dart';
5
+import 'package:localmind/core/services/android_assistant_service.dart';
6
+
7
+void main() {
8
+ TestWidgetsFlutterBinding.ensureInitialized();
9
+
10
+ const channel = MethodChannel('localmind/android_assistant_test');
11
+ final messenger =
12
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
13
+
14
+ tearDown(() {
15
+ messenger.setMockMethodCallHandler(channel, null);
16
+ });
17
+
18
+ test('maps native assistant role statuses', () async {
19
+ var nativeStatus = 'available';
20
+ messenger.setMockMethodCallHandler(channel, (call) async {
21
+ expect(call.method, 'getAssistantStatus');
22
+ return nativeStatus;
23
+ });
24
+ final service = AndroidAssistantService(
25
+ channel: channel,
26
+ supportedPlatform: true,
27
+ );
28
+ addTearDown(service.dispose);
29
+
30
+ expect(await service.getStatus(), AndroidAssistantStatus.available);
31
+ nativeStatus = 'active';
32
+ expect(await service.getStatus(), AndroidAssistantStatus.active);
33
+ nativeStatus = 'manual';
34
+ expect(await service.getStatus(), AndroidAssistantStatus.manual);
35
+ nativeStatus = 'unsupported';
36
+ expect(await service.getStatus(), AndroidAssistantStatus.unsupported);
37
+ nativeStatus = 'unexpected';
38
+ expect(await service.getStatus(), AndroidAssistantStatus.unknown);
39
+ });
40
+
41
+ test(
42
+ 'requests the role and opens assistant settings through native code',
43
+ () async {
44
+ final calls = <String>[];
45
+ messenger.setMockMethodCallHandler(channel, (call) async {
46
+ calls.add(call.method);
47
+ if (call.method == 'requestAssistantRole') return true;
48
+ return null;
49
+ });
50
+ final service = AndroidAssistantService(
51
+ channel: channel,
52
+ supportedPlatform: true,
53
+ );
54
+ addTearDown(service.dispose);
55
+
56
+ expect(await service.requestRole(), isTrue);
57
+ await service.openSettings();
58
+
59
+ expect(calls, ['requestAssistantRole', 'openAssistantSettings']);
60
+ },
61
+ );
62
+
63
+ test('emits cold-start and live assistant invocations', () async {
64
+ messenger.setMockMethodCallHandler(channel, (call) async {
65
+ if (call.method == 'consumePendingInvocation') return true;
66
+ return null;
67
+ });
68
+ final service = AndroidAssistantService(
69
+ channel: channel,
70
+ supportedPlatform: true,
71
+ );
72
+ addTearDown(service.dispose);
73
+ var invocationCount = 0;
74
+ final subscription = service.invocations.listen((_) => invocationCount++);
75
+ addTearDown(subscription.cancel);
76
+
77
+ await service.initialize();
78
+ await Future<void>.delayed(Duration.zero);
79
+ expect(invocationCount, 1);
80
+
81
+ final reply = Completer<ByteData?>();
82
+ await messenger.handlePlatformMessage(
83
+ channel.name,
84
+ const StandardMethodCodec().encodeMethodCall(
85
+ const MethodCall('assistantInvoked'),
86
+ ),
87
+ reply.complete,
88
+ );
89
+ await reply.future;
90
+ await Future<void>.delayed(Duration.zero);
91
+
92
+ expect(invocationCount, 2);
93
+ });
94
+
95
+ test('does not call native code on unsupported platforms', () async {
96
+ var called = false;
97
+ messenger.setMockMethodCallHandler(channel, (call) async {
98
+ called = true;
99
+ return null;
100
+ });
101
+ final service = AndroidAssistantService(
102
+ channel: channel,
103
+ supportedPlatform: false,
104
+ );
105
+ addTearDown(service.dispose);
106
+
107
+ await service.initialize();
108
+ expect(await service.getStatus(), AndroidAssistantStatus.unsupported);
109
+ expect(await service.requestRole(), isFalse);
110
+ await service.openSettings();
111
+
112
+ expect(called, isFalse);
113
+ });
114
+}