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