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