#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"