Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qgeopositioninfosource_geoclue2.cpp
Go to the documentation of this file.
1// Copyright (C) 2018 Denis Shienkov <denis.shienkov@gmail.com>
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtCore/QLoggingCategory>
7#include <QtCore/QSaveFile>
8#include <QtCore/QTimer>
9#include <QtDBus/QDBusPendingCallWatcher>
10
11// Auto-generated D-Bus files.
12#include <client_interface.h>
13#include "moc_client_interface.cpp" // includemocs
14#include <location_interface.h>
15#include "moc_location_interface.cpp" // includemocs
16#include "moc_manager_interface.cpp" // includemocs
17
18Q_DECLARE_LOGGING_CATEGORY(lcPositioningGeoclue2)
19
20QT_BEGIN_NAMESPACE
21
22using namespace QtPositioningPrivate;
23
24namespace {
25
26// NOTE: Copied from the /usr/include/libgeoclue-2.0/gclue-client.h
27enum GClueAccuracyLevel {
28 GCLUE_ACCURACY_LEVEL_NONE = 0,
29 GCLUE_ACCURACY_LEVEL_COUNTRY = 1,
30 GCLUE_ACCURACY_LEVEL_CITY = 4,
31 GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD = 5,
32 GCLUE_ACCURACY_LEVEL_STREET = 6,
33 GCLUE_ACCURACY_LEVEL_EXACT = 8
34};
35
36const char GEOCLUE2_SERVICE_NAME[] = "org.freedesktop.GeoClue2";
37const int MINIMUM_UPDATE_INTERVAL = 1000;
38const int UPDATE_TIMEOUT_COLD_START = 120000;
39static const auto desktopIdParameter = "desktopId";
40
41static QString lastPositionFilePath()
42{
43 return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
44 + QStringLiteral("/qtposition-geoclue2");
45}
46
47} // namespace
48
49QGeoPositionInfoSourceGeoclue2::QGeoPositionInfoSourceGeoclue2(const QVariantMap &parameters,
50 QObject *parent)
51 : QGeoPositionInfoSource(parent)
52 , m_requestTimer(new QTimer(this))
53 , m_manager(QLatin1String(GEOCLUE2_SERVICE_NAME),
54 QStringLiteral("/org/freedesktop/GeoClue2/Manager"),
55 QDBusConnection::systemBus(),
56 this)
57{
58 parseParameters(parameters);
59
60 qDBusRegisterMetaType<Timestamp>();
61
62 restoreLastPosition();
63
64 m_requestTimer->setSingleShot(true);
65 connect(m_requestTimer, &QTimer::timeout,
66 this, &QGeoPositionInfoSourceGeoclue2::requestUpdateTimeout);
67}
68
73
75{
76 QGeoPositionInfoSource::setUpdateInterval(msec);
77 configureClient();
78}
79
80QGeoPositionInfo QGeoPositionInfoSourceGeoclue2::lastKnownPosition(bool fromSatellitePositioningMethodsOnly) const
81{
82 if (fromSatellitePositioningMethodsOnly && !m_lastPositionFromSatellite)
83 return QGeoPositionInfo();
84 return m_lastPosition;
85}
86
88{
89 bool ok;
90 const auto accuracy = m_manager.property("AvailableAccuracyLevel").toUInt(&ok);
91 if (!ok) {
92 const_cast<QGeoPositionInfoSourceGeoclue2 *>(this)->setError(AccessError);
93 return NoPositioningMethods;
94 }
95
96 switch (accuracy) {
97 case GCLUE_ACCURACY_LEVEL_COUNTRY:
98 case GCLUE_ACCURACY_LEVEL_CITY:
99 case GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD:
100 case GCLUE_ACCURACY_LEVEL_STREET:
101 return NonSatellitePositioningMethods;
102 case GCLUE_ACCURACY_LEVEL_EXACT:
103 return AllPositioningMethods;
104 case GCLUE_ACCURACY_LEVEL_NONE:
105 default:
106 return NoPositioningMethods;
107 }
108}
109
111{
112 QGeoPositionInfoSource::setPreferredPositioningMethods(methods);
113 configureClient();
114}
115
117{
118 return MINIMUM_UPDATE_INTERVAL;
119}
120
122{
123 return m_error;
124}
125
127{
128 if (m_running) {
129 qCWarning(lcPositioningGeoclue2) << "Already running";
130 return;
131 }
132
133 qCDebug(lcPositioningGeoclue2) << "Starting updates";
134
135 m_error = QGeoPositionInfoSource::NoError;
136
137 m_running = true;
138
139 startClient();
140
141 if (m_lastPosition.isValid()) {
142 QMetaObject::invokeMethod(this, "positionUpdated", Qt::QueuedConnection,
143 Q_ARG(QGeoPositionInfo, m_lastPosition));
144 }
145}
146
148{
149 if (!m_running) {
150 qCWarning(lcPositioningGeoclue2) << "Already stopped";
151 return;
152 }
153
154 qCDebug(lcPositioningGeoclue2) << "Stopping updates";
155 m_running = false;
156
157 stopClient();
158}
159
161{
162 if (m_requestTimer->isActive()) {
163 qCDebug(lcPositioningGeoclue2) << "Request timer was active, ignoring startUpdates";
164 return;
165 }
166
167 m_error = QGeoPositionInfoSource::NoError;
168
169 if (timeout < minimumUpdateInterval() && timeout != 0) {
170 setError(QGeoPositionInfoSource::UpdateTimeoutError);
171 return;
172 }
173
174 m_requestTimer->start(timeout ? timeout : UPDATE_TIMEOUT_COLD_START);
175 startClient();
176}
177
178void QGeoPositionInfoSourceGeoclue2::setError(QGeoPositionInfoSource::Error error)
179{
180 m_error = error;
181 if (m_error != QGeoPositionInfoSource::NoError)
182 emit QGeoPositionInfoSource::errorOccurred(m_error);
183}
184
185void QGeoPositionInfoSourceGeoclue2::restoreLastPosition()
186{
187#if !defined(QT_NO_DATASTREAM)
188 const auto filePath = lastPositionFilePath();
189 QFile file(filePath);
190 if (file.open(QIODevice::ReadOnly)) {
191 QDataStream out(&file);
192 out >> m_lastPosition;
193 }
194#endif
195}
196
197void QGeoPositionInfoSourceGeoclue2::saveLastPosition()
198{
199#if !defined(QT_NO_DATASTREAM) && QT_CONFIG(temporaryfile)
200 if (!m_lastPosition.isValid())
201 return;
202
203 const auto filePath = lastPositionFilePath();
204 QSaveFile file(filePath);
205 if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
206 QDataStream out(&file);
207 // Only save position and timestamp.
208 out << QGeoPositionInfo(m_lastPosition.coordinate(), m_lastPosition.timestamp());
209 file.commit();
210 }
211#endif
212}
213
214void QGeoPositionInfoSourceGeoclue2::createClient()
215{
216 const QDBusPendingReply<QDBusObjectPath> reply = m_manager.GetClient();
217 const auto watcher = new QDBusPendingCallWatcher(reply, this);
218 connect(watcher, &QDBusPendingCallWatcher::finished, this,
219 [this](QDBusPendingCallWatcher *watcher) {
220 watcher->deleteLater();
221 const QDBusPendingReply<QDBusObjectPath> reply = *watcher;
222 if (reply.isError()) {
223 const auto error = reply.error();
224 qCWarning(lcPositioningGeoclue2) << "Unable to obtain the client:"
225 << error.name() << error.message();
226 setError(AccessError);
227 } else {
228 const QString clientPath = reply.value().path();
229 qCDebug(lcPositioningGeoclue2) << "Client path is:"
230 << clientPath;
231 delete m_client;
232 m_client = new OrgFreedesktopGeoClue2ClientInterface(
233 QLatin1String(GEOCLUE2_SERVICE_NAME),
234 clientPath,
235 QDBusConnection::systemBus(),
236 this);
237 if (!m_client->isValid()) {
238 const auto error = m_client->lastError();
239 qCCritical(lcPositioningGeoclue2) << "Unable to create the client object:"
240 << error.name() << error.message();
241 delete m_client;
242 setError(AccessError);
243 } else {
244 connect(m_client.data(), &OrgFreedesktopGeoClue2ClientInterface::LocationUpdated,
245 this, &QGeoPositionInfoSourceGeoclue2::handleNewLocation);
246
247 if (configureClient())
248 startClient();
249 }
250 }
251 });
252}
253
254void QGeoPositionInfoSourceGeoclue2::startClient()
255{
256 // only start the client if someone asked for it already
257 if (!m_running && !m_requestTimer->isActive())
258 return;
259
260 if (!m_client) {
261 createClient();
262 return;
263 }
264
265 const QDBusPendingReply<> reply = m_client->Start();
266 const auto watcher = new QDBusPendingCallWatcher(reply, this);
267 connect(watcher, &QDBusPendingCallWatcher::finished, this,
268 [this](QDBusPendingCallWatcher *watcher) {
269 watcher->deleteLater();
270 const QDBusPendingReply<> reply = *watcher;
271 if (reply.isError()) {
272 const auto error = reply.error();
273 qCCritical(lcPositioningGeoclue2) << "Unable to start the client:"
274 << error.name() << error.message();
275 delete m_client;
276 // This can potentially lead to calling ~QGeoPositionInfoSourceGeoclue2(),
277 // so do all the cleanup before.
278 setError(AccessError);
279 } else {
280 qCDebug(lcPositioningGeoclue2) << "Client successfully started";
281
282 const QDBusObjectPath location = m_client->location();
283 const QString path = location.path();
284 if (path.isEmpty() || path == QLatin1String("/"))
285 return;
286
287 handleNewLocation({}, location);
288 }
289 });
290}
291
292void QGeoPositionInfoSourceGeoclue2::stopClient()
293{
294 // Only stop client if updates are no longer wanted.
295 if (m_requestTimer->isActive() || m_running || !m_client)
296 return;
297
298 const QDBusPendingReply<> reply = m_client->Stop();
299 const auto watcher = new QDBusPendingCallWatcher(reply, this);
300 connect(watcher, &QDBusPendingCallWatcher::finished, this,
301 [this](QDBusPendingCallWatcher *watcher) {
302 watcher->deleteLater();
303 const QDBusPendingReply<> reply = *watcher;
304 if (reply.isError()) {
305 const auto error = reply.error();
306 qCCritical(lcPositioningGeoclue2) << "Unable to stop the client:"
307 << error.name() << error.message();
308 setError(AccessError);
309 } else {
310 qCDebug(lcPositioningGeoclue2) << "Client successfully stopped";
311 }
312 delete m_client;
313 });
314}
315
316bool QGeoPositionInfoSourceGeoclue2::configureClient()
317{
318 if (!m_client)
319 return false;
320
321 if (m_desktopId.isEmpty()) {
322 qCCritical(lcPositioningGeoclue2)
323 << "Unable to configure the client due to the desktop id is not set via"
324 << desktopIdParameter << "plugin parameter or QGuiApplication::desktopFileName";
325 setError(AccessError);
326 return false;
327 }
328
329 m_client->setDesktopId(m_desktopId);
330
331 const auto msecs = updateInterval();
332 const uint secs = qMax(uint(msecs), 0u) / 1000u;
333 m_client->setTimeThreshold(secs);
334
335 const auto methods = preferredPositioningMethods();
336 switch (methods) {
337 case SatellitePositioningMethods:
338 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_EXACT);
339 break;
340 case NonSatellitePositioningMethods:
341 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_STREET);
342 break;
343 case AllPositioningMethods:
344 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_EXACT);
345 break;
346 default:
347 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_NONE);
348 break;
349 }
350
351 return true;
352}
353
354void QGeoPositionInfoSourceGeoclue2::requestUpdateTimeout()
355{
356 qCDebug(lcPositioningGeoclue2) << "Request update timeout occurred";
357
358 setError(QGeoPositionInfoSource::UpdateTimeoutError);
359
360 stopClient();
361}
362
363void QGeoPositionInfoSourceGeoclue2::handleNewLocation(const QDBusObjectPath &oldLocation,
364 const QDBusObjectPath &newLocation)
365{
366 if (m_requestTimer->isActive())
367 m_requestTimer->stop();
368
369 const auto oldPath = oldLocation.path();
370 const auto newPath = newLocation.path();
371 qCDebug(lcPositioningGeoclue2) << "Old location object path:" << oldPath;
372 qCDebug(lcPositioningGeoclue2) << "New location object path:" << newPath;
373
374 OrgFreedesktopGeoClue2LocationInterface location(
375 QLatin1String(GEOCLUE2_SERVICE_NAME),
376 newPath,
377 QDBusConnection::systemBus(),
378 this);
379 if (!location.isValid()) {
380 const auto error = location.lastError();
381 qCCritical(lcPositioningGeoclue2) << "Unable to create the location object:"
382 << error.name() << error.message();
383 } else {
384 QGeoCoordinate coordinate(location.latitude(),
385 location.longitude());
386 const auto altitude = location.altitude();
387 if (altitude > std::numeric_limits<double>::lowest())
388 coordinate.setAltitude(altitude);
389
390 const Timestamp ts = location.timestamp();
391 if (ts.m_seconds == 0 && ts.m_microseconds == 0) {
392 const auto dt = QDateTime::currentDateTime();
393 m_lastPosition = QGeoPositionInfo(coordinate, dt);
394 } else {
395 auto dt = QDateTime::fromSecsSinceEpoch(qint64(ts.m_seconds));
396 dt = dt.addMSecs(ts.m_microseconds / 1000);
397 m_lastPosition = QGeoPositionInfo(coordinate, dt);
398 }
399
400 const auto accuracy = location.accuracy();
401 // We assume that an accuracy as 0.0 means that it comes from a sattelite.
402 m_lastPositionFromSatellite = qFuzzyCompare(accuracy, 0.0);
403
404 m_lastPosition.setAttribute(QGeoPositionInfo::HorizontalAccuracy, accuracy);
405 const auto speed = location.speed();
406 if (speed >= 0.0)
407 m_lastPosition.setAttribute(QGeoPositionInfo::GroundSpeed, speed);
408 const auto heading = location.heading();
409 if (heading >= 0.0)
410 m_lastPosition.setAttribute(QGeoPositionInfo::Direction, heading);
411
412 emit positionUpdated(m_lastPosition);
413 qCDebug(lcPositioningGeoclue2) << "New position:" << m_lastPosition;
414 }
415
416 stopClient();
417}
418
419void QGeoPositionInfoSourceGeoclue2::parseParameters(const QVariantMap &parameters)
420{
421 if (parameters.contains(desktopIdParameter))
422 m_desktopId = parameters.value(desktopIdParameter).toString();
423
424 if (m_desktopId.isEmpty() && qApp)
425 m_desktopId = qApp->property("desktopFileName").toString();
426
427#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
428 if (m_desktopId.isEmpty()) {
429 qCWarning(lcPositioningGeoclue2) << "Neither" << desktopIdParameter
430 << "plugin parameter nor QGuiApplication::desktopFileName"
431 << "has been set. Please consider setting one of the two.";
432 m_desktopId = QCoreApplication::applicationName();
433 }
434#endif
435}
436
437QT_END_NAMESPACE
438
439#include "moc_qgeopositioninfosource_geoclue2_p.cpp"
void startUpdates() override
Starts emitting updates at regular intervals as specified by setUpdateInterval().
PositioningMethods supportedPositioningMethods() const override
Returns the positioning methods available to this source.
Error error() const override
Returns the type of error that last occurred.
void requestUpdate(int timeout=5000) override
Attempts to get the current position and emit positionUpdated() with this information.
QGeoPositionInfo lastKnownPosition(bool fromSatellitePositioningMethodsOnly=false) const override
Returns an update containing the last known position, or a null update if none is available.
void stopUpdates() override
Stops emitting updates at regular intervals.
void setPreferredPositioningMethods(PositioningMethods methods) override