| @@ -1779,6 +1779,20 @@ fn takePendingFix() ?PendingFix { |
| 1779 | 1779 | return call; |
| 1780 | 1780 | } |
| 1781 | 1781 | |
| 1782 | +/// Read the slot without clearing it, for an answer that is not yet the answer. |
| 1783 | +fn peekPendingFix() ?PendingFix { |
| 1784 | + state_mutex.lock(); |
| 1785 | + defer state_mutex.unlock(); |
| 1786 | + return pending_fix; |
| 1787 | +} |
| 1788 | + |
| 1789 | +fn pendingFixIs(ticket: ios_async.Ticket) bool { |
| 1790 | + state_mutex.lock(); |
| 1791 | + defer state_mutex.unlock(); |
| 1792 | + const call = pending_fix orelse return false; |
| 1793 | + return call.ticket.index == ticket.index and call.ticket.generation == ticket.generation; |
| 1794 | +} |
| 1795 | + |
| 1782 | 1796 | fn takePendingFixIf(ticket: ios_async.Ticket) ?PendingFix { |
| 1783 | 1797 | state_mutex.lock(); |
| 1784 | 1798 | defer state_mutex.unlock(); |
| @@ -1800,6 +1814,21 @@ fn schedulePositionTimeout(ticket: ios_async.Ticket, timeout_ms: u32) void { |
| 1800 | 1814 | ios_async.scheduleDeadline(ticket, timeout_ms, positionTimedOut); |
| 1801 | 1815 | } |
| 1802 | 1816 | |
| 1817 | +/// How long a one-shot told "no fix yet" waits before asking again. |
| 1818 | +const no_fix_retry_ms: u32 = 1_000; |
| 1819 | + |
| 1820 | +/// Ask again for the fix `kCLErrorLocationUnknown` said was not there yet, |
| 1821 | +/// provided the call that was told to wait is still the one waiting. One that |
| 1822 | +/// has since been answered, timed out or displaced stops the asking, so its |
| 1823 | +/// own deadline is what bounds the whole wait. |
| 1824 | +fn retryPendingFix(context: ?*anyopaque) callconv(.c) void { |
| 1825 | + if (!is_darwin) return; |
| 1826 | + if (!pendingFixIs(ios_async.ticketFromDeadline(context))) return; |
| 1827 | + const mgr = ensureManager() catch return; |
| 1828 | + const sel_request = selector("requestLocation") catch return; |
| 1829 | + objc.msgSend(mgr, sel_request); |
| 1830 | +} |
| 1831 | + |
| 1803 | 1832 | /// Record the running watch. Overwriting is the whole behaviour: a second |
| 1804 | 1833 | /// `watchPosition` is idempotent in Swift, and the selectors are the same ones. |
| 1805 | 1834 | fn publishWatch(sels: Sels) void { |
| @@ -1960,13 +1989,39 @@ fn didUpdateLocations(_: Id, _: Id, _: Id, locations: Id) callconv(.c) void { |
| 1960 | 1989 | |
| 1961 | 1990 | fn didFailWithError(_: Id, _: Id, _: Id, err_object: Id) callconv(.c) void { |
| 1962 | 1991 | if (!is_darwin) return; |
| 1992 | + handleLocationFailure( |
| 1993 | + readNSString(err_object, "domain") orelse "(none)", |
| 1994 | + readNSInteger(err_object, "code"), |
| 1995 | + readNSString(err_object, "localizedDescription") orelse "(none)", |
| 1996 | + ); |
| 1997 | +} |
| 1963 | 1998 | |
| 1964 | | - // Read once and reused below: the log line is the one channel a |
| 1965 | | - // `BridgeError` enum leaves for a rejected caller, and the event is the |
| 1966 | | - // only channel a watch has at all. |
| 1967 | | - const description = readNSString(err_object, "localizedDescription") orelse "(none)"; |
| 1968 | | - const domain = readNSString(err_object, "domain") orelse "(none)"; |
| 1969 | | - const native_code = readNSInteger(err_object, "code"); |
| 1999 | +/// `kCLErrorLocationUnknown`: Core Location has no fix *yet*. |
| 2000 | +/// |
| 2001 | +/// Apple documents it as the manager being unable to get a location right |
| 2002 | +/// away, not as a failure. `requestLocation` gives up on it, though, so a |
| 2003 | +/// one-shot told this asks again shortly rather than failing a caller whose |
| 2004 | +/// fix was seconds off: a phone with a cold GPS, or a loaded simulator, where |
| 2005 | +/// it broke the slice job's whole location chain (#260). The caller's own |
| 2006 | +/// timeout still bounds the wait. |
| 2007 | +fn isNoFixYet(domain: []const u8, native_code: c_long) bool { |
| 2008 | + return native_code == 0 and std.mem.eql(u8, domain, "kCLErrorDomain"); |
| 2009 | +} |
| 2010 | + |
| 2011 | +/// What a CoreLocation error does, apart from reading it off the `NSError`. |
| 2012 | +fn handleLocationFailure(domain: []const u8, native_code: c_long, description: []const u8) void { |
| 2013 | + // Not a failure, so neither a rejection nor `craftLocationError`: a watch |
| 2014 | + // keeps running through it, since `startUpdatingLocation` does keep trying, |
| 2015 | + // and an error event would tell the page otherwise. |
| 2016 | + if (isNoFixYet(domain, native_code)) { |
| 2017 | + if (peekPendingFix()) |call| { |
| 2018 | + std.log.info("location: no fix yet (kCLErrorLocationUnknown); asking again in {d} ms", .{no_fix_retry_ms}); |
| 2019 | + ios_async.scheduleDeadline(call.ticket, no_fix_retry_ms, retryPendingFix); |
| 2020 | + } else { |
| 2021 | + std.log.info("location: no fix yet (kCLErrorLocationUnknown); Core Location is still trying", .{}); |
| 2022 | + } |
| 2023 | + return; |
| 2024 | + } |
| 1970 | 2025 | |
| 1971 | 2026 | std.log.warn( |
| 1972 | 2027 | "location: CLLocationManager failed - domain={s} code={d} description={s}", |
| @@ -2584,6 +2639,29 @@ test "the error event carries Swift's one-key message object" { |
| 2584 | 2639 | try testing.expect(parsed.value.object.get("domain") == null); |
| 2585 | 2640 | } |
| 2586 | 2641 | |
| 2642 | +test "no fix yet keeps the one-shot waiting, and a real failure still settles it" { |
| 2643 | + // #260: `kCLErrorLocationUnknown` rejected a getCurrentPosition whose fix |
| 2644 | + // was seconds away, and on a loaded simulator that broke the slice job's |
| 2645 | + // whole location chain. Core Location's own word for it is "not yet". |
| 2646 | + try testing.expect(isNoFixYet("kCLErrorDomain", 0)); |
| 2647 | + try testing.expect(!isNoFixYet("kCLErrorDomain", 1)); |
| 2648 | + try testing.expect(!isNoFixYet("another-domain", 0)); |
| 2649 | + |
| 2650 | + _ = takeWatchAndPending(); |
| 2651 | + const ticket: ios_async.Ticket = .{ .index = 4, .generation = 7 }; |
| 2652 | + try testing.expect(publishPendingFix(ticket, empty_sels) == null); |
| 2653 | + |
| 2654 | + handleLocationFailure("kCLErrorDomain", 0, "The operation couldn't be completed."); |
| 2655 | + const waiting = peekPendingFix() orelse return error.NoFixYetSettledTheCall; |
| 2656 | + try testing.expectEqual(ticket.generation, waiting.ticket.generation); |
| 2657 | + try testing.expect(pendingFixIs(ticket)); |
| 2658 | + try testing.expect(!pendingFixIs(.{ .index = 4, .generation = 8 })); |
| 2659 | + |
| 2660 | + // Anything else is still an answer, and it takes the call. |
| 2661 | + handleLocationFailure("kCLErrorDomain", 2, "network"); |
| 2662 | + try testing.expect(takePendingFix() == null); |
| 2663 | +} |
| 2664 | + |
| 2587 | 2665 | test "only Core Location denial becomes a permission error" { |
| 2588 | 2666 | try testing.expectEqual( |
| 2589 | 2667 | bridge_error.BridgeError.PermissionDenied, |