1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 Jolla Ltd.
4 ** Contact: Aaron McCarthy <aaron.mccarthy@jollamobile.com>
5 ** Copyright (C) 2016 The Qt Company Ltd.
6 ** Contact: https://www.qt.io/licensing/
7 **
8 ** This file is part of the QtPositioning module of the Qt Toolkit.
9 **
10 ** $QT_BEGIN_LICENSE:LGPL$
11 ** Commercial License Usage
12 ** Licensees holding valid commercial Qt licenses may use this file in
13 ** accordance with the commercial license agreement provided with the
14 ** Software or, alternatively, in accordance with the terms contained in
15 ** a written agreement between you and The Qt Company. For licensing terms
16 ** and conditions see https://www.qt.io/terms-conditions. For further
17 ** information use the contact form at https://www.qt.io/contact-us.
18 **
19 ** GNU Lesser General Public License Usage
20 ** Alternatively, this file may be used under the terms of the GNU Lesser
21 ** General Public License version 3 as published by the Free Software
22 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
23 ** packaging of this file. Please review the following information to
24 ** ensure the GNU Lesser General Public License version 3 requirements
25 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
26 **
27 ** GNU General Public License Usage
28 ** Alternatively, this file may be used under the terms of the GNU
29 ** General Public License version 2.0 or (at your option) the GNU General
30 ** Public license version 3 or any later version approved by the KDE Free
31 ** Qt Foundation. The licenses are as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
33 ** included in the packaging of this file. Please review the following
34 ** information to ensure the GNU General Public License requirements will
35 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
36 ** https://www.gnu.org/licenses/gpl-3.0.html.
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41 
42 #include "qgeopositioninfosource_geocluemaster.h"
43 
44 #include <geoclue_interface.h>
45 #include <position_interface.h>
46 #include <velocity_interface.h>
47 
48 #include <QtCore/QDateTime>
49 #include <QtCore/QFile>
50 #include <QtCore/QSaveFile>
51 #include <QtCore/QStandardPaths>
52 #include <QtCore/QVariantMap>
53 #include <QtCore/QtNumeric>
54 #include <QtCore/QLoggingCategory>
55 #include <QtDBus/QDBusMetaType>
56 
57 #ifndef QT_NO_DATASTREAM
58 #include <QtCore/QDataStream>
59 #endif
60 
61 Q_DECLARE_LOGGING_CATEGORY(lcPositioningGeoclue)
62 
63 #define MINIMUM_UPDATE_INTERVAL 1000
64 #define UPDATE_TIMEOUT_COLD_START 120000
65 
66 QT_BEGIN_NAMESPACE
67 
68 namespace
69 {
70 
knotsToMetersPerSecond(double knots)71 double knotsToMetersPerSecond(double knots)
72 {
73     return knots * 1852.0 / 3600.0;
74 }
75 
76 }
77 
QGeoPositionInfoSourceGeoclueMaster(QObject * parent)78 QGeoPositionInfoSourceGeoclueMaster::QGeoPositionInfoSourceGeoclueMaster(QObject *parent)
79 :   QGeoPositionInfoSource(parent), m_master(new QGeoclueMaster(this)), m_provider(0), m_pos(0),
80     m_vel(0), m_requestTimer(this), m_lastVelocityIsFresh(false), m_regularUpdateTimedOut(false),
81     m_lastVelocity(qQNaN()), m_lastDirection(qQNaN()), m_lastClimb(qQNaN()), m_lastPositionFromSatellite(false),
82     m_running(false), m_error(NoError)
83 {
84     qDBusRegisterMetaType<Accuracy>();
85 
86 #ifndef QT_NO_DATASTREAM
87     // Load the last known location
88     QFile file(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
89                QStringLiteral("/qtposition-geoclue"));
90     if (file.open(QIODevice::ReadOnly)) {
91         QDataStream out(&file);
92         out >> m_lastPosition;
93     }
94 #endif
95 
96     connect(m_master, SIGNAL(positionProviderChanged(QString,QString,QString,QString)),
97             this, SLOT(positionProviderChanged(QString,QString,QString,QString)));
98 
99     m_requestTimer.setSingleShot(true);
100     connect(&m_requestTimer, SIGNAL(timeout()), this, SLOT(requestUpdateTimeout()));
101 
102     setPreferredPositioningMethods(AllPositioningMethods);
103 }
104 
~QGeoPositionInfoSourceGeoclueMaster()105 QGeoPositionInfoSourceGeoclueMaster::~QGeoPositionInfoSourceGeoclueMaster()
106 {
107 #if !defined(QT_NO_DATASTREAM) && QT_CONFIG(temporaryfile)
108     if (m_lastPosition.isValid()) {
109         QSaveFile file(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
110                        QStringLiteral("/qtposition-geoclue"));
111         if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
112             QDataStream out(&file);
113             // Only save position and timestamp.
114             out << QGeoPositionInfo(m_lastPosition.coordinate(), m_lastPosition.timestamp());
115             file.commit();
116         }
117     }
118 #endif
119 
120     cleanupPositionSource();
121 }
122 
positionUpdateFailed()123 void QGeoPositionInfoSourceGeoclueMaster::positionUpdateFailed()
124 {
125     qCDebug(lcPositioningGeoclue) << "position update failed.";
126 
127     m_lastVelocityIsFresh = false;
128     if (m_running && !m_regularUpdateTimedOut) {
129         m_regularUpdateTimedOut = true;
130         emit updateTimeout();
131     }
132 }
133 
updatePosition(PositionFields fields,int timestamp,double latitude,double longitude,double altitude,Accuracy accuracy)134 void QGeoPositionInfoSourceGeoclueMaster::updatePosition(PositionFields fields, int timestamp,
135                                                          double latitude, double longitude,
136                                                          double altitude, Accuracy accuracy)
137 {
138     if (m_requestTimer.isActive())
139         m_requestTimer.stop();
140 
141     QGeoCoordinate coordinate(latitude, longitude);
142     if (fields & Altitude)
143         coordinate.setAltitude(altitude);
144 
145     m_lastPosition = QGeoPositionInfo(coordinate, QDateTime::fromSecsSinceEpoch(timestamp));
146 
147     m_lastPositionFromSatellite = accuracy.level() == Accuracy::Detailed;
148 
149     if (!qIsNaN(accuracy.horizontal()))
150         m_lastPosition.setAttribute(QGeoPositionInfo::HorizontalAccuracy, accuracy.horizontal());
151     if (!qIsNaN(accuracy.vertical()))
152         m_lastPosition.setAttribute(QGeoPositionInfo::VerticalAccuracy, accuracy.vertical());
153 
154     if (m_lastVelocityIsFresh) {
155         if (!qIsNaN(m_lastVelocity))
156             m_lastPosition.setAttribute(QGeoPositionInfo::GroundSpeed, m_lastVelocity);
157         if (!qIsNaN(m_lastDirection))
158             m_lastPosition.setAttribute(QGeoPositionInfo::Direction, m_lastDirection);
159         if (!qIsNaN(m_lastClimb))
160             m_lastPosition.setAttribute(QGeoPositionInfo::VerticalSpeed, m_lastClimb);
161         m_lastVelocityIsFresh = false;
162     }
163 
164     m_regularUpdateTimedOut = false;
165 
166     emit positionUpdated(m_lastPosition);
167 
168     qCDebug(lcPositioningGeoclue) << m_lastPosition;
169 
170     // Only stop positioning if regular updates not active.
171     if (!m_running) {
172         cleanupPositionSource();
173         m_master->releaseMasterClient();
174     }
175 }
176 
velocityUpdateFailed()177 void QGeoPositionInfoSourceGeoclueMaster::velocityUpdateFailed()
178 {
179     qCDebug(lcPositioningGeoclue) << "velocity update failed.";
180 
181     // Set the velocitydata non-fresh.
182     m_lastVelocityIsFresh = false;
183 }
184 
updateVelocity(VelocityFields fields,int timestamp,double speed,double direction,double climb)185 void QGeoPositionInfoSourceGeoclueMaster::updateVelocity(VelocityFields fields, int timestamp,
186                                                          double speed, double direction,
187                                                          double climb)
188 {
189     Q_UNUSED(timestamp);
190 
191     // Store the velocity and mark it as fresh. Simple but hopefully adequate.
192     m_lastVelocity = (fields & Speed) ? knotsToMetersPerSecond(speed) : qQNaN();
193     m_lastDirection = (fields & Direction) ? direction : qQNaN();
194     m_lastClimb = (fields & Climb) ? climb : qQNaN();
195     m_lastVelocityIsFresh = true;
196 
197     qCDebug(lcPositioningGeoclue) << m_lastVelocity << m_lastDirection << m_lastClimb;
198 }
199 
cleanupPositionSource()200 void QGeoPositionInfoSourceGeoclueMaster::cleanupPositionSource()
201 {
202     qCDebug(lcPositioningGeoclue) << "cleaning up position source";
203 
204     if (m_provider)
205         m_provider->RemoveReference();
206     delete m_provider;
207     m_provider = 0;
208     delete m_pos;
209     m_pos = 0;
210     delete m_vel;
211     m_vel = 0;
212 }
213 
setOptions()214 void QGeoPositionInfoSourceGeoclueMaster::setOptions()
215 {
216     if (!m_provider)
217         return;
218 
219     QVariantMap options;
220     options.insert(QStringLiteral("UpdateInterval"), updateInterval());
221 
222     m_provider->SetOptions(options);
223 }
224 
setUpdateInterval(int msec)225 void QGeoPositionInfoSourceGeoclueMaster::setUpdateInterval(int msec)
226 {
227     QGeoPositionInfoSource::setUpdateInterval(qMax(minimumUpdateInterval(), msec));
228     setOptions();
229 }
230 
setPreferredPositioningMethods(PositioningMethods methods)231 void QGeoPositionInfoSourceGeoclueMaster::setPreferredPositioningMethods(PositioningMethods methods)
232 {
233     PositioningMethods previousPreferredPositioningMethods = preferredPositioningMethods();
234     QGeoPositionInfoSource::setPreferredPositioningMethods(methods);
235     if (previousPreferredPositioningMethods == preferredPositioningMethods())
236         return;
237 
238     qCDebug(lcPositioningGeoclue) << "requested to set methods to" << methods
239                                   << ", and set them to:" << preferredPositioningMethods();
240 
241     m_lastVelocityIsFresh = false;
242     m_regularUpdateTimedOut = false;
243 
244     // Don't start Geoclue provider until necessary. Don't currently have a master client, no need
245     // no recreate one.
246     if (!m_master->hasMasterClient())
247         return;
248 
249     // Free potential previous sources, because new requirements can't be set for the client
250     // (creating a position object after changing requirements seems to fail).
251     cleanupPositionSource();
252     m_master->releaseMasterClient();
253 
254     // Restart Geoclue provider with new requirements.
255     configurePositionSource();
256     setOptions();
257 }
258 
lastKnownPosition(bool fromSatellitePositioningMethodsOnly) const259 QGeoPositionInfo QGeoPositionInfoSourceGeoclueMaster::lastKnownPosition(bool fromSatellitePositioningMethodsOnly) const
260 {
261     if (fromSatellitePositioningMethodsOnly && !m_lastPositionFromSatellite)
262         return QGeoPositionInfo();
263 
264     return m_lastPosition;
265 }
266 
supportedPositioningMethods() const267 QGeoPositionInfoSourceGeoclueMaster::PositioningMethods QGeoPositionInfoSourceGeoclueMaster::supportedPositioningMethods() const
268 {
269     return AllPositioningMethods;
270 }
271 
startUpdates()272 void QGeoPositionInfoSourceGeoclueMaster::startUpdates()
273 {
274     if (m_running) {
275         qCDebug(lcPositioningGeoclue) << "already running.";
276         return;
277     }
278 
279     m_running = true;
280 
281     qCDebug(lcPositioningGeoclue) << "starting updates";
282 
283     // Start Geoclue provider.
284     if (!m_master->hasMasterClient()) {
285         configurePositionSource();
286         setOptions();
287     }
288 
289     // Emit last known position on start.
290     if (m_lastPosition.isValid()) {
291         QMetaObject::invokeMethod(this, "positionUpdated", Qt::QueuedConnection,
292                                   Q_ARG(QGeoPositionInfo, m_lastPosition));
293     }
294 }
295 
minimumUpdateInterval() const296 int QGeoPositionInfoSourceGeoclueMaster::minimumUpdateInterval() const
297 {
298     return MINIMUM_UPDATE_INTERVAL;
299 }
300 
stopUpdates()301 void QGeoPositionInfoSourceGeoclueMaster::stopUpdates()
302 {
303     if (!m_running) {
304         qCDebug(lcPositioningGeoclue) << "already stopped.";
305         return;
306     }
307 
308     qCDebug(lcPositioningGeoclue) << "stopping updates";
309 
310     if (m_pos) {
311         disconnect(m_pos, SIGNAL(PositionChanged(qint32,qint32,double,double,double,Accuracy)),
312                    this, SLOT(positionChanged(qint32,qint32,double,double,double,Accuracy)));
313     }
314 
315     if (m_vel) {
316         disconnect(m_vel, SIGNAL(VelocityChanged(qint32,qint32,double,double,double)),
317                    this, SLOT(velocityChanged(qint32,qint32,double,double,double)));
318     }
319 
320     m_running = false;
321 
322     // Only stop positioning if single update not requested.
323     if (!m_requestTimer.isActive()) {
324         cleanupPositionSource();
325         m_master->releaseMasterClient();
326     }
327 }
328 
requestUpdate(int timeout)329 void QGeoPositionInfoSourceGeoclueMaster::requestUpdate(int timeout)
330 {
331     if (timeout < minimumUpdateInterval() && timeout != 0) {
332         emit updateTimeout();
333         return;
334     }
335     if (m_requestTimer.isActive()) {
336         qCDebug(lcPositioningGeoclue) << "request timer was active, ignoring startUpdates.";
337         return;
338     }
339 
340     if (!m_master->hasMasterClient()) {
341         configurePositionSource();
342         setOptions();
343     }
344 
345     // Create better logic for timeout value (specs leave it impl dependant).
346     // Especially if there are active updates ongoing, there is no point of waiting
347     // for whole cold start time.
348     m_requestTimer.start(timeout ? timeout : UPDATE_TIMEOUT_COLD_START);
349 
350     if (m_pos) {
351         QDBusPendingReply<qint32, qint32, double, double, double, Accuracy> reply = m_pos->GetPosition();
352         QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
353         connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
354                 this, SLOT(getPositionFinished(QDBusPendingCallWatcher*)));
355     }
356 }
357 
positionProviderChanged(const QString & name,const QString & description,const QString & service,const QString & path)358 void QGeoPositionInfoSourceGeoclueMaster::positionProviderChanged(const QString &name,
359                                                                   const QString &description,
360                                                                   const QString &service,
361                                                                   const QString &path)
362 {
363     Q_UNUSED(name);
364     Q_UNUSED(description);
365 
366     cleanupPositionSource();
367 
368     if (service.isEmpty() || path.isEmpty()) {
369         if (!m_regularUpdateTimedOut) {
370             m_regularUpdateTimedOut = true;
371             emit updateTimeout();
372         }
373         return;
374     }
375 
376     qCDebug(lcPositioningGeoclue) << "position provider changed to" << name;
377 
378     m_provider = new OrgFreedesktopGeoclueInterface(service, path, QDBusConnection::sessionBus());
379     m_provider->AddReference();
380 
381     m_pos = new OrgFreedesktopGeocluePositionInterface(service, path, QDBusConnection::sessionBus());
382 
383     if (m_running) {
384         connect(m_pos, SIGNAL(PositionChanged(qint32,qint32,double,double,double,Accuracy)),
385                 this, SLOT(positionChanged(qint32,qint32,double,double,double,Accuracy)));
386     }
387 
388     // Get the current position immediately.
389     QDBusPendingReply<qint32, qint32, double, double, double, Accuracy> reply = m_pos->GetPosition();
390     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
391     connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
392             this, SLOT(getPositionFinished(QDBusPendingCallWatcher*)));
393 
394     setOptions();
395 
396     m_vel = new OrgFreedesktopGeoclueVelocityInterface(service, path, QDBusConnection::sessionBus());
397     if (m_vel->isValid() && m_running) {
398         connect(m_vel, SIGNAL(VelocityChanged(qint32,qint32,double,double,double)),
399                 this, SLOT(velocityChanged(qint32,qint32,double,double,double)));
400     }
401 }
402 
requestUpdateTimeout()403 void QGeoPositionInfoSourceGeoclueMaster::requestUpdateTimeout()
404 {
405     qCDebug(lcPositioningGeoclue) << "request update timeout occurred.";
406 
407     // If we end up here, there has not been valid position update.
408     emit updateTimeout();
409 
410     // Only stop positioning if regular updates not active.
411     if (!m_running) {
412         cleanupPositionSource();
413         m_master->releaseMasterClient();
414     }
415 }
416 
getPositionFinished(QDBusPendingCallWatcher * watcher)417 void QGeoPositionInfoSourceGeoclueMaster::getPositionFinished(QDBusPendingCallWatcher *watcher)
418 {
419     QDBusPendingReply<qint32, qint32, double, double, double, Accuracy> reply = *watcher;
420     watcher->deleteLater();
421 
422     if (reply.isError())
423         return;
424 
425     PositionFields fields = static_cast<PositionFields>(reply.argumentAt<0>());
426 
427     qCDebug(lcPositioningGeoclue) << "got position update with fields" << int(fields);
428 
429     if (fields & Latitude && fields & Longitude) {
430         qint32 timestamp = reply.argumentAt<1>();
431         double latitude = reply.argumentAt<2>();
432         double longitude = reply.argumentAt<3>();
433         double altitude = reply.argumentAt<4>();
434         Accuracy accuracy = reply.argumentAt<5>();
435         updatePosition(fields, timestamp, latitude, longitude, altitude, accuracy);
436     }
437 }
438 
positionChanged(qint32 fields,qint32 timestamp,double latitude,double longitude,double altitude,const Accuracy & accuracy)439 void QGeoPositionInfoSourceGeoclueMaster::positionChanged(qint32 fields, qint32 timestamp, double latitude, double longitude, double altitude, const Accuracy &accuracy)
440 {
441     PositionFields pFields = static_cast<PositionFields>(fields);
442 
443     qCDebug(lcPositioningGeoclue) << "position changed with fields" << fields;
444 
445     if (pFields & Latitude && pFields & Longitude)
446         updatePosition(pFields, timestamp, latitude, longitude, altitude, accuracy);
447     else
448         positionUpdateFailed();
449 }
450 
velocityChanged(qint32 fields,qint32 timestamp,double speed,double direction,double climb)451 void QGeoPositionInfoSourceGeoclueMaster::velocityChanged(qint32 fields, qint32 timestamp, double speed, double direction, double climb)
452 {
453     VelocityFields vFields = static_cast<VelocityFields>(fields);
454 
455     if (vFields == NoVelocityFields)
456         velocityUpdateFailed();
457     else
458         updateVelocity(vFields, timestamp, speed, direction, climb);
459 }
460 
configurePositionSource()461 void QGeoPositionInfoSourceGeoclueMaster::configurePositionSource()
462 {
463     qCDebug(lcPositioningGeoclue);
464 
465     bool created = false;
466 
467     switch (preferredPositioningMethods()) {
468     case SatellitePositioningMethods:
469         created = m_master->createMasterClient(Accuracy::Detailed, QGeoclueMaster::ResourceGps);
470         break;
471     case NonSatellitePositioningMethods:
472         created = m_master->createMasterClient(Accuracy::None, QGeoclueMaster::ResourceCell | QGeoclueMaster::ResourceNetwork);
473         break;
474     case AllPositioningMethods:
475         created = m_master->createMasterClient(Accuracy::None, QGeoclueMaster::ResourceAll);
476         break;
477     default:
478         qWarning("QGeoPositionInfoSourceGeoclueMaster unknown preferred method.");
479         m_error = UnknownSourceError;
480         emit QGeoPositionInfoSource::error(m_error);
481         return;
482     }
483 
484     if (!created) {
485         m_error = UnknownSourceError;
486         emit QGeoPositionInfoSource::error(m_error);
487     }
488 }
489 
error() const490 QGeoPositionInfoSource::Error QGeoPositionInfoSourceGeoclueMaster::error() const
491 {
492     return m_error;
493 }
494 
495 QT_END_NAMESPACE
496