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