diff --git a/androidservice/androidservice.pro b/androidservice/androidservice.pro index 41e7d52e..75b241f7 100644 --- a/androidservice/androidservice.pro +++ b/androidservice/androidservice.pro @@ -2,7 +2,7 @@ TEMPLATE = lib TARGET = service CONFIG += dll QT += core androidextras -QT += network qml quick quickcontrols2 svg websockets bluetooth charts +QT += network qml quick quickcontrols2 svg websockets bluetooth charts nfc include(../config.pri) include(../android_openssl/openssl.pri) @@ -29,6 +29,8 @@ SOURCES += \ nymeaappservice/androidbinder.cpp \ ../nymea-app/stylecontroller.cpp \ ../nymea-app/platformhelper.cpp \ + ../nymea-app/nfchelper.cpp \ + ../nymea-app/nfcthingactionwriter.cpp \ ../nymea-app/platformintegration/android/platformhelperandroid.cpp \ service_main.cpp @@ -38,6 +40,8 @@ HEADERS += \ nymeaappservice/androidbinder.h \ ../nymea-app/stylecontroller.h \ ../nymea-app/platformhelper.h \ + ../nymea-app/nfchelper.h \ + ../nymea-app/nfcthingactionwriter.h \ ../nymea-app/platformintegration/android/platformhelperandroid.h \ DISTFILES += \ diff --git a/androidservice/controlviews/devicecontrolapplication.cpp b/androidservice/controlviews/devicecontrolapplication.cpp index 1b319967..06758a40 100644 --- a/androidservice/controlviews/devicecontrolapplication.cpp +++ b/androidservice/controlviews/devicecontrolapplication.cpp @@ -6,6 +6,8 @@ #include "libnymea-app-core.h" #include "../nymea-app/stylecontroller.h" #include "../nymea-app/platformhelper.h" +#include "../nymea-app/nfchelper.h" +#include "../nymea-app/nfcthingactionwriter.h" #include "../nymea-app/platformintegration/android/platformhelperandroid.h" #include @@ -13,6 +15,8 @@ #include #include #include +#include +#include QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -26,41 +30,190 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp setApplicationName("nymea-app"); setOrganizationName("nymea"); - QString nymeaId = QtAndroid::androidActivity().callObjectMethod("nymeaId").toString(); - QString thingId = QtAndroid::androidActivity().callObjectMethod("thingId").toString(); - QSettings settings; - NymeaDiscovery *discovery = new NymeaDiscovery(this); + m_discovery = new NymeaDiscovery(this); AWSClient::instance()->setConfig(settings.value("cloudEnvironment").toString()); - discovery->setAwsClient(AWSClient::instance()); - NymeaHost *host = discovery->nymeaHosts()->find(nymeaId); + m_discovery->setAwsClient(AWSClient::instance()); + m_engine = new Engine(this); + + m_qmlEngine = new QQmlApplicationEngine(this); + registerQmlTypes(); + qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); + qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); + qmlRegisterType("Nymea", 1, 0, "NfcThingActionWriter"); + qmlRegisterSingletonType("Nymea", 1, 0, "NfcHelper", NfcHelper::nfcHelperProvider); + + StyleController *styleController = new StyleController(this); + m_qmlEngine->rootContext()->setContextProperty("styleController", styleController); + m_qmlEngine->rootContext()->setContextProperty("engine", m_engine); + m_qmlEngine->rootContext()->setContextProperty("_engine", m_engine); + m_qmlEngine->rootContext()->setContextProperty("controlledThingId", ""); // Unknown at this point + + m_qmlEngine->load(QUrl(QLatin1String("qrc:/Main.qml"))); + + jboolean startedByNfc = QtAndroid::androidActivity().callMethod("startedByNfc", "()Z"); + if (startedByNfc) { + qDebug() << "**** Started by NFC"; + qDebug() << "Registering NFC handler and waiting for message."; + + QNearFieldManager *manager = new QNearFieldManager(this); + manager->registerNdefMessageHandler(this, SLOT(handleNdefMessage(QNdefMessage,QNearFieldTarget*))); + + } else { + qDebug() << "*** Started by other intent"; + qDebug() << "Expecing nymeaId and thingId in intent extras."; + QString nymeaId = QtAndroid::androidActivity().callObjectMethod("nymeaId").toString(); + QString thingId = QtAndroid::androidActivity().callObjectMethod("thingId").toString(); + + connectToNymea(nymeaId); + m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); + } +} + +void DeviceControlApplication::handleNdefMessage(QNdefMessage message, QNearFieldTarget *target) +{ + qDebug() << "************* NFC message!" << message.toByteArray(); + if (message.count() < 1) { + qWarning() << "NFC message doesn't contain any records..."; + return; + } + // NOTE: At this point we're only supporting one NDEF record per message + QNdefRecord record = message.first(); + QNdefNfcUriRecord uriRecord(record); + + QUrl url = uriRecord.uri(); + if (url.scheme() != "nymea") { + qWarning() << "NDEF URI record scheme is not \"nymea://\""; + return; + } + + QUuid nymeaId = QUuid(url.host()); + if (nymeaId.isNull()) { + qWarning() << "Invalid nymea UUID in NDEF record."; + return; + } + + QUuid thingId = QUuid(QUrlQuery(url).queryItemValue("t")); + if (thingId.isNull()) { + qWarning() << "Invalid thing in NDEF record"; + return; + } + + m_pendingNfcAction = url; + + connectToNymea(nymeaId); + m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); + + connect(m_engine->thingManager(), &DeviceManager::fetchingDataChanged, [this](){ + if (m_engine->jsonRpcClient()->connected() && !m_engine->thingManager()->fetchingData()) { + qDebug() << "Ready to process commands"; + runNfcAction(); + } + }); +} + +void DeviceControlApplication::connectToNymea(const QUuid &nymeaId) +{ + NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId); if (!host) { qWarning() << "No such nymea host:" << nymeaId; // TODO: We could wait here until the discovery finds it... But it really should be cached already... exit(1); } - - Engine *m_engine = new Engine(this); - - qDebug() << "Connecting to:" << host; + qDebug() << "Connecting to:" << host->name(); m_engine->jsonRpcClient()->connectToHost(host); - - qDebug() << "Creating QML view"; - QQmlApplicationEngine *qmlEngine = new QQmlApplicationEngine(this); - - registerQmlTypes(); - - qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); - qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); - - StyleController styleController; - qmlEngine->rootContext()->setContextProperty("styleController", &styleController); - qmlEngine->rootContext()->setContextProperty("engine", m_engine); - qmlEngine->rootContext()->setContextProperty("_engine", m_engine); - qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); - - qmlEngine->load(QUrl(QLatin1String("qrc:/Main.qml"))); } +void DeviceControlApplication::runNfcAction() +{ + if (!m_pendingNfcAction.isEmpty()) { + qDebug() << "NFC action:" << m_pendingNfcAction; + } + QUrl url = m_pendingNfcAction; + m_pendingNfcAction.clear(); + + if (url.scheme() != "nymea") { + qWarning() << "NDEF URI record scheme is not \"nymea://\" in" << url.toString(); + return; + } + + QUuid nymeaId = QUuid(url.host()); + if (nymeaId.isNull()) { + qWarning() << "Invalid nymea UUID" << url.host() << "in NDEF record" << url.toString(); + return; + } + + QUuid thingId = QUuid(QUrlQuery(url).queryItemValue("t")); + Device *thing = m_engine->thingManager()->things()->getThing(thingId); + if (!thing) { + qDebug() << "Thing" << thingId.toString() << "from" << url.toString() << "doesn't exist on nymea host" << nymeaId.toString(); + return; + } + + QList> queryItems = QUrlQuery(url.query()).queryItems(); + for (int i = 0; i < queryItems.count(); i++) { + QString entryName = queryItems.at(i).first; + if (entryName == "t") { + continue; + } + if (!entryName.startsWith("a")) { + qDebug() << "Only actions are supported. Skipping query item" << entryName; + continue; + } + + QString actionString = queryItems.at(i).second; + QStringList parts = actionString.split("#"); + if (parts.count() == 0) { + qDebug() << "Invalid action definition:" << actionString; + continue; + } + + if (parts.count() > 2) { + // The parameters might contain a #, let's merge them again + parts[1] = parts.mid(1).join('#'); + } + + QString actionTypeName = parts.at(0); + ActionType *actionType = thing->thingClass()->actionTypes()->findByName(actionTypeName); + if (!actionType) { + qWarning() << "Invalid action name" << actionType << "in url:" << url.toString(); + continue; + } + + QHash paramsInUri; + if (parts.count() > 1) { + QString paramsString = parts.at(1); + foreach (const QString ¶mString, paramsString.split("+")) { + QStringList parts = paramString.split(":"); + if (parts.count() != 2) { + qWarning() << "Invalid param format" << paramString << "in url:" << url.toString(); + continue; + } + paramsInUri.insert(parts.at(0), parts.at(1)); + } + } + + qDebug() << "Parameters in NFC uri:" << paramsInUri; + + QVariantList params; + for (int j = 0; j < actionType->paramTypes()->rowCount(); j++) { + ParamType *paramType = actionType->paramTypes()->get(j); + QVariantMap param; + param.insert("paramTypeId", paramType->id()); + if (paramsInUri.contains(paramType->name())) { + param.insert("value", paramsInUri.value(paramType->name())); + } else { + param.insert("value", paramType->defaultValue()); + } + params.append(param); + } + + qDebug() << "Action parameters:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); + + m_engine->thingManager()->executeAction(thingId, actionType->id(), params); + } +} + + diff --git a/androidservice/controlviews/devicecontrolapplication.h b/androidservice/controlviews/devicecontrolapplication.h index 4c80e850..edd6985c 100644 --- a/androidservice/controlviews/devicecontrolapplication.h +++ b/androidservice/controlviews/devicecontrolapplication.h @@ -2,6 +2,14 @@ #define DEVICECONTROLAPPLICATION_H #include +#include +#include +#include +#include + +#include "types/ruleactions.h" +#include "connection/discovery/nymeadiscovery.h" +#include "engine.h" class DeviceControlApplication : public QApplication { @@ -9,6 +17,21 @@ class DeviceControlApplication : public QApplication public: explicit DeviceControlApplication(int argc, char *argv[]); +private slots: + void handleNdefMessage(QNdefMessage message,QNearFieldTarget* target); + + void connectToNymea(const QUuid &nymeaId); + + void runNfcAction(); + +private: + NymeaDiscovery *m_discovery = nullptr; + Engine *m_engine = nullptr; + QQmlApplicationEngine *m_qmlEngine = nullptr; + + QUrl m_pendingNfcAction; + + }; #endif // DEVICECONTROLAPPLICATION_H diff --git a/androidservice/nymeaappservice/nymeaappservice.h b/androidservice/nymeaappservice/nymeaappservice.h index 6f6e653a..5e823efa 100644 --- a/androidservice/nymeaappservice/nymeaappservice.h +++ b/androidservice/nymeaappservice/nymeaappservice.h @@ -2,6 +2,8 @@ #define NYMEAAPPSERVICE_H #include +#include +#include #include "engine.h" diff --git a/libnymea-app/types/actiontypes.cpp b/libnymea-app/types/actiontypes.cpp index 7d17e648..14972811 100644 --- a/libnymea-app/types/actiontypes.cpp +++ b/libnymea-app/types/actiontypes.cpp @@ -49,11 +49,12 @@ ActionType *ActionTypes::get(int index) const ActionType *ActionTypes::getActionType(const QUuid &actionTypeId) const { foreach (ActionType *actionType, m_actionTypes) { + qDebug() << "checking:" << actionType->id(); if (actionType->id() == actionTypeId) { return actionType; } } - return 0; + return nullptr; } int ActionTypes::rowCount(const QModelIndex &parent) const diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 265e31b1..dd6ab995 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -236,5 +236,7 @@ ui/images/connections/nm-signal-100-secure.svg ui/images/connections/bluetooth.svg ui/images/connections/network-wired-disabled.svg + ui/images/nfc.svg + ui/images/smartphone.svg diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index 680de44b..704978c9 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -52,6 +52,8 @@ #include "pushnotifications.h" #include "applogcontroller.h" #include "ruletemplates/messages.h" +#include "nfchelper.h" +#include "nfcthingactionwriter.h" QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -66,7 +68,6 @@ QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) #endif } - int main(int argc, char *argv[]) { @@ -124,6 +125,8 @@ int main(int argc, char *argv[]) QQmlApplicationEngine *engine = new QQmlApplicationEngine(); qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); + qmlRegisterSingletonType("Nymea", 1, 0, "NfcHelper", NfcHelper::nfcHelperProvider); + qmlRegisterType("Nymea", 1, 0, "NfcThingActionWriter"); PushNotifications::instance()->connectClient(); qmlRegisterSingletonType("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider); diff --git a/nymea-app/nfchelper.cpp b/nymea-app/nfchelper.cpp new file mode 100644 index 00000000..cdac7ed2 --- /dev/null +++ b/nymea-app/nfchelper.cpp @@ -0,0 +1,28 @@ +#include "nfchelper.h" + +#include + +NfcHelper::NfcHelper(QObject *parent) : QObject(parent) +{ + +} + +NfcHelper *NfcHelper::instance() +{ + static NfcHelper *thiz = nullptr; + if (!thiz) { + thiz = new NfcHelper(); + } + return thiz; +} + +QObject *NfcHelper::nfcHelperProvider(QQmlEngine */*engine*/, QJSEngine */*scriptEngine*/) +{ + return instance(); +} + +bool NfcHelper::isAvailable() const +{ + QNearFieldManager manager; + return manager.isAvailable(); +} diff --git a/nymea-app/nfchelper.h b/nymea-app/nfchelper.h new file mode 100644 index 00000000..eeab2a66 --- /dev/null +++ b/nymea-app/nfchelper.h @@ -0,0 +1,23 @@ +#ifndef NFCHELPER_H +#define NFCHELPER_H + +#include +#include + +class NfcHelper : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isAvailable READ isAvailable CONSTANT) + +public: + static NfcHelper* instance(); + static QObject *nfcHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine); + + + bool isAvailable() const; + +private: + explicit NfcHelper(QObject *parent = nullptr); +}; + +#endif // NFCHELPER_H diff --git a/nymea-app/nfcthingactionwriter.cpp b/nymea-app/nfcthingactionwriter.cpp new file mode 100644 index 00000000..920fcca6 --- /dev/null +++ b/nymea-app/nfcthingactionwriter.cpp @@ -0,0 +1,185 @@ +#include "nfcthingactionwriter.h" +#include "types/deviceclass.h" +#include "types/statetype.h" +#include "types/ruleaction.h" +#include "types/ruleactionparams.h" +#include "types/ruleactionparam.h" + +#include +#include +#include +#include +#include +#include + +NfcThingActionWriter::NfcThingActionWriter(QObject *parent): + QObject(parent), + m_manager(new QNearFieldManager(this)), + m_actions(new RuleActions(this)) +{ + connect(m_manager, &QNearFieldManager::targetDetected, this, &NfcThingActionWriter::targetDetected); + connect(m_manager, &QNearFieldManager::targetLost, this, &NfcThingActionWriter::targetLost); + + connect(m_actions, &RuleActions::countChanged, this, &NfcThingActionWriter::updateContent); + + m_manager->startTargetDetection(); + +} + +NfcThingActionWriter::~NfcThingActionWriter() +{ + m_manager->stopTargetDetection(); +} + +bool NfcThingActionWriter::isAvailable() const +{ + return m_manager->isAvailable(); +} + +Engine *NfcThingActionWriter::engine() const +{ + return m_engine; +} + +void NfcThingActionWriter::setEngine(Engine *engine) +{ + if (m_engine != engine) { + m_engine = engine; + emit engineChanged(); + updateContent(); + } +} + +Device *NfcThingActionWriter::thing() const +{ + return m_thing; +} + +void NfcThingActionWriter::setThing(Device *thing) +{ + if (m_thing != thing) { + m_thing = thing; + emit thingChanged(); + updateContent(); + } +} + +RuleActions *NfcThingActionWriter::actions() const +{ + return m_actions; +} + +int NfcThingActionWriter::messageSize() const +{ + return m_currentMessage.toByteArray().size(); + int ret = 0; + for (int i = 0; i < m_currentMessage.size(); i++) { + ret += m_currentMessage.at(i).payload().size(); + } + return ret; +} + +NfcThingActionWriter::TagStatus NfcThingActionWriter::status() const +{ + return m_status; +} + +void NfcThingActionWriter::updateContent() +{ + qDebug() << "Updating" << m_engine << m_thing; + + // Creating an URI type record with this format: + // nymea:// + // ? t= + // & a[0]= + // & a[1]=#: + // & a[2]=#:+: + // & ... + + // NOTE: We're using actionType and paramType *name* instead of the ID because NFC tags are + // small and normally names are shorter than ids so we save some space. + + // NOTE: param values are percentage encoded to prevent messing with the parsing if they + // contain + or : + + QUrl url; + url.setScheme("nymea"); + if (!m_engine || !m_thing) { + return; + } + url.setHost(m_engine->jsonRpcClient()->currentHost()->uuid().toString().remove(QRegExp("[{}]"))); + + QUrlQuery query; + + query.addQueryItem("t", m_thing->id().toString().remove(QRegExp("[{}]"))); + + for (int i = 0; i < m_actions->rowCount(); i++) { + RuleAction *action = m_actions->get(i); + QStringList params; + ActionType *at = m_thing->thingClass()->actionTypes()->getActionType(action->actionTypeId()); + if (!at) { + qWarning() << "ActionType not found in thing" << action->actionTypeId(); + continue; + } + + for (int j = 0; j < action->ruleActionParams()->rowCount(); j++) { + RuleActionParam *param = action->ruleActionParams()->get(j); + ParamType *pt = at->paramTypes()->getParamType(param->paramTypeId()); + if (!pt) { + qWarning() << "ParamType not found in thing"; + continue; + } + params.append(pt->name() + ":" + param->value().toByteArray().toPercentEncoding()); + } + QString actionString = at->name(); + if (params.length() > 0) { + actionString += "#" + params.join("+"); + } + query.addQueryItem(QString("a[%1]").arg(i), actionString); + } + url.setQuery(query); + qDebug() << "writing message" << url; + + QNdefNfcUriRecord record; + record.setUri(url); + QNdefMessage message; + message.append(record); + + m_currentMessage = message; + emit messageSizeChanged(); + +} + +void NfcThingActionWriter::targetDetected(QNearFieldTarget *target) +{ + QDateTime startTime = QDateTime::currentDateTime(); + qDebug() << "target detected"; + connect(target, &QNearFieldTarget::error, this, [=](QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id){ + qDebug() << "Tag error:" << error; + m_status = TagStatusFailed; + emit statusChanged(); + }); + connect(target, &QNearFieldTarget::ndefMessagesWritten, this, [=](){ + qDebug() << "Tag written in" << startTime.msecsTo(QDateTime::currentDateTime()); + m_status = TagStatusWritten; + emit statusChanged(); + }); + + QNearFieldTarget::RequestId m_request = target->writeNdefMessages(QList() << m_currentMessage); + if (!m_request.isValid()) { + qDebug() << "Error writing tag"; + m_status = TagStatusFailed; + emit statusChanged(); + } + + m_status = TagStatusWriting; + emit statusChanged(); +} + +void NfcThingActionWriter::targetLost(QNearFieldTarget *target) +{ + qDebug() << "Target lost" << target; + m_status = TagStatusWaiting; + emit statusChanged(); +} + diff --git a/nymea-app/nfcthingactionwriter.h b/nymea-app/nfcthingactionwriter.h new file mode 100644 index 00000000..0463b7e5 --- /dev/null +++ b/nymea-app/nfcthingactionwriter.h @@ -0,0 +1,76 @@ +#ifndef NFCTHINGACTIONWRITER_H +#define NFCTHINGACTIONWRITER_H + +#include +#include +#include + +#include "types/device.h" +#include "engine.h" +#include "types/ruleactions.h" + +class NfcThingActionWriter : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isAvailable READ isAvailable CONSTANT) + Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(Device *thing READ thing WRITE setThing NOTIFY thingChanged) + Q_PROPERTY(RuleActions *actions READ actions CONSTANT) + Q_PROPERTY(int messageSize READ messageSize NOTIFY messageSizeChanged) + Q_PROPERTY(TagStatus status READ status NOTIFY statusChanged) + + +public: + enum TagStatus { + TagStatusWaiting, + TagStatusWriting, + TagStatusWritten, + TagStatusFailed + }; + Q_ENUM(TagStatus) + + static NfcThingActionWriter *instance(); + + explicit NfcThingActionWriter(QObject *parent = nullptr); + ~NfcThingActionWriter(); + + bool isAvailable() const; + + Engine *engine() const; + void setEngine(Engine *engine); + + Device *thing() const; + void setThing(Device *thing); + + RuleActions *actions() const; + + int messageSize() const; + + TagStatus status() const; + +signals: + void engineChanged(); + void thingChanged(); + + void messageSizeChanged(); + void statusChanged(); + +private slots: + void updateContent(); + + void targetDetected(QNearFieldTarget *target); + void targetLost(QNearFieldTarget *target); + +private: + QNearFieldManager *m_manager = nullptr; + Engine *m_engine = nullptr; + Device *m_thing = nullptr; + RuleActions* m_actions; + + TagStatus m_status = TagStatusWaiting; + + QNdefMessage m_currentMessage; + +}; + +#endif // NFCTHINGACTIONWRITER_H diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index 8bb1b85e..d8dd59f2 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 charts gui-private +QT += network qml quick quickcontrols2 svg websockets bluetooth charts gui-private nfc INCLUDEPATH += $$top_srcdir/libnymea-app LIBS += -L$$top_builddir/libnymea-app/ -lnymea-app @@ -14,6 +14,8 @@ PRE_TARGETDEPS += ../libnymea-app HEADERS += \ mainmenumodel.h \ + nfchelper.h \ + nfcthingactionwriter.h \ platformintegration/generic/raspberrypihelper.h \ stylecontroller.h \ pushnotifications.h \ @@ -24,6 +26,8 @@ HEADERS += \ SOURCES += main.cpp \ mainmenumodel.cpp \ + nfchelper.cpp \ + nfcthingactionwriter.cpp \ platformintegration/generic/raspberrypihelper.cpp \ stylecontroller.cpp \ pushnotifications.cpp \ @@ -162,3 +166,12 @@ BR=$$BRANDING target.path = /usr/bin INSTALLS += target + +contains(ANDROID_TARGET_ARCH,) { + ANDROID_ABIS = \ + armeabi-v7a \ + arm64-v8a +} + +ANDROID_ABIS = armeabi-v7a arm64-v8a + diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 8a09163c..3867539f 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -223,5 +223,6 @@ ui/components/BatteryStatusIcon.qml ui/components/SetupStatusIcon.qml ui/components/UpdateStatusIcon.qml + ui/magic/WriteNfcTagPage.qml diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 502cb107..10dfcb3a 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -96,7 +96,7 @@ Page { thingMenu.addItem(menuEntryComponent.createObject(thingMenu, {text: qsTr("Logs"), iconSource: "../images/logs.svg", functionName: "openDeviceLogPage"})) } - if (engine.jsonRpcClient.ensureServerVersion(1.6)) { + if (engine.jsonRpcClient.ensureServerVersion("1.6")) { thingMenu.addItem(menuEntryComponent.createObject(thingMenu, { text: Qt.binding(function() { return favoritesProxy.count === 0 ? qsTr("Mark as favorite") : qsTr("Remove from favorites")}), @@ -111,7 +111,20 @@ Page { functionName: "addToGroup" })) } + + print("*** creating menu") + print("NFC", NfcHelper.isAvailable) + if (NfcHelper.isAvailable) { + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, + { + text: qsTr("Write NFC tag"), + iconSource: "../images/nfc.svg", + functionName: "writeNfcTag" + + })); + } } + function openDeviceMagicPage() { pageStack.push(Qt.resolvedUrl("../magic/DeviceRulesPage.qml"), {device: root.device}) } @@ -138,6 +151,10 @@ Page { pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {device: root.device }); } + function writeNfcTag() { + pageStack.push(Qt.resolvedUrl("../magic/WriteNfcTagPage.qml"), {thing: root.thing}) + } + Component { id: menuEntryComponent IconMenuItem { diff --git a/nymea-app/ui/images/nfc.svg b/nymea-app/ui/images/nfc.svg new file mode 100644 index 00000000..6176e99e --- /dev/null +++ b/nymea-app/ui/images/nfc.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/smartphone.svg b/nymea-app/ui/images/smartphone.svg new file mode 100644 index 00000000..1cf0b449 --- /dev/null +++ b/nymea-app/ui/images/smartphone.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/magic/WriteNfcTagPage.qml b/nymea-app/ui/magic/WriteNfcTagPage.qml new file mode 100644 index 00000000..e4f216cb --- /dev/null +++ b/nymea-app/ui/magic/WriteNfcTagPage.qml @@ -0,0 +1,222 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +Page { + id: root + property Thing thing: null + readonly property ThingClass thingClass: thing.thingClass + + header: NymeaHeader { + text: qsTr("Write NFC tag") + onBackPressed: { + pageStack.pop() + } + } + + + NfcThingActionWriter { + id: nfcWriter + engine: _engine + thing: root.thing + } +// nfcHelper.writeThingStates(engine, root.thing) + + GridLayout { + anchors.fill: parent + columns: app.landscape ? 2 : 1 + + ColumnLayout { + Layout.preferredWidth: parent.width / parent.columns + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + + Label { + Layout.fillWidth: true + text: { + switch (nfcWriter.status) { + case NfcThingActionWriter.TagStatusWaiting: + return qsTr("Tap an NFC tag to link it to %1.").arg(root.thing.name) + case NfcThingActionWriter.TagStatusWriting: + return qsTr("Writing NFC tag...") + case NfcThingActionWriter.TagStatusWritten: + return qsTr("NFC tag linked to %1.").arg(root.thing.name) + case NfcThingActionWriter.TagStatusFailed: + return qsTr("Failed linking the NFC tag to %1.").arg(root.thing.name) + } + } + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Label { + Layout.fillWidth: true + text: qsTr("Required tag size: %1 bytes").arg(nfcWriter.messageSize) + font.pixelSize: app.smallFont + horizontalAlignment: Text.AlignHCenter + enabled: false + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize * 8 + + SequentialAnimation { + loops: Animation.Infinite + running: true + + PropertyAction { target: phoneIcon; property: "anchors.horizontalCenterOffset"; value: app.iconSize * 2 } + PropertyAction { target: phoneIcon; property: "scale"; value: 1.3 } + NumberAnimation { target: phoneIcon; property: "opacity"; duration: 500; to: 1 } + PauseAnimation { duration: 500 } + ParallelAnimation { + NumberAnimation { target: phoneIcon; property: "anchors.horizontalCenterOffset"; from: app.iconSize * 2; to: -app.iconSize * 2; duration: 1500; easing.type: Easing.OutQuad } + NumberAnimation { target: phoneIcon; property: "scale"; duration: 1500; from: 1.3; to: 1; easing.type: Easing.InOutQuad } + } + PauseAnimation { duration: 500 } + NumberAnimation { target: phoneIcon; property: "opacity"; duration: 500; to: 0 } + PauseAnimation { duration: 500 } + } + + + ColorIcon { + id: nfcIcon + name: "../images/nfc.svg" + height: app.iconSize * 2 + width: app.iconSize * 2 + anchors.centerIn: parent + anchors.horizontalCenterOffset: - app.iconSize * 2 + visible: nfcWriter.status == NfcThingActionWriter.TagStatusWaiting + } + + Item { + id: phoneIcon + height: app.iconSize * 5 + width: app.iconSize * 5 + scale: 1.5 + anchors.centerIn: parent + anchors.horizontalCenterOffset: app.iconSize * 2 + visible: nfcWriter.status == NfcThingActionWriter.TagStatusWaiting + + Rectangle { + anchors.fill: parent + anchors.leftMargin: phoneIcon.width * .21 + anchors.rightMargin: phoneIcon.width * .21 + anchors.topMargin: phoneIcon.height * .1 + anchors.bottomMargin: phoneIcon.height * .1 + color: app.backgroundColor + } + + ColorIcon { + name: "../images/smartphone.svg" + anchors.fill: parent + } + } + + Rectangle { + id: tick + anchors.centerIn: parent + height: app.iconSize * 6 + width: app.iconSize * 6 + radius: width / 2 + color: app.backgroundColor + border.width: 4 + border.color: app.foregroundColor + opacity: nfcWriter.status == NfcThingActionWriter.TagStatusWaiting ? 0 : 1 + Behavior on opacity { NumberAnimation { duration: 300 } } + + property bool shown: nfcWriter.status == NfcThingActionWriter.TagStatusWritten || nfcWriter.status == NfcThingActionWriter.TagStatusFailed + + BusyIndicator { + anchors.fill: parent + running: visible + visible: nfcWriter.status == NfcThingActionWriter.TagStatusWriting + } + + Item { + anchors.fill: parent + anchors.rightMargin: tick.shown ? 0 : parent.width + Behavior on anchors.rightMargin { NumberAnimation { duration: 500 } } + clip: true + + ColorIcon { + x: (tick.width - width) / 2 + y: (tick.height - height) / 2 + height: app.iconSize * 4 + width: app.iconSize * 4 + name: nfcWriter.status == NfcThingActionWriter.TagStatusFailed ? "../images/close.svg" : "../images/tick.svg" + color: nfcWriter.status == NfcThingActionWriter.TagStatusFailed ? "red" : "green" + } + } + } + } + } + + + ColumnLayout { + Layout.preferredWidth: parent.width / parent.columns + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: nfcWriter.actions + clip: true + delegate: RuleActionDelegate { + ruleAction: nfcWriter.actions.get(index) + width: parent.width + onRemoveRuleAction: nfcWriter.actions.removeRuleAction(index) + } + } + + Button { + text: qsTr("Add action") + Layout.fillWidth: true + Layout.margins: app.margins + onClicked: { + var action = nfcWriter.actions.createNewRuleAction() + action.thingId = root.thing.id + var page = pageStack.push("SelectRuleActionPage.qml", {ruleAction: action}); + page.done.connect(function() { + nfcWriter.actions.addRuleAction(action); + pageStack.pop(); + }) + page.backPressed.connect(function() { + action.destroy() + pageStack.pop(); + }) + } + } + } + } +} diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml index b8380cf5..5fea09f0 100644 --- a/packaging/android/AndroidManifest.xml +++ b/packaging/android/AndroidManifest.xml @@ -64,6 +64,11 @@ + + + + + @@ -95,6 +100,11 @@ + + + + + @@ -145,4 +155,5 @@ Remove the comment if you do not require these default features. --> + diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java index b35cef9c..b3febd4f 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java @@ -61,9 +61,8 @@ public class NymeaAppControlService extends ControlsProviderService { } } @Override public void onUpdate(UUID nymeaId, UUID thingId) { - Log.d(TAG, "onUpdate()"); if (m_updatePublisher != null && m_activeControlIds.contains(thingId.toString())) { - Log.d(TAG, "Updating publisher for thing: " + thingId); +// Log.d(TAG, "Updating publisher for thing: " + thingId); m_updatePublisher.onNext(thingToControl(nymeaId, thingId)); // m_updatePublisher.onComplete(); } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java index ffa9f070..3b94d25e 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java @@ -8,6 +8,9 @@ import android.telephony.TelephonyManager; import android.provider.Settings.Secure; import android.os.Vibrator; import android.os.Process; +import android.nfc.NfcAdapter; +import android.nfc.NdefMessage; +import android.os.Parcelable; // An activity spawned by android device controls on demand. @@ -26,11 +29,13 @@ public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings Log.d(TAG, "Resuming..."); } - @Override public void onDestroy() { Log.d(TAG, "Destroying..."); } + public boolean startedByNfc() { + return NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction()); + } public String nymeaId() { @@ -47,5 +52,4 @@ public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); v.vibrate(duration); } - } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java index aa0cb9da..95f1b608 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java @@ -154,7 +154,6 @@ public class NymeaAppServiceConnection implements ServiceConnection { private BroadcastReceiver serviceMessageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - Log.d(TAG, "In OnReceive broadcast receiver"); if (NymeaAppService.NYMEA_APP_BROADCAST.equals(intent.getAction())) { String payload = intent.getStringExtra("data"); try { @@ -170,7 +169,7 @@ public class NymeaAppServiceConnection implements ServiceConnection { { JSONObject data = new JSONObject(payload); JSONObject params = data.getJSONObject("params"); - Log.d(TAG, "Broadcast received from NymeaAppService: " + data.getString("notification")); +// Log.d(TAG, "Broadcast received from NymeaAppService: " + data.getString("notification")); Log.d(TAG, params.toString()); if (data.getString("notification").equals("ThingStateChanged")) { @@ -178,7 +177,7 @@ public class NymeaAppServiceConnection implements ServiceConnection { UUID thingId = UUID.fromString(params.getString("thingId")); UUID stateTypeId = UUID.fromString(params.getString("stateTypeId")); String value = params.getString("value"); - Log.d(TAG, "Thing state changed: " + thingId + " stateTypeId: " + stateTypeId + " value: " + value); +// Log.d(TAG, "Thing state changed: " + thingId + " stateTypeId: " + stateTypeId + " value: " + value); Thing thing = getThing(thingId); if (thing != null) {