@tduncan / Cortana / commits / 3557839

feat: implement Android system assistant integration with voice mode support

abdulmominsakib committed Aug 13, 2026 at 10:59 UTC 35578391b509cb7e941b0a1686fadd0a866e9986
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 +}