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
+}