diff options
author | Caroline Chao <caroline.chao@digia.com> | 2014-09-22 14:24:56 +0200 |
---|---|---|
committer | Caroline Chao <caroline.chao@digia.com> | 2014-09-24 15:07:29 +0200 |
commit | 5c90b15c31eca21cc8294624d34ec9417e5120a8 (patch) | |
tree | 61bf2f76e6422b6c0269e86e4c73df1262bf87d2 | |
parent | 235dcb8a5c6797eb5725f22ee356cd9fe11e241b (diff) |
Handle offline mode
- Load data from cache at startup even if not connection is there.
- Add time lists to cache
- Ignore Twitter reply when empty data are received from a request
- Cache feedback when no network is available and push feedback to
the cloud when the connection is restored.
- Cache favorite when offline
Change-Id: I7892881d710eb4fc96628078f59064ff9e8cd196
Reviewed-by: Niels Weber <niels.weber@digia.com>
-rw-r--r-- | qml/components/ModelsSingleton.qml | 109 | ||||
-rw-r--r-- | qml/components/TrackSwitcher.qml | 6 | ||||
-rw-r--r-- | qml/components/TweetModel.qml | 36 | ||||
-rw-r--r-- | qml/main.qml | 1 | ||||
-rw-r--r-- | src/applicationclient.cpp | 161 | ||||
-rw-r--r-- | src/applicationclient.h | 14 | ||||
-rw-r--r-- | src/model.cpp | 45 | ||||
-rw-r--r-- | src/model.h | 4 |
8 files changed, 303 insertions, 73 deletions
diff --git a/qml/components/ModelsSingleton.qml b/qml/components/ModelsSingleton.qml index 4dbf25b..ce2c0e7 100644 --- a/qml/components/ModelsSingleton.qml +++ b/qml/components/ModelsSingleton.qml @@ -45,7 +45,7 @@ import TalkSchedule 1.0 QtObject { id: object - property string conferenceId: "" + property string conferenceId: applicationClient.currentConferenceId property var currentConferenceTracks: [] property var currentConferenceEvents: [] property var currentConferenceDays: [] @@ -87,7 +87,7 @@ QtObject { } property var favoriteModel: Model { - // do not save favorite + fileNameTag: "FavoriteObject" onDataReady: getFavoriteIds() } @@ -154,24 +154,17 @@ QtObject { function queryUserConferenceFavorites() { var favQuery = applicationClient.client.query({ "objectType": "objects.Favorite", - "query": { - "favoriteEvent.id" : { "$in" : currentConferenceEvents } - }, - "include": { - "events": { - "objectType": "objects.Event", - "query": {"id": "$.favoriteEvent.id"}, - "result": "selectOne", - "include": { - "tracks": { - "objectType": "objects.Track", - "query": {"id": "$.track.id"}, - "result": "selectOne" - } - } - } - } - }) + "query": { + "favoriteEvent.id" : { "$in" : currentConferenceEvents } + }, + "include": { + "events": { + "objectType": "objects.Event", + "query": {"id": "$.favoriteEvent.id"}, + "result": "selectOne", + } + } + }) favQuery.finished.connect(function() { favoriteModel.onFinished(favQuery) }) @@ -180,22 +173,25 @@ QtObject { function saveFeedback(fbtext, eventId, rating) { //console.log("saveFeedback") - var reply = applicationClient.client.create({ - "objectType": "objects.Feedback", - "event": { - "id": eventId, - "objectType": "objects.Event" - }, - "rating": rating, - "feedbackText": fbtext - }) -// reply.finished.connect(function() { -// if (reply.errorType !== EnginioReply.NoError) { -// console.log("Failed to save feedback.\n") -// } else { -// console.log("Successfully saved feedback.\n") -// } -// }) + var feedback = { + "objectType": "objects.Feedback", + "event": { + "id": eventId, + "objectType": "objects.Event" + }, + "rating": rating, + "feedbackText": fbtext + } + var reply = applicationClient.client.create(feedback) + reply.finished.connect(function() { + if (reply.errorType !== EnginioReply.NoError) { + console.log("Failed to save feedback.\n") + if (reply.errorType === EnginioReply.NetworkError) + applicationClient.cacheFeedback(JSON.stringify(feedback)) + } else { + console.log("Successfully saved feedback.\n") + } + }) } function saveFavorite(saveEventId) @@ -206,19 +202,27 @@ QtObject { } busy = true //console.log("start saving favorite") - var reply = applicationClient.client.create({ - "objectType": "objects.Favorite", - "favoriteEvent": { - "id": saveEventId, - "objectType": "objects.Event" - } - }) + var favorite = { + "objectType": "objects.Favorite", + "favoriteEvent": { + "id": saveEventId, + "objectType": "objects.Event" + } + } + var reply = applicationClient.client.create(favorite) + var addFavorite = true + + eventModel.addFavorite(saveEventId) + // save to file so favorite data can be retrieves at startup + favoriteModel.appendAndSaveFavorites(saveEventId, addFavorite) + favoriteModel.addRow({"events_id":saveEventId}) reply.finished.connect(function() { - if (reply.errorType !== EnginioReply.NoError) + if (reply.errorType !== EnginioReply.NoError) { console.log("Failed to create an Favorite:\n" + JSON.stringify(reply.data, undefined, 2) + "\n\n") - else - eventModel.addFavorite(saveEventId) + if (reply.errorType === EnginioReply.NetworkError) + applicationClient.cacheFavorite(saveEventId, addFavorite) + } busy = false //console.log("favorite save done") }) @@ -241,9 +245,15 @@ QtObject { } }) + eventModel.removeFavorite(removeEventId) + var removeFavorite = false + favoriteModel.appendAndSaveFavorites(removeEventId, removeFavorite) + favoriteModel.removeRow(favoriteModel.indexOf("events_id", removeEventId)) favoriteQuery.finished.connect(function() { if (favoriteQuery.errorType !== EnginioReply.NoError) { console.log("Failed to query an Favorite:\n" + JSON.stringify(favoriteQuery.data, undefined, 2) + "\n\n") + if (favoriteQuery.errorType === EnginioReply.NetworkError) + applicationClient.cacheFavorite(removeEventId, removeFavorite) } else { if (favoriteQuery.data.results.length > 0) { @@ -252,10 +262,11 @@ QtObject { "id": favoriteQuery.data.results[0].id }) reply.finished.connect(function() { - if (favoriteQuery.errorType !== EnginioReply.NoError) + if (favoriteQuery.errorType !== EnginioReply.NoError) { console.log("Failed to remove an Favorite:\n" + JSON.stringify(reply.data, undefined, 2) + "\n\n") - else - eventModel.removeFavorite(removeEventId) + if (favoriteQuery.errorType === EnginioReply.NetworkError) + applicationClient.cacheFavorite(removeEventId, removeFavorite) + } }) } //console.log("favorite remove done") diff --git a/qml/components/TrackSwitcher.qml b/qml/components/TrackSwitcher.qml index 4d5d5c2..5a5c3bf 100644 --- a/qml/components/TrackSwitcher.qml +++ b/qml/components/TrackSwitcher.qml @@ -95,7 +95,11 @@ Rectangle { Connections { target: daysWitcher - onDayIdChanged: ModelsSingleton.timeListModel.tracksTodayModel = currentDayTracksModel + onDayIdChanged: { + ModelsSingleton.timeListModel.fileNameTag = "TimeObject." + daysWitcher.dayId + ModelsSingleton.timeListModel.load() + ModelsSingleton.timeListModel.tracksTodayModel = currentDayTracksModel + } } function setFirstEvent(time) diff --git a/qml/components/TweetModel.qml b/qml/components/TweetModel.qml index e9aec27..15fd310 100644 --- a/qml/components/TweetModel.qml +++ b/qml/components/TweetModel.qml @@ -66,17 +66,19 @@ Item { req.onreadystatechange = function() { status = req.readyState; if (status === XMLHttpRequest.DONE) { - var objectArray = JSON.parse(req.responseText); - if (objectArray.errors !== undefined) - console.log("Error fetching tweets: " + objectArray.errors[0].message) - else { - for (var key in objectArray.statuses) { - var jsonObject = objectArray.statuses[key]; - tweets.append(jsonObject); + if (req.responseText !== "") { // Nothing was retrieved, network error + var objectArray = JSON.parse(req.responseText); + if (objectArray.errors !== undefined) + console.log("Error fetching tweets: " + objectArray.errors[0].message) + else { + for (var key in objectArray.statuses) { + var jsonObject = objectArray.statuses[key]; + tweets.append(jsonObject); + } } + if (wasLoading == true) + tweetModel.isLoaded() } - if (wasLoading == true) - tweetModel.isLoaded() } wasLoading = (status === XMLHttpRequest.LOADING); } @@ -90,13 +92,15 @@ Item { authReq.setRequestHeader("Authorization", "Basic " + Qt.btoa(consumerKey + ":" + consumerSecret)); authReq.onreadystatechange = function() { if (authReq.readyState === XMLHttpRequest.DONE) { - var jsonResponse = JSON.parse(authReq.responseText); - if (jsonResponse.errors !== undefined) - console.log("Authentication error: " + jsonResponse.errors[0].message) - else - bearerToken = jsonResponse.access_token; - - reload() + if (authReq.responseText !== "") { // Nothing was retrieved, network error + var jsonResponse = JSON.parse(authReq.responseText); + if (jsonResponse.errors !== undefined) { + console.log("Authentication error: " + jsonResponse.errors[0].message) + } else { + bearerToken = jsonResponse.access_token; + reload() + } + } } } authReq.send("grant_type=client_credentials"); diff --git a/qml/main.qml b/qml/main.qml index 2553430..c931b35 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -258,6 +258,7 @@ ApplicationWindow { stack.forceActiveFocus() } } + Component.onCompleted: if (visible) animation.running = true } onVisibleChanged: if (visible) animation.running = true } diff --git a/src/applicationclient.cpp b/src/applicationclient.cpp index 4d5989a..079241f 100644 --- a/src/applicationclient.cpp +++ b/src/applicationclient.cpp @@ -44,12 +44,15 @@ #include <Enginio/enginiomodel.h> #include <Enginio/enginioreply.h> #include <Enginio/enginiooauth2authentication.h> +#include <QJsonArray> +#include <QJsonDocument> #include <QJsonObject> #include "fileio.h" #include "model.h" #include <QStringList> #include <QDebug> #include <QTimer> +#include <QIODevice> #define QUOTE_(x) #x #define QUOTE(x) QUOTE_(x) @@ -59,6 +62,8 @@ ApplicationClient::ApplicationClient() : init(true) { m_settings = new FileIO(this, "settings.txt"); + m_feedbackCache = new FileIO(this, "feedback.txt"); + m_favoriteCache = new FileIO(this, "favorite.txt"); timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(authenticate())); @@ -69,19 +74,24 @@ ApplicationClient::ApplicationClient() const QByteArray backId = QByteArray(BACKEND_ID); m_client->setBackendId(backId); + m_details = new QQmlPropertyMap(this); + m_details->insert(QLatin1String("location"), QVariant("")); + m_details->insert(QLatin1String("title"), QVariant("")); + m_details->insert(QLatin1String("TwitterTag"), QVariant("")); + m_details->insert(QLatin1String("infopage"), QVariant("")); + m_conferenceModel = new Model(this); m_conferenceModel->setFileNameTag("ConferencesObject"); + QString cachedConferenceId = m_settings->read(); + if (!cachedConferenceId.isEmpty()) { + m_conferenceModel->load(); + setCurrentConferenceId(cachedConferenceId); + } connect(m_client, SIGNAL(sessionAuthenticated(EnginioReply*)), this, SLOT(authenticationSuccess(EnginioReply*))); connect(m_client, SIGNAL(sessionAuthenticationError(EnginioReply*)), this, SLOT(errorClient(EnginioReply*))); connect(m_client, SIGNAL(error(EnginioReply*)), this, SLOT(errorClient(EnginioReply*))); getUserCredentials(); - - m_details = new QQmlPropertyMap(this); - m_details->insert(QLatin1String("location"), QVariant("")); - m_details->insert(QLatin1String("title"), QVariant("")); - m_details->insert(QLatin1String("TwitterTag"), QVariant("")); - m_details->insert(QLatin1String("infopage"), QVariant("")); } void ApplicationClient::errorClient(EnginioReply *reply) @@ -143,6 +153,9 @@ void ApplicationClient::authenticate() void ApplicationClient::authenticationSuccess(EnginioReply *reply) { //qDebug() << "Query the conference"; + emptyFavoriteCache(); + emptyFeedbackCache(); + int timeout = (reply->data().value("expires_in").toInt() - 20*60)*1000; timer->setSingleShot(true); timer->start(timeout); @@ -201,3 +214,139 @@ bool ApplicationClient::eventFilter(QObject *object, QEvent *event) #endif return QObject::eventFilter(object, event); } + +void ApplicationClient::cacheFeedback(QString feedback) +{ + QString content = m_feedbackCache->read(); + feedbackArray = QJsonDocument::fromJson(content.toUtf8()).array(); + feedbackArray.append(QJsonValue::fromVariant(feedback.toUtf8())); + QJsonDocument doc(feedbackArray); + m_feedbackCache->write(QString::fromUtf8(doc.toJson())); +} + +void ApplicationClient::emptyFeedbackCache() +{ + QString content = m_feedbackCache->read(); + if (content.isEmpty()) + return; + feedbackArray = QJsonDocument::fromJson(content.toUtf8()).array(); + if (!feedbackArray.isEmpty()) { + QString object = feedbackArray.at(0).toString(); + QJsonObject query = QJsonDocument::fromJson(object.toUtf8()).object(); + query["objectType"] = QString::fromUtf8("objects.Feedback"); + const EnginioReply *replyFeedback = m_client->create(query); + connect(replyFeedback, SIGNAL(finished(EnginioReply*)), this, SLOT(createFeedbackReply(EnginioReply*))); + } +} + +void ApplicationClient::createFeedbackReply(EnginioReply *reply) +{ + if (reply->errorType() == Enginio::NoError) { + feedbackArray.takeAt(0); + QJsonDocument doc(feedbackArray); + m_feedbackCache->write(QString::fromUtf8(doc.toJson())); + emptyFeedbackCache(); + } + reply->deleteLater(); +} + +void ApplicationClient::cacheFavorite(QString eventId, bool isAdded) +{ + QString content = m_favoriteCache->read(); + favoriteArray = QJsonDocument::fromJson(content.toUtf8()).array(); + bool hasBeenFound = false; + for (int i = 0; i < favoriteArray.count(); i++) { + QJsonObject favObject = favoriteArray.at(i).toObject(); + if (favObject.value("eventId").toString() == eventId) { + bool status = favObject.value("isAdded").toBool(); + if (status == isAdded) { // no change. should not happen though + return; + } else { + favoriteArray.removeAt(i); + hasBeenFound = true; + } + } + } + if (!hasBeenFound) { + QJsonObject objectToAdd; + objectToAdd["eventId"] = eventId; + objectToAdd["isAdded"] = isAdded; + favoriteArray.append(objectToAdd); + } + QJsonDocument doc(favoriteArray); + m_favoriteCache->write(QString::fromUtf8(doc.toJson())); +} + +void ApplicationClient::emptyFavoriteCache() +{ + QString content = m_favoriteCache->read(); + if (content.isEmpty()) + return; + + favoriteArray = QJsonDocument::fromJson(content.toUtf8()).array(); + if (!favoriteArray.isEmpty()) { + QJsonObject object = favoriteArray.at(0).toObject(); + + QJsonObject queryFav; + queryFav["objectType"] = QString::fromUtf8("objects.Favorite"); + QJsonObject favEvent; + favEvent["id"] = object.value("eventId").toString(); + favEvent["objectType"] = QString::fromUtf8("objects.Event"); + QJsonObject favEventObject; + favEventObject["favoriteEvent"] = favEvent; + + bool isAdded = object.value("isAdded").toBool(); + + if (isAdded) { + queryFav["favoriteEvent"] = favEvent; + const EnginioReply *replyCreateFavorite = m_client->create(queryFav); + connect(replyCreateFavorite, SIGNAL(finished(EnginioReply*)), this, SLOT(createFavoriteReply(EnginioReply*))); + } else { + queryFav["query"] = favEventObject; + const EnginioReply *replyQueryFavorite = m_client->query(queryFav); + connect(replyQueryFavorite, SIGNAL(finished(EnginioReply*)), this, SLOT(queryFavoriteReply(EnginioReply*))); + } + } +} + +void ApplicationClient::queryFavoriteReply(EnginioReply *reply) +{ + if (reply->errorType() == Enginio::NoError) { + QJsonArray array = reply->data().value("results").toArray(); + if (array.count() == 0) { + favoriteArray.takeAt(0); + QJsonDocument doc(favoriteArray); + m_favoriteCache->write(QString::fromUtf8(doc.toJson())); + emptyFavoriteCache(); + return; + } + QJsonObject queryFav; + queryFav["objectType"] = QString::fromUtf8("objects.Favorite"); + queryFav["id"] = array.at(0).toObject().value("id").toString(); + const EnginioReply *replyDeleteFavorite = m_client->remove(queryFav); + connect(replyDeleteFavorite, SIGNAL(finished(EnginioReply*)), this, SLOT(removeFavoriteReply(EnginioReply*))); + } + reply->deleteLater(); +} + +void ApplicationClient::createFavoriteReply(EnginioReply *reply) +{ + if (reply->errorType() == Enginio::NoError) { + favoriteArray.takeAt(0); + QJsonDocument doc(favoriteArray); + m_favoriteCache->write(QString::fromUtf8(doc.toJson())); + emptyFavoriteCache(); + } + reply->deleteLater(); +} + +void ApplicationClient::removeFavoriteReply(EnginioReply *reply) +{ + if (reply->errorType() == Enginio::NoError) { + favoriteArray.takeAt(0); + QJsonDocument doc(favoriteArray); + m_favoriteCache->write(QString::fromUtf8(doc.toJson())); + emptyFavoriteCache(); + } + reply->deleteLater(); +} diff --git a/src/applicationclient.h b/src/applicationclient.h index 6492f3f..e1ab3c1 100644 --- a/src/applicationclient.h +++ b/src/applicationclient.h @@ -44,6 +44,7 @@ #include <QObject> #include <QString> #include <QtQml/QQmlPropertyMap> +#include <QJsonArray> class EnginioClient; class EnginioModel; @@ -72,6 +73,9 @@ public: QQmlPropertyMap *currentConferenceDetails() const { return m_details; } + Q_INVOKABLE void cacheFeedback(QString feedback); + Q_INVOKABLE void cacheFavorite(QString favorite, bool isAdded); + protected: void getUserCredentials(); void createUser(); @@ -90,13 +94,21 @@ public slots: void errorClient(EnginioReply *reply); void userCreationReply(EnginioReply *reply); void queryConferenceReply(EnginioReply *reply); + void createFeedbackReply(EnginioReply *reply); + void createFavoriteReply(EnginioReply *reply); + void removeFavoriteReply(EnginioReply *reply); + void queryFavoriteReply(EnginioReply *reply); void authenticate(); + void emptyFeedbackCache(); + void emptyFavoriteCache(); private: EnginioClient *m_client; Model *m_conferenceModel; FileIO *m_userData; FileIO *m_settings; + FileIO *m_feedbackCache; + FileIO *m_favoriteCache; QString currentUsername; QString currentPassword; EnginioOAuth2Authentication *authenticator; @@ -104,6 +116,8 @@ private: QQmlPropertyMap *m_details; QTimer *timer; bool init; + QJsonArray feedbackArray; + QJsonArray favoriteArray; }; #endif // APPLICATIONCLIENT_H diff --git a/src/model.cpp b/src/model.cpp index 70637b6..46cff58 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -226,6 +226,51 @@ bool Model::load() return true; } +bool Model::appendAndSaveFavorites(const QString &data, bool isAdded) +{ + if (fileNameTag().isEmpty()) + return false; + + QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + QFile file(QString("%1/%2.%3").arg(path).arg(m_fileNameTag).arg(m_conferenceId)); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "On load couldn't open file" << file.fileName(); + return false; + } + + QString fileContent; + QString line; + QTextStream t(&file); + do { + line = t.readLine(); + fileContent += line; + } while (!line.isNull()); + + file.close(); + + QJsonObject object = QJsonDocument::fromJson(fileContent.toUtf8()).object(); + QJsonArray array = object.value("results").toArray(); + if (isAdded) { + QJsonObject newFav; + QJsonObject fav; + fav["id"] = data; + fav["objectType"] = QString::fromUtf8("objects.Event"); + newFav["favoriteEvent"] = fav; + newFav["events_id"] = data; + array.append(newFav); + } else { + for (int i = 0; i < array.count(); i++) { + QJsonObject tempObject = array.at(i).toObject(); + if (tempObject.value("favoriteEvent").toObject().value("id") == data) { + array.removeAt(i); + break; + } + } + } + object["results"] = array; + return save(object); +} + bool Model::parse(const QJsonObject &object) { bool dataHasChanged = true; diff --git a/src/model.h b/src/model.h index 60b968a..f608840 100644 --- a/src/model.h +++ b/src/model.h @@ -77,6 +77,9 @@ public: Q_INVOKABLE void removeRow(int index); Q_INVOKABLE QVariant indexOf(const QString &role, QVariant value); + Q_INVOKABLE bool load(); + Q_INVOKABLE bool appendAndSaveFavorites(const QString &data, bool isAdded); + Q_SIGNALS: void clientChanged(); void conferenceIdChanged(); @@ -88,7 +91,6 @@ public Q_SLOTS: private: bool save(const QJsonObject &object); - bool load(); bool parse(const QJsonObject &object); QHash<int, QByteArray> m_roleNames; |