diff --git a/packages/geolocator_watchos/CHANGELOG.md b/packages/geolocator_watchos/CHANGELOG.md
index 237eeee..22d9875 100644
--- a/packages/geolocator_watchos/CHANGELOG.md
+++ b/packages/geolocator_watchos/CHANGELOG.md
@@ -1,3 +1,64 @@
+## 0.2.0
+
+**Breaking**
+
+* `GeolocatorWatchosBackend`, the public seam tests fake, changed shape:
+ `requestLocation` takes the accuracy index; `stopLocationRequest`,
+ `positionCount`, `accuracyAuthorization`, `requestTemporaryFullAccuracy`
+ and `accuracyRequestsDone` are new; `startUpdates` takes `activityType` and
+ `allowBackground`. The native callback now carries a
+ `GeolocatorWatchosSignal` kind.
+* `getPositionStream` called while a stream runs returns that stream, with
+ the settings it started with, as geolocator_apple does. A second stream
+ with other settings used to replace the first one's.
+
+**Added**
+
+* `getLocationAccuracy` reports `accuracyAuthorization`: precise or reduced.
+* `requestTemporaryFullAccuracy` asks for full accuracy with a purpose key and
+ waits for the answer. Without `NSLocationTemporaryUsageDescriptionDictionary`
+ in the `Info.plist` it throws `PermissionDefinitionsNotFoundException`, as
+ geolocator does on iOS.
+* `getServiceStatusStream` starts with the current status, then reports each
+ change CoreLocation signals. It reads the services switch only when
+ CoreLocation reports an authorization change, not on every fix.
+* `AppleSettings` passed to `getPositionStream` now reach CoreLocation, read
+ through `toJson()`: `activityType` (for example `ActivityType.fitness` for
+ a run) and `allowBackgroundLocationUpdates`, which `AppleSettings` turns on
+ by default. Background updates turn on only when the `Info.plist`'s
+ `UIBackgroundModes` has `location`, because CoreLocation asserts otherwise;
+ `location` under `WKBackgroundModes` does not count (checked on the
+ watchOS 27.0 Simulator). A stream without `AppleSettings` gets
+ `CLActivityTypeOther`, not the previous stream's type.
+ `ActivityType.maritime` is `CLActivityTypeMaritime` when built with the
+ watchOS 27 SDK and run on watchOS 27, and `CLActivityTypeOtherNavigation`
+ otherwise, as in geolocator_apple; the package still builds with Xcode 26.
+* Five FFI exports: `geolocator_watchos_accuracy_authorization`,
+ `_request_temporary_full_accuracy`, `_accuracy_requests_done`,
+ `_stop_location_request` and `_position_count`.
+ `geolocator_watchos_start_updates` takes the activity type and the
+ background flag, and `geolocator_watchos_request_location` the accuracy.
+
+**Fixed**
+
+* `getCurrentPosition` has a `CLLocationManager` of its own, as in
+ geolocator_apple, so it no longer waits on a running stream's distance
+ filter, which timed it out when the wearer stood still, nor stops or
+ reconfigures the stream. It skips cached fixes more than 5 seconds old,
+ which CoreLocation hands over first; a running stream's next fix answers
+ it too.
+* A position stream emits every fix CoreLocation delivers, with no age
+ filter, as geolocator_apple's does: a reduced-accuracy fix can
+ legitimately be old.
+
+**Docs**
+
+* README and PORTING_REPORT: the three members move out of "Not supported on
+ watchOS"; a section on workouts and background updates, with App Review
+ guideline 2.5.4, and what is not yet checked on a physical watch.
+* Comments no longer say that Dart polls the cached fix: the delegate wakes
+ Dart.
+
## 0.1.1
* README: the install snippet names `geolocator: ^14.1.1` and this version.
diff --git a/packages/geolocator_watchos/PORTING_REPORT.md b/packages/geolocator_watchos/PORTING_REPORT.md
index 9304232..33f1c61 100644
--- a/packages/geolocator_watchos/PORTING_REPORT.md
+++ b/packages/geolocator_watchos/PORTING_REPORT.md
@@ -5,7 +5,7 @@
| Aspect | Result |
|---|---|
| Implementation | ✅ Working (CoreLocation FFI) |
-| watchOS capability | Full — query, permission, and position stream |
+| watchOS capability | Full — query, permission, accuracy, service status, and position stream. Accuracy, service status and the `AppleSettings` fields are checked by host tests; the temporary full-accuracy prompt and background updates are not yet checked on a physical watch |
| Host unit tests (`flutter-watchos test`) | ✅ pass |
| Upstream integration test | ○ none upstream (Baseflow manual demo) — verified by building + running the example on-sim |
| Internal unified demo | ✅ included |
@@ -70,11 +70,13 @@ watchOS" table lists the ✗ rows.
| `checkPermission` / `requestPermission` | ✅ when-in-use authorization |
| `isLocationServiceEnabled` | ✅ |
| `getLastKnownPosition` | ✅ the last cached fix |
-| `getCurrentPosition` / `getPositionStream` | ✅ pushed by the `CLLocationManager` delegate |
+| `getCurrentPosition` | ✅ a `CLLocationManager` of its own, as in geolocator_apple; skips cached fixes more than 5 seconds old |
+| `getPositionStream` | ✅ pushed by the `CLLocationManager` delegate, every fix; a second call while one runs gets that stream, as from geolocator_apple |
| `distanceBetween` / `bearingBetween` | ✅ the interface's own Dart code |
-| `getServiceStatusStream` | ✗ throws `UnimplementedError` when called: not implemented yet |
-| `getLocationAccuracy` | ✗ fails with `UnimplementedError`: not implemented yet |
-| `requestTemporaryFullAccuracy` | ✗ fails with `UnimplementedError`: not implemented |
+| `getServiceStatusStream` | ✅ starts with the current status, then each change CoreLocation reports |
+| `getLocationAccuracy` | ✅ `accuracyAuthorization`: precise or reduced |
+| `requestTemporaryFullAccuracy` | ◐ `requestTemporaryFullAccuracyAuthorizationWithPurposeKey`; throws `PermissionDefinitionsNotFoundException` without `NSLocationTemporaryUsageDescriptionDictionary`, as on iOS |
+| `AppleSettings` (via `toJson()`) | ◐ `activityType` (maritime from the watchOS 27 SDK and watchOS 27 on, `otherNavigation` before, as in geolocator_apple) and `allowBackgroundLocationUpdates` (only with `location` in `UIBackgroundModes`); `pauseLocationUpdatesAutomatically` and `showBackgroundLocationIndicator` do not exist on watchOS |
| `openAppSettings` / `openLocationSettings` | ✗ fail with `UnimplementedError`: watchOS has no settings page an app can open |
---
diff --git a/packages/geolocator_watchos/README.md b/packages/geolocator_watchos/README.md
index c0d1e45..3efd347 100644
--- a/packages/geolocator_watchos/README.md
+++ b/packages/geolocator_watchos/README.md
@@ -5,7 +5,8 @@ The watchOS implementation of [`geolocator`](https://pub.dev/packages/geolocator
Location comes from **CoreLocation** (`CLLocationManager`) over dart:ffi: the
delegate caches the latest fix and **pushes** a signal into Dart. The position
stream, `getCurrentPosition` and `requestPermission` all wait on it — nothing
-polls.
+polls. As in geolocator_apple, the position stream and `getCurrentPosition`
+each have a location manager of their own.
> Scaffolded by [`flutter-watchos plugin port`](https://github.com/flutterwatch/flutter-watchos)
> from `geolocator_apple`, then implemented and verified by hand.
@@ -18,7 +19,7 @@ This is a federated plugin implementation. Apps that already depend on
```yaml
dependencies:
geolocator: ^14.1.1
- geolocator_watchos: ^0.1.1
+ geolocator_watchos: ^0.2.0
```
The plugin registers automatically via Flutter's federated registry — no
@@ -37,12 +38,58 @@ Add a location usage description to the watch app's `Info.plist`:
|---|---|
| `checkPermission` / `requestPermission` | supported (when-in-use) |
| `isLocationServiceEnabled` | supported |
-| `getCurrentPosition` / `getPositionStream` | supported |
+| `getCurrentPosition` | supported; skips cached fixes more than 5 seconds old, and does not wait on a running stream |
+| `getPositionStream` | supported; while a stream runs, a second call gets it, with its settings, as from geolocator_apple |
| `getLastKnownPosition` | supported (last cached fix) |
+| `getLocationAccuracy` | supported (precise or reduced) |
+| `requestTemporaryFullAccuracy` | supported; needs `NSLocationTemporaryUsageDescriptionDictionary` |
+| `getServiceStatusStream` | supported; starts with the current status |
The watch has no *Always* background-location entitlement flow that iOS has;
authorization is when-in-use.
+### Workouts and background updates
+
+`AppleSettings` from `geolocator`, passed to `getPositionStream`, reaches
+CoreLocation here too: `activityType` sets `CLLocationManager.activityType`,
+and `allowBackgroundLocationUpdates`, which `AppleSettings` turns on by
+default, sets `allowsBackgroundLocationUpdates`. A running or cycling app
+passes `ActivityType.fitness`:
+
+```dart
+Geolocator.getPositionStream(
+ locationSettings: AppleSettings(
+ accuracy: LocationAccuracy.bestForNavigation,
+ activityType: ActivityType.fitness,
+ allowBackgroundLocationUpdates: true,
+ ),
+);
+```
+
+Background updates turn on only when the `Info.plist`'s `UIBackgroundModes`
+has `location`; without it the setting is ignored, because CoreLocation
+asserts and the app ends otherwise. It has to be `UIBackgroundModes`, also on
+the watch: `location` under `WKBackgroundModes` does not count and still
+trips the assertion.
+
+```xml
+UIBackgroundModes
+
+ location
+
+```
+App Review accepts background location only for a feature that needs it
+(App Review Guideline 2.5.4); say what it is for in the usage description.
+
+`pauseLocationUpdatesAutomatically` and `showBackgroundLocationIndicator` do
+not exist on watchOS and are ignored. `ActivityType.maritime` needs the
+watchOS 27 SDK and watchOS 27; otherwise it is `otherNavigation`, as in
+geolocator_apple. An app recording a workout keeps running with the wrist
+down through its `HKWorkoutSession`, not through this setting.
+
+Not yet checked on a physical watch: the temporary full-accuracy prompt, and
+updates with the wrist down.
+
## Status
| Platform | Implemented |
@@ -62,9 +109,6 @@ These members of `geolocator_platform_interface` 4.4.0 throw or fail on watchOS.
| Member | On watchOS | Why |
|---|---|---|
-| `getServiceStatusStream` | throws `UnimplementedError` when called | not implemented yet |
-| `getLocationAccuracy` | fails with `UnimplementedError` | not implemented yet |
-| `requestTemporaryFullAccuracy` | fails with `UnimplementedError` | not implemented |
| `openAppSettings` / `openLocationSettings` | fail with `UnimplementedError` | watchOS has no settings page an app can open |
## License
diff --git a/packages/geolocator_watchos/lib/geolocator_watchos.dart b/packages/geolocator_watchos/lib/geolocator_watchos.dart
index 11553ab..ba9a519 100644
--- a/packages/geolocator_watchos/lib/geolocator_watchos.dart
+++ b/packages/geolocator_watchos/lib/geolocator_watchos.dart
@@ -6,9 +6,15 @@
//
// Method-channel plugins are not supported on watchOS, so this package follows
// the FFI plugin model: `watchos/Classes/geolocator_watchos_ffi.m` wraps
-// CoreLocation (CLLocationManager) and caches the latest fix on a delegate;
-// this class resolves the symbols via `DynamicLibrary.process()` and polls the
-// cached fix (the same poll-based approach the other streaming plugins use).
+// CoreLocation and caches the latest fix on a delegate, which wakes Dart
+// through a `NativeCallable.listener` with the kind of change; this class
+// resolves the symbols via `DynamicLibrary.process()` and re-reads what it
+// waits on. As in geolocator_apple, the position stream and single position
+// requests each have a CLLocationManager of their own.
+//
+// `AppleSettings` from geolocator_apple is read through `toJson()`, so its
+// `activityType` and `allowBackgroundLocationUpdates` reach CoreLocation
+// without this package depending on geolocator_apple.
import 'dart:async';
import 'dart:ffi';
@@ -29,37 +35,88 @@ abstract class GeolocatorWatchosBackend {
/// with [checkPermission].
void requestPermission();
- /// Starts continuous updates at [accuracyIndex] (the `LocationAccuracy`
- /// index) with [distanceFilter] in metres (0 or less means none).
- void startUpdates(int accuracyIndex, double distanceFilter);
-
- /// Asks for a single fix.
- void requestLocation();
+ /// Starts the stream's continuous updates at [accuracyIndex] (the
+ /// `LocationAccuracy` index) with [distanceFilter] in metres (0 or less
+ /// means none).
+ ///
+ /// [activityType] is a `CLActivityType` raw value, or 6 for maritime;
+ /// anything else, 0 included, means `CLActivityTypeOther`.
+ /// [allowBackground] turns on background updates when the `Info.plist`'s
+ /// `UIBackgroundModes` has `location`, and is ignored otherwise.
+ void startUpdates(
+ int accuracyIndex,
+ double distanceFilter, {
+ int activityType = 0,
+ bool allowBackground = false,
+ });
+
+ /// Starts the single-request manager at [accuracyIndex]. It skips cached
+ /// fixes more than 5 seconds old and stops after the first one it accepts.
+ void requestLocation(int accuracyIndex);
+
+ /// Stops the single-request manager, for a request that gave up.
+ void stopLocationRequest();
/// The latest fix as the 10 doubles documented in the C header, or null.
List? readPosition();
- /// Stops continuous updates.
+ /// How many fixes native has delivered; it goes up with each new one.
+ int positionCount();
+
+ /// Stops the stream's continuous updates.
void stopUpdates();
- /// Registers the function native calls when a new fix lands, or `nullptr`
- /// to stop.
+ /// The `LocationAccuracyStatus` index the wearer allows: 0 reduced,
+ /// 1 precise.
+ int accuracyAuthorization();
+
+ /// Asks for temporary full accuracy, explained by [purposeKey]. Returns
+ /// false without asking when the `Info.plist` has no
+ /// `NSLocationTemporaryUsageDescriptionDictionary`.
+ bool requestTemporaryFullAccuracy(String purposeKey);
+
+ /// How many temporary full-accuracy requests have finished.
+ int accuracyRequestsDone();
+
+ /// Registers the function native calls, with a [GeolocatorWatchosSignal]
+ /// kind, when a new fix lands, the authorization or accuracy changes, or an
+ /// accuracy request ends; or `nullptr` to stop.
void setCallback(Pointer> callback);
}
+/// The kinds of change native signals, as `GEOLOCATOR_WATCHOS_SIGNAL_*` in
+/// the C header.
+abstract final class GeolocatorWatchosSignal {
+ /// A new fix, or a failed update.
+ static const int position = 0;
+
+ /// The authorization, or the location services switch, changed.
+ static const int authorization = 1;
+
+ /// A temporary full-accuracy request ended.
+ static const int accuracy = 2;
+}
+
/// watchOS implementation of [GeolocatorPlatform].
base class GeolocatorWatchos extends GeolocatorPlatform {
/// Test hook: set before first use to replace the native backend with a fake.
static GeolocatorWatchosBackend? backendOverride;
- /// How often the Dart side polls the native fix while awaiting one.
- /// How long to wait for the user to answer the system permission prompt.
+ /// How long to wait for the user to answer a system permission prompt.
///
/// The prompt has no timeout of its own; this only bounds the future.
static Duration permissionTimeout = const Duration(minutes: 2);
static GeolocatorWatchosBackend? _backend;
+ /// Pending `getCurrentPosition` calls. They share the single-request
+ /// manager, which stops when the last of them ends.
+ static int _singleRequests = 0;
+
+ /// The running position stream, which a second `getPositionStream` call
+ /// gets too, with the first call's settings, as from geolocator_apple.
+ Stream? _positionStream;
+
static GeolocatorWatchosBackend get _b =>
backendOverride ?? (_backend ??= _FfiBackend());
@@ -121,7 +178,8 @@ base class GeolocatorWatchos extends GeolocatorPlatform {
// Registered before the prompt: the user can answer faster than the call
// to request it returns.
- final void Function() stopListening = _Notifier.listen(_b, check);
+ final void Function() stopListening =
+ _Notifier.listen(_b, GeolocatorWatchosSignal.authorization, check);
_b.requestPermission();
check();
@@ -154,28 +212,31 @@ base class GeolocatorWatchos extends GeolocatorPlatform {
throw const PermissionDeniedException(
'Location permission is denied on this device.');
}
- final LocationAccuracy accuracy =
- locationSettings?.accuracy ?? LocationAccuracy.best;
final Duration timeLimit =
locationSettings?.timeLimit ?? const Duration(seconds: 30);
- final double distanceFilter =
- (locationSettings?.distanceFilter ?? 0).toDouble();
final Completer completer = Completer();
+ // The fix cached now is not an answer: only one delivered after this
+ // request is. Native skips the stale fixes the single-request manager
+ // hands over first; a running stream's next fix answers too, as in
+ // geolocator_apple.
+ final int before = _b.positionCount();
void check() {
+ if (_b.positionCount() == before || completer.isCompleted) {
+ return;
+ }
final List? fix = _b.readPosition();
- if (fix != null && !completer.isCompleted) {
+ if (fix != null) {
completer.complete(_toPosition(fix));
}
}
- final void Function() stopListening = _Notifier.listen(_b, check);
- _b.requestLocation();
- _b.startUpdates(accuracy.index, distanceFilter);
- // A cached fix may already satisfy this, in which case no delegate callback
- // is coming and waiting for one would burn the whole time limit.
- check();
+ final void Function() stopListening =
+ _Notifier.listen(_b, GeolocatorWatchosSignal.position, check);
+ _singleRequests++;
+ _b.requestLocation(
+ (locationSettings?.accuracy ?? LocationAccuracy.best).index);
try {
return await completer.future.timeout(
@@ -185,52 +246,155 @@ base class GeolocatorWatchos extends GeolocatorPlatform {
);
} finally {
stopListening();
- _b.stopUpdates();
+ _singleRequests--;
+ if (_singleRequests == 0) {
+ _b.stopLocationRequest();
+ }
}
}
+ /// The position stream. While one runs, a second call gets that stream,
+ /// with the settings it started with, as from geolocator_apple; it starts
+ /// with the settings of this call again once every listener has left.
+ ///
+ /// Each fix native delivers is emitted, with no age filter: CoreLocation
+ /// hands over its last fix as updates start, and a reduced-accuracy fix can
+ /// legitimately be old.
@override
Stream getPositionStream({
LocationSettings? locationSettings,
}) {
- final LocationAccuracy accuracy =
- locationSettings?.accuracy ?? LocationAccuracy.best;
- final double distanceFilter =
- (locationSettings?.distanceFilter ?? 0).toDouble();
+ final Stream? running = _positionStream;
+ if (running != null) {
+ return running;
+ }
late StreamController controller;
- List? last;
+ int seen = 0;
void Function()? stopListening;
void emitIfNew() {
+ final int count = _b.positionCount();
+ if (count == seen) {
+ return;
+ }
+ seen = count;
final List? fix = _b.readPosition();
- if (fix != null && !_sameFix(fix, last)) {
- last = fix;
+ if (fix != null) {
controller.add(_toPosition(fix));
}
}
controller = StreamController.broadcast(
onListen: () {
- stopListening = _Notifier.listen(_b, emitIfNew);
- _b.startUpdates(accuracy.index, distanceFilter);
- // CLLocationManager delivers the last known fix immediately on start,
- // which can land before the callback is in place.
- emitIfNew();
+ seen = _b.positionCount();
+ stopListening =
+ _Notifier.listen(_b, GeolocatorWatchosSignal.position, emitIfNew);
+ _start(locationSettings);
},
onCancel: () {
stopListening?.call();
stopListening = null;
+ _positionStream = null;
_b.stopUpdates();
},
);
- return controller.stream;
+ return _positionStream = controller.stream;
+ }
+
+ /// The `CLActivityType` raw value for each geolocator `ActivityType` index:
+ /// automotiveNavigation, fitness, otherNavigation, airborne, other,
+ /// maritime.
+ static const List _clActivityTypes = [2, 3, 4, 5, 1, 6];
+
+ /// Starts updates with [settings], including what an `AppleSettings`
+ /// carries beyond the base class: its `toJson()` has `activityType` and
+ /// `allowBackgroundLocationUpdates`.
+ static void _start(LocationSettings? settings) {
+ final Map json =
+ settings?.toJson() ?? const {};
+ final Object? activity = json['activityType'];
+ _b.startUpdates(
+ (settings?.accuracy ?? LocationAccuracy.best).index,
+ (settings?.distanceFilter ?? 0).toDouble(),
+ activityType:
+ activity is int && activity >= 0 && activity < _clActivityTypes.length
+ ? _clActivityTypes[activity]
+ : 0,
+ allowBackground: json['allowBackgroundLocationUpdates'] == true,
+ );
}
- static bool _sameFix(List a, List? b) =>
- b != null && a[0] == b[0] && a[1] == b[1] && a[9] == b[9];
+ @override
+ Future getLocationAccuracy() async =>
+ LocationAccuracyStatus.values[_b.accuracyAuthorization()];
+
+ /// Asks for full accuracy for this session.
+ ///
+ /// [purposeKey] names an entry of the `Info.plist`'s
+ /// `NSLocationTemporaryUsageDescriptionDictionary`, whose text the prompt
+ /// shows. Throws [PermissionDefinitionsNotFoundException] when the
+ /// `Info.plist` has no such dictionary, as geolocator does on iOS.
+ @override
+ Future requestTemporaryFullAccuracy({
+ required String purposeKey,
+ }) async {
+ final int before = _b.accuracyRequestsDone();
+ final Completer completer = Completer();
+
+ void check() {
+ if (_b.accuracyRequestsDone() > before && !completer.isCompleted) {
+ completer.complete();
+ }
+ }
+
+ // Registered before the prompt, which the wearer may answer at once.
+ final void Function() stopListening =
+ _Notifier.listen(_b, GeolocatorWatchosSignal.accuracy, check);
+ try {
+ if (!_b.requestTemporaryFullAccuracy(purposeKey)) {
+ throw const PermissionDefinitionsNotFoundException(
+ 'The temporary accuracy dictionary key is not set in the Info.plist',
+ );
+ }
+ check();
+ await completer.future.timeout(permissionTimeout, onTimeout: () {});
+ return LocationAccuracyStatus.values[_b.accuracyAuthorization()];
+ } finally {
+ stopListening();
+ }
+ }
+
+ /// Whether location services are on, starting with the current answer,
+ /// then each change.
+ ///
+ /// CoreLocation reports a change of the system switch as an authorization
+ /// change, which is when this re-reads it: `locationServicesEnabled`
+ /// blocks, so it is not asked on every fix.
+ @override
+ Stream getServiceStatusStream() =>
+ Stream.multi((MultiStreamController c) {
+ ServiceStatus? last;
+ void emit() {
+ final ServiceStatus now = _b.isServiceEnabled()
+ ? ServiceStatus.enabled
+ : ServiceStatus.disabled;
+ if (now != last) {
+ last = now;
+ c.add(now);
+ }
+ }
+
+ final void Function() stopListening =
+ _Notifier.listen(_b, GeolocatorWatchosSignal.authorization, emit);
+ // Creates the location manager, whose delegate reports the changes.
+ _b.checkPermission();
+ emit();
+ c.onCancel = stopListening;
+ }, isBroadcast: true);
}
-/// Signature of the native→Dart new-fix signal.
+/// Signature of the native→Dart signal, which carries a
+/// [GeolocatorWatchosSignal] kind.
typedef GeolocatorFixNative = Void Function(Int64);
/// Fan-out for the single native callback slot.
@@ -241,7 +405,7 @@ typedef GeolocatorFixNative = Void Function(Int64);
/// whichever finished last unregister the others, so they share one trampoline
/// and every waiter is notified.
class _Notifier {
- static final Set _listeners = {};
+ static final Map _listeners = {};
static NativeCallable? _callable;
/// Which backend the trampoline is currently registered with.
@@ -250,18 +414,21 @@ class _Notifier {
/// once and never again would leave a replaced backend permanently silent.
static GeolocatorWatchosBackend? _registeredWith;
- /// Calls [listener] whenever CoreLocation reports anything, and returns a
- /// function that stops it.
+ /// Calls [listener] whenever CoreLocation reports a change of [kind], a
+ /// [GeolocatorWatchosSignal], and returns a function that stops it.
static void Function() listen(
- GeolocatorWatchosBackend backend, void Function() listener) {
- _listeners.add(listener);
+ GeolocatorWatchosBackend backend, int kind, void Function() listener) {
+ _listeners[listener] = kind;
if (!identical(_registeredWith, backend)) {
final NativeCallable c = _callable ??= () {
final NativeCallable created =
- NativeCallable.listener((int _) {
+ NativeCallable.listener((int signalled) {
// Copied: a listener may remove itself while being notified.
- for (final void Function() l in _listeners.toList()) {
- l();
+ for (final MapEntry l
+ in _listeners.entries.toList()) {
+ if (l.value == signalled) {
+ l.key();
+ }
}
});
// Waiting on a fix should not, by itself, keep the isolate alive.
@@ -290,10 +457,14 @@ typedef _IntNative = Int32 Function();
typedef _IntDart = int Function();
typedef _VoidNative = Void Function();
typedef _VoidDart = void Function();
-typedef _StartNative = Void Function(Int32, Double);
-typedef _StartDart = void Function(int, double);
+typedef _StartNative = Void Function(Int32, Double, Int32, Int32);
+typedef _StartDart = void Function(int, double, int, int);
+typedef _Int64Native = Int64 Function();
+typedef _Int64Dart = int Function();
typedef _ReadNative = Int32 Function(Pointer);
typedef _ReadDart = int Function(Pointer);
+typedef _AccuracyNative = Void Function(Int32);
+typedef _AccuracyDart = void Function(int);
/// Resolves the CoreLocation C symbols and marshals the fix buffer.
class _FfiBackend implements GeolocatorWatchosBackend {
@@ -314,9 +485,15 @@ class _FfiBackend implements GeolocatorWatchosBackend {
late final _StartDart _startUpdates =
_lib.lookupFunction<_StartNative, _StartDart>(
'geolocator_watchos_start_updates');
- late final _VoidDart _requestLocation =
- _lib.lookupFunction<_VoidNative, _VoidDart>(
+ late final _AccuracyDart _requestLocation =
+ _lib.lookupFunction<_AccuracyNative, _AccuracyDart>(
'geolocator_watchos_request_location');
+ late final _VoidDart _stopLocationRequest =
+ _lib.lookupFunction<_VoidNative, _VoidDart>(
+ 'geolocator_watchos_stop_location_request');
+ late final _Int64Dart _positionCount =
+ _lib.lookupFunction<_Int64Native, _Int64Dart>(
+ 'geolocator_watchos_position_count');
late final _ReadDart _readPosition =
_lib.lookupFunction<_ReadNative, _ReadDart>(
'geolocator_watchos_read_position');
@@ -339,11 +516,23 @@ class _FfiBackend implements GeolocatorWatchosBackend {
void requestPermission() => _requestPermission();
@override
- void startUpdates(int accuracyIndex, double distanceFilter) =>
- _startUpdates(accuracyIndex, distanceFilter);
+ void startUpdates(
+ int accuracyIndex,
+ double distanceFilter, {
+ int activityType = 0,
+ bool allowBackground = false,
+ }) =>
+ _startUpdates(
+ accuracyIndex, distanceFilter, activityType, allowBackground ? 1 : 0);
+
+ @override
+ void requestLocation(int accuracyIndex) => _requestLocation(accuracyIndex);
@override
- void requestLocation() => _requestLocation();
+ void stopLocationRequest() => _stopLocationRequest();
+
+ @override
+ int positionCount() => _positionCount();
@override
List? readPosition() {
@@ -358,6 +547,31 @@ class _FfiBackend implements GeolocatorWatchosBackend {
@override
void stopUpdates() => _stopUpdates();
+ late final _IntDart _accuracyAuthorization =
+ _lib.lookupFunction<_IntNative, _IntDart>(
+ 'geolocator_watchos_accuracy_authorization');
+ late final int Function(Pointer) _requestTemporaryFullAccuracy =
+ _lib.lookupFunction),
+ int Function(Pointer)>(
+ 'geolocator_watchos_request_temporary_full_accuracy');
+ late final _Int64Dart _accuracyRequestsDone =
+ _lib.lookupFunction<_Int64Native, _Int64Dart>(
+ 'geolocator_watchos_accuracy_requests_done');
+
+ @override
+ int accuracyAuthorization() => _accuracyAuthorization();
+
+ @override
+ bool requestTemporaryFullAccuracy(String purposeKey) => using(
+ (Arena arena) =>
+ _requestTemporaryFullAccuracy(
+ purposeKey.toNativeUtf8(allocator: arena)) ==
+ 1,
+ );
+
+ @override
+ int accuracyRequestsDone() => _accuracyRequestsDone();
+
@override
void setCallback(Pointer> callback) =>
_setCallback(callback);
diff --git a/packages/geolocator_watchos/pubspec.yaml b/packages/geolocator_watchos/pubspec.yaml
index bd2cb5e..7a4a05e 100644
--- a/packages/geolocator_watchos/pubspec.yaml
+++ b/packages/geolocator_watchos/pubspec.yaml
@@ -1,6 +1,6 @@
name: geolocator_watchos
description: "watchOS implementation of the geolocator plugin, providing position and permission handling from CoreLocation (CLLocationManager) via dart:ffi."
-version: 0.1.1
+version: 0.2.0
homepage: https://flutterwatch.dev
repository: https://github.com/flutterwatch/plugins/tree/main/packages/geolocator_watchos
issue_tracker: https://github.com/flutterwatch/plugins/issues
@@ -28,6 +28,8 @@ dev_dependencies:
flutter_lints: ^4.0.0
flutter_test:
sdk: flutter
+ # Only for the host tests, which pass its AppleSettings as an app does.
+ geolocator_apple: ^2.4.2
flutter:
plugin:
@@ -51,6 +53,11 @@ flutter:
- geolocator_watchos_request_permission
- geolocator_watchos_start_updates
- geolocator_watchos_request_location
+ - geolocator_watchos_stop_location_request
- geolocator_watchos_read_position
+ - geolocator_watchos_position_count
- geolocator_watchos_stop_updates
+ - geolocator_watchos_accuracy_authorization
+ - geolocator_watchos_request_temporary_full_accuracy
+ - geolocator_watchos_accuracy_requests_done
- geolocator_watchos_set_callback
diff --git a/packages/geolocator_watchos/test/geolocator_watchos_test.dart b/packages/geolocator_watchos/test/geolocator_watchos_test.dart
index dfd7a98..9fb563e 100644
--- a/packages/geolocator_watchos/test/geolocator_watchos_test.dart
+++ b/packages/geolocator_watchos/test/geolocator_watchos_test.dart
@@ -3,7 +3,7 @@
// found in the LICENSE file.
//
// Host-side unit tests. The native CoreLocation backend is replaced with a
-// fake so permission mapping and the fix-polling flow are verified off-device;
+// fake so permission mapping and the fix-delivery flow are verified off-device;
// the real CLLocationManager path is exercised on a physical watch.
import 'dart:async';
@@ -11,32 +11,99 @@ import 'dart:async';
import 'dart:ffi';
import 'package:flutter_test/flutter_test.dart';
+import 'package:geolocator_apple/geolocator_apple.dart'
+ show ActivityType, AppleSettings;
import 'package:geolocator_platform_interface/geolocator_platform_interface.dart';
import 'package:geolocator_watchos/geolocator_watchos.dart';
class _FakeBackend implements GeolocatorWatchosBackend {
bool serviceEnabled = true;
+ int serviceChecks = 0;
int status = 4; // authorizedWhenInUse
List? fix;
+ int count = 0;
int startCount = 0;
int stopCount = 0;
int requestPermissionCount = 0;
+ /// What the single-request manager delivers once asked, or null for
+ /// nothing.
+ List? singleFix;
+ int requestLocationCount = 0;
+ int stopRequestCount = 0;
+ int? lastRequestAccuracy;
+
+ /// What CoreLocation hands over as the stream starts, or null for nothing.
+ List? fixOnStart;
+
@override
- bool isServiceEnabled() => serviceEnabled;
+ bool isServiceEnabled() {
+ serviceChecks++;
+ return serviceEnabled;
+ }
+
@override
int checkPermission() => status;
@override
void requestPermission() => requestPermissionCount++;
+ int lastAccuracyIndex = -1;
+ double lastDistanceFilter = -1;
+ int lastActivityType = -1;
+ bool? lastAllowBackground;
+
+ @override
+ void startUpdates(
+ int accuracyIndex,
+ double distanceFilter, {
+ int activityType = 0,
+ bool allowBackground = false,
+ }) {
+ startCount++;
+ lastAccuracyIndex = accuracyIndex;
+ lastDistanceFilter = distanceFilter;
+ lastActivityType = activityType;
+ lastAllowBackground = allowBackground;
+ final List? first = fixOnStart;
+ if (first != null) {
+ deliver(first);
+ }
+ }
+
@override
- void startUpdates(int accuracyIndex, double distanceFilter) => startCount++;
+ void requestLocation(int accuracyIndex) {
+ requestLocationCount++;
+ lastRequestAccuracy = accuracyIndex;
+ final List? next = singleFix;
+ if (next != null) {
+ deliver(next);
+ }
+ }
+
@override
- void requestLocation() {}
+ void stopLocationRequest() => stopRequestCount++;
@override
List? readPosition() => fix;
@override
+ int positionCount() => count;
+ @override
void stopUpdates() => stopCount++;
+ int accuracy = 1; // precise
+ bool hasTemporaryUsageDictionary = true;
+ int accuracyDone = 0;
+ final List purposeKeys = [];
+
+ @override
+ int accuracyAuthorization() => accuracy;
+ @override
+ bool requestTemporaryFullAccuracy(String purposeKey) {
+ purposeKeys.add(purposeKey);
+ return hasTemporaryUsageDictionary;
+ }
+
+ @override
+ int accuracyRequestsDone() => accuracyDone;
+
Pointer>? _callback;
/// Whether native would currently wake Dart.
@@ -46,16 +113,28 @@ class _FakeBackend implements GeolocatorWatchosBackend {
void setCallback(Pointer> cb) =>
_callback = cb == nullptr ? null : cb;
- /// Fires the native signal, as any CLLocationManager delegate callback does.
- void signal() => _callback?.asFunction()(0);
+ /// Fires the native signal of [kind], as a CLLocationManager delegate
+ /// callback does.
+ void signal(int kind) => _callback?.asFunction()(kind);
/// Delivers [next] the way a location update would.
void deliver(List next) {
fix = next;
- signal();
+ count++;
+ signal(GeolocatorWatchosSignal.position);
}
}
+/// Settings whose `toJson()` names an activity type no geolocator version
+/// has.
+class _UnknownActivitySettings extends LocationSettings {
+ const _UnknownActivitySettings();
+
+ @override
+ Map toJson() =>
+ super.toJson()..addAll({'activityType': 42});
+}
+
/// A representative fix: San Francisco, with distinct field values.
List _fix() => [
37.7749, // latitude
@@ -67,7 +146,7 @@ List _fix() => [
1.0, // headingAccuracy
0.0, // speed
0.5, // speedAccuracy
- 1700000000000.0, // timestamp millis
+ DateTime.now().millisecondsSinceEpoch.toDouble(), // timestamp millis
];
void main() {
@@ -110,7 +189,7 @@ void main() {
// The user answers the prompt; CoreLocation reports it through
// locationManagerDidChangeAuthorization, not through any pollable value.
fake.status = 4;
- fake.signal();
+ fake.signal(GeolocatorWatchosSignal.authorization);
expect(await pending, LocationPermission.whileInUse);
});
@@ -128,33 +207,62 @@ void main() {
expect(await geo.isLocationServiceEnabled(), isFalse);
});
- test('getCurrentPosition returns a mapped Position and stops updates',
- () async {
- fake.fix = _fix();
- final Position pos = await geo.getCurrentPosition();
- expect(pos.latitude, 37.7749);
- expect(pos.longitude, -122.4194);
- expect(pos.accuracy, 5.0);
- expect(fake.startCount, 1);
- expect(fake.stopCount, 1);
- });
+ group('getCurrentPosition', () {
+ test('asks the single-request manager and maps its fix', () async {
+ fake.singleFix = _fix();
+ final Position pos = await geo.getCurrentPosition(
+ locationSettings:
+ const LocationSettings(accuracy: LocationAccuracy.low));
+ expect(pos.latitude, 37.7749);
+ expect(pos.longitude, -122.4194);
+ expect(pos.accuracy, 5.0);
+ expect(fake.requestLocationCount, 1);
+ expect(fake.lastRequestAccuracy, LocationAccuracy.low.index);
+ expect(fake.stopRequestCount, 1);
+ expect(fake.startCount, 0, reason: "the stream's manager is not used");
+ });
- test('getCurrentPosition throws when permission is denied', () async {
- fake.status = 2; // denied -> deniedForever
- expect(
- geo.getCurrentPosition(),
- throwsA(isA()),
- );
- });
+ test('throws when permission is denied', () async {
+ fake.status = 2; // denied -> deniedForever
+ await expectLater(
+ geo.getCurrentPosition(),
+ throwsA(isA()),
+ );
+ });
+
+ test('times out when no fix arrives, and stops asking', () async {
+ await expectLater(
+ geo.getCurrentPosition(
+ locationSettings:
+ const LocationSettings(timeLimit: Duration(milliseconds: 10))),
+ throwsA(isA()),
+ );
+ expect(fake.stopRequestCount, 1);
+ });
- test('getCurrentPosition times out when no fix arrives', () async {
- fake.fix = null;
- expect(
- geo.getCurrentPosition(
+ test('the fix cached before the request is not the answer', () async {
+ fake.fix = _fix()..[0] = 1.0;
+ final Future pending = geo.getCurrentPosition(
locationSettings:
- const LocationSettings(timeLimit: Duration(milliseconds: 10))),
- throwsA(isA()),
- );
+ const LocationSettings(timeLimit: Duration(seconds: 2)));
+ await pumpEventQueue();
+ // A signal without a new fix, as a failed update sends.
+ fake.signal(GeolocatorWatchosSignal.position);
+ await pumpEventQueue();
+ fake.deliver(_fix()..[0] = 10.0);
+ expect((await pending).latitude, 10.0);
+ });
+
+ test('two at once share the single-request manager', () async {
+ final Future a = geo.getCurrentPosition();
+ final Future b = geo.getCurrentPosition();
+ await pumpEventQueue();
+ expect(fake.requestLocationCount, 2);
+ fake.deliver(_fix()..[0] = 12.0);
+ expect((await a).latitude, 12.0);
+ expect((await b).latitude, 12.0);
+ expect(fake.stopRequestCount, 1, reason: 'stopped once both are done');
+ });
});
test('getLastKnownPosition returns null when there is no fix', () async {
@@ -163,34 +271,254 @@ void main() {
expect((await geo.getLastKnownPosition())!.latitude, 37.7749);
});
- test('getPositionStream emits the fix already available on listen', () async {
- // CLLocationManager hands over its last known fix as soon as updates
- // start, which can land before the callback is registered.
- fake.fix = _fix();
- final Position first = await geo.getPositionStream().first;
- expect(first.latitude, 37.7749);
- expect(fake.stopCount, greaterThanOrEqualTo(1));
+ group('getPositionStream', () {
+ test('emits the fix CoreLocation hands over at start, however old',
+ () async {
+ fake.fixOnStart = _fix()..[9] = 1700000000000.0;
+ final Position first = await geo.getPositionStream().first;
+ expect(first.timestamp.millisecondsSinceEpoch, 1700000000000);
+ expect(fake.stopCount, 1);
+ });
+
+ test('emits each fix native delivers, and nothing else', () async {
+ final List seen = [];
+ final StreamSubscription sub =
+ geo.getPositionStream().listen(seen.add);
+ await pumpEventQueue();
+ expect(fake.isRegistered, isTrue);
+
+ final List moved = _fix()..[0] = 40.7128; // New York
+ fake.deliver(moved);
+ await pumpEventQueue();
+ // A second fix at the same place is a fix too.
+ fake.deliver(moved);
+ await pumpEventQueue();
+ // Signals without a new fix are not.
+ fake.signal(GeolocatorWatchosSignal.position);
+ fake.signal(GeolocatorWatchosSignal.authorization);
+ await pumpEventQueue();
+
+ expect(seen.map((Position p) => p.latitude), [40.7128, 40.7128]);
+
+ await sub.cancel();
+ expect(fake.isRegistered, isFalse);
+ expect(fake.stopCount, 1);
+ });
+
+ test('a second call gets the running stream, with its settings', () async {
+ final Stream first = geo.getPositionStream(
+ locationSettings: AppleSettings(
+ accuracy: LocationAccuracy.bestForNavigation,
+ distanceFilter: 25));
+ final StreamSubscription a = first.listen((_) {});
+ final Stream second = geo.getPositionStream(
+ locationSettings:
+ const LocationSettings(accuracy: LocationAccuracy.low));
+ expect(identical(first, second), isTrue);
+ final StreamSubscription b = second.listen((_) {});
+ await pumpEventQueue();
+ expect(fake.startCount, 1);
+ expect(fake.lastAccuracyIndex, LocationAccuracy.bestForNavigation.index);
+ expect(fake.lastDistanceFilter, 25);
+ expect(fake.lastAllowBackground, isTrue);
+
+ await a.cancel();
+ expect(fake.stopCount, 0, reason: 'the second listener remains');
+ await b.cancel();
+ expect(fake.stopCount, 1);
+
+ // Once it has ended, a new stream starts with its own settings.
+ final StreamSubscription c = geo
+ .getPositionStream(
+ locationSettings:
+ const LocationSettings(accuracy: LocationAccuracy.low))
+ .listen((_) {});
+ await pumpEventQueue();
+ expect(fake.startCount, 2);
+ expect(fake.lastAccuracyIndex, LocationAccuracy.low.index);
+ await c.cancel();
+ });
+
+ test(
+ 'a single fix during a distance-filtered stream does not wait for '
+ 'the stream', () async {
+ final StreamSubscription sub = geo
+ .getPositionStream(
+ locationSettings: const LocationSettings(distanceFilter: 100))
+ .listen((_) {});
+ await pumpEventQueue();
+ expect(fake.startCount, 1);
+
+ // The wearer stands still: the stream delivers nothing, the
+ // single-request manager does.
+ fake.singleFix = _fix()..[0] = 20.0;
+ final Position pos = await geo.getCurrentPosition(
+ locationSettings:
+ const LocationSettings(timeLimit: Duration(seconds: 2)));
+ expect(pos.latitude, 20.0);
+ expect(fake.requestLocationCount, 1);
+ expect(fake.startCount, 1, reason: 'the stream keeps its settings');
+ expect(fake.stopCount, 0, reason: 'the stream still runs');
+
+ await sub.cancel();
+ expect(fake.stopCount, 1);
+ });
});
- test('getPositionStream emits each new fix native signals', () async {
- final List seen = [];
- final StreamSubscription sub =
- geo.getPositionStream().listen(seen.add);
- await pumpEventQueue();
- expect(fake.isRegistered, isTrue);
+ group('accuracy', () {
+ test('getLocationAccuracy maps the authorization', () async {
+ fake.accuracy = 1;
+ expect(await geo.getLocationAccuracy(), LocationAccuracyStatus.precise);
+ fake.accuracy = 0;
+ expect(await geo.getLocationAccuracy(), LocationAccuracyStatus.reduced);
+ });
- final List moved = _fix()..[0] = 40.7128; // New York
- fake.deliver(moved);
- await pumpEventQueue();
+ test('requestTemporaryFullAccuracy waits for the answer', () async {
+ fake.accuracy = 0;
+ bool done = false;
+ final Future pending = geo
+ .requestTemporaryFullAccuracy(purposeKey: 'Route')
+ .then((LocationAccuracyStatus s) {
+ done = true;
+ return s;
+ });
+ await pumpEventQueue();
+ expect(fake.purposeKeys, ['Route']);
+ expect(done, isFalse, reason: 'the wearer has not answered yet');
- // The same fix again must not emit — a repeated signal is not movement.
- fake.deliver(moved);
- await pumpEventQueue();
+ fake.accuracy = 1;
+ fake.accuracyDone++;
+ fake.signal(GeolocatorWatchosSignal.accuracy);
+ expect(await pending, LocationAccuracyStatus.precise);
+ expect(fake.isRegistered, isFalse);
+ });
+
+ test(
+ 'requestTemporaryFullAccuracy throws without the Info.plist '
+ 'dictionary', () async {
+ fake.hasTemporaryUsageDictionary = false;
+ await expectLater(
+ geo.requestTemporaryFullAccuracy(purposeKey: 'Route'),
+ throwsA(isA()),
+ );
+ expect(fake.isRegistered, isFalse);
+ });
+
+ test('requestTemporaryFullAccuracy returns the accuracy on timeout',
+ () async {
+ GeolocatorWatchos.permissionTimeout = const Duration(milliseconds: 20);
+ fake.accuracy = 0;
+ expect(await geo.requestTemporaryFullAccuracy(purposeKey: 'Route'),
+ LocationAccuracyStatus.reduced);
+ GeolocatorWatchos.permissionTimeout = const Duration(minutes: 2);
+ });
+ });
+
+ group('service status', () {
+ test('starts with the current status, then each change', () async {
+ final List seen = [];
+ final StreamSubscription sub =
+ geo.getServiceStatusStream().listen(seen.add);
+ await pumpEventQueue();
+
+ // The trampoline runs asynchronously and re-reads the status, so each
+ // change is let through before the next.
+ fake.serviceEnabled = false;
+ fake.signal(GeolocatorWatchosSignal.authorization);
+ await pumpEventQueue();
+ // no change: not repeated
+ fake.signal(GeolocatorWatchosSignal.authorization);
+ await pumpEventQueue();
+ fake.serviceEnabled = true;
+ fake.signal(GeolocatorWatchosSignal.authorization);
+ await pumpEventQueue();
+
+ expect(seen, [
+ ServiceStatus.enabled,
+ ServiceStatus.disabled,
+ ServiceStatus.enabled,
+ ]);
+ await sub.cancel();
+ expect(fake.isRegistered, isFalse);
+ });
+
+ test('a fix does not re-read the services switch', () async {
+ final StreamSubscription sub =
+ geo.getServiceStatusStream().listen((_) {});
+ await pumpEventQueue();
+ final int checks = fake.serviceChecks;
+ fake.deliver(_fix());
+ await pumpEventQueue();
+ expect(fake.serviceChecks, checks);
+ await sub.cancel();
+ });
+
+ test('a second listener starts with the status now', () async {
+ final StreamSubscription first =
+ geo.getServiceStatusStream().listen((_) {});
+ fake.serviceEnabled = false;
+ expect(await geo.getServiceStatusStream().first, ServiceStatus.disabled);
+ await first.cancel();
+ });
+ });
+
+ group('settings', () {
+ test('plain settings leave the activity type and background off', () async {
+ final StreamSubscription sub = geo
+ .getPositionStream(
+ locationSettings:
+ const LocationSettings(accuracy: LocationAccuracy.medium))
+ .listen((_) {});
+ await pumpEventQueue();
+ expect(fake.lastAccuracyIndex, LocationAccuracy.medium.index);
+ expect(fake.lastActivityType, 0);
+ expect(fake.lastAllowBackground, isFalse);
+ await sub.cancel();
+ });
+
+ test('AppleSettings fitness and background reach CoreLocation', () async {
+ final StreamSubscription sub = geo
+ .getPositionStream(
+ locationSettings: AppleSettings(
+ accuracy: LocationAccuracy.bestForNavigation,
+ activityType: ActivityType.fitness))
+ .listen((_) {});
+ await pumpEventQueue();
+ expect(fake.lastActivityType, 3, reason: 'CLActivityTypeFitness');
+ expect(fake.lastAllowBackground, isTrue,
+ reason: "AppleSettings' default");
+ expect(fake.lastAccuracyIndex, LocationAccuracy.bestForNavigation.index);
+ await sub.cancel();
+ });
- expect(seen.map((Position p) => p.latitude), [40.7128]);
+ test('every geolocator activity type reaches its CLActivityType', () async {
+ // CLActivityType raw values; 6 is the maritime value native maps.
+ const Map expected = {
+ ActivityType.other: 1,
+ ActivityType.automotiveNavigation: 2,
+ ActivityType.fitness: 3,
+ ActivityType.otherNavigation: 4,
+ ActivityType.airborne: 5,
+ ActivityType.maritime: 6,
+ };
+ for (final ActivityType type in ActivityType.values) {
+ final StreamSubscription sub = geo
+ .getPositionStream(
+ locationSettings: AppleSettings(activityType: type))
+ .listen((_) {});
+ await pumpEventQueue();
+ expect(fake.lastActivityType, expected[type], reason: type.name);
+ await sub.cancel();
+ }
+ });
- await sub.cancel();
- expect(fake.isRegistered, isFalse);
- expect(fake.stopCount, greaterThanOrEqualTo(1));
+ test('an unknown activity index leaves the default', () async {
+ final StreamSubscription sub = geo
+ .getPositionStream(locationSettings: const _UnknownActivitySettings())
+ .listen((_) {});
+ await pumpEventQueue();
+ expect(fake.lastActivityType, 0);
+ await sub.cancel();
+ });
});
}
diff --git a/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.h b/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.h
index 9edd98c..1edb44e 100644
--- a/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.h
+++ b/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.h
@@ -4,10 +4,13 @@
//
// watchOS implementation of `geolocator`, over dart:ffi.
//
-// Backed by CoreLocation (CLLocationManager), which is available on watchOS.
-// Location updates arrive asynchronously on a delegate, which caches the
-// latest fix; the Dart side polls `read_position`. Permission requests and
-// authorization status map to geolocator's LocationPermission.
+// Backed by CoreLocation, which is available on watchOS. As in
+// geolocator_apple, two CLLocationManagers share one delegate: one runs the
+// position stream with its settings, the other answers single position
+// requests. The delegate caches the latest fix, counts the fixes it accepts,
+// and wakes Dart through the callback below; Dart reads the fix back with
+// `read_position`. Permission requests and authorization status map to
+// geolocator's LocationPermission.
#ifndef GEOLOCATOR_WATCHOS_FFI_H
#define GEOLOCATOR_WATCHOS_FFI_H
@@ -34,15 +37,27 @@ int geolocator_watchos_check_permission(void);
GEOLOCATOR_WATCHOS_EXPORT
void geolocator_watchos_request_permission(void);
-// Begins continuous updates. `accuracy` is a geolocator LocationAccuracy index
-// (0 lowest … 4 best, 5 bestForNavigation, 6 reduced); `distance_filter` is in
-// metres (<= 0 means none).
+// Begins the stream's continuous updates. `accuracy` is a geolocator
+// LocationAccuracy index (0 lowest … 4 best, 5 bestForNavigation, 6 reduced);
+// `distance_filter` is in metres (<= 0 means none). `activity_type` is a
+// CLActivityType raw value, or 6 for maritime; any other value, 0 included,
+// means CLActivityTypeOther. A nonzero `allow_background` turns on
+// allowsBackgroundLocationUpdates, but only when the Info.plist's
+// UIBackgroundModes has "location": CoreLocation asserts otherwise, and does
+// so even when WKBackgroundModes has it.
GEOLOCATOR_WATCHOS_EXPORT
-void geolocator_watchos_start_updates(int accuracy, double distance_filter);
+void geolocator_watchos_start_updates(int accuracy, double distance_filter,
+ int activity_type, int allow_background);
-// Requests a single location fix.
+// Starts the single-request manager at `accuracy` (as above), with no
+// distance filter. It skips cached fixes more than 5 seconds old and stops
+// itself after the first fix it accepts.
GEOLOCATOR_WATCHOS_EXPORT
-void geolocator_watchos_request_location(void);
+void geolocator_watchos_request_location(int accuracy);
+
+// Stops the single-request manager, for a request that gave up.
+GEOLOCATOR_WATCHOS_EXPORT
+void geolocator_watchos_stop_location_request(void);
// Fills out[0..9] with latitude, longitude, accuracy, altitude,
// altitudeAccuracy, heading, headingAccuracy, speed, speedAccuracy, and the
@@ -50,16 +65,44 @@ void geolocator_watchos_request_location(void);
GEOLOCATOR_WATCHOS_EXPORT
int geolocator_watchos_read_position(double* out);
-// Stops continuous updates.
+// How many fixes either manager has delivered: it goes up with each one, so
+// Dart can tell a new fix from the cached one, even at the same place.
+GEOLOCATOR_WATCHOS_EXPORT
+int64_t geolocator_watchos_position_count(void);
+
+// Stops the stream's continuous updates.
GEOLOCATOR_WATCHOS_EXPORT
void geolocator_watchos_stop_updates(void);
-/// Called from the CLLocationManager delegate when a new fix lands.
+// The accuracy the wearer allows, as a geolocator LocationAccuracyStatus
+// index: 0 reduced, 1 precise.
+GEOLOCATOR_WATCHOS_EXPORT
+int geolocator_watchos_accuracy_authorization(void);
+
+// Asks for full accuracy for this session, explained by `purpose_key`, a key
+// of the Info.plist's NSLocationTemporaryUsageDescriptionDictionary. Returns 0
+// without asking when the Info.plist has no such dictionary, 1 otherwise; the
+// answer is read with `accuracy_authorization` once `accuracy_requests_done`
+// has gone up.
+GEOLOCATOR_WATCHOS_EXPORT
+int geolocator_watchos_request_temporary_full_accuracy(const char* purpose_key);
+
+// How many temporary full-accuracy requests have finished.
+GEOLOCATOR_WATCHOS_EXPORT
+int64_t geolocator_watchos_accuracy_requests_done(void);
+
+// What a signal reports, so Dart re-reads only what changed.
+#define GEOLOCATOR_WATCHOS_SIGNAL_POSITION 0 // a fix, or a failed update
+#define GEOLOCATOR_WATCHOS_SIGNAL_AUTHORIZATION 1 // authorization or service
+#define GEOLOCATOR_WATCHOS_SIGNAL_ACCURACY 2 // an accuracy request ended
+
+/// Called from the CLLocationManager delegate when a new fix lands, the
+/// authorization or accuracy changes, or a temporary accuracy request ends.
///
-/// Carries no value: the Dart end is a `NativeCallable.listener` and runs
-/// asynchronously, so it re-reads `..._read_position` rather than trusting a
-/// pointer captured at signal time.
-typedef void (*geolocator_watchos_cb)(int64_t unused);
+/// Carries one of the kinds above and no value: the Dart end is a
+/// `NativeCallable.listener` and runs asynchronously, so it re-reads what it
+/// waits on rather than trusting a pointer captured at signal time.
+typedef void (*geolocator_watchos_cb)(int64_t kind);
/// Registers the function to wake Dart on a new fix, or NULL to stop.
GEOLOCATOR_WATCHOS_EXPORT
diff --git a/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.m b/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.m
index 9ff5207..35f3bf3 100644
--- a/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.m
+++ b/packages/geolocator_watchos/watchos/Classes/geolocator_watchos_ffi.m
@@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
-// CoreLocation-backed geolocation for watchOS. A shared CLLocationManager with
-// a delegate caches the most recent fix; the FFI `read_position` copies it out
-// for the Dart poller. Accuracy indices and the Position field order match
+// CoreLocation-backed geolocation for watchOS. As in geolocator_apple, the
+// position stream and single position requests each have a CLLocationManager,
+// so a single request neither waits on the stream's distance filter nor
+// changes its settings. One delegate serves both: it caches the most recent
+// fix, counts the fixes it accepts, and wakes Dart; the FFI `read_position`
+// copies the fix out. Accuracy indices and the Position field order match
// geolocator.
#import "geolocator_watchos_ffi.h"
@@ -19,9 +22,19 @@
int has;
} Fix;
+// A single request skips fixes older than this, as geolocator_apple does:
+// CoreLocation hands over its last fix first, however old.
+static const NSTimeInterval kMaxFixAgeSeconds = 5.0;
+
static os_unfair_lock _lock = OS_UNFAIR_LOCK_INIT;
static Fix _fix;
+static int64_t _fixCount = 0;
static geolocator_watchos_cb _callback = NULL;
+static int64_t _accuracyRequestsDone = 0;
+
+// Created once, on the main queue; see _createManagers.
+static CLLocationManager* _streamManager;
+static CLLocationManager* _singleManager;
void geolocator_watchos_set_callback(geolocator_watchos_cb callback) {
os_unfair_lock_lock(&_lock);
@@ -29,14 +42,14 @@ void geolocator_watchos_set_callback(geolocator_watchos_cb callback) {
os_unfair_lock_unlock(&_lock);
}
-// Wakes Dart. The callback pointer is read under the lock but invoked outside
-// it, because Dart may call straight back into this file.
-static void _signal(void) {
+// Wakes Dart with what changed. The callback pointer is read under the lock
+// but invoked outside it, because Dart may call straight back into this file.
+static void _signal(int64_t kind) {
os_unfair_lock_lock(&_lock);
geolocator_watchos_cb callback = _callback;
os_unfair_lock_unlock(&_lock);
if (callback != NULL) {
- callback(0);
+ callback(kind);
}
}
@@ -51,6 +64,13 @@ - (void)locationManager:(CLLocationManager*)manager
if (location == nil) {
return;
}
+ BOOL oneTime = manager == _singleManager;
+ if (oneTime && -[location.timestamp timeIntervalSinceNow] > kMaxFixAgeSeconds) {
+ return;
+ }
+ if (oneTime) {
+ [manager stopUpdatingLocation];
+ }
os_unfair_lock_lock(&_lock);
_fix.latitude = location.coordinate.latitude;
_fix.longitude = location.coordinate.longitude;
@@ -63,39 +83,61 @@ - (void)locationManager:(CLLocationManager*)manager
_fix.speedAccuracy = location.speedAccuracy;
_fix.timestampMillis = location.timestamp.timeIntervalSince1970 * 1000.0;
_fix.has = 1;
+ _fixCount++;
os_unfair_lock_unlock(&_lock);
- _signal();
+ _signal(GEOLOCATOR_WATCHOS_SIGNAL_POSITION);
}
- (void)locationManager:(CLLocationManager*)manager
didFailWithError:(NSError*)error {
// Leave the last cached fix in place; the caller's deadline decides.
// Still wake Dart: a waiter should re-check rather than sit until timeout.
- _signal();
+ _signal(GEOLOCATOR_WATCHOS_SIGNAL_POSITION);
}
// Authorization changes are the other thing Dart waits on — the answer to the
-// system permission prompt arrives here, not on any value it could poll.
+// system permission prompt arrives here, not on any value it could poll. A
+// change of the location services switch arrives here too. Both managers
+// report it, so the stream's alone wakes Dart.
- (void)locationManagerDidChangeAuthorization:(CLLocationManager*)manager {
- _signal();
+ if (manager != _singleManager) {
+ _signal(GEOLOCATOR_WATCHOS_SIGNAL_AUTHORIZATION);
+ }
}
@end
-static CLLocationManager* _manager(void) {
- static CLLocationManager* manager;
- static GeolocatorWatchosDelegate* delegate;
+static GeolocatorWatchosDelegate* _delegate;
+
+static void _createManagers(void) {
static dispatch_once_t once;
dispatch_once(&once, ^{
// CLLocationManager must be created on a thread with a run loop; the
// main queue is guaranteed to have one.
- dispatch_sync(dispatch_get_main_queue(), ^{
- delegate = [[GeolocatorWatchosDelegate alloc] init];
- manager = [[CLLocationManager alloc] init];
- manager.delegate = delegate;
- });
+ void (^create)(void) = ^{
+ _delegate = [[GeolocatorWatchosDelegate alloc] init];
+ _streamManager = [[CLLocationManager alloc] init];
+ _streamManager.delegate = _delegate;
+ _singleManager = [[CLLocationManager alloc] init];
+ _singleManager.delegate = _delegate;
+ };
+ if ([NSThread isMainThread]) {
+ create();
+ } else {
+ dispatch_sync(dispatch_get_main_queue(), create);
+ }
});
- return manager;
+}
+
+// The stream's manager, which also answers permission and accuracy queries.
+static CLLocationManager* _manager(void) {
+ _createManagers();
+ return _streamManager;
+}
+
+static CLLocationManager* _oneTimeManager(void) {
+ _createManagers();
+ return _singleManager;
}
static CLLocationAccuracy _accuracyFor(int index) {
@@ -133,20 +175,62 @@ void geolocator_watchos_request_permission(void) {
});
}
-void geolocator_watchos_start_updates(int accuracy, double distance_filter) {
+// Whether the Info.plist allows background location updates. CoreLocation
+// asserts when allowsBackgroundLocationUpdates turns on without "location" in
+// UIBackgroundModes, and on watchOS too it reads only that key: "location"
+// under WKBackgroundModes still trips the assertion (checked on the watchOS
+// 27.0 Simulator).
+static BOOL _hasBackgroundLocationMode(void) {
+ id modes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
+ return [modes isKindOfClass:[NSArray class]] && [modes containsObject:@"location"];
+}
+
+static CLActivityType _activityTypeFor(int raw) {
+ if (raw == 6) {
+ // CLActivityTypeMaritime is declared only from the watchOS 27 SDK on;
+ // Xcode 26 builds, and older watches, get what geolocator_apple
+ // falls back to.
+#if __WATCH_OS_VERSION_MAX_ALLOWED >= 270000
+ if (@available(watchOS 27.0, *)) {
+ return CLActivityTypeMaritime;
+ }
+#endif
+ return CLActivityTypeOtherNavigation;
+ }
+ return raw >= CLActivityTypeOther && raw <= CLActivityTypeAirborne
+ ? (CLActivityType)raw
+ : CLActivityTypeOther;
+}
+
+void geolocator_watchos_start_updates(int accuracy, double distance_filter,
+ int activity_type, int allow_background) {
CLLocationManager* m = _manager();
+ BOOL background = allow_background != 0 && _hasBackgroundLocationMode();
dispatch_async(dispatch_get_main_queue(), ^{
m.desiredAccuracy = _accuracyFor(accuracy);
m.distanceFilter =
distance_filter > 0 ? distance_filter : kCLDistanceFilterNone;
+ // Set every time, so a stream without AppleSettings does not keep the
+ // previous stream's type.
+ m.activityType = _activityTypeFor(activity_type);
+ m.allowsBackgroundLocationUpdates = background;
[m startUpdatingLocation];
});
}
-void geolocator_watchos_request_location(void) {
- CLLocationManager* m = _manager();
+void geolocator_watchos_request_location(int accuracy) {
+ CLLocationManager* m = _oneTimeManager();
+ dispatch_async(dispatch_get_main_queue(), ^{
+ m.desiredAccuracy = _accuracyFor(accuracy);
+ m.distanceFilter = kCLDistanceFilterNone;
+ [m startUpdatingLocation];
+ });
+}
+
+void geolocator_watchos_stop_location_request(void) {
+ CLLocationManager* m = _oneTimeManager();
dispatch_async(dispatch_get_main_queue(), ^{
- [m requestLocation];
+ [m stopUpdatingLocation];
});
}
@@ -169,9 +253,49 @@ int geolocator_watchos_read_position(double* out) {
return has;
}
+int64_t geolocator_watchos_position_count(void) {
+ os_unfair_lock_lock(&_lock);
+ int64_t count = _fixCount;
+ os_unfair_lock_unlock(&_lock);
+ return count;
+}
+
void geolocator_watchos_stop_updates(void) {
CLLocationManager* m = _manager();
dispatch_async(dispatch_get_main_queue(), ^{
[m stopUpdatingLocation];
});
}
+
+int geolocator_watchos_accuracy_authorization(void) {
+ return _manager().accuracyAuthorization == CLAccuracyAuthorizationFullAccuracy
+ ? 1
+ : 0;
+}
+
+int geolocator_watchos_request_temporary_full_accuracy(const char* purpose_key) {
+ if ([[NSBundle mainBundle] objectForInfoDictionaryKey:
+ @"NSLocationTemporaryUsageDescriptionDictionary"] == nil) {
+ return 0;
+ }
+ // Copied now: Dart releases the string as soon as this returns.
+ NSString* key = purpose_key == NULL ? @"" : [NSString stringWithUTF8String:purpose_key];
+ CLLocationManager* m = _manager();
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [m requestTemporaryFullAccuracyAuthorizationWithPurposeKey:key
+ completion:^(NSError* error) {
+ os_unfair_lock_lock(&_lock);
+ _accuracyRequestsDone++;
+ os_unfair_lock_unlock(&_lock);
+ _signal(GEOLOCATOR_WATCHOS_SIGNAL_ACCURACY);
+ }];
+ });
+ return 1;
+}
+
+int64_t geolocator_watchos_accuracy_requests_done(void) {
+ os_unfair_lock_lock(&_lock);
+ int64_t done = _accuracyRequestsDone;
+ os_unfair_lock_unlock(&_lock);
+ return done;
+}