diff --git a/CMakeLists.txt b/CMakeLists.txt index 25708418..52268530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ option(NBT_USE_ZLIB "Build NBT library with zlib support" OFF) option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests. add_subdirectory(libraries/libnbtplusplus) +add_subdirectory(libraries/ganalytics) # google analytics library add_subdirectory(libraries/hoedown) # markdown parser add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker diff --git a/libraries/ganalytics/CMakeLists.txt b/libraries/ganalytics/CMakeLists.txt new file mode 100644 index 00000000..f6051d05 --- /dev/null +++ b/libraries/ganalytics/CMakeLists.txt @@ -0,0 +1,10 @@ +project(ganalytics) + +find_package(Qt5Core) +find_package(Qt5Gui) +find_package(Qt5Network) + +add_library(ganalytics STATIC ganalytics.cpp ganalytics.h) +qt5_use_modules(ganalytics Core Gui Network) +target_include_directories(ganalytics PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(ganalytics PRIVATE -DQT_GUI_LIB) diff --git a/libraries/ganalytics/LICENSE.txt b/libraries/ganalytics/LICENSE.txt new file mode 100644 index 00000000..795497ff --- /dev/null +++ b/libraries/ganalytics/LICENSE.txt @@ -0,0 +1,24 @@ +Copyright (c) 2014-2015, University of Applied Sciences Augsburg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the University of Applied Sciences Augsburg nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +OODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +UT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libraries/ganalytics/README.md b/libraries/ganalytics/README.md new file mode 100644 index 00000000..d7e1e33c --- /dev/null +++ b/libraries/ganalytics/README.md @@ -0,0 +1,34 @@ +qt-google-analytics +================ + +Qt5 classes for providing google analytics usage in a Qt/QML application. + +## Building +Include ```qt-google-analytics.pri``` in your .pro file. + +## Using +Please make sure you have set your application information using ```QApplication::setApplicationName``` and ```QApplication::setApplicationVersion```. + +### In C++: +``` +GAnalytics tracker("UA-my-id"); +tracker.sendScreenView("Main Screen"); +``` + +### In QtQuick: +Register the class on the C++ side using ```qmlRegisterType("analytics", 0, 1, "Tracker");``` +``` +Tracker { + id: tracker + trackingID: "UA-my-id" +} + +[...] +tracker.sendScreenView("Main Screen") +``` + +There is also an example application in the examples folder. + +## License +Copyright (c) 2014-2016, University of Applied Sciences Augsburg. +All rights reserved. Distributed under the terms and conditions of the BSD License. See separate LICENSE.txt. diff --git a/libraries/ganalytics/ganalytics.cpp b/libraries/ganalytics/ganalytics.cpp new file mode 100644 index 00000000..e2caad50 --- /dev/null +++ b/libraries/ganalytics/ganalytics.cpp @@ -0,0 +1,923 @@ +#include "ganalytics.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef QT_GUI_LIB +#include +#include +#endif // QT_GUI_LIB + +#ifdef QT_QML_LIB +#include +#include +#endif // QT_QML_LIB + +struct QueryBuffer +{ + QUrlQuery postQuery; + QDateTime time; +}; + +/** + * Class Private + * Private members and functions. + */ +class GAnalytics::Private : public QObject +{ + Q_OBJECT + +public: + explicit Private(GAnalytics *parent = 0); + ~Private(); + + GAnalytics *q; + + QNetworkAccessManager *networkManager; + + QQueue messageQueue; + QTimer timer; + QNetworkRequest request; + GAnalytics::LogLevel logLevel; + + QString trackingID; + QString clientID; + QString userID; + QString appName; + QString appVersion; + QString language; + QString screenResolution; + QString viewportSize; + + bool isSending; + + const static int fourHours = 4 * 60 * 60 * 1000; + const static QString dateTimeFormat; + +public: + void logMessage(GAnalytics::LogLevel level, const QString &message); + + QUrlQuery buildStandardPostQuery(const QString &type); +#ifdef QT_GUI_LIB + QString getScreenResolution(); +#endif // QT_GUI_LIB + QString getUserAgent(); + QString getSystemInfo(); + QList persistMessageQueue(); + void readMessagesFromFile(const QList &dataList); + QString getClientID(); + QString getUserID(); + void setUserID(const QString &userID); + void enqueQueryWithCurrentTime(const QUrlQuery &query); + void setIsSending(bool doSend); + +signals: + void postNextMessage(); + +public slots: + void postMessage(); + void postMessageFinished(); +}; + +const QString GAnalytics::Private::dateTimeFormat = "yyyy,MM,dd-hh:mm::ss:zzz"; + +/** + * Constructor + * Constructs an object of class Private. + * @param parent + */ +GAnalytics::Private::Private(GAnalytics *parent) +: QObject(parent) +, q(parent) +, networkManager(NULL) +, request(QUrl("http://www.google-analytics.com/collect")) +, logLevel(GAnalytics::Error) +, isSending(false) +{ + clientID = getClientID(); + userID = getUserID(); + language = QLocale::system().name().toLower().replace("_", "-"); +#ifdef QT_GUI_LIB + screenResolution = getScreenResolution(); +#endif // QT_GUI_LIB + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + appName = QCoreApplication::instance()->applicationName(); + appVersion = QCoreApplication::instance()->applicationVersion(); + request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent()); + connect(this, SIGNAL(postNextMessage()), this, SLOT(postMessage())); + timer.start(30000); + connect(&timer, SIGNAL(timeout()), this, SLOT(postMessage())); +} + +/** + * Destructor + * Delete an object of class Private. + */ +GAnalytics::Private::~Private() +{ +} + +void GAnalytics::Private::logMessage(LogLevel level, const QString &message) +{ + if (logLevel > level) + { + return; + } + + qDebug() << "[Analytics]" << message; +} + +/** + * Build the POST query. Adds all parameter to the query + * which are used in every POST. + * @param type Type of POST message. The event which is to post. + * @return query Most used parameter in a query for a POST. + */ +QUrlQuery GAnalytics::Private::buildStandardPostQuery(const QString &type) +{ + QUrlQuery query; + query.addQueryItem("v", "1"); + query.addQueryItem("tid", trackingID); + query.addQueryItem("cid", clientID); + if(!userID.isEmpty()) + { + query.addQueryItem("uid", userID); + } + query.addQueryItem("t", type); + query.addQueryItem("ul", language); + +#ifdef QT_GUI_LIB + query.addQueryItem("vp", viewportSize); + query.addQueryItem("sr", screenResolution); +#endif // QT_GUI_LIB + + return query; +} + +#ifdef QT_GUI_LIB +/** + * Get devicese screen resolution. + * @return A QString like "800x600". + */ +QString GAnalytics::Private::getScreenResolution() +{ + QScreen *screen = QGuiApplication::primaryScreen(); + QSize size = screen->size(); + + return QString("%1x%2").arg(size.width()).arg(size.height()); +} +#endif // QT_GUI_LIB + + +/** + * Try to gain information about the system where this application + * is running. It needs to get the name and version of the operating + * system, the language and screen resolution. + * All this information will be send in POST messages. + * @return agent A QString with all the information formatted for a POST message. + */ +QString GAnalytics::Private::getUserAgent() +{ + QString locale = QLocale::system().name(); + QString system = getSystemInfo(); + + return QString("%1/%2 (%3; %4) GAnalytics/1.0 (Qt/%5)").arg(appName).arg(appVersion).arg(system).arg(locale).arg(QT_VERSION_STR); +} + + +#ifdef Q_OS_MAC +/** + * Only on Mac OS X + * Get the Operating system name and version. + * @return os The operating system name and version in a string. + */ +QString GAnalytics::Private::getSystemInfo() +{ + QSysInfo::MacVersion version = QSysInfo::macVersion(); + QString os; + switch (version) + { + case QSysInfo::MV_9: + os = "Macintosh; Mac OS 9"; + break; + case QSysInfo::MV_10_0: + os = "Macintosh; Mac OS 10.0"; + break; + case QSysInfo::MV_10_1: + os = "Macintosh; Mac OS 10.1"; + break; + case QSysInfo::MV_10_2: + os = "Macintosh; Mac OS 10.2"; + break; + case QSysInfo::MV_10_3: + os = "Macintosh; Mac OS 10.3"; + break; + case QSysInfo::MV_10_4: + os = "Macintosh; Mac OS 10.4"; + break; + case QSysInfo::MV_10_5: + os = "Macintosh; Mac OS 10.5"; + break; + case QSysInfo::MV_10_6: + os = "Macintosh; Mac OS 10.6"; + break; + case QSysInfo::MV_10_7: + os = "Macintosh; Mac OS 10.7"; + break; + case QSysInfo::MV_10_8: + os = "Macintosh; Mac OS 10.8"; + break; + case QSysInfo::MV_10_9: + os = "Macintosh; Mac OS 10.9"; + break; + case QSysInfo::MV_10_10: + os = "Macintosh; Mac OS 10.10"; + break; + case QSysInfo::MV_10_11: + os = "Macintosh; Mac OS 10.11"; + break; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + case QSysInfo::MV_10_12: + os = "Macintosh; Mac OS 10.12"; + break; +#endif + case QSysInfo::MV_Unknown: + os = "Macintosh; Mac OS unknown"; + break; + case QSysInfo::MV_IOS_5_0: + os = "iPhone; iOS 5.0"; + break; + case QSysInfo::MV_IOS_5_1: + os = "iPhone; iOS 5.1"; + break; + case QSysInfo::MV_IOS_6_0: + os = "iPhone; iOS 6.0"; + break; + case QSysInfo::MV_IOS_6_1: + os = "iPhone; iOS 6.1"; + break; + case QSysInfo::MV_IOS_7_0: + os = "iPhone; iOS 7.0"; + break; + case QSysInfo::MV_IOS_7_1: + os = "iPhone; iOS 7.1"; + break; + case QSysInfo::MV_IOS_8_0: + os = "iPhone; iOS 8.0"; + break; + case QSysInfo::MV_IOS_8_1: + os = "iPhone; iOS 8.1"; + break; + case QSysInfo::MV_IOS_8_2: + os = "iPhone; iOS 8.2"; + break; + case QSysInfo::MV_IOS_8_3: + os = "iPhone; iOS 8.3"; + break; + case QSysInfo::MV_IOS_8_4: + os = "iPhone; iOS 8.4"; + break; + case QSysInfo::MV_IOS_9_0: + os = "iPhone; iOS 9.0"; + break; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + case QSysInfo::MV_IOS_9_1: + os = "iPhone; iOS 9.1"; + break; + case QSysInfo::MV_IOS_9_2: + os = "iPhone; iOS 9.2"; + break; + case QSysInfo::MV_IOS_9_3: + os = "iPhone; iOS 9.3"; + break; + case QSysInfo::MV_IOS_10_0: + os = "iPhone; iOS 10.0"; + break; +#endif + case QSysInfo::MV_IOS: + os = "iPhone; iOS unknown"; + break; + default: + os = "Macintosh"; + break; + } + return os; +} +#endif + +#ifdef Q_OS_WIN +/** + * Only on Windows + * Get operating system and its version. + * @return os A QString containing the oprating systems name and version. + */ +QString GAnalytics::Private::getSystemInfo() +{ + QSysInfo::WinVersion version = QSysInfo::windowsVersion(); + QString os("Windows; "); + switch (version) + { + case QSysInfo::WV_95: + os += "Win 95"; + break; + case QSysInfo::WV_98: + os += "Win 98"; + break; + case QSysInfo::WV_Me: + os += "Win ME"; + break; + case QSysInfo::WV_NT: + os += "Win NT"; + break; + case QSysInfo::WV_2000: + os += "Win 2000"; + break; + case QSysInfo::WV_2003: + os += "Win Server 2003"; + break; + case QSysInfo::WV_VISTA: + os += "Win Vista"; + break; + case QSysInfo::WV_WINDOWS7: + os += "Win 7"; + break; + case QSysInfo::WV_WINDOWS8: + os += "Win 8"; + break; + case QSysInfo::WV_WINDOWS8_1: + os += "Win 8.1"; + break; + case QSysInfo::WV_WINDOWS10: + os += "Win 10"; + break; + default: + os = "Windows; unknown"; + break; + } + return os; +} +#endif + +#if defined(Q_OS_ANDROID) +#include + +QString GAnalytics::Private::getSystemInfo() +{ + return QString("Linux; U; Android %1; %2 %3 Build/%4; %5") + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build$VERSION", "RELEASE").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "MANUFACTURER").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "MODEL").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "ID").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "BRAND").toString()); +} +#elif defined(Q_OS_LINUX) +#include + +/** + * Only on Unix systems. + * Get operation system name and version. + * @return os A QString with the name and version of the operating system. + */ +QString GAnalytics::Private::getSystemInfo() +{ + struct utsname buf; + uname(&buf); + QString system(buf.sysname); + QString release(buf.release); + + return system + "; " + release; +} +#endif + + +/** + * The message queue contains a list of QueryBuffer object. + * QueryBuffer holds a QUrlQuery object and a QDateTime object. + * These both object are freed from the buffer object and + * inserted as QString objects in a QList. + * @return dataList The list with concartinated queue data. + */ +QList GAnalytics::Private::persistMessageQueue() +{ + QList dataList; + foreach (QueryBuffer buffer, messageQueue) + { + dataList << buffer.postQuery.toString(); + dataList << buffer.time.toString(dateTimeFormat); + } + + return dataList; +} + +/** + * Reads persistent messages from a file. + * Gets all message data as a QList. + * Two lines in the list build a QueryBuffer object. + */ +void GAnalytics::Private::readMessagesFromFile(const QList &dataList) +{ + QListIterator iter(dataList); + while (iter.hasNext()) + { + QString queryString = iter.next(); + QString dateString = iter.next(); + QUrlQuery query; + query.setQuery(queryString); + QDateTime dateTime = QDateTime::fromString(dateString, dateTimeFormat); + QueryBuffer buffer; + buffer.postQuery = query; + buffer.time = dateTime; + messageQueue.enqueue(buffer); + } +} + +/** + * Change the user id. + * @param userID A string with the user id. + */ +void GAnalytics::Private::setUserID(const QString &userID) +{ + this->userID = userID; + QSettings settings; + settings.setValue("GAnalytics-uid", userID); +} + +/** + * Get the user id. + * User id once created is stored in application settings. + * @return userID A string with the user id. + */ +QString GAnalytics::Private::getUserID() +{ + QSettings settings; + QString userID = settings.value("GAnalytics-uid", QString("")).toString(); + + return userID; +} + +/** + * Get the client id. + * Client id once created is stored in application settings. + * @return clientID A string with the client id. + */ +QString GAnalytics::Private::getClientID() +{ + QSettings settings; + QString clientID; + if (!settings.contains("GAnalytics-cid")) + { + clientID = QUuid::createUuid().toString(); + settings.setValue("GAnalytics-cid", clientID); + } + else + { + clientID = settings.value("GAnalytics-cid").toString(); + } + + return clientID; +} + +/** + * Takes a QUrlQuery object and wrapp it together with + * a QTime object into a QueryBuffer struct. These struct + * will be stored in the message queue. + * @param query + */ +void GAnalytics::Private::enqueQueryWithCurrentTime(const QUrlQuery &query) +{ + QueryBuffer buffer; + buffer.postQuery = query; + buffer.time = QDateTime::currentDateTime(); + + messageQueue.enqueue(buffer); +} + +/** + * Change status of class. Emit signal that status was changed. + * @param doSend + */ +void GAnalytics::Private::setIsSending(bool doSend) +{ + if (doSend) + { + timer.stop(); + } + else + { + timer.start(); + } + + bool changed = (isSending != doSend); + + isSending = doSend; + + if (changed) + { + emit q->isSendingChanged(isSending); + } +} + + +/** + * CONSTRUCTOR GAnalytics + * ------------------------------------------------------------------------------------------------------------ + * Constructs the GAnalytics Object. + * @param parent The application which uses this object. + * @param trackingID + * @param clientID + * @param withGet Determines wheather the messages are send with GET or POST. + */ +GAnalytics::GAnalytics(QObject *parent) +: QObject(parent) +, d(new Private(this)) +{ +} + +GAnalytics::GAnalytics(const QString &trackingID, QObject *parent) +: QObject(parent) +, d(new Private(this)) +{ + setTrackingID(trackingID); +} + +/** + * Destructor of class GAnalytics. + */ +GAnalytics::~GAnalytics() +{ + delete d; +} + +void GAnalytics::setLogLevel(GAnalytics::LogLevel logLevel) +{ + if (d->logLevel != logLevel) + { + d->logLevel = logLevel; + emit logLevelChanged(); + } +} + +GAnalytics::LogLevel GAnalytics::logLevel() const +{ + return d->logLevel; +} + +// SETTER and GETTER +void GAnalytics::setViewportSize(const QString &viewportSize) +{ + if (d->viewportSize != viewportSize) + { + d->viewportSize = viewportSize; + emit viewportSizeChanged(); + } +} + +QString GAnalytics::viewportSize() const +{ + return d->viewportSize; +} + +void GAnalytics::setLanguage(const QString &language) +{ + if (d->language != language) + { + d->language = language; + emit languageChanged(); + } +} + +QString GAnalytics::language() const +{ + return d->language; +} + +void GAnalytics::setTrackingID(const QString &trackingID) +{ + if (d->trackingID != trackingID) + { + d->trackingID = trackingID; + emit trackingIDChanged(); + } +} + +QString GAnalytics::trackingID() const +{ + return d->trackingID; +} + +void GAnalytics::setSendInterval(int milliseconds) +{ + if (d->timer.interval() != milliseconds) + { + d->timer.setInterval(milliseconds); + emit sendIntervalChanged(); + } +} + +int GAnalytics::sendInterval() const +{ + return (d->timer.interval()); +} + +void GAnalytics::startSending() +{ + if (!isSending()) + emit d->postNextMessage(); +} + +bool GAnalytics::isSending() const +{ + return d->isSending; +} + +void GAnalytics::setNetworkAccessManager(QNetworkAccessManager *networkAccessManager) +{ + if (d->networkManager != networkAccessManager) + { + // Delete the old network manager if it was our child + if (d->networkManager && d->networkManager->parent() == this) + { + d->networkManager->deleteLater(); + } + + d->networkManager = networkAccessManager; + } +} + +QNetworkAccessManager *GAnalytics::networkAccessManager() const +{ + return d->networkManager; +} + +static void appendCustomValues(QUrlQuery &query, const QVariantMap &customValues) { + for(QVariantMap::const_iterator iter = customValues.begin(); iter != customValues.end(); ++iter) { + query.addQueryItem(iter.key(), iter.value().toString()); + } +} + + +/** +* SentAppview is called when the user changed the applications view. +* Deprecated because after SDK Version 3.08 and up no more "appview" event: +* Use sendScreenView() instead +* @param appName +* @param appVersion +* @param screenName +*/ +void GAnalytics::sendAppView(const QString &screenName, + const QVariantMap &customValues) +{ + sendScreenView(screenName, customValues); +} + +/** + * Sent screen view is called when the user changed the applications view. + * These action of the user should be noticed and reported. Therefore + * a QUrlQuery is build in this method. It holts all the parameter for + * a http POST. The UrlQuery will be stored in a message Queue. + * @param appName + * @param appVersion + * @param screenName + */ +void GAnalytics::sendScreenView(const QString &screenName, + const QVariantMap &customValues) +{ + d->logMessage(Info, QString("ScreenView: %1").arg(screenName)); + + QUrlQuery query = d->buildStandardPostQuery("screenview"); + query.addQueryItem("cd", screenName); + query.addQueryItem("an", d->appName); + query.addQueryItem("av", d->appVersion); + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * This method is called whenever a button was pressed in the application. + * A query for a POST message will be created to report this event. The + * created query will be stored in a message queue. + * @param eventCategory + * @param eventAction + * @param eventLabel + * @param eventValue + */ +void GAnalytics::sendEvent(const QString &category, const QString &action, + const QString &label, const QVariant &value, + const QVariantMap &customValues) +{ + QUrlQuery query = d->buildStandardPostQuery("event"); + query.addQueryItem("an", d->appName); + query.addQueryItem("av", d->appVersion); + query.addQueryItem("ec", category); + query.addQueryItem("ea", action); + if (! label.isEmpty()) + query.addQueryItem("el", label); + if (value.isValid()) + query.addQueryItem("ev", value.toString()); + + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * Method is called after an exception was raised. It builds a + * query for a POST message. These query will be stored in a + * message queue. + * @param exceptionDescription + * @param exceptionFatal + */ +void GAnalytics::sendException(const QString &exceptionDescription, + bool exceptionFatal, + const QVariantMap &customValues) +{ + QUrlQuery query = d->buildStandardPostQuery("exception"); + query.addQueryItem("an", d->appName); + query.addQueryItem("av", d->appVersion); + + query.addQueryItem("exd", exceptionDescription); + + if (exceptionFatal) + { + query.addQueryItem("exf", "1"); + } + else + { + query.addQueryItem("exf", "0"); + } + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * Session starts. This event will be sent by a POST message. + * Query is setup in this method and stored in the message + * queue. + */ +void GAnalytics::startSession() +{ + QVariantMap customValues; + customValues.insert("sc", "start"); + sendEvent("Session", "Start", QString(), QVariant(), customValues); +} + +/** + * Session ends. This event will be sent by a POST message. + * Query is setup in this method and stored in the message + * queue. + */ +void GAnalytics::endSession() +{ + QVariantMap customValues; + customValues.insert("sc", "end"); + sendEvent("Session", "End", QString(), QVariant(), customValues); +} + +/** + * This function is called by a timer interval. + * The function tries to send a messages from the queue. + * If message was successfully send then this function + * will be called back to send next message. + * If message queue contains more than one message then + * the connection will kept open. + * The message POST is asyncroniously when the server + * answered a signal will be emitted. + */ +void GAnalytics::Private::postMessage() +{ + if (messageQueue.isEmpty()) + { + setIsSending(false); + return; + } + else + { + setIsSending(true); + } + + QString connection = "close"; + if (messageQueue.count() > 1) + { + connection = "keep-alive"; + } + + QueryBuffer buffer = messageQueue.head(); + QDateTime sendTime = QDateTime::currentDateTime(); + qint64 timeDiff = buffer.time.msecsTo(sendTime); + + if(timeDiff > fourHours) + { + // too old. + messageQueue.dequeue(); + emit postNextMessage(); + return; + } + + buffer.postQuery.addQueryItem("qt", QString::number(timeDiff)); + request.setRawHeader("Connection", connection.toUtf8()); + request.setHeader(QNetworkRequest::ContentLengthHeader, buffer.postQuery.toString().length()); + + // Create a new network access manager if we don't have one yet + if (networkManager == NULL) + { + networkManager = new QNetworkAccessManager(this); + } + + QNetworkReply *reply = networkManager->post(request, buffer.postQuery.query(QUrl::EncodeUnicode).toUtf8()); + connect(reply, SIGNAL(finished()), this, SLOT(postMessageFinished())); +} + +/** + * NetworkAccsessManager has finished to POST a message. + * If POST message was successfully send then the message + * query should be removed from queue. + * SIGNAL "postMessage" will be emitted to send next message + * if there is any. + * If message couldn't be send then next try is when the + * timer emits its signal. + */ +void GAnalytics::Private::postMessageFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + int httpStausCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (httpStausCode < 200 || httpStausCode > 299) + { + logMessage(GAnalytics::Error, QString("Error posting message: %s").arg(reply->errorString())); + + // An error ocurred. + setIsSending(false); + return; + } + else + { + logMessage(GAnalytics::Debug, "Message sent"); + } + + messageQueue.dequeue(); + emit postNextMessage(); + reply->deleteLater(); +} + + +/** + * Qut stream to persist class GAnalytics. + * @param outStream + * @param analytics + * @return + */ +QDataStream &operator<<(QDataStream &outStream, const GAnalytics &analytics) +{ + outStream << analytics.d->persistMessageQueue(); + + return outStream; +} + + +/** + * In stream to read GAnalytics from file. + * @param inStream + * @param analytics + * @return + */ +QDataStream &operator >>(QDataStream &inStream, GAnalytics &analytics) +{ + QList dataList; + inStream >> dataList; + analytics.d->readMessagesFromFile(dataList); + + return inStream; +} + +#ifdef QT_QML_LIB +void GAnalytics::classBegin() +{ + // Get the network access manager from the QmlEngine + QQmlContext *context = QQmlEngine::contextForObject(this); + if (context) + { + QQmlEngine *engine = context->engine(); + setNetworkAccessManager(engine->networkAccessManager()); + } +} + +void GAnalytics::componentComplete() +{ +} +#endif // QT_QML_LIB + +#include "ganalytics.moc" + diff --git a/libraries/ganalytics/ganalytics.h b/libraries/ganalytics/ganalytics.h new file mode 100644 index 00000000..f67e737b --- /dev/null +++ b/libraries/ganalytics/ganalytics.h @@ -0,0 +1,108 @@ +#ifndef GANALYTICS_H +#define GANALYTICS_H + +#include +#include + +#ifdef QT_QML_LIB +#include +#endif // QT_QML_LIB + +class QNetworkAccessManager; + +class GAnalytics : public QObject +#ifdef QT_QML_LIB + , public QQmlParserStatus +#endif // QT_QML_LIB +{ + Q_OBJECT +#ifdef QT_QML_LIB + Q_INTERFACES(QQmlParserStatus) +#endif // QT_QML_LIB + Q_ENUMS(LogLevel) + Q_PROPERTY(LogLevel logLevel READ logLevel WRITE setLogLevel NOTIFY logLevelChanged) + Q_PROPERTY(QString viewportSize READ viewportSize WRITE setViewportSize NOTIFY viewportSizeChanged) + Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged) + Q_PROPERTY(QString trackingID READ trackingID WRITE setTrackingID NOTIFY trackingIDChanged) + Q_PROPERTY(int sendInterval READ sendInterval WRITE setSendInterval NOTIFY sendIntervalChanged) + Q_PROPERTY(bool isSending READ isSending NOTIFY isSendingChanged) + +public: + explicit GAnalytics(QObject *parent = 0); + explicit GAnalytics(const QString &trackingID, QObject *parent = 0); + ~GAnalytics(); + +public: + enum LogLevel + { + Debug, + Info, + Error + }; + + void setLogLevel(LogLevel logLevel); + LogLevel logLevel() const; + + // Getter and Setters + void setViewportSize(const QString &viewportSize); + QString viewportSize() const; + + void setLanguage(const QString &language); + QString language() const; + + void setTrackingID(const QString &trackingID); + QString trackingID() const; + + void setSendInterval(int milliseconds); + int sendInterval() const; + + void startSending(); + bool isSending() const; + + /// Get or set the network access manager. If none is set, the class creates its own on the first request + void setNetworkAccessManager(QNetworkAccessManager *networkAccessManager); + QNetworkAccessManager *networkAccessManager() const; + +#ifdef QT_QML_LIB + // QQmlParserStatus interface + void classBegin(); + void componentComplete(); +#endif // QT_QML_LIB + +public slots: + void sendScreenView(const QString &screenName, + const QVariantMap &customValues = QVariantMap()); + void sendAppView(const QString &screenName, + const QVariantMap &customValues = QVariantMap()); + void sendEvent(const QString &category, + const QString &action, + const QString &label = QString(), + const QVariant &value = QVariant(), + const QVariantMap &customValues = QVariantMap()); + void sendException(const QString &exceptionDescription, + bool exceptionFatal = true, + const QVariantMap &customValues = QVariantMap()); + void startSession(); + void endSession(); + + +signals: + void logLevelChanged(); + void viewportSizeChanged(); + void languageChanged(); + void trackingIDChanged(); + void sendIntervalChanged(); + void isSendingChanged(bool isSending); + +private: + class Private; + Private *d; + + friend QDataStream& operator<<(QDataStream &outStream, const GAnalytics &analytics); + friend QDataStream& operator>>(QDataStream &inStream, GAnalytics &analytics); +}; + +QDataStream& operator<<(QDataStream &outStream, const GAnalytics &analytics); +QDataStream& operator>>(QDataStream &inStream, GAnalytics &analytics); + +#endif // GANALYTICS_H