1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 Denis Shienkov <denis.shienkov@gmail.com>
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the QtPositioning module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39
40 #include "qgeopositioninfosource_geoclue2_p.h"
41
42 #include <QtCore/QLoggingCategory>
43 #include <QtCore/QSaveFile>
44 #include <QtCore/QScopedPointer>
45 #include <QtCore/QTimer>
46 #include <QtDBus/QDBusPendingCallWatcher>
47
48 // Auto-generated D-Bus files.
49 #include <client_interface.h>
50 #include <location_interface.h>
51
52 Q_DECLARE_LOGGING_CATEGORY(lcPositioningGeoclue2)
53
54 QT_BEGIN_NAMESPACE
55
56 namespace {
57
58 // NOTE: Copied from the /usr/include/libgeoclue-2.0/gclue-client.h
59 enum GClueAccuracyLevel {
60 GCLUE_ACCURACY_LEVEL_NONE = 0,
61 GCLUE_ACCURACY_LEVEL_COUNTRY = 1,
62 GCLUE_ACCURACY_LEVEL_CITY = 4,
63 GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD = 5,
64 GCLUE_ACCURACY_LEVEL_STREET = 6,
65 GCLUE_ACCURACY_LEVEL_EXACT = 8
66 };
67
68 const char GEOCLUE2_SERVICE_NAME[] = "org.freedesktop.GeoClue2";
69 const int MINIMUM_UPDATE_INTERVAL = 1000;
70 const int UPDATE_TIMEOUT_COLD_START = 120000;
71
lastPositionFilePath()72 static QString lastPositionFilePath()
73 {
74 return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
75 + QStringLiteral("/qtposition-geoclue2");
76 }
77
78 } // namespace
79
QGeoPositionInfoSourceGeoclue2(QObject * parent)80 QGeoPositionInfoSourceGeoclue2::QGeoPositionInfoSourceGeoclue2(QObject *parent)
81 : QGeoPositionInfoSource(parent)
82 , m_requestTimer(new QTimer(this))
83 , m_manager(QLatin1String(GEOCLUE2_SERVICE_NAME),
84 QStringLiteral("/org/freedesktop/GeoClue2/Manager"),
85 QDBusConnection::systemBus(),
86 this)
87 {
88 qDBusRegisterMetaType<Timestamp>();
89
90 restoreLastPosition();
91
92 m_requestTimer->setSingleShot(true);
93 connect(m_requestTimer, &QTimer::timeout,
94 this, &QGeoPositionInfoSourceGeoclue2::requestUpdateTimeout);
95 }
96
~QGeoPositionInfoSourceGeoclue2()97 QGeoPositionInfoSourceGeoclue2::~QGeoPositionInfoSourceGeoclue2()
98 {
99 saveLastPosition();
100 }
101
setUpdateInterval(int msec)102 void QGeoPositionInfoSourceGeoclue2::setUpdateInterval(int msec)
103 {
104 QGeoPositionInfoSource::setUpdateInterval(msec);
105 configureClient();
106 }
107
lastKnownPosition(bool fromSatellitePositioningMethodsOnly) const108 QGeoPositionInfo QGeoPositionInfoSourceGeoclue2::lastKnownPosition(bool fromSatellitePositioningMethodsOnly) const
109 {
110 if (fromSatellitePositioningMethodsOnly && !m_lastPositionFromSatellite)
111 return QGeoPositionInfo();
112 return m_lastPosition;
113 }
114
supportedPositioningMethods() const115 QGeoPositionInfoSourceGeoclue2::PositioningMethods QGeoPositionInfoSourceGeoclue2::supportedPositioningMethods() const
116 {
117 bool ok;
118 const auto accuracy = m_manager.property("AvailableAccuracyLevel").toUInt(&ok);
119 if (!ok) {
120 const_cast<QGeoPositionInfoSourceGeoclue2 *>(this)->setError(AccessError);
121 return NoPositioningMethods;
122 }
123
124 switch (accuracy) {
125 case GCLUE_ACCURACY_LEVEL_COUNTRY:
126 case GCLUE_ACCURACY_LEVEL_CITY:
127 case GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD:
128 case GCLUE_ACCURACY_LEVEL_STREET:
129 return NonSatellitePositioningMethods;
130 case GCLUE_ACCURACY_LEVEL_EXACT:
131 return AllPositioningMethods;
132 case GCLUE_ACCURACY_LEVEL_NONE:
133 default:
134 return NoPositioningMethods;
135 }
136 }
137
setPreferredPositioningMethods(PositioningMethods methods)138 void QGeoPositionInfoSourceGeoclue2::setPreferredPositioningMethods(PositioningMethods methods)
139 {
140 QGeoPositionInfoSource::setPreferredPositioningMethods(methods);
141 configureClient();
142 }
143
minimumUpdateInterval() const144 int QGeoPositionInfoSourceGeoclue2::minimumUpdateInterval() const
145 {
146 return MINIMUM_UPDATE_INTERVAL;
147 }
148
error() const149 QGeoPositionInfoSource::Error QGeoPositionInfoSourceGeoclue2::error() const
150 {
151 return m_error;
152 }
153
startUpdates()154 void QGeoPositionInfoSourceGeoclue2::startUpdates()
155 {
156 if (m_running) {
157 qCWarning(lcPositioningGeoclue2) << "Already running";
158 return;
159 }
160
161 qCDebug(lcPositioningGeoclue2) << "Starting updates";
162 m_running = true;
163
164 startClient();
165
166 if (m_lastPosition.isValid()) {
167 QMetaObject::invokeMethod(this, "positionUpdated", Qt::QueuedConnection,
168 Q_ARG(QGeoPositionInfo, m_lastPosition));
169 }
170 }
171
stopUpdates()172 void QGeoPositionInfoSourceGeoclue2::stopUpdates()
173 {
174 if (!m_running) {
175 qCWarning(lcPositioningGeoclue2) << "Already stopped";
176 return;
177 }
178
179 qCDebug(lcPositioningGeoclue2) << "Stopping updates";
180 m_running = false;
181
182 stopClient();
183 }
184
requestUpdate(int timeout)185 void QGeoPositionInfoSourceGeoclue2::requestUpdate(int timeout)
186 {
187 if (timeout < minimumUpdateInterval() && timeout != 0) {
188 emit updateTimeout();
189 return;
190 }
191
192 if (m_requestTimer->isActive()) {
193 qCDebug(lcPositioningGeoclue2) << "Request timer was active, ignoring startUpdates";
194 return;
195 }
196
197 m_requestTimer->start(timeout ? timeout : UPDATE_TIMEOUT_COLD_START);
198 startClient();
199 }
200
setError(QGeoPositionInfoSource::Error error)201 void QGeoPositionInfoSourceGeoclue2::setError(QGeoPositionInfoSource::Error error)
202 {
203 m_error = error;
204 emit QGeoPositionInfoSource::error(m_error);
205 }
206
restoreLastPosition()207 void QGeoPositionInfoSourceGeoclue2::restoreLastPosition()
208 {
209 #if !defined(QT_NO_DATASTREAM)
210 const auto filePath = lastPositionFilePath();
211 QFile file(filePath);
212 if (file.open(QIODevice::ReadOnly)) {
213 QDataStream out(&file);
214 out >> m_lastPosition;
215 }
216 #endif
217 }
218
saveLastPosition()219 void QGeoPositionInfoSourceGeoclue2::saveLastPosition()
220 {
221 #if !defined(QT_NO_DATASTREAM) && QT_CONFIG(temporaryfile)
222 if (!m_lastPosition.isValid())
223 return;
224
225 const auto filePath = lastPositionFilePath();
226 QSaveFile file(filePath);
227 if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
228 QDataStream out(&file);
229 // Only save position and timestamp.
230 out << QGeoPositionInfo(m_lastPosition.coordinate(), m_lastPosition.timestamp());
231 file.commit();
232 }
233 #endif
234 }
235
createClient()236 void QGeoPositionInfoSourceGeoclue2::createClient()
237 {
238 const QDBusPendingReply<QDBusObjectPath> reply = m_manager.GetClient();
239 const auto watcher = new QDBusPendingCallWatcher(reply, this);
240 connect(watcher, &QDBusPendingCallWatcher::finished,
241 [this](QDBusPendingCallWatcher *watcher) {
242 const QScopedPointer<QDBusPendingCallWatcher, QScopedPointerDeleteLater>
243 scopedWatcher(watcher);
244 const QDBusPendingReply<QDBusObjectPath> reply = *scopedWatcher;
245 if (reply.isError()) {
246 const auto error = reply.error();
247 qCWarning(lcPositioningGeoclue2) << "Unable to obtain the client patch:"
248 << error.name() + error.message();
249 setError(AccessError);
250 } else {
251 const QString clientPath = reply.value().path();
252 qCDebug(lcPositioningGeoclue2) << "Client path is:"
253 << clientPath;
254 delete m_client;
255 m_client = new OrgFreedesktopGeoClue2ClientInterface(
256 QLatin1String(GEOCLUE2_SERVICE_NAME),
257 clientPath,
258 QDBusConnection::systemBus(),
259 this);
260 if (!m_client->isValid()) {
261 const auto error = m_client->lastError();
262 qCCritical(lcPositioningGeoclue2) << "Unable to create the client object:"
263 << error.name() << error.message();
264 setError(AccessError);
265 delete m_client;
266 } else {
267 connect(m_client.data(), &OrgFreedesktopGeoClue2ClientInterface::LocationUpdated,
268 this, &QGeoPositionInfoSourceGeoclue2::handleNewLocation);
269
270 if (configureClient())
271 startClient();
272 }
273 }
274 });
275 }
276
startClient()277 void QGeoPositionInfoSourceGeoclue2::startClient()
278 {
279 // only start the client if someone asked for it already
280 if (!m_running && !m_requestTimer->isActive())
281 return;
282
283 if (!m_client) {
284 createClient();
285 return;
286 }
287
288 const QDBusPendingReply<> reply = m_client->Start();
289 const auto watcher = new QDBusPendingCallWatcher(reply, this);
290 connect(watcher, &QDBusPendingCallWatcher::finished,
291 [this](QDBusPendingCallWatcher *watcher) {
292 const QScopedPointer<QDBusPendingCallWatcher, QScopedPointerDeleteLater>
293 scopedWatcher(watcher);
294 const QDBusPendingReply<> reply = *scopedWatcher;
295 if (reply.isError()) {
296 const auto error = reply.error();
297 qCCritical(lcPositioningGeoclue2) << "Unable to start the client:"
298 << error.name() << error.message();
299 setError(AccessError);
300 delete m_client;
301 } else {
302 qCDebug(lcPositioningGeoclue2) << "Client successfully started";
303
304 const QDBusObjectPath location = m_client->location();
305 const QString path = location.path();
306 if (path.isEmpty() || path == QLatin1String("/"))
307 return;
308
309 handleNewLocation({}, location);
310 }
311 });
312 }
313
stopClient()314 void QGeoPositionInfoSourceGeoclue2::stopClient()
315 {
316 // Only stop client if updates are no longer wanted.
317 if (m_requestTimer->isActive() || m_running || !m_client)
318 return;
319
320 const QDBusPendingReply<> reply = m_client->Stop();
321 const auto watcher = new QDBusPendingCallWatcher(reply, this);
322 connect(watcher, &QDBusPendingCallWatcher::finished,
323 [this](QDBusPendingCallWatcher *watcher) {
324 const QScopedPointer<QDBusPendingCallWatcher, QScopedPointerDeleteLater>
325 scopedWatcher(watcher);
326 const QDBusPendingReply<> reply = *scopedWatcher;
327 if (reply.isError()) {
328 const auto error = reply.error();
329 qCCritical(lcPositioningGeoclue2) << "Unable to stop the client:"
330 << error.name() << error.message();
331 setError(AccessError);
332 } else {
333 qCDebug(lcPositioningGeoclue2) << "Client successfully stopped";
334 }
335 delete m_client;
336 });
337 }
338
configureClient()339 bool QGeoPositionInfoSourceGeoclue2::configureClient()
340 {
341 if (!m_client)
342 return false;
343
344 auto desktopId = QString::fromUtf8(qgetenv("QT_GEOCLUE_APP_DESKTOP_ID"));
345 if (desktopId.isEmpty())
346 desktopId = QCoreApplication::applicationName();
347 if (desktopId.isEmpty()) {
348 qCCritical(lcPositioningGeoclue2) << "Unable to configure the client "
349 "due to the application desktop id "
350 "is not set via QT_GEOCLUE_APP_DESKTOP_ID "
351 "envirorment variable or QCoreApplication::applicationName";
352 setError(AccessError);
353 return false;
354 }
355
356 m_client->setDesktopId(desktopId);
357
358 const auto msecs = updateInterval();
359 const uint secs = qMax(uint(msecs), 0u) / 1000u;
360 m_client->setTimeThreshold(secs);
361
362 const auto methods = preferredPositioningMethods();
363 switch (methods) {
364 case SatellitePositioningMethods:
365 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_EXACT);
366 break;
367 case NonSatellitePositioningMethods:
368 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_STREET);
369 break;
370 case AllPositioningMethods:
371 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_EXACT);
372 break;
373 default:
374 m_client->setRequestedAccuracyLevel(GCLUE_ACCURACY_LEVEL_NONE);
375 break;
376 }
377
378 return true;
379 }
380
requestUpdateTimeout()381 void QGeoPositionInfoSourceGeoclue2::requestUpdateTimeout()
382 {
383 qCDebug(lcPositioningGeoclue2) << "Request update timeout occurred";
384
385 emit updateTimeout();
386
387 stopClient();
388 }
389
handleNewLocation(const QDBusObjectPath & oldLocation,const QDBusObjectPath & newLocation)390 void QGeoPositionInfoSourceGeoclue2::handleNewLocation(const QDBusObjectPath &oldLocation,
391 const QDBusObjectPath &newLocation)
392 {
393 if (m_requestTimer->isActive())
394 m_requestTimer->stop();
395
396 const auto oldPath = oldLocation.path();
397 const auto newPath = newLocation.path();
398 qCDebug(lcPositioningGeoclue2) << "Old location object path:" << oldPath;
399 qCDebug(lcPositioningGeoclue2) << "New location object path:" << newPath;
400
401 OrgFreedesktopGeoClue2LocationInterface location(
402 QLatin1String(GEOCLUE2_SERVICE_NAME),
403 newPath,
404 QDBusConnection::systemBus(),
405 this);
406 if (!location.isValid()) {
407 const auto error = location.lastError();
408 qCCritical(lcPositioningGeoclue2) << "Unable to create the location object:"
409 << error.name() << error.message();
410 } else {
411 QGeoCoordinate coordinate(location.latitude(),
412 location.longitude());
413 if (const auto altitude = location.altitude() > std::numeric_limits<double>::min())
414 coordinate.setAltitude(altitude);
415
416 const Timestamp ts = location.timestamp();
417 if (ts.m_seconds == 0 && ts.m_microseconds == 0) {
418 const auto dt = QDateTime::currentDateTime();
419 m_lastPosition = QGeoPositionInfo(coordinate, dt);
420 } else {
421 auto dt = QDateTime::fromSecsSinceEpoch(qint64(ts.m_seconds));
422 dt = dt.addMSecs(ts.m_microseconds / 1000);
423 m_lastPosition = QGeoPositionInfo(coordinate, dt);
424 }
425
426 const auto accuracy = location.accuracy();
427 // We assume that an accuracy as 0.0 means that it comes from a sattelite.
428 m_lastPositionFromSatellite = qFuzzyCompare(accuracy, 0.0);
429
430 m_lastPosition.setAttribute(QGeoPositionInfo::HorizontalAccuracy, accuracy);
431 if (const auto speed = location.speed() >= 0.0)
432 m_lastPosition.setAttribute(QGeoPositionInfo::GroundSpeed, speed);
433 if (const auto heading = location.heading() >= 0.0)
434 m_lastPosition.setAttribute(QGeoPositionInfo::Direction, heading);
435
436 emit positionUpdated(m_lastPosition);
437 qCDebug(lcPositioningGeoclue2) << "New position:" << m_lastPosition;
438 }
439
440 stopClient();
441 }
442
443 QT_END_NAMESPACE
444