@tduncan / Cortana / commits / f17cb93

feat(sms): add read-only SMS inbox AI tools with permission gating

Fixes #54. Mirrors location MCP pattern: disabled by default, Android-only (hidden on iOS), on-demand query with count clamp and address/search filters.

abdulmominsakib committed Sep 18, 2026 at 10:47 UTC f17cb937c1200707673b1ffff42e6bb5104bf545
27 files changed +557 -1
android/app/src/main/AndroidManifest.xml
+1
@@ -13,6 +13,7 @@
13 <uses-permission android:name="android.permission.WRITE_CALENDAR" />
14 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
15 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
16 + <uses-permission android:name="android.permission.READ_SMS" />
17
18 <application
19 android:label="LocalMind"
lib/core/providers/app_providers.dart
+2
@@ -141,6 +141,8 @@ class SettingsNotifier extends Notifier<AppSettings> {
141 _update(state.copyWith(calendarToolsEnabled: value));
142 void setLocationToolsEnabled(bool value) =>
143 _update(state.copyWith(locationToolsEnabled: value));
144 + void setSmsToolsEnabled(bool value) =>
145 + _update(state.copyWith(smsToolsEnabled: value));
146 void setAutoCollapseThinking(bool value) =>
147 _update(state.copyWith(autoCollapseThinking: value));
148
lib/features/chat/data/tools/builtin_tool_provider.dart
+96
@@ -4,14 +4,17 @@ import 'tool_definition.dart';
4 import 'tool_registry.dart';
5 import 'calendar_service.dart';
6 import 'location_service.dart';
7 +import 'sms_service.dart';
8
9 class BuiltInToolProvider implements ToolProvider {
10 final bool calendarToolsEnabled;
11 final bool locationToolsEnabled;
12 + final bool smsToolsEnabled;
13
14 BuiltInToolProvider({
15 this.calendarToolsEnabled = false,
16 this.locationToolsEnabled = false,
17 + this.smsToolsEnabled = false,
18 });
19
20 @override
@@ -139,6 +142,44 @@ class BuiltInToolProvider implements ToolProvider {
142 ]);
143 }
144
145 + if (smsToolsEnabled) {
146 + tools.addAll(const [
147 + ToolDefinition(
148 + name: 'sms.query_messages',
149 + description:
150 + 'Query recent SMS messages from the device inbox and sent box. '
151 + 'Returns sender address, date, kind, and body. '
152 + 'Use this when the user asks to look up a code, '
153 + 'find a message from a sender, or review recent texts. '
154 + 'Android-only; keep counts small and prefer address filters.',
155 + inputSchema: {
156 + 'type': 'object',
157 + 'properties': {
158 + 'address': {
159 + 'type': 'string',
160 + 'description':
161 + 'Optional sender/recipient filter (phone number or name).',
162 + },
163 + 'count': {
164 + 'type': 'integer',
165 + 'description': 'Max messages to return (1-50). Defaults to 20.',
166 + },
167 + 'query': {
168 + 'type': 'string',
169 + 'description':
170 + 'Optional case-insensitive substring filter over address and body.',
171 + },
172 + 'include_body': {
173 + 'type': 'boolean',
174 + 'description': 'Set false for metadata only. Defaults to true.',
175 + },
176 + },
177 + },
178 + providerType: ToolProviderType.builtIn,
179 + ),
180 + ]);
181 + }
182 +
183 return tools;
184 }
185
@@ -167,6 +208,10 @@ class BuiltInToolProvider implements ToolProvider {
208 case 'location.get_current_location':
209 return _getCurrentLocation();
210
211 + // -- SMS tools --
212 + case 'sms.query_messages':
213 + return _querySmsMessages(args);
214 +
215 default:
216 return const ToolExecutionResult.failure('Unknown built-in tool');
217 }
@@ -299,4 +344,55 @@ class BuiltInToolProvider implements ToolProvider {
344 return ToolExecutionResult.failure('Failed to get location: $e');
345 }
346 }
347 +
348 + // ---------------------------------------------------------------------------
349 + // SMS tool implementations
350 + // ---------------------------------------------------------------------------
351 +
352 + Future<ToolExecutionResult> _querySmsMessages(
353 + Map<String, dynamic> args,
354 + ) async {
355 + try {
356 + final sms = SmsService.instance;
357 + if (!await sms.hasAccess()) {
358 + return const ToolExecutionResult.failure(
359 + 'SMS permission not granted. '
360 + 'Please enable SMS Access in Settings.',
361 + );
362 + }
363 +
364 + final address = args['address'] as String?;
365 + final query = args['query'] as String?;
366 + final count = switch (args['count']) {
367 + final int v => v,
368 + final num v => v.toInt(),
369 + _ => 20,
370 + };
371 + final includeBody = switch (args['include_body']) {
372 + final bool v => v,
373 + _ => true,
374 + };
375 +
376 + final result = await sms.queryMessages(
377 + address: address,
378 + count: count,
379 + query: query,
380 + includeBody: includeBody,
381 + );
382 + if (result.isEmpty) {
383 + return ToolExecutionResult.success(
384 + 'No SMS messages found matching the query.',
385 + );
386 + }
387 + return ToolExecutionResult.success(
388 + const JsonEncoder.withIndent(' ').convert(result),
389 + );
390 + } on UnsupportedError catch (e) {
391 + return ToolExecutionResult.failure(e.message ?? e.toString());
392 + } on StateError catch (e) {
393 + return ToolExecutionResult.failure(e.message);
394 + } catch (e) {
395 + return ToolExecutionResult.failure('Failed to query SMS: $e');
396 + }
397 + }
398 }
lib/features/chat/data/tools/sms_service.dart new
+117
@@ -0,0 +1,117 @@
1 +import 'dart:io';
2 +
3 +import 'package:flutter_sms_inbox/flutter_sms_inbox.dart';
4 +import 'package:permission_handler/permission_handler.dart';
5 +
6 +/// Thin wrapper around [SmsQuery] + [Permission.sms] for the built-in
7 +/// SMS tools.
8 +///
9 +/// All methods return plain Dart types (strings / maps) so tool results can
10 +/// be serialised without leaking platform model classes into the tool layer.
11 +///
12 +/// SMS is queried on demand only (no background listening, no caching,
13 +/// no persistence). Bodies are returned only on explicit query; callers
14 +/// should keep `count` small and prefer `address` filters.
15 +///
16 +/// Android-only: iOS has no public SMS-read API, so every method throws
17 +/// [UnsupportedError] on iOS.
18 +class SmsService {
19 + SmsService._();
20 + static final instance = SmsService._();
21 +
22 + final SmsQuery _query = SmsQuery();
23 +
24 + // ---------------------------------------------------------------------------
25 + // Permissions
26 + // ---------------------------------------------------------------------------
27 +
28 + /// Request SMS read access. Returns `true` when granted.
29 + ///
30 + /// Returns `false` on iOS (unsupported) without prompting.
31 + Future<bool> requestAccess() async {
32 + if (!Platform.isAndroid) return false;
33 + final status = await Permission.sms.status;
34 + if (status.isGranted) return true;
35 + final result = await Permission.sms.request();
36 + return result.isGranted;
37 + }
38 +
39 + /// Check current permission status without prompting.
40 + Future<bool> hasAccess() async {
41 + if (!Platform.isAndroid) return false;
42 + final status = await Permission.sms.status;
43 + return status.isGranted;
44 + }
45 +
46 + // ---------------------------------------------------------------------------
47 + // SMS — read
48 + // ---------------------------------------------------------------------------
49 +
50 + /// Queries recent SMS messages, newest first when [sort] is true.
51 + ///
52 + /// [count] is clamped to 1..50 (defaults to 20). Pass [address] to
53 + /// restrict to a single sender/recipient, [query] for a case-insensitive
54 + /// substring match against address/body, and [includeBody] to omit bodies
55 + /// (metadata only).
56 + ///
57 + /// Throws [UnsupportedError] on non-Android platforms and [StateError]
58 + /// when SMS permission is missing.
59 + Future<List<Map<String, dynamic>>> queryMessages({
60 + String? address,
61 + int count = 20,
62 + String? query,
63 + bool includeBody = true,
64 + }) async {
65 + if (!Platform.isAndroid) {
66 + throw UnsupportedError(
67 + 'SMS reading is only supported on Android. '
68 + 'iOS does not expose an SMS-read API.',
69 + );
70 + }
71 + if (!await hasAccess()) {
72 + throw StateError(
73 + 'SMS permission not granted. '
74 + 'Please enable SMS Access in Settings.',
75 + );
76 + }
77 +
78 + final safeCount = count.clamp(1, 50);
79 + final trimmedAddress = address?.trim();
80 + final normalizedQuery = query?.trim().toLowerCase();
81 +
82 + final messages = await _query.querySms(
83 + address: (trimmedAddress == null || trimmedAddress.isEmpty)
84 + ? null
85 + : trimmedAddress,
86 + count: safeCount,
87 + kinds: const [SmsQueryKind.inbox, SmsQueryKind.sent],
88 + sort: true,
89 + );
90 +
91 + final filtered = normalizedQuery == null || normalizedQuery.isEmpty
92 + ? messages
93 + : messages
94 + .where((m) {
95 + final haystack = '${m.address ?? ''} ${m.body ?? ''}'
96 + .toLowerCase();
97 + return haystack.contains(normalizedQuery);
98 + })
99 + .toList(growable: false);
100 +
101 + return filtered
102 + .take(safeCount)
103 + .map(
104 + (m) => {
105 + 'id': m.id,
106 + 'address': m.address,
107 + 'date': m.date?.toIso8601String(),
108 + 'date_sent': m.dateSent?.toIso8601String(),
109 + 'kind': m.kind?.name,
110 + 'thread_id': m.threadId,
111 + 'is_read': m.isRead,
112 + if (includeBody) 'body': m.body,
113 + },
114 + )
115 + .toList(growable: false);
116 + }
117 +}
lib/features/chat/providers/tooling_providers.dart
+1
@@ -16,6 +16,7 @@ final builtInToolProviderProvider = Provider<BuiltInToolProvider>((ref) {
16 return BuiltInToolProvider(
17 calendarToolsEnabled: settings.calendarToolsEnabled,
18 locationToolsEnabled: settings.locationToolsEnabled,
19 + smsToolsEnabled: settings.smsToolsEnabled,
20 );
21 });
22
lib/features/mcp/views/mcp_tools_screen.dart
+37 -1
@@ -1,3 +1,5 @@
1 +import 'dart:io';
2 +
3 import 'package:flutter/material.dart';
4 import 'package:flutter_riverpod/flutter_riverpod.dart';
5 import 'package:hugeicons/hugeicons.dart';
@@ -7,6 +9,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
9 import '../../chat/data/mcp_server_manager.dart';
10 import '../../chat/data/tools/calendar_service.dart';
11 import '../../chat/data/tools/location_service.dart';
12 +import '../../chat/data/tools/sms_service.dart';
13 import '../../chat/data/tools/tool_definition.dart';
14 import '../../chat/providers/chat_mcp_providers.dart';
15 import '../../chat/providers/tooling_providers.dart';
@@ -165,6 +168,37 @@ class McpToolsScreen extends ConsumerWidget {
168 }
169 },
170 ),
171 + if (!Platform.isIOS)
172 + _McpToggleSetting(
173 + label: l10n.sms_access,
174 + description: l10n.sms_access_desc,
175 + value: settings.smsToolsEnabled,
176 + onChanged: (value) async {
177 + if (value) {
178 + final sms = SmsService.instance;
179 + final granted = await sms.requestAccess();
180 + if (granted) {
181 + ref
182 + .read(settingsProvider.notifier)
183 + .setSmsToolsEnabled(true);
184 + } else {
185 + if (context.mounted) {
186 + ScaffoldMessenger.maybeOf(
187 + context,
188 + )?.showSnackBar(
189 + SnackBar(
190 + content: Text(l10n.sms_permission_denied),
191 + ),
192 + );
193 + }
194 + }
195 + } else {
196 + ref
197 + .read(settingsProvider.notifier)
198 + .setSmsToolsEnabled(false);
199 + }
200 + },
201 + ),
202 _McpToggleSetting(
203 label: l10n.enable_example_server,
204 description: l10n.example_mcp_server_desc,
@@ -437,7 +471,9 @@ class _ToolRow extends StatelessWidget {
471 ? HugeIcons.strokeRoundedCalendar01
472 : (tool.name.startsWith('location.')
473 ? HugeIcons.strokeRoundedLocation01
440 - : HugeIcons.strokeRoundedCalculate)),
474 + : (tool.name.startsWith('sms.')
475 + ? HugeIcons.strokeRoundedMessage01
476 + : HugeIcons.strokeRoundedCalculate))),
477 size: 18,
478 color: isMcp
479 ? theme.colorScheme.primary
lib/features/settings/data/models/app_settings.dart
+6
@@ -59,6 +59,7 @@ class AppSettings {
59 final bool showSystemMessagesInChat;
60 final bool calendarToolsEnabled;
61 final bool locationToolsEnabled;
62 + final bool smsToolsEnabled;
63 final bool autoCollapseThinking;
64 final List<McpIntegration> savedMcpIntegrations;
65
@@ -109,6 +110,7 @@ class AppSettings {
110 this.showSystemMessagesInChat = true,
111 this.calendarToolsEnabled = false,
112 this.locationToolsEnabled = false,
113 + this.smsToolsEnabled = false,
114 this.autoCollapseThinking = false,
115 this.savedMcpIntegrations = const [],
116 });
@@ -160,6 +162,7 @@ class AppSettings {
162 bool? showSystemMessagesInChat,
163 bool? calendarToolsEnabled,
164 bool? locationToolsEnabled,
165 + bool? smsToolsEnabled,
166 bool? autoCollapseThinking,
167 List<McpIntegration>? savedMcpIntegrations,
168 }) {
@@ -233,6 +236,7 @@ class AppSettings {
236 showSystemMessagesInChat ?? this.showSystemMessagesInChat,
237 calendarToolsEnabled: calendarToolsEnabled ?? this.calendarToolsEnabled,
238 locationToolsEnabled: locationToolsEnabled ?? this.locationToolsEnabled,
239 + smsToolsEnabled: smsToolsEnabled ?? this.smsToolsEnabled,
240 autoCollapseThinking: autoCollapseThinking ?? this.autoCollapseThinking,
241 savedMcpIntegrations: savedMcpIntegrations ?? this.savedMcpIntegrations,
242 );
@@ -286,6 +290,7 @@ class AppSettings {
290 'showSystemMessagesInChat': showSystemMessagesInChat,
291 'calendarToolsEnabled': calendarToolsEnabled,
292 'locationToolsEnabled': locationToolsEnabled,
293 + 'smsToolsEnabled': smsToolsEnabled,
294 'autoCollapseThinking': autoCollapseThinking,
295 'savedMcpIntegrations': savedMcpIntegrations
296 .map((i) => i.toJson())
@@ -343,6 +348,7 @@ class AppSettings {
348 showSystemMessagesInChat: map['showSystemMessagesInChat'] ?? true,
349 calendarToolsEnabled: map['calendarToolsEnabled'] ?? false,
350 locationToolsEnabled: map['locationToolsEnabled'] ?? false,
351 + smsToolsEnabled: map['smsToolsEnabled'] ?? false,
352 autoCollapseThinking: map['autoCollapseThinking'] ?? false,
353 savedMcpIntegrations: _parseSavedMcpIntegrations(
354 map['savedMcpIntegrations'],
lib/l10n/app_en.arb
+12
@@ -3381,6 +3381,18 @@
3381 "@location_permission_denied": {
3382 "description": "Snackbar message when location permission is denied"
3383 },
3384 + "sms_access": "SMS Access",
3385 + "@sms_access": {
3386 + "description": "Toggle for SMS tools feature"
3387 + },
3388 + "sms_access_desc": "Allow AI to read recent SMS messages (e.g. codes, sender lookup)",
3389 + "@sms_access_desc": {
3390 + "description": "Description for SMS tools toggle"
3391 + },
3392 + "sms_permission_denied": "SMS permission denied. Please grant SMS access in your device settings.",
3393 + "@sms_permission_denied": {
3394 + "description": "Snackbar message when SMS permission is denied"
3395 + },
3396 "builtin_ai_not_supported": "Built-in AI is not supported on this device.",
3397 "@builtin_ai_not_supported": {
3398 "description": "Error shown when built-in AI (Gemini Nano / Apple Intelligence) is not supported on the user's hardware or OS"
lib/l10n/app_localizations.dart
+18
@@ -5679,6 +5679,24 @@ abstract class AppLocalizations {
5679 /// **'Location permission denied. Please grant location access in your device settings.'**
5680 String get location_permission_denied;
5681
5682 + /// Toggle for SMS tools feature
5683 + ///
5684 + /// In en, this message translates to:
5685 + /// **'SMS Access'**
5686 + String get sms_access;
5687 +
5688 + /// Description for SMS tools toggle
5689 + ///
5690 + /// In en, this message translates to:
5691 + /// **'Allow AI to read recent SMS messages (e.g. codes, sender lookup)'**
5692 + String get sms_access_desc;
5693 +
5694 + /// Snackbar message when SMS permission is denied
5695 + ///
5696 + /// In en, this message translates to:
5697 + /// **'SMS permission denied. Please grant SMS access in your device settings.'**
5698 + String get sms_permission_denied;
5699 +
5700 /// Error shown when built-in AI (Gemini Nano / Apple Intelligence) is not supported on the user's hardware or OS
5701 ///
5702 /// In en, this message translates to:
lib/l10n/app_localizations_ar.dart
+11
@@ -3037,6 +3037,17 @@ class AppLocalizationsAr extends AppLocalizations {
3037 String get location_permission_denied =>
3038 'Location permission denied. Please grant location access in your device settings.';
3039
3040 + @override
3041 + String get sms_access => 'SMS Access';
3042 +
3043 + @override
3044 + String get sms_access_desc =>
3045 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3046 +
3047 + @override
3048 + String get sms_permission_denied =>
3049 + 'SMS permission denied. Please grant SMS access in your device settings.';
3050 +
3051 @override
3052 String get builtin_ai_not_supported =>
3053 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_bn.dart
+11
@@ -3063,6 +3063,17 @@ class AppLocalizationsBn extends AppLocalizations {
3063 String get location_permission_denied =>
3064 'Location permission denied. Please grant location access in your device settings.';
3065
3066 + @override
3067 + String get sms_access => 'SMS Access';
3068 +
3069 + @override
3070 + String get sms_access_desc =>
3071 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3072 +
3073 + @override
3074 + String get sms_permission_denied =>
3075 + 'SMS permission denied. Please grant SMS access in your device settings.';
3076 +
3077 @override
3078 String get builtin_ai_not_supported =>
3079 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_de.dart
+11
@@ -3052,6 +3052,17 @@ class AppLocalizationsDe extends AppLocalizations {
3052 String get location_permission_denied =>
3053 'Location permission denied. Please grant location access in your device settings.';
3054
3055 + @override
3056 + String get sms_access => 'SMS Access';
3057 +
3058 + @override
3059 + String get sms_access_desc =>
3060 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3061 +
3062 + @override
3063 + String get sms_permission_denied =>
3064 + 'SMS permission denied. Please grant SMS access in your device settings.';
3065 +
3066 @override
3067 String get builtin_ai_not_supported =>
3068 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_en.dart
+11
@@ -3042,6 +3042,17 @@ class AppLocalizationsEn extends AppLocalizations {
3042 String get location_permission_denied =>
3043 'Location permission denied. Please grant location access in your device settings.';
3044
3045 + @override
3046 + String get sms_access => 'SMS Access';
3047 +
3048 + @override
3049 + String get sms_access_desc =>
3050 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3051 +
3052 + @override
3053 + String get sms_permission_denied =>
3054 + 'SMS permission denied. Please grant SMS access in your device settings.';
3055 +
3056 @override
3057 String get builtin_ai_not_supported =>
3058 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_es.dart
+11
@@ -3066,6 +3066,17 @@ class AppLocalizationsEs extends AppLocalizations {
3066 String get location_permission_denied =>
3067 'Location permission denied. Please grant location access in your device settings.';
3068
3069 + @override
3070 + String get sms_access => 'SMS Access';
3071 +
3072 + @override
3073 + String get sms_access_desc =>
3074 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3075 +
3076 + @override
3077 + String get sms_permission_denied =>
3078 + 'SMS permission denied. Please grant SMS access in your device settings.';
3079 +
3080 @override
3081 String get builtin_ai_not_supported =>
3082 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_fr.dart
+11
@@ -3055,6 +3055,17 @@ class AppLocalizationsFr extends AppLocalizations {
3055 String get location_permission_denied =>
3056 'Location permission denied. Please grant location access in your device settings.';
3057
3058 + @override
3059 + String get sms_access => 'SMS Access';
3060 +
3061 + @override
3062 + String get sms_access_desc =>
3063 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3064 +
3065 + @override
3066 + String get sms_permission_denied =>
3067 + 'SMS permission denied. Please grant SMS access in your device settings.';
3068 +
3069 @override
3070 String get builtin_ai_not_supported =>
3071 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_hi.dart
+11
@@ -3053,6 +3053,17 @@ class AppLocalizationsHi extends AppLocalizations {
3053 String get location_permission_denied =>
3054 'Location permission denied. Please grant location access in your device settings.';
3055
3056 + @override
3057 + String get sms_access => 'SMS Access';
3058 +
3059 + @override
3060 + String get sms_access_desc =>
3061 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3062 +
3063 + @override
3064 + String get sms_permission_denied =>
3065 + 'SMS permission denied. Please grant SMS access in your device settings.';
3066 +
3067 @override
3068 String get builtin_ai_not_supported =>
3069 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_it.dart
+11
@@ -3069,6 +3069,17 @@ class AppLocalizationsIt extends AppLocalizations {
3069 String get location_permission_denied =>
3070 'Location permission denied. Please grant location access in your device settings.';
3071
3072 + @override
3073 + String get sms_access => 'SMS Access';
3074 +
3075 + @override
3076 + String get sms_access_desc =>
3077 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3078 +
3079 + @override
3080 + String get sms_permission_denied =>
3081 + 'SMS permission denied. Please grant SMS access in your device settings.';
3082 +
3083 @override
3084 String get builtin_ai_not_supported =>
3085 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_ja.dart
+11
@@ -2992,6 +2992,17 @@ class AppLocalizationsJa extends AppLocalizations {
2992 String get location_permission_denied =>
2993 'Location permission denied. Please grant location access in your device settings.';
2994
2995 + @override
2996 + String get sms_access => 'SMS Access';
2997 +
2998 + @override
2999 + String get sms_access_desc =>
3000 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3001 +
3002 + @override
3003 + String get sms_permission_denied =>
3004 + 'SMS permission denied. Please grant SMS access in your device settings.';
3005 +
3006 @override
3007 String get builtin_ai_not_supported =>
3008 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_ko.dart
+11
@@ -2976,6 +2976,17 @@ class AppLocalizationsKo extends AppLocalizations {
2976 String get location_permission_denied =>
2977 '위치 권한이 거부되었습니다. 기기 설정에서 위치 액세스 권한을 부여하세요.';
2978
2979 + @override
2980 + String get sms_access => 'SMS Access';
2981 +
2982 + @override
2983 + String get sms_access_desc =>
2984 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
2985 +
2986 + @override
2987 + String get sms_permission_denied =>
2988 + 'SMS permission denied. Please grant SMS access in your device settings.';
2989 +
2990 @override
2991 String get builtin_ai_not_supported => '내장 AI는 이 장치에서 지원되지 않습니다.';
2992
lib/l10n/app_localizations_pt.dart
+11
@@ -3082,6 +3082,17 @@ class AppLocalizationsPt extends AppLocalizations {
3082 String get location_permission_denied =>
3083 'Permissão de localização negada. Conceda acesso à localização nas configurações do seu dispositivo.';
3084
3085 + @override
3086 + String get sms_access => 'SMS Access';
3087 +
3088 + @override
3089 + String get sms_access_desc =>
3090 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3091 +
3092 + @override
3093 + String get sms_permission_denied =>
3094 + 'SMS permission denied. Please grant SMS access in your device settings.';
3095 +
3096 @override
3097 String get builtin_ai_not_supported =>
3098 'A IA integrada não é compatível com este dispositivo.';
lib/l10n/app_localizations_ru.dart
+11
@@ -3067,6 +3067,17 @@ class AppLocalizationsRu extends AppLocalizations {
3067 String get location_permission_denied =>
3068 'Location permission denied. Please grant location access in your device settings.';
3069
3070 + @override
3071 + String get sms_access => 'SMS Access';
3072 +
3073 + @override
3074 + String get sms_access_desc =>
3075 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3076 +
3077 + @override
3078 + String get sms_permission_denied =>
3079 + 'SMS permission denied. Please grant SMS access in your device settings.';
3080 +
3081 @override
3082 String get builtin_ai_not_supported =>
3083 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_tr.dart
+11
@@ -3072,6 +3072,17 @@ class AppLocalizationsTr extends AppLocalizations {
3072 String get location_permission_denied =>
3073 'Location permission denied. Please grant location access in your device settings.';
3074
3075 + @override
3076 + String get sms_access => 'SMS Access';
3077 +
3078 + @override
3079 + String get sms_access_desc =>
3080 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
3081 +
3082 + @override
3083 + String get sms_permission_denied =>
3084 + 'SMS permission denied. Please grant SMS access in your device settings.';
3085 +
3086 @override
3087 String get builtin_ai_not_supported =>
3088 'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_zh.dart
+11
@@ -2968,6 +2968,17 @@ class AppLocalizationsZh extends AppLocalizations {
2968 String get location_permission_denied =>
2969 'Location permission denied. Please grant location access in your device settings.';
2970
2971 + @override
2972 + String get sms_access => 'SMS Access';
2973 +
2974 + @override
2975 + String get sms_access_desc =>
2976 + 'Allow AI to read recent SMS messages (e.g. codes, sender lookup)';
2977 +
2978 + @override
2979 + String get sms_permission_denied =>
2980 + 'SMS permission denied. Please grant SMS access in your device settings.';
2981 +
2982 @override
2983 String get builtin_ai_not_supported =>
2984 'Built-in AI is not supported on this device.';
lib/l10n/untranslated.txt
+48
@@ -6,6 +6,9 @@
6 "location_access",
7 "location_access_desc",
8 "location_permission_denied",
9 + "sms_access",
10 + "sms_access_desc",
11 + "sms_permission_denied",
12 "builtin_ai_not_supported",
13 "builtin_ai_unsupported_desc",
14 "builtin_ai_unsupported_chip",
@@ -23,6 +26,9 @@
26 "location_access",
27 "location_access_desc",
28 "location_permission_denied",
29 + "sms_access",
30 + "sms_access_desc",
31 + "sms_permission_denied",
32 "builtin_ai_not_supported",
33 "builtin_ai_unsupported_desc",
34 "builtin_ai_unsupported_chip",
@@ -40,6 +46,9 @@
46 "location_access",
47 "location_access_desc",
48 "location_permission_denied",
49 + "sms_access",
50 + "sms_access_desc",
51 + "sms_permission_denied",
52 "builtin_ai_not_supported",
53 "builtin_ai_unsupported_desc",
54 "builtin_ai_unsupported_chip",
@@ -57,6 +66,9 @@
66 "location_access",
67 "location_access_desc",
68 "location_permission_denied",
69 + "sms_access",
70 + "sms_access_desc",
71 + "sms_permission_denied",
72 "builtin_ai_not_supported",
73 "builtin_ai_unsupported_desc",
74 "builtin_ai_unsupported_chip",
@@ -74,6 +86,9 @@
86 "location_access",
87 "location_access_desc",
88 "location_permission_denied",
89 + "sms_access",
90 + "sms_access_desc",
91 + "sms_permission_denied",
92 "builtin_ai_not_supported",
93 "builtin_ai_unsupported_desc",
94 "builtin_ai_unsupported_chip",
@@ -91,6 +106,9 @@
106 "location_access",
107 "location_access_desc",
108 "location_permission_denied",
109 + "sms_access",
110 + "sms_access_desc",
111 + "sms_permission_denied",
112 "builtin_ai_not_supported",
113 "builtin_ai_unsupported_desc",
114 "builtin_ai_unsupported_chip",
@@ -108,6 +126,9 @@
126 "location_access",
127 "location_access_desc",
128 "location_permission_denied",
129 + "sms_access",
130 + "sms_access_desc",
131 + "sms_permission_denied",
132 "builtin_ai_not_supported",
133 "builtin_ai_unsupported_desc",
134 "builtin_ai_unsupported_chip",
@@ -125,12 +146,27 @@
146 "location_access",
147 "location_access_desc",
148 "location_permission_denied",
149 + "sms_access",
150 + "sms_access_desc",
151 + "sms_permission_denied",
152 "builtin_ai_not_supported",
153 "builtin_ai_unsupported_desc",
154 "builtin_ai_unsupported_chip",
155 "builtin_ai_enable"
156 ],
157
158 + "ko": [
159 + "sms_access",
160 + "sms_access_desc",
161 + "sms_permission_denied"
162 + ],
163 +
164 + "pt": [
165 + "sms_access",
166 + "sms_access_desc",
167 + "sms_permission_denied"
168 + ],
169 +
170 "ru": [
171 "file_explorer_not_found",
172 "export_data_failed",
@@ -142,6 +178,9 @@
178 "location_access",
179 "location_access_desc",
180 "location_permission_denied",
181 + "sms_access",
182 + "sms_access_desc",
183 + "sms_permission_denied",
184 "builtin_ai_not_supported",
185 "builtin_ai_unsupported_desc",
186 "builtin_ai_unsupported_chip",
@@ -159,6 +198,9 @@
198 "location_access",
199 "location_access_desc",
200 "location_permission_denied",
201 + "sms_access",
202 + "sms_access_desc",
203 + "sms_permission_denied",
204 "builtin_ai_not_supported",
205 "builtin_ai_unsupported_desc",
206 "builtin_ai_unsupported_chip",
@@ -176,6 +218,9 @@
218 "location_access",
219 "location_access_desc",
220 "location_permission_denied",
221 + "sms_access",
222 + "sms_access_desc",
223 + "sms_permission_denied",
224 "builtin_ai_not_supported",
225 "builtin_ai_unsupported_desc",
226 "builtin_ai_unsupported_chip",
@@ -193,6 +238,9 @@
238 "location_access",
239 "location_access_desc",
240 "location_permission_denied",
241 + "sms_access",
242 + "sms_access_desc",
243 + "sms_permission_denied",
244 "builtin_ai_not_supported",
245 "builtin_ai_unsupported_desc",
246 "builtin_ai_unsupported_chip",
pubspec.lock
+8
@@ -770,6 +770,14 @@ packages:
770 url: "https://pub.dev"
771 source: hosted
772 version: "0.1.3"
773 + flutter_sms_inbox:
774 + dependency: "direct main"
775 + description:
776 + name: flutter_sms_inbox
777 + sha256: "319721d50e5502ce811bb4ba2bbc9d12396da7a5764558087dc88d4b5909bcbb"
778 + url: "https://pub.dev"
779 + source: hosted
780 + version: "1.0.5"
781 flutter_svg:
782 dependency: transitive
783 description:
pubspec.yaml
+1
@@ -94,6 +94,7 @@ dependencies:
94 flutter_gemma_builtin_ai: ^0.2.1
95 geolocator: ^14.0.3
96 geocoding: ^5.0.0
97 + flutter_sms_inbox: ^1.0.5
98
99 dev_dependencies:
100 flutter_test:
test/features/chat/tools/builtin_sms_tool_test.dart new
+56
@@ -0,0 +1,56 @@
1 +import 'package:flutter_test/flutter_test.dart';
2 +import 'package:localmind/features/chat/data/tools/builtin_tool_provider.dart';
3 +import 'package:localmind/features/settings/data/models/app_settings.dart';
4 +
5 +void main() {
6 + group('SMS built-in tool gating', () {
7 + test('sms tool is off by default', () async {
8 + final provider = BuiltInToolProvider();
9 + final tools = await provider.listTools();
10 + expect(tools.any((t) => t.name == 'sms.query_messages'), isFalse);
11 + });
12 +
13 + test('sms tool is listed when enabled', () async {
14 + final provider = BuiltInToolProvider(smsToolsEnabled: true);
15 + final tools = await provider.listTools();
16 + expect(tools.any((t) => t.name == 'sms.query_messages'), isTrue);
17 + });
18 +
19 + test('calendar/location flags alone do not enable sms', () async {
20 + final provider = BuiltInToolProvider(
21 + calendarToolsEnabled: true,
22 + locationToolsEnabled: true,
23 + );
24 + final tools = await provider.listTools();
25 + expect(tools.any((t) => t.name == 'sms.query_messages'), isFalse);
26 + expect(tools.any((t) => t.name.startsWith('calendar.')), isTrue);
27 + expect(
28 + tools.any((t) => t.name == 'location.get_current_location'),
29 + isTrue,
30 + );
31 + });
32 +
33 + test('executing sms tool without permission fails gracefully', () async {
34 + final provider = BuiltInToolProvider(smsToolsEnabled: true);
35 + final result = await provider.execute('sms.query_messages', {});
36 + expect(result.success, isFalse);
37 + expect(result.error, isNotNull);
38 + });
39 + });
40 +
41 + group('AppSettings smsToolsEnabled', () {
42 + test('defaults to false', () {
43 + expect(AppSettings().smsToolsEnabled, isFalse);
44 + });
45 +
46 + test('survives toMap/fromMap round-trip', () {
47 + final settings = AppSettings().copyWith(smsToolsEnabled: true);
48 + final restored = AppSettings.fromMap(settings.toMap());
49 + expect(restored.smsToolsEnabled, isTrue);
50 + expect(
51 + AppSettings.fromMap(AppSettings().toMap()).smsToolsEnabled,
52 + isFalse,
53 + );
54 + });
55 + });
56 +}