diff --git a/.gitmodules b/.gitmodules index 2be744c1..71fb2c3b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "nymea-remoteproxy"] path = nymea-remoteproxy url = https://github.com/guh/nymea-remoteproxy.git +[submodule "qtcloudmessaging"] + path = qtcloudmessaging + url = https://github.com/qt/qtcloudmessaging.git +[submodule "QtFirebase"] + path = QtFirebase + url = https://github.com/Larpon/QtFirebase.git diff --git a/QtFirebase b/QtFirebase new file mode 160000 index 00000000..593032a7 --- /dev/null +++ b/QtFirebase @@ -0,0 +1 @@ +Subproject commit 593032a7c613d40c55212ec10790400fd4a30c8f diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index d66e59b4..f42dd7c8 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -605,6 +605,61 @@ void AWSClient::getId() }); } +void AWSClient::registerPushNotificationEndpoint(const QString ®istrationId) +{ + if (!isLoggedIn()) { + qWarning() << "Not logged in at AWS. Can't register push endpoint"; + return; + } + if (tokensExpired()) { + qDebug() << "Cannot register push endpoint. Need to refresh our tokens"; + refreshAccessToken(); + m_callQueue.append(QueuedCall("registerPushNotificationEndpoint", registrationId)); + return; + } + qDebug() << "Registering push notification endpoint."; + + QUrl url(QString("https://%1/notifications/endpoints/%2").arg(m_configs.at(m_usedConfigIndex).apiEndpoint).arg(m_userId)); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("x-api-idToken", m_idToken); + + qDebug() << "POST" << url.toString(); + qDebug() << "HEADERS:"; + foreach (const QByteArray &hdr, request.rawHeaderList()) { + qDebug() << hdr << ":" << request.rawHeader(hdr); + } + + QVariantMap payload; + payload.insert("registrationId", registrationId); + payload.insert("channel", "GCM"); + payload.insert("mobileDeviceDisplayName", "test device"); + payload.insert("mobileDeviceUuid", "12345678"); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(payload); + + QNetworkReply *reply = m_nam->post(request, jsonDoc.toJson(QJsonDocument::Compact)); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Error registering push notification endpoint:" << reply->error() << reply->errorString() << qUtf8Printable(data); +// emit deleteAccountResult(LoginErrorUnknownError); + return; + } + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Failed to parse JSON from server" << error.errorString() << qUtf8Printable(data); +// emit deleteAccountResult(LoginErrorUnknownError); + return; + } +// emit deleteAccountResult(LoginErrorNoError); +// logout(); + qDebug() << "Push notification endpoint registered" << data; + }); + +} + QByteArray AWSClient::idToken() const { return m_idToken; diff --git a/libnymea-app-core/connection/awsclient.h b/libnymea-app-core/connection/awsclient.h index 3ef89562..58ee2dc2 100644 --- a/libnymea-app-core/connection/awsclient.h +++ b/libnymea-app-core/connection/awsclient.h @@ -118,6 +118,8 @@ public: Q_INVOKABLE bool postToMQTT(const QString &boxId, const QString ×tamp, std::function callback); Q_INVOKABLE void getId(); + Q_INVOKABLE void registerPushNotificationEndpoint(const QString ®istrationId); + bool tokensExpired() const; QByteArray idToken() const; QString cognitoIdentityId() const; diff --git a/libnymea-app-core/connection/cloudtransport.cpp b/libnymea-app-core/connection/cloudtransport.cpp index e8dc0071..53e48daf 100644 --- a/libnymea-app-core/connection/cloudtransport.cpp +++ b/libnymea-app-core/connection/cloudtransport.cpp @@ -95,13 +95,13 @@ NymeaTransportInterface::ConnectionState CloudTransport::connectionState() const void CloudTransport::sendData(const QByteArray &data) { - qDebug() << "should send" << data; +// qDebug() << "Cloud transport: Sending data:" << data; m_remoteproxyConnection->sendData(data); } void CloudTransport::ignoreSslErrors(const QList &errors) { - qDebug() << "Ignoring SSL errors" << errors; + qDebug() << "CloudTransport: Ignoring SSL errors" << errors; m_remoteproxyConnection->ignoreSslErrors(errors); } diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index ecbe52f0..6b85a26f 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -13,6 +13,7 @@ include(../config.pri) include(../nymea-remoteproxy/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pri) + QT -= gui QT += network websockets bluetooth diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index e3895e19..860b8221 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -26,6 +26,8 @@ #include #ifdef Q_OS_ANDROID +#include +#include #include #endif @@ -69,6 +71,22 @@ int main(int argc, char *argv[]) Engine::instance(); QQmlApplicationEngine *engine = new QQmlApplicationEngine(); + +#ifdef Q_OS_ANDROID + QCloudMessaging *pushServices = new QCloudMessaging(); + QCloudMessagingFirebaseProvider *m_firebaseService = new QCloudMessagingFirebaseProvider(); + + QVariantMap provider_params; + provider_params["SERVER_API_KEY"] = "AIzaSyAvKQXY4-kZw9Y7MTqVDoF2XCvC7fnhKUs"; + + pushServices->registerProvider("GoogleFireBase", m_firebaseService, provider_params); + pushServices->connectClient("GoogleFireBase", "nymea:app", QVariantMap()); + + pushServices->subscribeToChannel("ChatRoom", "GoogleFireBase", "nymea:app"); + + engine->rootContext()->setContextProperty("pushServices", pushServices); +#endif + #ifdef BRANDING engine->rootContext()->setContextProperty("appBranding", BRANDING); #else diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index 4fba9a75..53049f53 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -2,7 +2,7 @@ TEMPLATE=app TARGET=nymea-app include(../config.pri) -QT += network qml quick quickcontrols2 svg websockets bluetooth +QT += network qml quick quickcontrols2 svg websockets bluetooth #cloudmessaging INCLUDEPATH += $$top_srcdir/libnymea-common \ $$top_srcdir/libnymea-app-core @@ -32,26 +32,34 @@ equals(STYLES_PATH, "") { RESOURCES += $${STYLES_PATH}/styles.qrc } -contains(ANDROID_TARGET_ARCH,armeabi-v7a) { - ANDROID_EXTRA_LIBS = \ - /opt/android-openssl/prebuilt/armeabi-v7a/libcrypto.so \ - /opt/android-openssl/prebuilt/armeabi-v7a/libssl.so -} - android { ANDROID_PACKAGE_SOURCE_DIR = $$PWD/../packaging/android - QT += androidextras +# QTFIREBASE_CONFIG+=messaging +# QTFIREBASE_SDK_PATH=/opt/firebase_cpp_sdk/ +# include(../QtFirebase/qtfirebase.pri) + + INCLUDEPATH += /opt/firebase_cpp_sdk/include + + QT += androidextras cloudmessagingfirebase DISTFILES += \ $$ANDROID_PACKAGE_SOURCE_DIR/AndroidManifest.xml \ + $$ANDROID_PACKAGE_SOURCE_DIR/google-services.json \ $$ANDROID_PACKAGE_SOURCE_DIR/gradle/wrapper/gradle-wrapper.jar \ $$ANDROID_PACKAGE_SOURCE_DIR/gradlew \ $$ANDROID_PACKAGE_SOURCE_DIR/res/values/libs.xml \ $$ANDROID_PACKAGE_SOURCE_DIR/build.gradle \ $$ANDROID_PACKAGE_SOURCE_DIR/gradle/wrapper/gradle-wrapper.properties \ $$ANDROID_PACKAGE_SOURCE_DIR/gradlew.bat \ + $$ANDROID_PACKAGE_SOURCE_DIR/src/io/guh/nymeaapp/NymeaAppActivity.java \ + $$ANDROID_PACKAGE_SOURCE_DIR/src/io/guh/nymeaapp/NymeaAppNotificationService.java \ $$ANDROID_PACKAGE_SOURCE_DIR/LICENSE + + + ANDROID_EXTRA_LIBS = \ + /opt/android-openssl/prebuilt/armeabi-v7a/libcrypto.so \ + /opt/android-openssl/prebuilt/armeabi-v7a/libssl.so } macx: { diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 433648a9..5e370dd2 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -160,7 +160,7 @@ Page { spacing: app.margins visible: Engine.deviceManager.fetchingData BusyIndicator { - anchors.horizontalCenter: parent.horizontalCenter + Layout.alignment: Qt.AlignHCenter running: parent.visible } Label { diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 7a4ca9bb..578f5fb4 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -6,6 +6,8 @@ import Qt.labs.settings 1.0 import QtQuick.Window 2.3 import Nymea 1.0 +//import QtFirebase 1.0 + ApplicationWindow { id: app visible: true @@ -51,6 +53,10 @@ ApplicationWindow { Component.onCompleted: { pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml")) + + var clientUuid = pushServices.clientToken("GoogleFireBase", "nymea:app"); + print("Messaging client uuid:", clientUuid) + Engine.awsClient.registerPushNotificationEndpoint(clientUuid); } Connections { @@ -375,6 +381,58 @@ ApplicationWindow { } } + Connections { + target:pushServices + onMessageReceived:{ + console.log("Message to " + providerId + " service to " + clientId + " client.") + console.log("Message: " + message) + + var msg_in_json = JSON.parse(message); + + // Example to respond to embedded system request: + if (msg_in_json.command === "REQUESTING_TEMPERATURE") + embeddedPublishTemperatureToServer(msg_in_json.serverID, mydevicecommand.getTemperature()); + + // Or firebase the message itself is a container of the info. + updateGameNotification(message); + } + + onServiceStateUpdated: { + print("push service state updated", state) + } + + // Own Uuid to be used or broadcasted to server. + onClientTokenReceived: { + + console.log("MY Uuid:"+rid) + + // Id this is server code: + serverUuid = rid; + + // Id this is client code: + clientUuid = rid; + + } + } + + +// Messaging { +// id: messaging + +// onReadyChanged: { +// App.log("Messaging.ready", ready) +// } +// onTokenChanged: { +// App.log("Messaging.token", token) +// } +// onDataChanged: { +// App.log("Messaging.data", JSON.stringify(data)) +// } +// onMessageReceived: { +// App.log("onMessageReceived","Messaging.data", JSON.stringify(data)) +// } +// } + KeyboardLoader { id: keyboardRect anchors { left: parent.left; bottom: parent.bottom; right: parent.right } diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml index d72575a6..c3cd3ee5 100644 --- a/packaging/android/AndroidManifest.xml +++ b/packaging/android/AndroidManifest.xml @@ -1,7 +1,7 @@ - + @@ -32,6 +32,8 @@ + + @@ -55,15 +57,24 @@ * minimal - useful for Quick Controls 2 apps, it is much faster than "full" * none - useful for apps that don't use any of the above Qt modules --> - + - + - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/android/res/drawable-hdpi/ic_stat_notificationicon.png b/packaging/android/res/drawable-hdpi/ic_stat_notificationicon.png new file mode 100644 index 00000000..8918827f Binary files /dev/null and b/packaging/android/res/drawable-hdpi/ic_stat_notificationicon.png differ diff --git a/packaging/android/res/drawable-ldpi/icon.png b/packaging/android/res/drawable-ldpi/icon.png new file mode 100644 index 00000000..bf47e160 Binary files /dev/null and b/packaging/android/res/drawable-ldpi/icon.png differ diff --git a/packaging/android/res/drawable-mdpi/ic_stat_notificationicon.png b/packaging/android/res/drawable-mdpi/ic_stat_notificationicon.png new file mode 100644 index 00000000..9501aea4 Binary files /dev/null and b/packaging/android/res/drawable-mdpi/ic_stat_notificationicon.png differ diff --git a/packaging/android/res/drawable-xhdpi/ic_stat_notificationicon.png b/packaging/android/res/drawable-xhdpi/ic_stat_notificationicon.png new file mode 100644 index 00000000..3154f6d4 Binary files /dev/null and b/packaging/android/res/drawable-xhdpi/ic_stat_notificationicon.png differ diff --git a/packaging/android/res/drawable-xxhdpi/ic_stat_notificationicon.png b/packaging/android/res/drawable-xxhdpi/ic_stat_notificationicon.png new file mode 100644 index 00000000..2b5d2069 Binary files /dev/null and b/packaging/android/res/drawable-xxhdpi/ic_stat_notificationicon.png differ diff --git a/packaging/android/res/drawable-xxxhdpi/ic_stat_notificationicon.png b/packaging/android/res/drawable-xxxhdpi/ic_stat_notificationicon.png new file mode 100644 index 00000000..1eebf05a Binary files /dev/null and b/packaging/android/res/drawable-xxxhdpi/ic_stat_notificationicon.png differ diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java new file mode 100644 index 00000000..651b3bea --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java @@ -0,0 +1,44 @@ +package io.guh.nymeaapp; +import android.util.Log; +import android.content.Intent; +import android.os.Bundle; +import com.google.firebase.messaging.MessageForwardingService; + +public class NymeaAppActivity extends org.qtproject.qt5.android.bindings.QtActivity +{ + // The key in the intent's extras that maps to the incoming message's message ID. Only sent by + // the server, GmsCore sends EXTRA_MESSAGE_ID_KEY below. Server can't send that as it would get + // stripped by the client. + private static final String EXTRA_MESSAGE_ID_KEY_SERVER = "message_id"; + + // An alternate key value in the intent's extras that also maps to the incoming message's message + // ID. Used by upstream, and set by GmsCore. + private static final String EXTRA_MESSAGE_ID_KEY = "google.message_id"; + + // The key in the intent's extras that maps to the incoming message's sender value. + private static final String EXTRA_FROM = "google.message_id"; + + + @Override + protected void onNewIntent(Intent intent) + { + +// Bundle extras = intent.getExtras(); +// String from = extras.getString(EXTRA_FROM); +// String messageId = extras.getString(EXTRA_MESSAGE_ID_KEY); +// Log.d("*************** messageid", messageId); +//// Log.d("Bundle", extras); + +// if (messageId == null) { +// messageId = extras.getString(EXTRA_MESSAGE_ID_KEY_SERVER); +// } +// // if (from != null && messageId != null) { +// Intent message = new Intent(this, MessageForwardingService.class); +// message.setAction(MessageForwardingService.ACTION_REMOTE_INTENT); +// message.putExtras(intent); +// message.setData(intent.getData()); +// startService(message); +// // } + setIntent(intent); + } +} diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppNotificationService.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppNotificationService.java new file mode 100644 index 00000000..5b8d2da3 --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppNotificationService.java @@ -0,0 +1,75 @@ +package io.guh.nymeaapp; + +import com.google.firebase.messaging.RemoteMessage; +import com.google.firebase.messaging.FirebaseMessagingService; + +import android.util.Log; +import android.content.Intent; +import android.app.PendingIntent; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.net.Uri; +import android.content.Context; +import android.provider.Settings.System; +import android.os.Build; + +import android.support.v4.app.NotificationCompat; + +import java.util.Random; + +public class NymeaAppNotificationService extends FirebaseMessagingService { + + + /** + * Called when message is received. + * + * @param remoteMessage Object representing the message received from Firebase Cloud Messaging. + */ + // [START receive_message] + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + // If the application is in the foreground handle both data and notification messages here. + // Also if you intend on generating your own notifications as a result of a received FCM + // message, here is where that should be initiated. See sendNotification method below. + sendNotification(remoteMessage); + } + // [END receive_message] + + /** + * Create and show a simple notification containing the received FCM message. + * + * @param remoteMessage FCM RemoteMessage received. + */ + private void sendNotification(RemoteMessage remoteMessage) { + + Intent intent = new Intent(this, NymeaAppActivity.class); +// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); +// PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, PendingIntent.FLAG_ONE_SHOT); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, 0); + + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "notify_001") + .setSmallIcon(R.drawable.ic_stat_notificationicon) + .setColor(0xFF57BAAE) + .setContentTitle(remoteMessage.getData().get("title")) + .setContentText(remoteMessage.getData().get("body")) + .setAutoCancel(true) + .setSound(android.provider.Settings.System.DEFAULT_RINGTONE_URI) + .setContentIntent(pendingIntent); + + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel("notify_001", "Channel human readable title", NotificationManager.IMPORTANCE_HIGH); + notificationManager.createNotificationChannel(channel); + } + + + int notificationId = new Random().nextInt(60000); + + Log.d("Posting Notification", remoteMessage.getMessageId()); + notificationManager.notify(notificationId, notificationBuilder.build()); + + } + +} diff --git a/qtcloudmessaging b/qtcloudmessaging new file mode 160000 index 00000000..bf62d04b --- /dev/null +++ b/qtcloudmessaging @@ -0,0 +1 @@ +Subproject commit bf62d04b3d6aad292b7dab46d7c4567965450537