Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.vaadin.flow.component.geolocation;

import java.io.Serializable;
import java.util.Objects;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
Expand All @@ -41,11 +42,15 @@
* <p>
* <b>Two usage modes:</b>
* <ul>
* <li>{@link #get(SerializableConsumer)} — one-shot position request. Use this
* when the application only needs to know the user's location at a single
* moment (e.g. on a button click). The callback receives a
* {@link GeolocationOutcome} — either a {@link GeolocationPosition} or a
* {@link GeolocationError}.</li>
* <li>{@link #get(SerializableConsumer, SerializableConsumer)} — one-shot
* position request. Use this when the application only needs to know the user's
* location at a single moment (e.g. on a button click). Takes a pair of
* callbacks — one for a successful {@link GeolocationPosition}, one for a
* {@link GeolocationError} — mirroring the W3C
* {@code getCurrentPosition(success, error)} pair and matching
* {@link GeolocationTracker#addPositionListener
* GeolocationTracker.addPositionListener}. An overload accepts a trailing
* {@link GeolocationOptions} for accuracy / timeout / cache-age tuning.</li>
* <li>{@link #track(Component)} — continuous tracking that keeps the server
* updated as the user moves. Returns a {@link GeolocationTracker} whose
* {@link GeolocationTracker#valueSignal() valueSignal()} is a reactive signal
Expand Down Expand Up @@ -75,13 +80,10 @@
* <pre>
* Button locate = new Button("Use my location");
* locate.addClickListener(
* e -&gt; UI.getCurrent().getGeolocation().get(outcome -&gt; {
* switch (outcome) {
* case GeolocationPosition pos -&gt; showNearest(
* pos.coords().latitude(), pos.coords().longitude());
* case GeolocationError err -&gt; showManualEntry();
* }
* }));
* e -&gt; UI.getCurrent().getGeolocation()
Comment thread
mcollovati marked this conversation as resolved.
Outdated
* .get(pos -&gt; showNearest(pos.coords().latitude(),
* pos.coords().longitude()),
* err -&gt; showManualEntry()));
* </pre>
*
* <p>
Expand Down Expand Up @@ -176,21 +178,29 @@ private static GeolocationClient resolveClient(UI ui) {
}

/**
* Requests the user's current position once. The callback receives a
* {@link GeolocationOutcome} — either a {@link GeolocationPosition} or a
* {@link GeolocationError}. Use {@code switch} pattern matching on the
* outcome; no dead "pending" arm is needed because one-shot requests never
* produce that value.
* Requests the user's current position once. On a successful reading
* {@code onSuccess} is invoked with the {@link GeolocationPosition}; if the
* browser reports an error instead {@code onError} is invoked with the
* {@link GeolocationError}. The pair mirrors the W3C
* {@code getCurrentPosition(success, error)} signature and matches
* {@link GeolocationTracker#addPositionListener
* GeolocationTracker.addPositionListener}, so callers can share the same
* handler shape between one-shot and watch APIs.
* <p>
* The call returns immediately. The browser may show a permission dialog on
* the first call; after the user responds, the callback is invoked on the
* UI thread.
* the first call; after the user responds, exactly one of the callbacks is
* invoked on the UI thread.
*
* @param callback
* invoked with the outcome once the browser reports it
* @param onSuccess
* invoked with the position on a successful reading; not
* {@code null}
* @param onError
* invoked with the error if the browser reports one; not
* {@code null}
*/
public void get(SerializableConsumer<GeolocationOutcome> callback) {
get(null, callback);
public void get(SerializableConsumer<GeolocationPosition> onSuccess,
SerializableConsumer<GeolocationError> onError) {
get(onSuccess, onError, null);
}

/**
Expand All @@ -199,25 +209,35 @@ public void get(SerializableConsumer<GeolocationOutcome> callback) {
* See {@link GeolocationOptions} for the available settings.
* <p>
* The call returns immediately. The browser may show a permission dialog on
* the first call; after the user responds, the callback is invoked on the
* UI thread.
* the first call; after the user responds, exactly one of the callbacks is
* invoked on the UI thread.
*
* @param onSuccess
* invoked with the position on a successful reading; not
* {@code null}
* @param onError
* invoked with the error if the browser reports one; not
* {@code null}
* @param options
* accuracy / timeout / cache-age tuning, or {@code null} to use
* the browser defaults
* @param callback
* invoked with the outcome once the browser reports it
*/
public void get(@Nullable GeolocationOptions options,
SerializableConsumer<GeolocationOutcome> callback) {
public void get(SerializableConsumer<GeolocationPosition> onSuccess,
SerializableConsumer<GeolocationError> onError,
@Nullable GeolocationOptions options) {
Comment thread
mcollovati marked this conversation as resolved.
Objects.requireNonNull(onSuccess, "onSuccess callback cannot be null");
Objects.requireNonNull(onError, "onError callback cannot be null");
client.get(options).whenComplete((outcome, error) -> {
if (error != null) {
LOGGER.debug("Geolocation get() failed", error);
callback.accept(new GeolocationError(
onError.accept(new GeolocationError(
GeolocationErrorCode.UNKNOWN.code(),
"Client-side geolocation bridge failure"));
} else {
callback.accept(outcome);
return;
}
switch (outcome) {
case GeolocationPosition position -> onSuccess.accept(position);
case GeolocationError outcomeError -> onError.accept(outcomeError);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,20 @@
* {@link GeolocationPosition}.
* <p>
* This is one of the three possible values of a
* {@link GeolocationTracker#valueSignal()} signal, and one of the two values a
* {@link Geolocation#get} callback can receive. Typical application code
* switches on {@link #errorCode()} to react to the specific reason:
* {@link GeolocationTracker#valueSignal()} signal, and the value passed to the
* error callback of {@link Geolocation#get Geolocation.get}. Typical
* application code switches on {@link #errorCode()} to react to the specific
* reason:
*
* <pre>
* ui.getGeolocation().get(result -&gt; {
* if (result instanceof GeolocationError err) {
* switch (err.errorCode()) {
* case PERMISSION_DENIED -&gt;
* showExplanation("Location is blocked for this site.");
* case POSITION_UNAVAILABLE -&gt;
* showRetry("Could not determine your location.");
* case TIMEOUT -&gt; showRetry("Location request took too long.");
* case UNKNOWN -&gt; showGenericError(err.message());
* }
* ui.getGeolocation().get(pos -&gt; showNearest(pos), err -&gt; {
* switch (err.errorCode()) {
* case PERMISSION_DENIED -&gt;
* showExplanation("Location is blocked for this site.");
* case POSITION_UNAVAILABLE -&gt;
* showRetry("Could not determine your location.");
* case TIMEOUT -&gt; showRetry("Location request took too long.");
* case UNKNOWN -&gt; showGenericError(err.message());
Comment thread
mcollovati marked this conversation as resolved.
Outdated
* }
* });
* </pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,14 @@
/**
* The actual answer to a geolocation request — either a successful reading or
* an error. Narrower than {@link GeolocationResult}: the "waiting for first
* reading" {@link GeolocationPending} state is excluded because one-shot
* {@link Geolocation#get} never produces it.
* reading" {@link GeolocationPending} state is excluded because a one-shot
* request never produces it.
* <p>
* Returned to the callback of {@link Geolocation#get}. Use this instead of
* {@link GeolocationResult} when you only need to handle the Position / Error
* branches and want the {@code switch} to stay exhaustive without a dead
* Pending arm.
*
* <pre>
* ui.getGeolocation().get(outcome -&gt; {
* switch (outcome) {
* case GeolocationPosition pos -&gt; showNearest(pos);
* case GeolocationError err -&gt; showManualEntry();
* }
* });
* </pre>
* Used as the result type of the internal {@link GeolocationClient#get} future,
* where the sum-type encoding keeps Pending out of the contract. Application
* code rarely references this type directly: {@link Geolocation#get
* Geolocation.get} delivers the position or the error through separate
* callbacks.
*/
public sealed interface GeolocationOutcome extends GeolocationResult
permits GeolocationPosition, GeolocationError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
/**
* The initial state of a newly started tracking session, held by
* {@link GeolocationTracker#valueSignal()} until the browser reports its first
* position or error. One-shot {@link Geolocation#get} callbacks never receive
* this value.
* position or error. One-shot {@link Geolocation#get} requests never produce
* this value — they deliver a position or an error through separate callbacks.
*/
public record GeolocationPending() implements GeolocationResult {
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
* moment in time they were taken.
* <p>
* This is one of the three possible values of a
* {@link GeolocationTracker#valueSignal()} signal, and one of the two values a
* {@link Geolocation#get} callback can receive.
* {@link GeolocationTracker#valueSignal()} signal, and the value passed to the
* success callback of {@link Geolocation#get Geolocation.get}.
*
* @param coords
* the latitude/longitude and related fields; see
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
* <li>{@link GeolocationPosition} — a successful reading.</li>
* <li>{@link GeolocationError} — the browser reported an error.</li>
* </ul>
* For the one-shot {@link Geolocation#get} callback use the narrower
* {@link GeolocationOutcome}, which excludes {@link GeolocationPending}
* (one-shot requests never produce that value).
* One-shot {@link Geolocation#get} requests never produce
* {@link GeolocationPending}; they deliver the position and the error through
* separate callbacks instead.
* <p>
* The sealed hierarchy is designed for exhaustive pattern matching. A
* {@code switch} covering the three permitted variants is guaranteed complete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
Expand Down Expand Up @@ -71,7 +70,8 @@ void lookupFactory_resolvedAtConstruction_clientReceivesGetCalls() {
.thenReturn(unused -> fake);

UI freshUi = new MockUI();
freshUi.getGeolocation().get(outcome -> {
freshUi.getGeolocation().get(pos -> {
}, err -> {
});

assertEquals(1, fake.getCalls.size(),
Expand All @@ -83,7 +83,8 @@ void setClient_routesGetThroughInstalledClient() {
FakeClient fake = new FakeClient();
ui.getGeolocation().setClient(fake);

ui.getGeolocation().get(outcome -> {
ui.getGeolocation().get(pos -> {
}, err -> {
});

assertEquals(1, fake.getCalls.size(),
Expand Down Expand Up @@ -131,21 +132,21 @@ void track_handleIsNullAfterStop() {
}

@Test
void get_callbackReceivesUnknownErrorWhenClientFutureFailsExceptionally() {
void get_onErrorReceivesUnknownErrorWhenClientFutureFailsExceptionally() {
FakeClient fake = new FakeClient();
fake.nextGetResult = CompletableFuture
.failedFuture(new RuntimeException(
"Client-side geolocation.get failed: boom"));
ui.getGeolocation().setClient(fake);

AtomicReference<@Nullable GeolocationOutcome> received = new AtomicReference<>();
ui.getGeolocation().get(received::set);
AtomicReference<@Nullable GeolocationPosition> position = new AtomicReference<>();
AtomicReference<@Nullable GeolocationError> received = new AtomicReference<>();
Comment thread
mcollovati marked this conversation as resolved.
Outdated
ui.getGeolocation().get(position::set, received::set);

GeolocationOutcome outcome = received.get();
assertNotNull(outcome,
"callback must fire even when the JS bridge fails");
GeolocationError err = assertInstanceOf(GeolocationError.class, outcome,
"infra failure should surface as a GeolocationError");
GeolocationError err = received.get();
assertNotNull(err, "onError must fire even when the JS bridge fails");
assertNull(position.get(),
"onSuccess must stay silent when the bridge fails");
assertEquals(GeolocationErrorCode.UNKNOWN, err.errorCode(),
"error code should be UNKNOWN for client-bridge failures");
assertFalse(err.message().contains("boom"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ void get_executesPromiseJs() {
TestComponent component = new TestComponent();
ui.add(component);

ui.getGeolocation().get(result -> {
ui.getGeolocation().get(pos -> {
}, err -> {
});

List<PendingJavaScriptInvocation> invocations = ui
Expand All @@ -166,43 +167,47 @@ void get_executesPromiseJs() {
}

@Test
void get_callbackReceivesPosition() {
void get_onSuccessReceivesPositionAndOnErrorIsSilent() {
TestComponent component = new TestComponent();
ui.add(component);

List<GeolocationOutcome> received = new ArrayList<>();
ui.getGeolocation().get(received::add);
List<GeolocationPosition> positions = new ArrayList<>();
List<GeolocationError> errors = new ArrayList<>();
ui.getGeolocation().get(positions::add, errors::add);

resolvePromise(ui,
resultJson(position(60.1699, 24.9384, 10.0), null, "GRANTED"));

assertEquals(1, received.size());
assertInstanceOf(GeolocationPosition.class, received.get(0));
assertEquals(60.1699,
((GeolocationPosition) received.get(0)).coords().latitude());
assertEquals(1, positions.size());
assertEquals(60.1699, positions.get(0).coords().latitude());
assertTrue(errors.isEmpty(),
"onError must not fire for a successful reading");
}

@Test
void get_callbackReceivesError() {
void get_onErrorReceivesErrorAndOnSuccessIsSilent() {
TestComponent component = new TestComponent();
ui.add(component);

List<GeolocationOutcome> received = new ArrayList<>();
ui.getGeolocation().get(received::add);
List<GeolocationPosition> positions = new ArrayList<>();
List<GeolocationError> errors = new ArrayList<>();
ui.getGeolocation().get(positions::add, errors::add);

resolvePromise(ui, resultJson(null, error(1, "denied"), "DENIED"));

assertEquals(1, received.size());
assertInstanceOf(GeolocationError.class, received.get(0));
assertEquals(1, ((GeolocationError) received.get(0)).code());
assertEquals(1, errors.size());
assertEquals(1, errors.get(0).code());
assertTrue(positions.isEmpty(),
"onSuccess must not fire when the browser reports an error");
}

@Test
void get_updatesAvailabilityFromResponse() {
TestComponent component = new TestComponent();
ui.add(component);

ui.getGeolocation().get(result -> {
ui.getGeolocation().get(pos -> {
}, err -> {
});
resolvePromise(ui,
resultJson(position(60.0, 25.0, 10.0), null, "GRANTED"));
Expand Down
Loading
Loading