add a platform helper

This commit is contained in:
Michael Zanetti 2018-09-19 13:03:46 +02:00
parent 21e8814fb3
commit 152354d75f
18 changed files with 406 additions and 138 deletions

6
.gitmodules vendored
View File

@ -4,9 +4,3 @@
[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

View File

@ -605,7 +605,7 @@ void AWSClient::getId()
});
}
void AWSClient::registerPushNotificationEndpoint(const QString &registrationId)
void AWSClient::registerPushNotificationEndpoint(const QString &registrationId, const QString &deviceDisplayName, const QString mobileDeviceId)
{
if (!isLoggedIn()) {
qWarning() << "Not logged in at AWS. Can't register push endpoint";
@ -617,44 +617,39 @@ void AWSClient::registerPushNotificationEndpoint(const QString &registrationId)
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);
}
// qDebug() << "POST" << url.toString();
// qDebug() << "HEADERS:";
// foreach (const QByteArray &hdr, request.rawHeaderList()) {
// qDebug() << hdr << ":" << request.rawHeader(hdr);
// }
QVariantMap payload;
payload.insert("registrationId", registrationId);
#ifdef Q_OS_ANDROID
payload.insert("channel", "GCM");
payload.insert("mobileDeviceDisplayName", "test device");
payload.insert("mobileDeviceUuid", "12345678");
#else
payload.insert("channel", "APNS");
#endif
payload.insert("mobileDeviceDisplayName", deviceDisplayName);
payload.insert("mobileDeviceUuid", mobileDeviceId);
QJsonDocument jsonDoc = QJsonDocument::fromVariant(payload);
qDebug() << "Registering push notification endpoint:" << payload.value("channel").toString();
QNetworkReply *reply = m_nam->post(request, jsonDoc.toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
connect(reply, &QNetworkReply::finished, 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;
});

View File

@ -118,7 +118,7 @@ public:
Q_INVOKABLE bool postToMQTT(const QString &boxId, const QString &timestamp, std::function<void(bool)> callback);
Q_INVOKABLE void getId();
Q_INVOKABLE void registerPushNotificationEndpoint(const QString &registrationId);
Q_INVOKABLE void registerPushNotificationEndpoint(const QString &registrationId, const QString &deviceDisplayName, const QString mobileDeviceId);
bool tokensExpired() const;
QByteArray idToken() const;

View File

@ -26,9 +26,10 @@
#include <QSysInfo>
#ifdef Q_OS_ANDROID
#include <QtCloudMessaging>
#include <QtCloudMessagingFirebase>
#include <QtAndroidExtras/QtAndroid>
#include "platformintegration/android/platformhelperandroid.h"
#else
#include "platformintegration/generic/platformhelpergeneric.h"
#endif
#include "libnymea-app-core.h"
@ -36,6 +37,19 @@
#include "stylecontroller.h"
#include "pushnotifications.h"
QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
Q_UNUSED(scriptEngine)
#ifdef Q_OS_ANDROID
return new PlatformHelperAndroid();
#else
return new PlatformHelperGeneric();
#endif
}
int main(int argc, char *argv[])
{
@ -73,21 +87,9 @@ int main(int argc, char *argv[])
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
qmlRegisterSingletonType<PlatformHelper>("Nymea", 1, 0, "PlatformHelper", platformHelperProvider);
PushNotifications::instance()->connectClient();
qmlRegisterSingletonType<PushNotifications>("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider);
#ifdef BRANDING

View File

@ -2,7 +2,7 @@ TEMPLATE=app
TARGET=nymea-app
include(../config.pri)
QT += network qml quick quickcontrols2 svg websockets bluetooth #cloudmessaging
QT += network qml quick quickcontrols2 svg websockets bluetooth
INCLUDEPATH += $$top_srcdir/libnymea-common \
$$top_srcdir/libnymea-app-core
@ -18,11 +18,15 @@ PRE_TARGETDEPS += ../libnymea-app-core ../libnymea-common
HEADERS += \
stylecontroller.h \
pushnotifications.h
pushnotifications.h \
platformhelper.h \
platformintegration/generic/platformhelpergeneric.h
SOURCES += main.cpp \
stylecontroller.cpp \
pushnotifications.cpp
pushnotifications.cpp \
platformhelper.cpp \
platformintegration/generic/platformhelpergeneric.cpp
OTHER_FILES += $$files(*.qml, true)
@ -37,13 +41,12 @@ equals(STYLES_PATH, "") {
android {
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/../packaging/android
# QTFIREBASE_CONFIG+=messaging
# QTFIREBASE_SDK_PATH=/opt/firebase_cpp_sdk/
# include(../QtFirebase/qtfirebase.pri)
INCLUDEPATH += /opt/firebase_cpp_sdk/include
LIBS += -L/opt/firebase_cpp_sdk/libs/android/armeabi-v7a/gnustl/ -lfirebase_messaging -lfirebase_app
QT += androidextras cloudmessagingfirebase
QT += androidextras
HEADERS += platformintegration/android/platformhelperandroid.h
SOURCES += platformintegration/android/platformhelperandroid.cpp
DISTFILES += \
$$ANDROID_PACKAGE_SOURCE_DIR/AndroidManifest.xml \

View File

@ -0,0 +1,7 @@
#include "platformhelper.h"
PlatformHelper::PlatformHelper(QObject *parent) : QObject(parent)
{
}

View File

@ -0,0 +1,29 @@
#ifndef PLATFORMHELPER_H
#define PLATFORMHELPER_H
#include <QObject>
class PlatformHelper : public QObject
{
Q_OBJECT
Q_PROPERTY(bool hasPermissions READ hasPermissions NOTIFY permissionsRequestFinished)
Q_PROPERTY(QString deviceSerial READ deviceSerial CONSTANT)
Q_PROPERTY(QString deviceModel READ deviceModel CONSTANT)
Q_PROPERTY(QString deviceManufacturer READ deviceManufacturer CONSTANT)
public:
explicit PlatformHelper(QObject *parent = nullptr);
virtual ~PlatformHelper() = default;
Q_INVOKABLE virtual void requestPermissions() = 0;
virtual bool hasPermissions() const = 0;
virtual QString deviceSerial() const = 0;
virtual QString deviceModel() const = 0;
virtual QString deviceManufacturer() const = 0;
signals:
void permissionsRequestFinished();
};
#endif // PLATFORMHELPER_H

View File

@ -0,0 +1,53 @@
#include "platformhelperandroid.h"
#include <QAndroidJniObject>
#include <QtAndroid>
#include <QDebug>
static PlatformHelperAndroid *m_instance;
PlatformHelperAndroid::PlatformHelperAndroid(QObject *parent) : PlatformHelper(parent)
{
m_instance = this;
}
void PlatformHelperAndroid::requestPermissions()
{
QtAndroid::requestPermissions({"android.permission.READ_PHONE_STATE"}, &PlatformHelperAndroid::permissionRequestFinished);
}
bool PlatformHelperAndroid::hasPermissions() const
{
QtAndroid::PermissionResult r = QtAndroid::checkPermission("android.permission.READ_PHONE_STATE");
return r == QtAndroid::PermissionResult::Granted;
}
QString PlatformHelperAndroid::deviceSerial() const
{
QtAndroid::PermissionResult r = QtAndroid::checkPermission("android.permission.READ_PHONE_STATE");
if (r != QtAndroid::PermissionResult::Granted) {
qWarning() << "Cannot read device serial. No permissions";
return "";
}
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;");
return activity.callObjectMethod<jstring>("deviceSerial").toString();
}
QString PlatformHelperAndroid::deviceModel() const
{
return QAndroidJniObject::callStaticObjectMethod<jstring>("io/guh/nymeaapp/NymeaAppActivity","deviceModel").toString();
}
QString PlatformHelperAndroid::deviceManufacturer() const
{
return QAndroidJniObject::callStaticObjectMethod<jstring>("io/guh/nymeaapp/NymeaAppActivity","deviceManufacturer").toString();
}
void PlatformHelperAndroid::permissionRequestFinished(const QtAndroid::PermissionResultMap &result)
{
foreach (const QString &key, result.keys()) {
qDebug() << "Permission result:" << key << static_cast<int>(result.value(key));
}
emit m_instance->permissionsRequestFinished();
}

View File

@ -0,0 +1,26 @@
#ifndef PLATFORMHELPERANDROID_H
#define PLATFORMHELPERANDROID_H
#include <QObject>
#include "platformhelper.h"
#include <QtAndroid>
class PlatformHelperAndroid : public PlatformHelper
{
Q_OBJECT
public:
explicit PlatformHelperAndroid(QObject *parent = nullptr);
Q_INVOKABLE void requestPermissions() override;
bool hasPermissions() const override;
QString deviceSerial() const override;
QString deviceModel() const override;
QString deviceManufacturer() const override;
private:
static void permissionRequestFinished(const QtAndroid::PermissionResultMap &);
};
#endif // PLATFORMHELPERANDROID_H

View File

@ -0,0 +1,35 @@
#include "platformhelpergeneric.h"
PlatformHelperGeneric::PlatformHelperGeneric(QObject *parent) : PlatformHelper(parent)
{
}
void PlatformHelperGeneric::requestPermissions()
{
emit permissionsRequestFinished();
}
bool PlatformHelperGeneric::hasPermissions() const
{
return true;
}
QString PlatformHelperGeneric::deviceSerial() const
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
return QSysInfo::machineUniqueId();
#else
return "1234567890";
#endif
}
QString PlatformHelperGeneric::deviceModel() const
{
return QSysInfo::prettyProductName();
}
QString PlatformHelperGeneric::deviceManufacturer() const
{
return QSysInfo::productType();
}

View File

@ -0,0 +1,25 @@
#ifndef PLATFORMHELPERGENERIC_H
#define PLATFORMHELPERGENERIC_H
#include <QObject>
#include "platformhelper.h"
class PlatformHelperGeneric : public PlatformHelper
{
Q_OBJECT
public:
explicit PlatformHelperGeneric(QObject *parent = nullptr);
Q_INVOKABLE virtual void requestPermissions() override;
virtual bool hasPermissions() const override;
virtual QString deviceSerial() const override;
virtual QString deviceModel() const override;
virtual QString deviceManufacturer() const override;
signals:
public slots:
};
#endif // PLATFORMHELPERGENERIC_H

View File

@ -1,12 +1,22 @@
#include "pushnotifications.h"
#if defined(Q_OS_ANDROID)
#include <QtAndroid>
#include <QtAndroidExtras>
#include <QAndroidJniObject>
#endif
static PushNotifications *m_client_pointer;
PushNotifications::PushNotifications(QObject *parent) : QObject(parent)
{
connectClient();
}
QObject *PushNotifications::pushNotificationsProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
Q_UNUSED(scriptEngine)
return instance();
}
@ -16,13 +26,58 @@ PushNotifications *PushNotifications::instance()
return pushNotifications;
}
QString PushNotifications::apnsRegistrationToken() const
void PushNotifications::connectClient()
{
return m_apnsToken;
#ifdef Q_OS_ANDROID
m_firebaseApp = ::firebase::App::Create(::firebase::AppOptions(), QAndroidJniEnvironment(),
QtAndroid::androidActivity().object());
m_client_pointer = this;
m_firebase_initializer.Initialize(m_firebaseApp,
nullptr, [](::firebase::App * fapp, void *) {
qDebug() << "Trying to initialize Firebase Messaging";
return ::firebase::messaging::Initialize(
*fapp,
(::firebase::messaging::Listener *)m_client_pointer);
});
while (m_firebase_initializer.InitializeLastResult().status() !=
firebase::kFutureStatusComplete) {
qDebug() << "Firebase: InitializeLastResult wait...";
}
#endif
}
void PushNotifications::disconnectClient()
{
#ifdef Q_OS_ANDROID
::firebase::messaging::Terminate();
#endif
}
QString PushNotifications::token() const
{
return m_token;
}
void PushNotifications::setAPNSRegistrationToken(const QString &apnsRegistrationToken)
{
m_apnsToken = apnsRegistrationToken;
apnsRegistrationTokenChanged(); //emit signal
m_token = apnsRegistrationToken;
emit tokenChanged();
}
#ifdef Q_OS_ANDROID
void PushNotifications::OnMessage(const firebase::messaging::Message &message)
{
qDebug() << "Firebase message received:" << QString::fromStdString(message.from);
}
void PushNotifications::OnTokenReceived(const char *token)
{
qDebug() << "Firebase token received:" << token;
m_token = QString(token);
emit tokenChanged();
}
#endif

View File

@ -4,26 +4,50 @@
#include <QObject>
#include <QQmlEngine>
#ifdef Q_OS_ANDROID
#include "firebase/app.h"
#include "firebase/messaging.h"
#include "firebase/util.h"
#endif
#ifdef Q_OS_ANDROID
class PushNotifications : public QObject, firebase::messaging::Listener
#else
class PushNotifications : public QObject
#endif
{
Q_OBJECT
Q_PROPERTY(QString token READ token NOTIFY tokenChanged)
public:
explicit PushNotifications(QObject *parent = nullptr);
static QObject* pushNotificationsProvider(QQmlEngine *engine, QJSEngine *scriptEngine);
static PushNotifications* instance();
QString apnsRegistrationToken() const;
void connectClient();
void disconnectClient();
QString token() const;
// Called by Objective-C++
void setAPNSRegistrationToken(const QString &apnsRegistrationToken);
signals:
void gcmRegistrationTokenChanged();
void apnsRegistrationTokenChanged();
void registeredChanged();
void tokenChanged();
protected:
#ifdef Q_OS_ANDROID
//! Firebase overrides
virtual void OnMessage(const ::firebase::messaging::Message &message) override;
virtual void OnTokenReceived(const char *token) override;
private:
::firebase::App *m_firebaseApp = nullptr;
::firebase::ModuleInitializer m_firebase_initializer;
#endif
private:
QString m_gcmToken;
QString m_apnsToken;
QString m_token;
};
#endif // PUSHNOTIFICATIONS_H

View File

@ -53,10 +53,19 @@ 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);
setupPushNotifications();
}
Connections {
target: PlatformHelper
onHasPermissionsChanged: {
setupPushNotifications(false)
}
}
Connections {
target: Engine.awsClient
onIsLoggedInChanged: {
setupPushNotifications()
}
}
Connections {
@ -119,6 +128,22 @@ ApplicationWindow {
}
}
function setupPushNotifications(askForPermissions) {
if (askForPermissions === undefined) {
askForPermissions = true;
}
if (Engine.awsClient.isLoggedIn) {
if (!PlatformHelper.hasPermissions) {
if (askForPermissions) {
PlatformHelper.requestPermissions();
}
} else {
Engine.awsClient.registerPushNotificationEndpoint(PushNotifications.token, PlatformHelper.deviceManufacturer + " " + PlatformHelper.deviceModel, PlatformHelper.deviceSerial);
}
}
}
// Workaround flickering on pageStack animations when the white background shines through
Rectangle {
anchors.fill: parent
@ -381,58 +406,6 @@ 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 }

View File

@ -16,7 +16,7 @@ Page {
anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter; margins: app.margins }
spacing: app.margins
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
Layout.alignment: Qt.AlignHCenter
running: parent.visible
}
Label {

View File

@ -1,5 +1,6 @@
<?xml version="1.0"?>
<manifest package="io.guh.nymeaapp" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0" android:versionCode="1" android:installLocation="auto">
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="nymea:app" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation" android:name="io.guh.nymeaapp.NymeaAppActivity" android:label="nymea:app" android:screenOrientation="unspecified" android:launchMode="singleTop">
<intent-filter>
@ -32,7 +33,7 @@
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Messages maps -->
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notifyicon" />
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_stat_notificationicon"/>
<!-- Splash screen -->
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splash"/>
@ -85,4 +86,5 @@
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
</manifest>

View File

@ -69,4 +69,17 @@ android {
lintOptions {
abortOnError false
}
/***************************************************************************
* This is a hack to copy the output apk one level up
* old versions of gradle have put it to build/apks/android-build-debug.apk
* new versions put it to build/apks/debug/android-build-debug.apk
* which breaks qtcreator deployment
* Remote this when QtCreator is updated to support new gradle
**************************************************************************/
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "../" + outputFileName
}
}
}

View File

@ -1,28 +1,60 @@
package io.guh.nymeaapp;
import android.util.Log;
import android.content.Intent;
import android.content.Context;
import android.os.Bundle;
import com.google.firebase.messaging.MessageForwardingService;
import android.os.Build;
import android.telephony.TelephonyManager;
//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)
public String deviceSerial()
{
TelephonyManager TM = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
// IMEI No.
String imeiNo = TM.getDeviceId();
// IMSI No.
String imsiNo = TM.getSubscriberId();
// SIM Serial No.
String simSerialNo = TM.getSimSerialNumber();
// Android Unique ID
// String androidId = System.getString(this.getContentResolver(),Settings.Secure.ANDROID_ID);
return imeiNo;
}
public static String deviceManufacturer()
{
return Build.MANUFACTURER;
}
public static String deviceModel()
{
return Build.MODEL;
}
// // 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);
@ -39,6 +71,6 @@ public class NymeaAppActivity extends org.qtproject.qt5.android.bindings.QtActiv
// message.setData(intent.getData());
// startService(message);
// // }
setIntent(intent);
}
// setIntent(intent);
// }
}