26 files changed
+611
-2
android/app/src/main/AndroidManifest.xml
+2
@@ -10,6 +10,8 @@
10
<uses-permission android:name="android.permission.RECORD_AUDIO" />
11
<uses-permission android:name="android.permission.READ_CALENDAR" />
12
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
13
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
14
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
15
16
<application
17
android:label="LocalMind"
ios/Runner/Info.plist
+2
@@ -107,6 +107,8 @@
107
<string>Full access to view and edit your calendar events through AI chat.</string>
108
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
109
<string>Add calendar events through AI chat without reading existing events.</string>
110
+ <key>NSLocationWhenInUseUsageDescription</key>
111
+ <string>Share your current location with AI chat only when you ask, including a human-readable place name.</string>
112
<key>ITSAppUsesNonExemptEncryption</key>
113
<false/>
114
</dict>
lib/core/providers/app_providers.dart
+2
@@ -139,6 +139,8 @@ class SettingsNotifier extends Notifier<AppSettings> {
139
_update(state.copyWith(showSystemMessagesInChat: value));
140
void setCalendarToolsEnabled(bool value) =>
141
_update(state.copyWith(calendarToolsEnabled: value));
142
+ void setLocationToolsEnabled(bool value) =>
143
+ _update(state.copyWith(locationToolsEnabled: value));
144
void setAutoCollapseThinking(bool value) =>
145
_update(state.copyWith(autoCollapseThinking: value));
146
lib/features/chat/data/tools/builtin_tool_provider.dart
+51
-1
@@ -3,11 +3,16 @@ import 'dart:convert';
3
import 'tool_definition.dart';
4
import 'tool_registry.dart';
5
import 'calendar_service.dart';
6
+import 'location_service.dart';
7
8
class BuiltInToolProvider implements ToolProvider {
9
final bool calendarToolsEnabled;
10
+ final bool locationToolsEnabled;
11
10
- BuiltInToolProvider({this.calendarToolsEnabled = false});
12
+ BuiltInToolProvider({
13
+ this.calendarToolsEnabled = false,
14
+ this.locationToolsEnabled = false,
15
+ });
16
17
@override
18
Future<List<ToolDefinition>> listTools() async {
@@ -118,6 +123,22 @@ class BuiltInToolProvider implements ToolProvider {
123
]);
124
}
125
126
+ if (locationToolsEnabled) {
127
+ tools.addAll(const [
128
+ ToolDefinition(
129
+ name: 'location.get_current_location',
130
+ description:
131
+ 'Get the user\'s current location from the device GPS. '
132
+ 'Returns latitude, longitude, accuracy, and a reverse-geocoded '
133
+ 'human-readable place (city, region, country, formatted address). '
134
+ 'Use this when the user asks for local context such as nearby '
135
+ 'places, weather, or directions.',
136
+ inputSchema: {'type': 'object', 'properties': {}},
137
+ providerType: ToolProviderType.builtIn,
138
+ ),
139
+ ]);
140
+ }
141
+
142
return tools;
143
}
144
@@ -142,6 +163,10 @@ class BuiltInToolProvider implements ToolProvider {
163
case 'calendar.list_calendars':
164
return _listCalendars();
165
166
+ // -- Location tools --
167
+ case 'location.get_current_location':
168
+ return _getCurrentLocation();
169
+
170
default:
171
return const ToolExecutionResult.failure('Unknown built-in tool');
172
}
@@ -249,4 +274,29 @@ class BuiltInToolProvider implements ToolProvider {
274
return ToolExecutionResult.failure('Failed to list calendars: $e');
275
}
276
}
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Location tool implementations
280
+ // ---------------------------------------------------------------------------
281
+
282
+ Future<ToolExecutionResult> _getCurrentLocation() async {
283
+ try {
284
+ final location = LocationService.instance;
285
+ if (!await location.hasAccess()) {
286
+ return const ToolExecutionResult.failure(
287
+ 'Location permission not granted. '
288
+ 'Please enable Location Access in Settings.',
289
+ );
290
+ }
291
+
292
+ final result = await location.getCurrentLocation();
293
+ return ToolExecutionResult.success(
294
+ const JsonEncoder.withIndent(' ').convert(result),
295
+ );
296
+ } on StateError catch (e) {
297
+ return ToolExecutionResult.failure(e.message);
298
+ } catch (e) {
299
+ return ToolExecutionResult.failure('Failed to get location: $e');
300
+ }
301
+ }
302
}
lib/features/chat/data/tools/location_service.dart
new
+145
@@ -0,0 +1,145 @@
1
+import 'dart:async';
2
+
3
+import 'package:geocoding/geocoding.dart';
4
+import 'package:geolocator/geolocator.dart';
5
+
6
+/// Thin wrapper around [Geolocator] + [Geocoding] for the built-in
7
+/// location 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
+/// Location is fetched on demand only (no background tracking, no caching,
13
+/// no persistence). Reverse-geocoding is best-effort: when it fails the
14
+/// raw coordinates are still returned with a `geocoding_error` note.
15
+class LocationService {
16
+ LocationService._();
17
+ static final instance = LocationService._();
18
+
19
+ final Geocoding _geocoding = Geocoding();
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Permissions
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /// Request when-in-use location access. Returns `true` when granted.
26
+ Future<bool> requestAccess() async {
27
+ final serviceEnabled = await Geolocator.isLocationServiceEnabled();
28
+ if (!serviceEnabled) return false;
29
+
30
+ var permission = await Geolocator.checkPermission();
31
+ if (permission == LocationPermission.denied) {
32
+ permission = await Geolocator.requestPermission();
33
+ }
34
+ return permission == LocationPermission.always ||
35
+ permission == LocationPermission.whileInUse;
36
+ }
37
+
38
+ /// Check current permission status without prompting.
39
+ Future<bool> hasAccess() async {
40
+ final permission = await Geolocator.checkPermission();
41
+ return permission == LocationPermission.always ||
42
+ permission == LocationPermission.whileInUse;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Location
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /// Returns a JSON-friendly map with coordinates and reverse-geocoded place.
50
+ ///
51
+ /// Throws a [StateError] with a human-readable message when location
52
+ /// services are disabled, permission is missing, or the fix times out.
53
+ Future<Map<String, dynamic>> getCurrentLocation() async {
54
+ final serviceEnabled = await Geolocator.isLocationServiceEnabled();
55
+ if (!serviceEnabled) {
56
+ throw StateError(
57
+ 'Location services are disabled. '
58
+ 'Please enable location services on your device.',
59
+ );
60
+ }
61
+
62
+ final permission = await Geolocator.checkPermission();
63
+ if (permission == LocationPermission.deniedForever) {
64
+ throw StateError(
65
+ 'Location permission permanently denied. '
66
+ 'Please grant location access in your device settings.',
67
+ );
68
+ }
69
+ if (permission != LocationPermission.always &&
70
+ permission != LocationPermission.whileInUse) {
71
+ throw StateError(
72
+ 'Location permission not granted. '
73
+ 'Please enable Location Access in Settings.',
74
+ );
75
+ }
76
+
77
+ late final Position position;
78
+ try {
79
+ position = await Geolocator.getCurrentPosition(
80
+ locationSettings: const LocationSettings(
81
+ accuracy: LocationAccuracy.high,
82
+ timeLimit: Duration(seconds: 15),
83
+ ),
84
+ );
85
+ } on TimeoutException {
86
+ throw StateError(
87
+ 'Timed out while getting your location. Please try again.',
88
+ );
89
+ }
90
+
91
+ final result = <String, dynamic>{
92
+ 'latitude': position.latitude,
93
+ 'longitude': position.longitude,
94
+ 'accuracy_m': position.accuracy,
95
+ 'timestamp': position.timestamp.toIso8601String(),
96
+ };
97
+
98
+ try {
99
+ final placemarks = await _geocoding.placemarkFromCoordinates(
100
+ position.latitude,
101
+ position.longitude,
102
+ );
103
+ if (placemarks.isNotEmpty) {
104
+ final place = placemarks.first;
105
+ final placemark = <String, dynamic>{
106
+ 'name': _nullIfEmpty(place.name),
107
+ 'street': _nullIfEmpty(place.street),
108
+ 'locality': _nullIfEmpty(place.locality),
109
+ 'sub_admin_area': _nullIfEmpty(place.subAdministrativeArea),
110
+ 'admin_area': _nullIfEmpty(place.administrativeArea),
111
+ 'postal_code': _nullIfEmpty(place.postalCode),
112
+ 'country': _nullIfEmpty(place.country),
113
+ 'iso_country_code': _nullIfEmpty(place.isoCountryCode),
114
+ };
115
+ result['placemark'] = placemark;
116
+ result['formatted_address'] = _formatAddress(place);
117
+ }
118
+ } catch (e) {
119
+ result['geocoding_error'] =
120
+ 'Reverse-geocoding unavailable ($e). Coordinates are still valid.';
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Helpers
128
+ // ---------------------------------------------------------------------------
129
+
130
+ String? _nullIfEmpty(String? value) {
131
+ if (value == null || value.trim().isEmpty) return null;
132
+ return value;
133
+ }
134
+
135
+ String _formatAddress(Placemark place) {
136
+ final parts = [
137
+ place.street,
138
+ place.locality,
139
+ place.administrativeArea,
140
+ place.postalCode,
141
+ place.country,
142
+ ].where((p) => p != null && p.trim().isNotEmpty).toList(growable: false);
143
+ return parts.join(', ');
144
+ }
145
+}
lib/features/chat/providers/tooling_providers.dart
+1
@@ -15,6 +15,7 @@ final builtInToolProviderProvider = Provider<BuiltInToolProvider>((ref) {
15
final settings = ref.watch(settingsProvider);
16
return BuiltInToolProvider(
17
calendarToolsEnabled: settings.calendarToolsEnabled,
18
+ locationToolsEnabled: settings.locationToolsEnabled,
19
);
20
});
21
lib/features/mcp/views/mcp_tools_screen.dart
+34
-1
@@ -6,6 +6,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
6
7
import '../../chat/data/mcp_server_manager.dart';
8
import '../../chat/data/tools/calendar_service.dart';
9
+import '../../chat/data/tools/location_service.dart';
10
import '../../chat/data/tools/tool_definition.dart';
11
import '../../chat/providers/chat_mcp_providers.dart';
12
import '../../chat/providers/tooling_providers.dart';
@@ -134,6 +135,36 @@ class McpToolsScreen extends ConsumerWidget {
135
}
136
},
137
),
138
+ _McpToggleSetting(
139
+ label: l10n.location_access,
140
+ description: l10n.location_access_desc,
141
+ value: settings.locationToolsEnabled,
142
+ onChanged: (value) async {
143
+ if (value) {
144
+ final location = LocationService.instance;
145
+ final granted = await location.requestAccess();
146
+ if (granted) {
147
+ ref
148
+ .read(settingsProvider.notifier)
149
+ .setLocationToolsEnabled(true);
150
+ } else {
151
+ if (context.mounted) {
152
+ ScaffoldMessenger.of(context).showSnackBar(
153
+ SnackBar(
154
+ content: Text(
155
+ l10n.location_permission_denied,
156
+ ),
157
+ ),
158
+ );
159
+ }
160
+ }
161
+ } else {
162
+ ref
163
+ .read(settingsProvider.notifier)
164
+ .setLocationToolsEnabled(false);
165
+ }
166
+ },
167
+ ),
168
_McpToggleSetting(
169
label: l10n.enable_example_server,
170
description: l10n.example_mcp_server_desc,
@@ -404,7 +435,9 @@ class _ToolRow extends StatelessWidget {
435
? HugeIcons.strokeRoundedShare01
436
: (tool.name.startsWith('calendar.')
437
? HugeIcons.strokeRoundedCalendar01
407
- : HugeIcons.strokeRoundedCalculate),
438
+ : (tool.name.startsWith('location.')
439
+ ? HugeIcons.strokeRoundedLocation01
440
+ : HugeIcons.strokeRoundedCalculate)),
441
size: 18,
442
color: isMcp
443
? theme.colorScheme.primary
lib/features/settings/data/models/app_settings.dart
+6
@@ -58,6 +58,7 @@ class AppSettings {
58
final bool roleSwapButtonEnabled;
59
final bool showSystemMessagesInChat;
60
final bool calendarToolsEnabled;
61
+ final bool locationToolsEnabled;
62
final bool autoCollapseThinking;
63
final List<McpIntegration> savedMcpIntegrations;
64
@@ -107,6 +108,7 @@ class AppSettings {
108
this.roleSwapButtonEnabled = false,
109
this.showSystemMessagesInChat = true,
110
this.calendarToolsEnabled = false,
111
+ this.locationToolsEnabled = false,
112
this.autoCollapseThinking = false,
113
this.savedMcpIntegrations = const [],
114
});
@@ -157,6 +159,7 @@ class AppSettings {
159
bool? roleSwapButtonEnabled,
160
bool? showSystemMessagesInChat,
161
bool? calendarToolsEnabled,
162
+ bool? locationToolsEnabled,
163
bool? autoCollapseThinking,
164
List<McpIntegration>? savedMcpIntegrations,
165
}) {
@@ -229,6 +232,7 @@ class AppSettings {
232
showSystemMessagesInChat:
233
showSystemMessagesInChat ?? this.showSystemMessagesInChat,
234
calendarToolsEnabled: calendarToolsEnabled ?? this.calendarToolsEnabled,
235
+ locationToolsEnabled: locationToolsEnabled ?? this.locationToolsEnabled,
236
autoCollapseThinking: autoCollapseThinking ?? this.autoCollapseThinking,
237
savedMcpIntegrations: savedMcpIntegrations ?? this.savedMcpIntegrations,
238
);
@@ -281,6 +285,7 @@ class AppSettings {
285
'roleSwapButtonEnabled': roleSwapButtonEnabled,
286
'showSystemMessagesInChat': showSystemMessagesInChat,
287
'calendarToolsEnabled': calendarToolsEnabled,
288
+ 'locationToolsEnabled': locationToolsEnabled,
289
'autoCollapseThinking': autoCollapseThinking,
290
'savedMcpIntegrations': savedMcpIntegrations
291
.map((i) => i.toJson())
@@ -337,6 +342,7 @@ class AppSettings {
342
roleSwapButtonEnabled: map['roleSwapButtonEnabled'] ?? false,
343
showSystemMessagesInChat: map['showSystemMessagesInChat'] ?? true,
344
calendarToolsEnabled: map['calendarToolsEnabled'] ?? false,
345
+ locationToolsEnabled: map['locationToolsEnabled'] ?? false,
346
autoCollapseThinking: map['autoCollapseThinking'] ?? false,
347
savedMcpIntegrations: _parseSavedMcpIntegrations(
348
map['savedMcpIntegrations'],
lib/l10n/app_en.arb
+12
@@ -3369,6 +3369,18 @@
3369
"@calendar_permission_denied": {
3370
"description": "Snackbar message when calendar permission is denied"
3371
},
3372
+ "location_access": "Location Access",
3373
+ "@location_access": {
3374
+ "description": "Toggle for location tools feature"
3375
+ },
3376
+ "location_access_desc": "Allow AI to get your current location with place name",
3377
+ "@location_access_desc": {
3378
+ "description": "Description for location tools toggle"
3379
+ },
3380
+ "location_permission_denied": "Location permission denied. Please grant location access in your device settings.",
3381
+ "@location_permission_denied": {
3382
+ "description": "Snackbar message when location permission is denied"
3383
+ },
3384
"builtin_ai_not_supported": "Built-in AI is not supported on this device.",
3385
"@builtin_ai_not_supported": {
3386
"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
@@ -5657,6 +5657,24 @@ abstract class AppLocalizations {
5657
/// **'Calendar permission denied. Please grant calendar access in your device settings.'**
5658
String get calendar_permission_denied;
5659
5660
+ /// Toggle for location tools feature
5661
+ ///
5662
+ /// In en, this message translates to:
5663
+ /// **'Location Access'**
5664
+ String get location_access;
5665
+
5666
+ /// Description for location tools toggle
5667
+ ///
5668
+ /// In en, this message translates to:
5669
+ /// **'Allow AI to get your current location with place name'**
5670
+ String get location_access_desc;
5671
+
5672
+ /// Snackbar message when location permission is denied
5673
+ ///
5674
+ /// In en, this message translates to:
5675
+ /// **'Location permission denied. Please grant location access in your device settings.'**
5676
+ String get location_permission_denied;
5677
+
5678
/// Error shown when built-in AI (Gemini Nano / Apple Intelligence) is not supported on the user's hardware or OS
5679
///
5680
/// In en, this message translates to:
lib/l10n/app_localizations_ar.dart
+11
@@ -3026,6 +3026,17 @@ class AppLocalizationsAr extends AppLocalizations {
3026
String get calendar_permission_denied =>
3027
'Calendar permission denied. Please grant calendar access in your device settings.';
3028
3029
+ @override
3030
+ String get location_access => 'Location Access';
3031
+
3032
+ @override
3033
+ String get location_access_desc =>
3034
+ 'Allow AI to get your current location with place name';
3035
+
3036
+ @override
3037
+ String get location_permission_denied =>
3038
+ 'Location permission denied. Please grant location access in your device settings.';
3039
+
3040
@override
3041
String get builtin_ai_not_supported =>
3042
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_bn.dart
+11
@@ -3052,6 +3052,17 @@ class AppLocalizationsBn extends AppLocalizations {
3052
String get calendar_permission_denied =>
3053
'Calendar permission denied. Please grant calendar access in your device settings.';
3054
3055
+ @override
3056
+ String get location_access => 'Location Access';
3057
+
3058
+ @override
3059
+ String get location_access_desc =>
3060
+ 'Allow AI to get your current location with place name';
3061
+
3062
+ @override
3063
+ String get location_permission_denied =>
3064
+ 'Location permission denied. Please grant location 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_de.dart
+11
@@ -3041,6 +3041,17 @@ class AppLocalizationsDe extends AppLocalizations {
3041
String get calendar_permission_denied =>
3042
'Calendar permission denied. Please grant calendar access in your device settings.';
3043
3044
+ @override
3045
+ String get location_access => 'Location Access';
3046
+
3047
+ @override
3048
+ String get location_access_desc =>
3049
+ 'Allow AI to get your current location with place name';
3050
+
3051
+ @override
3052
+ String get location_permission_denied =>
3053
+ 'Location permission denied. Please grant location access in your device settings.';
3054
+
3055
@override
3056
String get builtin_ai_not_supported =>
3057
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_en.dart
+11
@@ -3031,6 +3031,17 @@ class AppLocalizationsEn extends AppLocalizations {
3031
String get calendar_permission_denied =>
3032
'Calendar permission denied. Please grant calendar access in your device settings.';
3033
3034
+ @override
3035
+ String get location_access => 'Location Access';
3036
+
3037
+ @override
3038
+ String get location_access_desc =>
3039
+ 'Allow AI to get your current location with place name';
3040
+
3041
+ @override
3042
+ String get location_permission_denied =>
3043
+ 'Location permission denied. Please grant location access in your device settings.';
3044
+
3045
@override
3046
String get builtin_ai_not_supported =>
3047
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_es.dart
+11
@@ -3055,6 +3055,17 @@ class AppLocalizationsEs extends AppLocalizations {
3055
String get calendar_permission_denied =>
3056
'Calendar permission denied. Please grant calendar access in your device settings.';
3057
3058
+ @override
3059
+ String get location_access => 'Location Access';
3060
+
3061
+ @override
3062
+ String get location_access_desc =>
3063
+ 'Allow AI to get your current location with place name';
3064
+
3065
+ @override
3066
+ String get location_permission_denied =>
3067
+ 'Location permission denied. Please grant location 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_fr.dart
+11
@@ -3044,6 +3044,17 @@ class AppLocalizationsFr extends AppLocalizations {
3044
String get calendar_permission_denied =>
3045
'Calendar permission denied. Please grant calendar access in your device settings.';
3046
3047
+ @override
3048
+ String get location_access => 'Location Access';
3049
+
3050
+ @override
3051
+ String get location_access_desc =>
3052
+ 'Allow AI to get your current location with place name';
3053
+
3054
+ @override
3055
+ String get location_permission_denied =>
3056
+ 'Location permission denied. Please grant location access in your device settings.';
3057
+
3058
@override
3059
String get builtin_ai_not_supported =>
3060
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_hi.dart
+11
@@ -3042,6 +3042,17 @@ class AppLocalizationsHi extends AppLocalizations {
3042
String get calendar_permission_denied =>
3043
'Calendar permission denied. Please grant calendar access in your device settings.';
3044
3045
+ @override
3046
+ String get location_access => 'Location Access';
3047
+
3048
+ @override
3049
+ String get location_access_desc =>
3050
+ 'Allow AI to get your current location with place name';
3051
+
3052
+ @override
3053
+ String get location_permission_denied =>
3054
+ 'Location permission denied. Please grant location 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_it.dart
+11
@@ -3058,6 +3058,17 @@ class AppLocalizationsIt extends AppLocalizations {
3058
String get calendar_permission_denied =>
3059
'Calendar permission denied. Please grant calendar access in your device settings.';
3060
3061
+ @override
3062
+ String get location_access => 'Location Access';
3063
+
3064
+ @override
3065
+ String get location_access_desc =>
3066
+ 'Allow AI to get your current location with place name';
3067
+
3068
+ @override
3069
+ String get location_permission_denied =>
3070
+ 'Location permission denied. Please grant location access in your device settings.';
3071
+
3072
@override
3073
String get builtin_ai_not_supported =>
3074
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_ja.dart
+11
@@ -2981,6 +2981,17 @@ class AppLocalizationsJa extends AppLocalizations {
2981
String get calendar_permission_denied =>
2982
'Calendar permission denied. Please grant calendar access in your device settings.';
2983
2984
+ @override
2985
+ String get location_access => 'Location Access';
2986
+
2987
+ @override
2988
+ String get location_access_desc =>
2989
+ 'Allow AI to get your current location with place name';
2990
+
2991
+ @override
2992
+ String get location_permission_denied =>
2993
+ 'Location permission denied. Please grant location access in your device settings.';
2994
+
2995
@override
2996
String get builtin_ai_not_supported =>
2997
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_ru.dart
+11
@@ -3056,6 +3056,17 @@ class AppLocalizationsRu extends AppLocalizations {
3056
String get calendar_permission_denied =>
3057
'Calendar permission denied. Please grant calendar access in your device settings.';
3058
3059
+ @override
3060
+ String get location_access => 'Location Access';
3061
+
3062
+ @override
3063
+ String get location_access_desc =>
3064
+ 'Allow AI to get your current location with place name';
3065
+
3066
+ @override
3067
+ String get location_permission_denied =>
3068
+ 'Location permission denied. Please grant location access in your device settings.';
3069
+
3070
@override
3071
String get builtin_ai_not_supported =>
3072
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_tr.dart
+11
@@ -3061,6 +3061,17 @@ class AppLocalizationsTr extends AppLocalizations {
3061
String get calendar_permission_denied =>
3062
'Calendar permission denied. Please grant calendar access in your device settings.';
3063
3064
+ @override
3065
+ String get location_access => 'Location Access';
3066
+
3067
+ @override
3068
+ String get location_access_desc =>
3069
+ 'Allow AI to get your current location with place name';
3070
+
3071
+ @override
3072
+ String get location_permission_denied =>
3073
+ 'Location permission denied. Please grant location access in your device settings.';
3074
+
3075
@override
3076
String get builtin_ai_not_supported =>
3077
'Built-in AI is not supported on this device.';
lib/l10n/app_localizations_zh.dart
+11
@@ -2957,6 +2957,17 @@ class AppLocalizationsZh extends AppLocalizations {
2957
String get calendar_permission_denied =>
2958
'Calendar permission denied. Please grant calendar access in your device settings.';
2959
2960
+ @override
2961
+ String get location_access => 'Location Access';
2962
+
2963
+ @override
2964
+ String get location_access_desc =>
2965
+ 'Allow AI to get your current location with place name';
2966
+
2967
+ @override
2968
+ String get location_permission_denied =>
2969
+ 'Location permission denied. Please grant location access in your device settings.';
2970
+
2971
@override
2972
String get builtin_ai_not_supported =>
2973
'Built-in AI is not supported on this device.';
lib/l10n/untranslated.txt
+36
@@ -3,6 +3,9 @@
3
"calendar_access",
4
"calendar_access_desc",
5
"calendar_permission_denied",
6
+ "location_access",
7
+ "location_access_desc",
8
+ "location_permission_denied",
9
"builtin_ai_not_supported",
10
"builtin_ai_unsupported_desc",
11
"builtin_ai_unsupported_chip",
@@ -17,6 +20,9 @@
20
"calendar_access",
21
"calendar_access_desc",
22
"calendar_permission_denied",
23
+ "location_access",
24
+ "location_access_desc",
25
+ "location_permission_denied",
26
"builtin_ai_not_supported",
27
"builtin_ai_unsupported_desc",
28
"builtin_ai_unsupported_chip",
@@ -31,6 +37,9 @@
37
"calendar_access",
38
"calendar_access_desc",
39
"calendar_permission_denied",
40
+ "location_access",
41
+ "location_access_desc",
42
+ "location_permission_denied",
43
"builtin_ai_not_supported",
44
"builtin_ai_unsupported_desc",
45
"builtin_ai_unsupported_chip",
@@ -45,6 +54,9 @@
54
"calendar_access",
55
"calendar_access_desc",
56
"calendar_permission_denied",
57
+ "location_access",
58
+ "location_access_desc",
59
+ "location_permission_denied",
60
"builtin_ai_not_supported",
61
"builtin_ai_unsupported_desc",
62
"builtin_ai_unsupported_chip",
@@ -59,6 +71,9 @@
71
"calendar_access",
72
"calendar_access_desc",
73
"calendar_permission_denied",
74
+ "location_access",
75
+ "location_access_desc",
76
+ "location_permission_denied",
77
"builtin_ai_not_supported",
78
"builtin_ai_unsupported_desc",
79
"builtin_ai_unsupported_chip",
@@ -73,6 +88,9 @@
88
"calendar_access",
89
"calendar_access_desc",
90
"calendar_permission_denied",
91
+ "location_access",
92
+ "location_access_desc",
93
+ "location_permission_denied",
94
"builtin_ai_not_supported",
95
"builtin_ai_unsupported_desc",
96
"builtin_ai_unsupported_chip",
@@ -87,6 +105,9 @@
105
"calendar_access",
106
"calendar_access_desc",
107
"calendar_permission_denied",
108
+ "location_access",
109
+ "location_access_desc",
110
+ "location_permission_denied",
111
"builtin_ai_not_supported",
112
"builtin_ai_unsupported_desc",
113
"builtin_ai_unsupported_chip",
@@ -101,6 +122,9 @@
122
"calendar_access",
123
"calendar_access_desc",
124
"calendar_permission_denied",
125
+ "location_access",
126
+ "location_access_desc",
127
+ "location_permission_denied",
128
"builtin_ai_not_supported",
129
"builtin_ai_unsupported_desc",
130
"builtin_ai_unsupported_chip",
@@ -115,6 +139,9 @@
139
"calendar_access",
140
"calendar_access_desc",
141
"calendar_permission_denied",
142
+ "location_access",
143
+ "location_access_desc",
144
+ "location_permission_denied",
145
"builtin_ai_not_supported",
146
"builtin_ai_unsupported_desc",
147
"builtin_ai_unsupported_chip",
@@ -129,6 +156,9 @@
156
"calendar_access",
157
"calendar_access_desc",
158
"calendar_permission_denied",
159
+ "location_access",
160
+ "location_access_desc",
161
+ "location_permission_denied",
162
"builtin_ai_not_supported",
163
"builtin_ai_unsupported_desc",
164
"builtin_ai_unsupported_chip",
@@ -143,6 +173,9 @@
173
"calendar_access",
174
"calendar_access_desc",
175
"calendar_permission_denied",
176
+ "location_access",
177
+ "location_access_desc",
178
+ "location_permission_denied",
179
"builtin_ai_not_supported",
180
"builtin_ai_unsupported_desc",
181
"builtin_ai_unsupported_chip",
@@ -157,6 +190,9 @@
190
"calendar_access",
191
"calendar_access_desc",
192
"calendar_permission_denied",
193
+ "location_access",
194
+ "location_access_desc",
195
+ "location_permission_denied",
196
"builtin_ai_not_supported",
197
"builtin_ai_unsupported_desc",
198
"builtin_ai_unsupported_chip",
pubspec.lock
+104
@@ -812,6 +812,102 @@ packages:
812
url: "https://pub.dev"
813
source: hosted
814
version: "0.2.4"
815
+ geoclue:
816
+ dependency: transitive
817
+ description:
818
+ name: geoclue
819
+ sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
820
+ url: "https://pub.dev"
821
+ source: hosted
822
+ version: "0.1.1"
823
+ geocoding:
824
+ dependency: "direct main"
825
+ description:
826
+ name: geocoding
827
+ sha256: "40f845e0a5505d4b47da991057f2ab25cd1c68bc4570255c7d4cfbb03a952063"
828
+ url: "https://pub.dev"
829
+ source: hosted
830
+ version: "5.0.0"
831
+ geocoding_android:
832
+ dependency: transitive
833
+ description:
834
+ name: geocoding_android
835
+ sha256: "17e88a69bb0e1ae44597d70df2d2fbdb2f503672cf665421a16ab56ab97e4ead"
836
+ url: "https://pub.dev"
837
+ source: hosted
838
+ version: "5.1.0"
839
+ geocoding_darwin:
840
+ dependency: transitive
841
+ description:
842
+ name: geocoding_darwin
843
+ sha256: "97552aa24f1453defc17e4d8e40b13ddf097e669c2edfa17ed86349cacdbc1ae"
844
+ url: "https://pub.dev"
845
+ source: hosted
846
+ version: "1.0.3"
847
+ geocoding_platform_interface:
848
+ dependency: transitive
849
+ description:
850
+ name: geocoding_platform_interface
851
+ sha256: "5164b8871705af7573698b3aa0746337faf818d72b488988741bcc7aeedd2f6e"
852
+ url: "https://pub.dev"
853
+ source: hosted
854
+ version: "5.0.0"
855
+ geolocator:
856
+ dependency: "direct main"
857
+ description:
858
+ name: geolocator
859
+ sha256: e146a6d63776582651e97a79cbe459f8e1211b100101fadcd84db83361fa599f
860
+ url: "https://pub.dev"
861
+ source: hosted
862
+ version: "14.0.3"
863
+ geolocator_android:
864
+ dependency: transitive
865
+ description:
866
+ name: geolocator_android
867
+ sha256: "86ea1654e4f61ff51466848e91c116b422d6010ea269fda0fbe1af7e9e742ce1"
868
+ url: "https://pub.dev"
869
+ source: hosted
870
+ version: "5.0.3"
871
+ geolocator_apple:
872
+ dependency: transitive
873
+ description:
874
+ name: geolocator_apple
875
+ sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
876
+ url: "https://pub.dev"
877
+ source: hosted
878
+ version: "2.3.14"
879
+ geolocator_linux:
880
+ dependency: transitive
881
+ description:
882
+ name: geolocator_linux
883
+ sha256: "3da7420f11c3496511a5bd3c18fd67b88e5659f12e46b7ce00a788f6996e850a"
884
+ url: "https://pub.dev"
885
+ source: hosted
886
+ version: "0.2.6"
887
+ geolocator_platform_interface:
888
+ dependency: transitive
889
+ description:
890
+ name: geolocator_platform_interface
891
+ sha256: "94db8255dc183d268765df682580440617ca35877fc82cacb5420ad03b86198d"
892
+ url: "https://pub.dev"
893
+ source: hosted
894
+ version: "4.3.0"
895
+ geolocator_web:
896
+ dependency: transitive
897
+ description:
898
+ name: geolocator_web
899
+ sha256: "19e485a0f8d6a88abcf9c53cba3a4105e14b7435ed8ac1c108c067b938fe8429"
900
+ url: "https://pub.dev"
901
+ source: hosted
902
+ version: "4.1.4"
903
+ geolocator_windows:
904
+ dependency: transitive
905
+ description:
906
+ name: geolocator_windows
907
+ sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
908
+ url: "https://pub.dev"
909
+ source: hosted
910
+ version: "0.2.5"
911
glob:
912
dependency: transitive
913
description:
@@ -852,6 +948,14 @@ packages:
948
url: "https://pub.dev"
949
source: hosted
950
version: "2.3.2"
951
+ gsettings:
952
+ dependency: transitive
953
+ description:
954
+ name: gsettings
955
+ sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
956
+ url: "https://pub.dev"
957
+ source: hosted
958
+ version: "0.2.8"
959
haptic_feedback:
960
dependency: "direct main"
961
description:
pubspec.yaml
+2
@@ -92,6 +92,8 @@ dependencies:
92
apple_mlx:
93
path: packages/apple_mlx
94
flutter_gemma_builtin_ai: ^0.2.1
95
+ geolocator: ^14.0.3
96
+ geocoding: ^5.0.0
97
98
dev_dependencies:
99
flutter_test:
test/features/chat/tools/builtin_location_tool_test.dart
new
+64
@@ -0,0 +1,64 @@
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('Location built-in tool gating', () {
7
+ test('location tool is off by default', () async {
8
+ final provider = BuiltInToolProvider();
9
+ final tools = await provider.listTools();
10
+ expect(
11
+ tools.any((t) => t.name == 'location.get_current_location'),
12
+ isFalse,
13
+ );
14
+ });
15
+
16
+ test('location tool is listed when enabled', () async {
17
+ final provider = BuiltInToolProvider(locationToolsEnabled: true);
18
+ final tools = await provider.listTools();
19
+ expect(
20
+ tools.any((t) => t.name == 'location.get_current_location'),
21
+ isTrue,
22
+ );
23
+ });
24
+
25
+ test('calendar flag alone does not enable location', () async {
26
+ final provider = BuiltInToolProvider(calendarToolsEnabled: true);
27
+ final tools = await provider.listTools();
28
+ expect(
29
+ tools.any((t) => t.name == 'location.get_current_location'),
30
+ isFalse,
31
+ );
32
+ expect(tools.any((t) => t.name.startsWith('calendar.')), isTrue);
33
+ });
34
+
35
+ test(
36
+ 'executing location tool without permission fails gracefully',
37
+ () async {
38
+ final provider = BuiltInToolProvider(locationToolsEnabled: true);
39
+ final result = await provider.execute(
40
+ 'location.get_current_location',
41
+ {},
42
+ );
43
+ expect(result.success, isFalse);
44
+ expect(result.error, isNotNull);
45
+ },
46
+ );
47
+ });
48
+
49
+ group('AppSettings locationToolsEnabled', () {
50
+ test('defaults to false', () {
51
+ expect(AppSettings().locationToolsEnabled, isFalse);
52
+ });
53
+
54
+ test('survives toMap/fromMap round-trip', () {
55
+ final settings = AppSettings().copyWith(locationToolsEnabled: true);
56
+ final restored = AppSettings.fromMap(settings.toMap());
57
+ expect(restored.locationToolsEnabled, isTrue);
58
+ expect(
59
+ AppSettings.fromMap(AppSettings().toMap()).locationToolsEnabled,
60
+ isFalse,
61
+ );
62
+ });
63
+ });
64
+}