1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
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 "qgeoareamonitor_polling.h"
41 #include <QtPositioning/qgeocoordinate.h>
42 #include <QtPositioning/qgeorectangle.h>
43 #include <QtPositioning/qgeocircle.h>
44 
45 #include <QtCore/qmetaobject.h>
46 #include <QtCore/qtimer.h>
47 #include <QtCore/qdebug.h>
48 #include <QtCore/qmutex.h>
49 
50 #include <mutex>
51 
52 #define UPDATE_INTERVAL_5S  5000
53 
54 typedef QHash<QString, QGeoAreaMonitorInfo> MonitorTable;
55 
56 
areaEnteredSignal()57 static QMetaMethod areaEnteredSignal()
58 {
59     static QMetaMethod signal = QMetaMethod::fromSignal(&QGeoAreaMonitorPolling::areaEntered);
60     return signal;
61 }
62 
areaExitedSignal()63 static QMetaMethod areaExitedSignal()
64 {
65     static QMetaMethod signal = QMetaMethod::fromSignal(&QGeoAreaMonitorPolling::areaExited);
66     return signal;
67 }
68 
monitorExpiredSignal()69 static QMetaMethod monitorExpiredSignal()
70 {
71     static QMetaMethod signal = QMetaMethod::fromSignal(&QGeoAreaMonitorPolling::monitorExpired);
72     return signal;
73 }
74 
75 class QGeoAreaMonitorPollingPrivate : public QObject
76 {
77     Q_OBJECT
78 public:
QGeoAreaMonitorPollingPrivate()79     QGeoAreaMonitorPollingPrivate()
80     {
81         nextExpiryTimer = new QTimer(this);
82         nextExpiryTimer->setSingleShot(true);
83         connect(nextExpiryTimer, SIGNAL(timeout()),
84                 this, SLOT(timeout()));
85     }
86 
startMonitoring(const QGeoAreaMonitorInfo & monitor)87     void startMonitoring(const QGeoAreaMonitorInfo &monitor)
88     {
89         const std::lock_guard<QRecursiveMutex> locker(mutex);
90 
91         activeMonitorAreas.insert(monitor.identifier(), monitor);
92         singleShotTrigger.remove(monitor.identifier());
93 
94         checkStartStop();
95         setupNextExpiryTimeout();
96     }
97 
requestUpdate(const QGeoAreaMonitorInfo & monitor,int signalId)98     void requestUpdate(const QGeoAreaMonitorInfo &monitor, int signalId)
99     {
100         const std::lock_guard<QRecursiveMutex> locker(mutex);
101 
102         activeMonitorAreas.insert(monitor.identifier(), monitor);
103         singleShotTrigger.insert(monitor.identifier(), signalId);
104 
105         checkStartStop();
106         setupNextExpiryTimeout();
107     }
108 
stopMonitoring(const QGeoAreaMonitorInfo & monitor)109     QGeoAreaMonitorInfo stopMonitoring(const QGeoAreaMonitorInfo &monitor)
110     {
111         const std::lock_guard<QRecursiveMutex> locker(mutex);
112 
113         QGeoAreaMonitorInfo mon = activeMonitorAreas.take(monitor.identifier());
114 
115         checkStartStop();
116         setupNextExpiryTimeout();
117 
118         return mon;
119     }
120 
registerClient(QGeoAreaMonitorPolling * client)121     void registerClient(QGeoAreaMonitorPolling *client)
122     {
123         const std::lock_guard<QRecursiveMutex> locker(mutex);
124 
125         connect(this, SIGNAL(timeout(QGeoAreaMonitorInfo)),
126                 client, SLOT(timeout(QGeoAreaMonitorInfo)));
127 
128         connect(this, SIGNAL(positionError(QGeoPositionInfoSource::Error)),
129                 client, SLOT(positionError(QGeoPositionInfoSource::Error)));
130 
131         connect(this, SIGNAL(areaEventDetected(QGeoAreaMonitorInfo,QGeoPositionInfo,bool)),
132                 client, SLOT(processAreaEvent(QGeoAreaMonitorInfo,QGeoPositionInfo,bool)));
133 
134         registeredClients.append(client);
135     }
136 
deregisterClient(QGeoAreaMonitorPolling * client)137     void deregisterClient(QGeoAreaMonitorPolling *client)
138     {
139         const std::lock_guard<QRecursiveMutex> locker(mutex);
140 
141         registeredClients.removeAll(client);
142         if (registeredClients.isEmpty())
143             checkStartStop();
144     }
145 
setPositionSource(QGeoPositionInfoSource * newSource)146     void setPositionSource(QGeoPositionInfoSource *newSource)
147     {
148         const std::lock_guard<QRecursiveMutex> locker(mutex);
149 
150         if (newSource == source)
151             return;
152 
153         if (source)
154             delete source;
155 
156         source = newSource;
157 
158         if (source) {
159             source->setParent(this);
160             source->moveToThread(this->thread());
161             if (source->updateInterval() == 0)
162                 source->setUpdateInterval(UPDATE_INTERVAL_5S);
163             disconnect(source, 0, 0, 0); //disconnect all
164             connect(source, SIGNAL(positionUpdated(QGeoPositionInfo)),
165                     this, SLOT(positionUpdated(QGeoPositionInfo)));
166             connect(source, SIGNAL(error(QGeoPositionInfoSource::Error)),
167                     this, SIGNAL(positionError(QGeoPositionInfoSource::Error)));
168             checkStartStop();
169         }
170     }
171 
positionSource() const172     QGeoPositionInfoSource* positionSource() const
173     {
174         const std::lock_guard<QRecursiveMutex> locker(mutex);
175         return source;
176     }
177 
activeMonitors() const178     MonitorTable activeMonitors() const
179     {
180         const std::lock_guard<QRecursiveMutex> locker(mutex);
181 
182         return activeMonitorAreas;
183     }
184 
checkStartStop()185     void checkStartStop()
186     {
187         const std::lock_guard<QRecursiveMutex> locker(mutex);
188 
189         bool signalsConnected = false;
190         foreach (const QGeoAreaMonitorPolling *client, registeredClients) {
191             if (client->signalsAreConnected) {
192                 signalsConnected = true;
193                 break;
194             }
195         }
196 
197         if (signalsConnected && !activeMonitorAreas.isEmpty()) {
198             if (source)
199                 source->startUpdates();
200             else
201                 //translated to InsufficientPositionInfo
202                 emit positionError(QGeoPositionInfoSource::ClosedError);
203         } else {
204             if (source)
205                 source->stopUpdates();
206         }
207     }
208 
209 private:
setupNextExpiryTimeout()210     void setupNextExpiryTimeout()
211     {
212         nextExpiryTimer->stop();
213         activeExpiry.first = QDateTime();
214         activeExpiry.second = QString();
215 
216         foreach (const QGeoAreaMonitorInfo &info, activeMonitors()) {
217             if (info.expiration().isValid()) {
218                 if (!activeExpiry.first.isValid()) {
219                     activeExpiry.first = info.expiration();
220                     activeExpiry.second = info.identifier();
221                     continue;
222                 }
223                 if (info.expiration() < activeExpiry.first) {
224                     activeExpiry.first = info.expiration();
225                     activeExpiry.second = info.identifier();
226                 }
227             }
228         }
229 
230         if (activeExpiry.first.isValid())
231             nextExpiryTimer->start(QDateTime::currentDateTime().msecsTo(activeExpiry.first));
232     }
233 
234 
235     //returns true if areaEntered should be emitted
processInsideArea(const QString & monitorIdent)236     bool processInsideArea(const QString &monitorIdent)
237     {
238         if (!insideArea.contains(monitorIdent)) {
239             if (singleShotTrigger.value(monitorIdent, -1) == areaEnteredSignal().methodIndex()) {
240                 //this is the finishing singleshot event
241                 singleShotTrigger.remove(monitorIdent);
242                 activeMonitorAreas.remove(monitorIdent);
243                 setupNextExpiryTimeout();
244             } else {
245                 insideArea.insert(monitorIdent);
246             }
247             return true;
248         }
249 
250         return false;
251     }
252 
253     //returns true if areaExited should be emitted
processOutsideArea(const QString & monitorIdent)254     bool processOutsideArea(const QString &monitorIdent)
255     {
256         if (insideArea.contains(monitorIdent)) {
257             if (singleShotTrigger.value(monitorIdent, -1) == areaExitedSignal().methodIndex()) {
258                 //this is the finishing singleShot event
259                 singleShotTrigger.remove(monitorIdent);
260                 activeMonitorAreas.remove(monitorIdent);
261                 setupNextExpiryTimeout();
262             } else {
263                 insideArea.remove(monitorIdent);
264             }
265             return true;
266         }
267         return false;
268     }
269 
270 
271 
272 Q_SIGNALS:
273     void timeout(const QGeoAreaMonitorInfo &info);
274     void positionError(const QGeoPositionInfoSource::Error error);
275     void areaEventDetected(const QGeoAreaMonitorInfo &minfo,
276                            const QGeoPositionInfo &pinfo, bool isEnteredEvent);
277 private Q_SLOTS:
timeout()278     void timeout()
279     {
280         /*
281          * Don't block timer firing even if monitorExpiredSignal is not connected.
282          * This allows us to continue to remove the existing monitors as they expire.
283          **/
284         const QGeoAreaMonitorInfo info = activeMonitorAreas.take(activeExpiry.second);
285         setupNextExpiryTimeout();
286         emit timeout(info);
287 
288     }
289 
positionUpdated(const QGeoPositionInfo & info)290     void positionUpdated(const QGeoPositionInfo &info)
291     {
292         foreach (const QGeoAreaMonitorInfo &monInfo, activeMonitors()) {
293             const QString identifier = monInfo.identifier();
294             if (monInfo.area().contains(info.coordinate())) {
295                 if (processInsideArea(identifier))
296                     emit areaEventDetected(monInfo, info, true);
297             } else {
298                 if (processOutsideArea(identifier))
299                     emit areaEventDetected(monInfo, info, false);
300             }
301         }
302     }
303 
304 private:
305     QPair<QDateTime, QString> activeExpiry;
306     QHash<QString, int> singleShotTrigger;
307     QTimer* nextExpiryTimer;
308     QSet<QString> insideArea;
309 
310     MonitorTable activeMonitorAreas;
311 
312     QGeoPositionInfoSource* source = nullptr;
313     QList<QGeoAreaMonitorPolling*> registeredClients;
314     mutable QRecursiveMutex mutex;
315 };
316 
Q_GLOBAL_STATIC(QGeoAreaMonitorPollingPrivate,pollingPrivate)317 Q_GLOBAL_STATIC(QGeoAreaMonitorPollingPrivate, pollingPrivate)
318 
319 
320 QGeoAreaMonitorPolling::QGeoAreaMonitorPolling(QObject *parent)
321     : QGeoAreaMonitorSource(parent), signalsAreConnected(false)
322 {
323     d = pollingPrivate();
324     lastError = QGeoAreaMonitorSource::NoError;
325     d->registerClient(this);
326     //hookup to default source if existing
327     if (!positionInfoSource())
328         setPositionInfoSource(QGeoPositionInfoSource::createDefaultSource(this));
329 }
330 
~QGeoAreaMonitorPolling()331 QGeoAreaMonitorPolling::~QGeoAreaMonitorPolling()
332 {
333     d->deregisterClient(this);
334 }
335 
positionInfoSource() const336 QGeoPositionInfoSource* QGeoAreaMonitorPolling::positionInfoSource() const
337 {
338     return d->positionSource();
339 }
340 
setPositionInfoSource(QGeoPositionInfoSource * source)341 void QGeoAreaMonitorPolling::setPositionInfoSource(QGeoPositionInfoSource *source)
342 {
343     d->setPositionSource(source);
344 }
345 
error() const346 QGeoAreaMonitorSource::Error QGeoAreaMonitorPolling::error() const
347 {
348     return lastError;
349 }
350 
startMonitoring(const QGeoAreaMonitorInfo & monitor)351 bool QGeoAreaMonitorPolling::startMonitoring(const QGeoAreaMonitorInfo &monitor)
352 {
353     if (!monitor.isValid())
354         return false;
355 
356     //reject an expiry in the past
357     if (monitor.expiration().isValid() &&
358             (monitor.expiration() < QDateTime::currentDateTime()))
359         return false;
360 
361     //don't accept persistent monitor since we don't support it
362     if (monitor.isPersistent())
363         return false;
364 
365     //update or insert
366     d->startMonitoring(monitor);
367 
368     return true;
369 }
370 
idForSignal(const char * signal)371 int QGeoAreaMonitorPolling::idForSignal(const char *signal)
372 {
373     const QByteArray sig = QMetaObject::normalizedSignature(signal + 1);
374     const QMetaObject * const mo = metaObject();
375 
376     return mo->indexOfSignal(sig.constData());
377 }
378 
requestUpdate(const QGeoAreaMonitorInfo & monitor,const char * signal)379 bool QGeoAreaMonitorPolling::requestUpdate(const QGeoAreaMonitorInfo &monitor, const char *signal)
380 {
381     if (!monitor.isValid())
382         return false;
383     //reject an expiry in the past
384     if (monitor.expiration().isValid() &&
385             (monitor.expiration() < QDateTime::currentDateTime()))
386         return false;
387 
388     //don't accept persistent monitor since we don't support it
389     if (monitor.isPersistent())
390         return false;
391 
392     if (!signal)
393         return false;
394 
395     const int signalId = idForSignal(signal);
396     if (signalId < 0)
397         return false;
398 
399     //only accept area entered or exit signal
400     if (signalId != areaEnteredSignal().methodIndex() &&
401         signalId != areaExitedSignal().methodIndex())
402     {
403         return false;
404     }
405 
406     d->requestUpdate(monitor, signalId);
407 
408     return true;
409 }
410 
stopMonitoring(const QGeoAreaMonitorInfo & monitor)411 bool QGeoAreaMonitorPolling::stopMonitoring(const QGeoAreaMonitorInfo &monitor)
412 {
413     QGeoAreaMonitorInfo info = d->stopMonitoring(monitor);
414 
415     return info.isValid();
416 }
417 
activeMonitors() const418 QList<QGeoAreaMonitorInfo> QGeoAreaMonitorPolling::activeMonitors() const
419 {
420     return d->activeMonitors().values();
421 }
422 
activeMonitors(const QGeoShape & region) const423 QList<QGeoAreaMonitorInfo> QGeoAreaMonitorPolling::activeMonitors(const QGeoShape &region) const
424 {
425     QList<QGeoAreaMonitorInfo> results;
426     if (region.isEmpty())
427         return results;
428 
429     const MonitorTable list = d->activeMonitors();
430     foreach (const QGeoAreaMonitorInfo &monitor, list) {
431         if (region.contains(monitor.area().center()))
432             results.append(monitor);
433     }
434 
435     return results;
436 }
437 
supportedAreaMonitorFeatures() const438 QGeoAreaMonitorSource::AreaMonitorFeatures QGeoAreaMonitorPolling::supportedAreaMonitorFeatures() const
439 {
440     return {};
441 }
442 
connectNotify(const QMetaMethod &)443 void QGeoAreaMonitorPolling::connectNotify(const QMetaMethod &/*signal*/)
444 {
445     if (!signalsAreConnected &&
446         (isSignalConnected(areaEnteredSignal()) ||
447          isSignalConnected(areaExitedSignal())) )
448     {
449         signalsAreConnected = true;
450         d->checkStartStop();
451     }
452 }
453 
disconnectNotify(const QMetaMethod &)454 void QGeoAreaMonitorPolling::disconnectNotify(const QMetaMethod &/*signal*/)
455 {
456     if (!isSignalConnected(areaEnteredSignal()) &&
457         !isSignalConnected(areaExitedSignal()))
458     {
459         signalsAreConnected = false;
460         d->checkStartStop();
461     }
462 }
463 
positionError(const QGeoPositionInfoSource::Error error)464 void QGeoAreaMonitorPolling::positionError(const QGeoPositionInfoSource::Error error)
465 {
466     switch (error) {
467     case QGeoPositionInfoSource::AccessError:
468         lastError = QGeoAreaMonitorSource::AccessError;
469         break;
470     case QGeoPositionInfoSource::UnknownSourceError:
471         lastError = QGeoAreaMonitorSource::UnknownSourceError;
472         break;
473     case QGeoPositionInfoSource::ClosedError:
474         lastError = QGeoAreaMonitorSource::InsufficientPositionInfo;
475         break;
476     case QGeoPositionInfoSource::NoError:
477         return;
478     }
479 
480     emit QGeoAreaMonitorSource::error(lastError);
481 }
482 
timeout(const QGeoAreaMonitorInfo & monitor)483 void QGeoAreaMonitorPolling::timeout(const QGeoAreaMonitorInfo& monitor)
484 {
485     if (isSignalConnected(monitorExpiredSignal()))
486         emit monitorExpired(monitor);
487 }
488 
processAreaEvent(const QGeoAreaMonitorInfo & minfo,const QGeoPositionInfo & pinfo,bool isEnteredEvent)489 void QGeoAreaMonitorPolling::processAreaEvent(const QGeoAreaMonitorInfo &minfo,
490                                               const QGeoPositionInfo &pinfo, bool isEnteredEvent)
491 {
492     if (isEnteredEvent)
493         emit areaEntered(minfo, pinfo);
494     else
495         emit areaExited(minfo, pinfo);
496 }
497 
498 #include "qgeoareamonitor_polling.moc"
499 #include "moc_qgeoareamonitor_polling.cpp"
500