@tduncan / Cortana / commits / a729619

feat: add location-based AI tools with geocoding and location permission handling

abdulmominsakib committed Sep 5, 2026 at 21:01 UTC a729619b05c2f0100c4f9e8142fc8fa619a24d11
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 +}