1/*
2 * Copyright (C) 2019 Igalia S.L.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "GeoclueGeolocationProvider.h"
28
29#include <WebCore/GeolocationPosition.h>
30#include <gio/gio.h>
31#include <glib/gi18n-lib.h>
32#include <wtf/glib/GUniquePtr.h>
33
34#if USE(GLIB_EVENT_LOOP)
35#include <wtf/glib/RunLoopSourcePriority.h>
36#endif
37
38namespace WebKit {
39
40GeoclueGeolocationProvider::GeoclueGeolocationProvider()
41 : m_destroyManagerLaterTimer(RunLoop::current(), this, &GeoclueGeolocationProvider::destroyManager)
42{
43#if USE(GLIB_EVENT_LOOP)
44 m_destroyManagerLaterTimer.setPriority(RunLoopSourcePriority::ReleaseUnusedResourcesTimer);
45#endif
46}
47
48GeoclueGeolocationProvider::~GeoclueGeolocationProvider()
49{
50 stop();
51}
52
53void GeoclueGeolocationProvider::start(UpdateNotifyFunction&& updateNotifyFunction)
54{
55 m_destroyManagerLaterTimer.stop();
56 m_updateNotifyFunction = WTFMove(updateNotifyFunction);
57 m_isRunning = true;
58
59 if (!m_manager) {
60 g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr,
61 "org.freedesktop.GeoClue2", "/org/freedesktop/GeoClue2/Manager", "org.freedesktop.GeoClue2.Manager", nullptr,
62 [](GObject*, GAsyncResult* result, gpointer userData) {
63 auto& provider = *static_cast<GeoclueGeolocationProvider*>(userData);
64 GUniqueOutPtr<GError> error;
65 GRefPtr<GDBusProxy> proxy = adoptGRef(g_dbus_proxy_new_for_bus_finish(result, &error.outPtr()));
66 if (error) {
67 provider.didFail(_("Failed to connect to geolocation service"));
68 return;
69 }
70 provider.setupManager(WTFMove(proxy));
71 }, this);
72 return;
73 }
74
75 startClient();
76}
77
78void GeoclueGeolocationProvider::stop()
79{
80 if (!m_isRunning)
81 return;
82
83 m_isRunning = false;
84 m_updateNotifyFunction = nullptr;
85 g_cancellable_cancel(m_cancellable.get());
86 stopClient();
87 destroyManagerLater();
88}
89
90void GeoclueGeolocationProvider::setEnableHighAccuracy(bool enabled)
91{
92 if (m_isHighAccuracyEnabled == enabled)
93 return;
94
95 requestAccuracyLevel();
96}
97
98void GeoclueGeolocationProvider::destroyManagerLater()
99{
100 if (!m_manager)
101 return;
102
103 if (m_destroyManagerLaterTimer.isActive())
104 return;
105
106 m_destroyManagerLaterTimer.startOneShot(60_s);
107}
108
109void GeoclueGeolocationProvider::destroyManager()
110{
111 ASSERT(!m_isRunning);
112 m_client = nullptr;
113 m_manager = nullptr;
114}
115
116void GeoclueGeolocationProvider::setupManager(GRefPtr<GDBusProxy>&& proxy)
117{
118 m_manager = WTFMove(proxy);
119 if (!m_isRunning) {
120 destroyManagerLater();
121 return;
122 }
123
124 g_dbus_proxy_call(m_manager.get(), "CreateClient", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
125 [](GObject* manager, GAsyncResult* result, gpointer userData) {
126 auto& provider = *static_cast<GeoclueGeolocationProvider*>(userData);
127 GUniqueOutPtr<GError> error;
128 GRefPtr<GVariant> returnValue = adoptGRef(g_dbus_proxy_call_finish(G_DBUS_PROXY(manager), result, &error.outPtr()));
129 if (error) {
130 provider.didFail(_("Failed to connect to geolocation service"));
131 return;
132 }
133 const char* clientPath;
134 g_variant_get(returnValue.get(), "(&o)", &clientPath);
135 provider.createClient(clientPath);
136 }, this);
137}
138
139void GeoclueGeolocationProvider::createClient(const char* clientPath)
140{
141 if (!m_isRunning) {
142 destroyManagerLater();
143 return;
144 }
145
146 g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr,
147 "org.freedesktop.GeoClue2", clientPath, "org.freedesktop.GeoClue2.Client", nullptr,
148 [](GObject*, GAsyncResult* result, gpointer userData) {
149 auto& provider = *static_cast<GeoclueGeolocationProvider*>(userData);
150 GUniqueOutPtr<GError> error;
151 GRefPtr<GDBusProxy> proxy = adoptGRef(g_dbus_proxy_new_for_bus_finish(result, &error.outPtr()));
152 if (error) {
153 provider.didFail(_("Failed to connect to geolocation service"));
154 return;
155 }
156 provider.setupClient(WTFMove(proxy));
157 }, this);
158}
159
160void GeoclueGeolocationProvider::setupClient(GRefPtr<GDBusProxy>&& proxy)
161{
162 m_client = WTFMove(proxy);
163 if (!m_isRunning) {
164 destroyManagerLater();
165 return;
166 }
167
168 // Geoclue2 requires the client to provide a desktop ID for security
169 // reasons, which should identify the application requesting the location.
170 // We use the application ID configured for the default GApplication, and
171 // also fallback to our old behavior of using g_get_prgname().
172 const char* applicationID = nullptr;
173 if (auto* defaultApplication = g_application_get_default())
174 applicationID = g_application_get_application_id(defaultApplication);
175 if (!applicationID)
176 applicationID = g_get_prgname();
177 g_dbus_proxy_call(m_client.get(), "org.freedesktop.DBus.Properties.Set",
178 g_variant_new("(ssv)", "org.freedesktop.GeoClue2.Client", "DesktopId", g_variant_new_string(applicationID)),
179 G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr, nullptr);
180
181 requestAccuracyLevel();
182
183 startClient();
184}
185
186void GeoclueGeolocationProvider::startClient()
187{
188 if (!m_client)
189 return;
190
191 g_signal_connect(m_client.get(), "g-signal", G_CALLBACK(clientLocationUpdatedCallback), this);
192
193 m_cancellable = adoptGRef(g_cancellable_new());
194 g_dbus_proxy_call(m_client.get(), "Start", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, m_cancellable.get(),
195 [](GObject* client, GAsyncResult* result, gpointer userData) {
196 auto& provider = *static_cast<GeoclueGeolocationProvider*>(userData);
197 GUniqueOutPtr<GError> error;
198 GRefPtr<GVariant> returnValue = adoptGRef(g_dbus_proxy_call_finish(G_DBUS_PROXY(client), result, &error.outPtr()));
199 if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
200 return;
201
202 if (error) {
203 provider.didFail(_("Failed to determine position from geolocation service"));
204 return;
205 }
206 }, this);
207}
208
209void GeoclueGeolocationProvider::stopClient()
210{
211 if (!m_client)
212 return;
213
214 g_signal_handlers_disconnect_matched(m_client.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
215 m_cancellable = nullptr;
216 g_dbus_proxy_call(m_client.get(), "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr, nullptr);
217}
218
219void GeoclueGeolocationProvider::requestAccuracyLevel()
220{
221 if (!m_client)
222 return;
223
224 // GeoclueAccuracyLevelCity = 4, GeoclueAccuracyLevelExact = 8.
225 unsigned accuracy = m_isHighAccuracyEnabled ? 8 : 4;
226 g_dbus_proxy_call(m_client.get(), "org.freedesktop.DBus.Properties.Set",
227 g_variant_new("(ssv)", "org.freedesktop.GeoClue2.Client", "RequestedAccuracyLevel", g_variant_new_uint32(accuracy)),
228 G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr, nullptr);
229}
230
231void GeoclueGeolocationProvider::clientLocationUpdatedCallback(GDBusProxy* client, gchar*, gchar* signal, GVariant* parameters, gpointer userData)
232{
233 if (g_strcmp0(signal, "LocationUpdated"))
234 return;
235
236 const char* locationPath;
237 g_variant_get(parameters, "(o&o)", nullptr, &locationPath);
238 auto& provider = *static_cast<GeoclueGeolocationProvider*>(userData);
239 provider.createLocation(locationPath);
240}
241
242void GeoclueGeolocationProvider::createLocation(const char* locationPath)
243{
244 g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr,
245 "org.freedesktop.GeoClue2", locationPath, "org.freedesktop.GeoClue2.Location", m_cancellable.get(),
246 [](GObject*, GAsyncResult* result, gpointer userData) {
247 auto& provider = *static_cast<GeoclueGeolocationProvider*>(userData);
248 GUniqueOutPtr<GError> error;
249 GRefPtr<GDBusProxy> proxy = adoptGRef(g_dbus_proxy_new_for_bus_finish(result, &error.outPtr()));
250 if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
251 return;
252
253 if (error) {
254 provider.didFail(_("Failed to determine position from geolocation service"));
255 return;
256 }
257 provider.locationUpdated(WTFMove(proxy));
258 }, this);
259}
260
261void GeoclueGeolocationProvider::locationUpdated(GRefPtr<GDBusProxy>&& proxy)
262{
263 WebCore::GeolocationPosition position;
264 GRefPtr<GVariant> property = adoptGRef(g_dbus_proxy_get_cached_property(proxy.get(), "Latitude"));
265 position.latitude = g_variant_get_double(property.get());
266 property = adoptGRef(g_dbus_proxy_get_cached_property(proxy.get(), "Longitude"));
267 position.longitude = g_variant_get_double(property.get());
268 property = adoptGRef(g_dbus_proxy_get_cached_property(proxy.get(), "Accuracy"));
269 position.accuracy = g_variant_get_double(property.get());
270 property = adoptGRef(g_dbus_proxy_get_cached_property(proxy.get(), "Altitude"));
271 position.altitude = g_variant_get_double(property.get());
272 property = adoptGRef(g_dbus_proxy_get_cached_property(proxy.get(), "Speed"));
273 position.speed = g_variant_get_double(property.get());
274 property = adoptGRef(g_dbus_proxy_get_cached_property(proxy.get(), "Heading"));
275 position.heading = g_variant_get_double(property.get());
276 property = adoptGRef(g_dbus_proxy_get_cached_property(proxy.get(), "Timestamp"));
277 guint64 timestamp;
278 g_variant_get(property.get(), "(tt)", &timestamp, nullptr);
279 position.timestamp = static_cast<double>(timestamp);
280 m_updateNotifyFunction(WTFMove(position), WTF::nullopt);
281}
282
283void GeoclueGeolocationProvider::didFail(CString errorMessage)
284{
285 m_updateNotifyFunction({ }, errorMessage);
286}
287
288} // namespace WebKit
289