From c8a91ba9b1daed4bd54463b2b7dfca3dd9a8590d Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 20 Sep 2020 17:59:06 +0200 Subject: [PATCH] Support multiple nymea hosts --- .../controlviews/devicecontrolapplication.cpp | 26 +-- .../nymeaappservice/androidbinder.cpp | 196 +++++++++++++----- .../nymeaappservice/androidbinder.h | 8 +- .../nymeaappservice/nymeaappservice.cpp | 67 +++--- .../nymeaappservice/nymeaappservice.h | 4 +- config.pri | 3 + .../guh/nymeaapp/NymeaAppControlService.java | 93 ++++++--- .../nymeaapp/NymeaAppControlsActivity.java | 7 +- .../nymeaapp/NymeaAppServiceConnection.java | 186 ++++++++++------- .../src/io/guh/nymeaapp/NymeaHost.java | 13 ++ 10 files changed, 410 insertions(+), 193 deletions(-) create mode 100644 packaging/android/src/io/guh/nymeaapp/NymeaHost.java diff --git a/androidservice/controlviews/devicecontrolapplication.cpp b/androidservice/controlviews/devicecontrolapplication.cpp index 99978162..1b319967 100644 --- a/androidservice/controlviews/devicecontrolapplication.cpp +++ b/androidservice/controlviews/devicecontrolapplication.cpp @@ -26,27 +26,29 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp setApplicationName("nymea-app"); setOrganizationName("nymea"); - qDebug() << "Creating QML view"; - QQmlApplicationEngine *qmlEngine = new QQmlApplicationEngine(this); - - Engine *m_engine = new Engine(this); + QString nymeaId = QtAndroid::androidActivity().callObjectMethod("nymeaId").toString(); + QString thingId = QtAndroid::androidActivity().callObjectMethod("thingId").toString(); QSettings settings; - settings.beginGroup("tabSettings0"); - QUuid lastConnected = settings.value("lastConnectedHost").toUuid(); - settings.endGroup(); NymeaDiscovery *discovery = new NymeaDiscovery(this); AWSClient::instance()->setConfig(settings.value("cloudEnvironment").toString()); discovery->setAwsClient(AWSClient::instance()); + NymeaHost *host = discovery->nymeaHosts()->find(nymeaId); - NymeaHost *host = discovery->nymeaHosts()->find(lastConnected); - qDebug() << "**** Tab settings" << lastConnected << host; - if (host) { - m_engine->jsonRpcClient()->connectToHost(host); + 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); } - QString thingId = QtAndroid::androidActivity().callObjectMethod("thingId").toString(); + Engine *m_engine = new Engine(this); + + qDebug() << "Connecting to:" << host; + m_engine->jsonRpcClient()->connectToHost(host); + + qDebug() << "Creating QML view"; + QQmlApplicationEngine *qmlEngine = new QQmlApplicationEngine(this); registerQmlTypes(); diff --git a/androidservice/nymeaappservice/androidbinder.cpp b/androidservice/nymeaappservice/androidbinder.cpp index 8b3416b9..dd0159ea 100644 --- a/androidservice/nymeaappservice/androidbinder.cpp +++ b/androidservice/nymeaappservice/androidbinder.cpp @@ -8,32 +8,53 @@ #include #include -AndroidBinder::AndroidBinder(Engine * engine): - m_engine(engine) +AndroidBinder::AndroidBinder(NymeaAppService *service): + m_service(service) { - QAndroidParcel parcel; - parcel.writeData("foobar"); - transact(10, parcel); } bool AndroidBinder::onTransact(int code, const QAndroidParcel &data, const QAndroidParcel &reply, QAndroidBinder::CallType flags) { qDebug() << "onTransact: code " << code << ", flags " << int(flags); - switch (code) { - case 0: { // Status request - qDebug() << "Engine is:" << m_engine->jsonRpcClient()->connected(); - bool isReady = m_engine->jsonRpcClient()->connected() && !m_engine->thingManager()->fetchingData(); - isReady = false; - reply.handle().callMethod("writeBoolean", "(Z)V", isReady); - if (isReady) { - reply.handle().callMethod("writeString", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(m_engine->jsonRpcClient()->currentHost()->name()).object()); +// QString payload = data.readData(); + QString payload = data.handle().callObjectMethod("readString").toString(); + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(payload.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Error parsing JSON from parcel:" << error.errorString(); + qWarning() << payload; + return false; + } + QVariantMap request = jsonDoc.toVariant().toMap(); + + if (request.value("method").toString() == "GetInstances") { + QVariantMap params; + QVariantList instances; + foreach (const QUuid &nymeaId, m_service->engines().keys()) { + Engine *engine = m_service->engines().value(nymeaId); + QVariantMap instance; + instance.insert("id", nymeaId); + instance.insert("isReady", engine->jsonRpcClient()->connected() && !engine->thingManager()->fetchingData()); + instance.insert("name", engine->jsonRpcClient()->currentHost()->name()); + instances.append(instance); + } + params.insert("instances", instances); + sendReply(reply, params); + return true; + } + + if (request.value("method").toString() == "GetThings") { + QUuid nymeaId = request.value("params").toMap().value("nymeaId").toUuid(); + Engine *engine = m_service->engines().value(nymeaId); + if (!engine) { + qWarning() << "Android client requested things for an invalid nymea instance:" << nymeaId; + return false; } - } break; - case 1: {// Things request QVariantList thingsList; - for (int i = 0; i < m_engine->thingManager()->things()->rowCount(); i++) { - Device *thing = m_engine->thingManager()->things()->get(i); + for (int i = 0; i < engine->thingManager()->things()->rowCount(); i++) { + Device *thing = engine->thingManager()->things()->get(i); QVariantMap thingMap; thingMap.insert("id", thing->id()); thingMap.insert("name", thing->name()); @@ -62,41 +83,110 @@ bool AndroidBinder::onTransact(int code, const QAndroidParcel &data, const QAndr thingMap.insert("actions", actions); thingsList.append(thingMap); } - QJsonDocument jsonDoc = QJsonDocument::fromVariant(thingsList); - reply.handle().callMethod("writeString", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(jsonDoc.toJson()).object()); - } break; - case 2: {// ExecuteAction -// QString thingId = data.handle().callMethod("readString", "").toString(); -// jstring atId = data.handle().callMethod("readString", ""); -// QString actionTypeId = QAndroidJniObject::fromLocalRef(atId).toString(); -// jstring p = data.handle().callMethod("readString", ""); -// QString param = QAndroidJniObject::fromLocalRef(p).toString(); - qDebug() << "ExecuteAction"; - QString thingId = data.readData(); - QString actionTypeId = data.readData(); - QString param = data.readData(); - qDebug() << "**** executeAction:" << thingId << actionTypeId << param; - - // FIXME: Only works with state generated actions! - QVariantMap paramMap; - paramMap.insert("paramTypeId", actionTypeId); - paramMap.insert("value", param); - m_engine->thingManager()->executeAction(thingId, actionTypeId, {paramMap}); - - } break; -// default: -// QAndroidBinder binder = data.readBinder(); - -// qDebug() << TAG << ": onTransact() received non-name data" << data.readVariant(); -// reply.writeVariant(QVariant("Cannot process this!")); - -// // send back message -// QAndroidParcel sendData, replyData; -// sendData.writeVariant(QVariant("Send me only names!")); -// binder.transact(0, sendData, &replyData); -// qDebug() << TAG << ": onTransact() received " << replyData.readData(); - -// break; + QVariantMap params; + params.insert("things", thingsList); + sendReply(reply, params); + return true; } - return true; + + if (request.value("method").toString() == "ExecuteAction") { + qDebug() << "ExecuteAction"; + QUuid nymeaId = request.value("params").toMap().value("nymeaId").toUuid(); + Engine *engine = m_service->engines().value(nymeaId); + if (!engine) { + qWarning() << "Android client requested executeAction for an invalid nymea instance:" << nymeaId; + return false; + } + QUuid thingId = request.value("params").toMap().value("thingId").toUuid(); + QUuid actionTypeId = request.value("params").toMap().value("actionTypeId").toUuid(); + QVariantList params = request.value("params").toMap().value("params").toList(); + + qDebug() << "**** executeAction:" << thingId << actionTypeId << params; + engine->thingManager()->executeAction(thingId, actionTypeId, params); + } + +// switch (code) { +// case 0: { // Status request +// bool isReady = m_engine->jsonRpcClient()->connected() && !m_engine->thingManager()->fetchingData(); +// isReady = false; +// reply.handle().callMethod("writeBoolean", "(Z)V", isReady); +// if (isReady) { +// reply.handle().callMethod("writeString", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(m_engine->jsonRpcClient()->currentHost()->name()).object()); +// } +// } break; +// case 1: {// Things request +// QVariantList thingsList; +// for (int i = 0; i < m_engine->thingManager()->things()->rowCount(); i++) { +// Device *thing = m_engine->thingManager()->things()->get(i); +// QVariantMap thingMap; +// thingMap.insert("id", thing->id()); +// thingMap.insert("name", thing->name()); +// thingMap.insert("className", thing->thingClass()->displayName()); +// thingMap.insert("interfaces", thing->thingClass()->interfaces()); +// QVariantList states; +// for (int j = 0; j < thing->states()->rowCount(); j++) { +// State *state = thing->states()->get(j); +// QVariantMap stateMap; +// stateMap.insert("stateTypeId", state->stateTypeId()); +// stateMap.insert("name", thing->thingClass()->stateTypes()->getStateType(state->stateTypeId())->name()); +// stateMap.insert("displayName", thing->thingClass()->stateTypes()->getStateType(state->stateTypeId())->displayName()); +// stateMap.insert("value", state->value()); +// states.append(stateMap); +// } +// thingMap.insert("states", states); +// QVariantList actions; +// for (int j = 0; j < thing->thingClass()->actionTypes()->rowCount(); j++) { +// ActionType *actionType = thing->thingClass()->actionTypes()->get(j); +// QVariantMap actionMap; +// actionMap.insert("actionTypeId", actionType->id()); +// actionMap.insert("name", actionType->name()); +// actionMap.insert("displayName", actionType->displayName()); +// actions.append(actionMap); +// } +// thingMap.insert("actions", actions); +// thingsList.append(thingMap); +// } +// QJsonDocument jsonDoc = QJsonDocument::fromVariant(thingsList); +// reply.handle().callMethod("writeString", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(jsonDoc.toJson()).object()); +// } break; +// case 2: {// ExecuteAction +//// QString thingId = data.handle().callMethod("readString", "").toString(); +//// jstring atId = data.handle().callMethod("readString", ""); +//// QString actionTypeId = QAndroidJniObject::fromLocalRef(atId).toString(); +//// jstring p = data.handle().callMethod("readString", ""); +//// QString param = QAndroidJniObject::fromLocalRef(p).toString(); +// qDebug() << "ExecuteAction"; +// QString thingId = data.readData(); +// QString actionTypeId = data.readData(); +// QString param = data.readData(); +// qDebug() << "**** executeAction:" << thingId << actionTypeId << param; + +// // FIXME: Only works with state generated actions! +// QVariantMap paramMap; +// paramMap.insert("paramTypeId", actionTypeId); +// paramMap.insert("value", param); +// m_engine->thingManager()->executeAction(thingId, actionTypeId, {paramMap}); + +// } break; +//// default: +//// QAndroidBinder binder = data.readBinder(); + +//// qDebug() << TAG << ": onTransact() received non-name data" << data.readVariant(); +//// reply.writeVariant(QVariant("Cannot process this!")); + +//// // send back message +//// QAndroidParcel sendData, replyData; +//// sendData.writeVariant(QVariant("Send me only names!")); +//// binder.transact(0, sendData, &replyData); +//// qDebug() << TAG << ": onTransact() received " << replyData.readData(); + +//// break; +// } + return false; +} + +void AndroidBinder::sendReply(const QAndroidParcel &reply, const QVariantMap ¶ms) +{ + QString payload = QJsonDocument::fromVariant(params).toJson(); + reply.handle().callMethod("writeString", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(payload).object()); } diff --git a/androidservice/nymeaappservice/androidbinder.h b/androidservice/nymeaappservice/androidbinder.h index 0647cff4..36a9b795 100644 --- a/androidservice/nymeaappservice/androidbinder.h +++ b/androidservice/nymeaappservice/androidbinder.h @@ -3,17 +3,21 @@ #include +#include "nymeaappservice.h" #include "engine.h" class AndroidBinder : public QAndroidBinder { public: - explicit AndroidBinder(Engine *engine); + explicit AndroidBinder(NymeaAppService *service); bool onTransact(int code, const QAndroidParcel &data, const QAndroidParcel &reply, QAndroidBinder::CallType flags) override; private: - Engine *m_engine = nullptr; + void sendReply(const QAndroidParcel &reply, const QVariantMap ¶ms); + +private: + NymeaAppService *m_service = nullptr; }; #endif // ANDROIDBINDER_H diff --git a/androidservice/nymeaappservice/nymeaappservice.cpp b/androidservice/nymeaappservice/nymeaappservice.cpp index 756401b4..d2c673ff 100644 --- a/androidservice/nymeaappservice/nymeaappservice.cpp +++ b/androidservice/nymeaappservice/nymeaappservice.cpp @@ -11,44 +11,63 @@ NymeaAppService::NymeaAppService(int argc, char **argv): QAndroidService(argc, argv, [=](const QAndroidIntent &) { - return new AndroidBinder{m_engine}; + return new AndroidBinder{this}; }) { setApplicationName("nymea-app"); setOrganizationName("nymea"); - m_engine = new Engine(this); - QSettings settings; - settings.beginGroup("tabSettings0"); - QUuid lastConnected = settings.value("lastConnectedHost").toUuid(); - settings.endGroup(); NymeaDiscovery *discovery = new NymeaDiscovery(this); AWSClient::instance()->setConfig(settings.value("cloudEnvironment").toString()); discovery->setAwsClient(AWSClient::instance()); - NymeaHost *host = discovery->nymeaHosts()->find(lastConnected); - if (host) { - m_engine->jsonRpcClient()->connectToHost(host); + + for (int i = 0; i < 5; i++) { + settings.beginGroup(QString("tabSettings%1").arg(i)); + QUuid lastConnected = settings.value("lastConnectedHost").toUuid(); + settings.endGroup(); + + if (lastConnected.isNull()) { + continue; + } + NymeaHost *host = discovery->nymeaHosts()->find(lastConnected); + if (!host) { + continue; + } + + Engine *engine = new Engine(this); + engine->jsonRpcClient()->connectToHost(host); + m_engines.insert(host->uuid(), engine); + + + QObject::connect(engine->thingManager(), &DeviceManager::thingStateChanged, [=](const QUuid &thingId, const QUuid &stateTypeId, const QVariant &value){ + QVariantMap params; + params.insert("nymeaId", engine->jsonRpcClient()->currentHost()->uuid()); + params.insert("thingId", thingId); + params.insert("stateTypeId", stateTypeId); + params.insert("value", value); + sendNotification("ThingStateChanged", params); + }); + + connect(engine->thingManager(), &DeviceManager::fetchingDataChanged, [=]() { + qDebug() << "Fetching data changed"; + QVariantMap params; + params.insert("nymeaId", engine->jsonRpcClient()->currentHost()->uuid()); + params.insert("isReady", !engine->thingManager()->fetchingData()); + qDebug() << "Nymea host is ready" << engine->jsonRpcClient()->currentHost()->uuid(); + sendNotification("ReadyStateChanged", params); + }); } - QObject::connect(m_engine->thingManager(), &DeviceManager::thingStateChanged, [=](const QUuid &thingId, const QUuid &stateTypeId, const QVariant &value){ - QVariantMap params; - params.insert("thingId", thingId); - params.insert("stateTypeId", stateTypeId); - params.insert("value", value); - sendNotification("ThingStateChanged", params); - }); + qDebug() << "NymeaAppService started."; - connect(m_engine->thingManager(), &DeviceManager::fetchingDataChanged, [=]() { - QVariantMap params; - params.insert("isReady", !m_engine->thingManager()->fetchingData()); - if (m_engine->jsonRpcClient()->connected()) { - params.insert("systemName", m_engine->jsonRpcClient()->currentHost()->name()); - } - sendNotification("ReadyStateChanged", params); - }); +} + +QHash NymeaAppService::engines() const +{ + return m_engines; } void NymeaAppService::sendNotification(const QString ¬ification, const QVariantMap ¶ms) diff --git a/androidservice/nymeaappservice/nymeaappservice.h b/androidservice/nymeaappservice/nymeaappservice.h index 4c2c4068..6f6e653a 100644 --- a/androidservice/nymeaappservice/nymeaappservice.h +++ b/androidservice/nymeaappservice/nymeaappservice.h @@ -11,12 +11,14 @@ class NymeaAppService : public QAndroidService public: explicit NymeaAppService(int argc, char** argv); + QHash engines() const; + private: void sendNotification(const QString ¬ification, const QVariantMap ¶ms); private: - Engine *m_engine = nullptr; + QHash m_engines; }; diff --git a/config.pri b/config.pri index 9d0cae52..1dfee639 100644 --- a/config.pri +++ b/config.pri @@ -11,3 +11,6 @@ APP_REVISION=$$member(VERSION_INFO, 1) DEFINES+=APP_VERSION=\\\"$${APP_VERSION}\\\" android:QMAKE_POST_LINK += cp $$top_srcdir/version.txt $$top_builddir/ + +DISTFILES += \ + $$PWD/packaging/android/src/io/guh/nymeaapp/NymeaHost.java diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java index 4899b8e8..f9b91d3d 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java @@ -36,7 +36,10 @@ public class NymeaAppControlService extends ControlsProviderService { private String TAG = "nymea-app: NymeaAppControlService"; private NymeaAppServiceConnection m_serviceConnection; + // For publishing all available private ReplayProcessor m_publisherForAll; + private ArrayList m_pendingForAll = new ArrayList(); // pending nymea ids to query + private ReplayProcessor m_updatePublisher; private List m_activeControlIds; @@ -44,52 +47,84 @@ public class NymeaAppControlService extends ControlsProviderService { private void ensureServiceConnection() { if (m_serviceConnection == null) { m_serviceConnection = new NymeaAppServiceConnection(getBaseContext()) { - @Override public void onReady() { - process(); + @Override public void onConnectedChanged(boolean connected) { + Log.d(TAG, "Connected to NymeaAppService. Known hosts: " + m_serviceConnection.getHosts().size()); + if (connected && m_publisherForAll != null) { + Log.d(TAG, "Processing all"); + processAll(); + } } - @Override public void onUpdate(UUID thingId) { + @Override public void onReadyChanged(UUID nymeaId, boolean ready) { + Log.d(TAG, "Nymea instance " + nymeaId.toString() + " ready state changed: " + Boolean.toString(ready)); + if (ready) { + process(nymeaId); + } + } + @Override public void onUpdate(UUID nymeaId, UUID thingId) { Log.d(TAG, "onUpdate()"); if (m_updatePublisher != null && m_activeControlIds.contains(thingId.toString())) { - Thing thing = m_serviceConnection.getThing(thingId); - Log.d(TAG, "Updating publisher for thing: " + thing.name + " id: " + thing.id); - m_updatePublisher.onNext(thingToControl(thing)); + Log.d(TAG, "Updating publisher for thing: " + thingId); + m_updatePublisher.onNext(thingToControl(nymeaId, thingId)); // m_updatePublisher.onComplete(); } } }; } - if (!m_serviceConnection.isConnected()) { - Intent serviceIntent = new Intent(this, NymeaAppService.class); - bindService(serviceIntent, m_serviceConnection, Context.BIND_AUTO_CREATE); + Intent serviceIntent = new Intent(this, NymeaAppService.class); + bindService(serviceIntent, m_serviceConnection, Context.BIND_AUTO_CREATE); + } + + private void processAll() { + ensureServiceConnection(); + if (m_serviceConnection.connected()) { + // Need to add all the pending before processing + if (m_publisherForAll != null) { + for (UUID nymeaId: m_serviceConnection.getHosts().keySet()) { + m_pendingForAll.add(nymeaId); + } + } + for (UUID nymeaId: m_serviceConnection.getHosts().keySet()) { + process(nymeaId); + } + } else { + Log.d(TAG, "Not connected to NymeaAppService yet..."); } } - private void process() { + private void process(UUID nymeaId) { Log.d(TAG, "Processing..."); ensureServiceConnection(); - if (!m_serviceConnection.isReady()) { + if (!m_serviceConnection.connected()) { + Log.d(TAG, "NymeaAppService not connected to nymea instance " + nymeaId + " yet."); + return; + } + if (!m_serviceConnection.getHosts().keySet().contains(nymeaId)) { Log.d(TAG, "Service connection is not ready yet..."); return; } - for (Thing thing : m_serviceConnection.getThings()) { + for (Thing thing : m_serviceConnection.getHosts().get(nymeaId).things.values()) { Log.d(TAG, "Processing thing: " + thing.name); if (m_publisherForAll != null) { Log.d(TAG, "Adding stateless"); - m_publisherForAll.onNext(thingToControl(thing)); + m_publisherForAll.onNext(thingToControl(nymeaId, thing.id)); } if (m_updatePublisher != null) { if (m_activeControlIds.contains(thing.id.toString())) { Log.d(TAG, "Adding stateful"); - m_updatePublisher.onNext(thingToControl(thing)); + m_updatePublisher.onNext(thingToControl(nymeaId, thing.id)); } } } + if (m_pendingForAll.contains(nymeaId)) { + m_pendingForAll.remove(nymeaId); + } + // The publisher for all needs to be completed when done - if (m_publisherForAll != null) { + if (m_publisherForAll != null && m_pendingForAll.isEmpty()) { Log.d(TAG, "Completing all publisher"); m_publisherForAll.onComplete(); } @@ -103,7 +138,7 @@ public class NymeaAppControlService extends ControlsProviderService { public Publisher createPublisherForAllAvailable() { Log.d(TAG, "Creating publishers for all"); m_publisherForAll = ReplayProcessor.create(); - process(); + processAll(); return FlowAdapters.toFlowPublisher(m_publisherForAll); } @@ -112,19 +147,20 @@ public class NymeaAppControlService extends ControlsProviderService { Log.d(TAG, "Creating publishers for " + Integer.toString(controlIds.size())); m_updatePublisher = ReplayProcessor.create(); m_activeControlIds = controlIds; - process(); + processAll(); return FlowAdapters.toFlowPublisher(m_updatePublisher); } @Override public void performControlAction(String controlId, ControlAction action, Consumer consumer) { Log.d(TAG, "Performing control action: " + controlId); -//// PendingAction pendingAction = new PendingAction(); -//// pendingAction.thingId = controlId; -//// pendingAction.actionTypeId = ""; -//// pendingAction.consumer = consumer; -//// m_pendingActions.put( + UUID nymeaId = m_serviceConnection.hostForThing(UUID.fromString(controlId)); + if (nymeaId == null) { + Log.d(TAG, "Nymea host not found for thing id: " + controlId); + consumer.accept(ControlAction.RESPONSE_FAIL); + return; + } Thing thing = m_serviceConnection.getThing(UUID.fromString(controlId)); if (thing == null) { Log.d(TAG, "Thing not found for id: " + controlId); @@ -162,17 +198,21 @@ public class NymeaAppControlService extends ControlsProviderService { return; } - m_serviceConnection.executeAction(thing.id, actionTypeId, param); + m_serviceConnection.executeAction(nymeaId, thing.id, actionTypeId, param); consumer.accept(ControlAction.RESPONSE_OK); } private HashMap m_intents = new HashMap(); - private Control thingToControl(Thing thing) { + private Control thingToControl(UUID nymeaId, UUID thingId) { // Log.d(TAG, "Creating control for thing: " + thing.name + " id: " + thing.id); - // NOTE: intentId 1 doesn't work for some reason I don't understand yet... so let's make sure we never add "1" to it by always added 100 + NymeaHost nymeaHost = m_serviceConnection.getHosts().get(nymeaId); + Thing thing = nymeaHost.things.get(thingId); + + // NOTE: intentId 1 doesn't work for some reason I don't understand yet... + // so let's make sure we never add "1" to it by always added 100 int intentId = m_intents.size() + 100; PendingIntent pi; if (m_intents.containsKey(thing.id)) { @@ -184,6 +224,7 @@ public class NymeaAppControlService extends ControlsProviderService { Context context = getBaseContext(); Intent intent = new Intent(context, NymeaAppControlsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + intent.putExtra("nymeaId", nymeaId.toString()); intent.putExtra("thingId", thing.id.toString()); pi = PendingIntent.getActivity(context, intentId, intent, PendingIntent.FLAG_UPDATE_CURRENT); Log.d(TAG, "Created pendingintent for " + thing.name + " with id " + intentId + " and extra " + thing.id); @@ -191,7 +232,7 @@ public class NymeaAppControlService extends ControlsProviderService { Control.StatefulBuilder builder = new Control.StatefulBuilder(thing.id.toString(), pi) .setTitle(thing.name) .setSubtitle(thing.className) - .setStructure(m_serviceConnection.nymeaName()); + .setStructure(nymeaHost.name); if (thing.interfaces.contains("impulsebasedgaragedoor")) { builder.setDeviceType(DeviceTypes.TYPE_GARAGE); diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java index 01274bf3..ffa9f070 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java @@ -32,10 +32,13 @@ public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings } + public String nymeaId() + { + return getIntent().getStringExtra("nymeaId"); + } + public String thingId() { - Log.d(TAG, "ThingId called!"); - Log.d(TAG, getIntent().getStringExtra("thingId")); return getIntent().getStringExtra("thingId"); } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java index 7fd7f8fd..90573796 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java @@ -3,11 +3,13 @@ package io.guh.nymeaapp; import java.util.List; import java.util.ArrayList; import java.util.UUID; +import java.util.HashMap; import android.util.Log; import android.os.IBinder; import android.os.Parcel; +import android.os.RemoteException; import android.content.Intent; import android.content.IntentFilter; @@ -29,59 +31,63 @@ import org.json.*; public class NymeaAppServiceConnection implements ServiceConnection { private static final String TAG = "nymea-app: NymeaAppServiceConnection"; private IBinder m_service; - private boolean m_isConnectedToNymea = false; - private boolean m_isReady = false; private Context m_context; - private String m_nymeaName = "nymea"; - private ArrayList m_things = new ArrayList<>(); + private boolean m_connected = false; + private HashMap m_nymeaHosts = new HashMap(); public NymeaAppServiceConnection(Context context) { super(); m_context = context; } - final public boolean isConnected() { - return m_service != null; + final public boolean connected() { + return m_connected; + } + public void onConnectedChanged(boolean connected) {}; + + public final HashMap getHosts() { + return m_nymeaHosts; } - final public boolean isConnectedToNymea() { - return m_isConnectedToNymea; - } - - final public String nymeaName() { - return m_nymeaName; - } - - final public boolean isReady() { - return m_isReady; - } - - final public ArrayList getThings() { - return m_things; - } final public Thing getThing(UUID thingId) { - for (int i = 0; i < m_things.size(); i++) { - if (m_things.get(i).id.equals(thingId)) { - return m_things.get(i); + for (HashMap.Entry entry : m_nymeaHosts.entrySet()) { + Thing thing = entry.getValue().things.get(thingId); + if (thing != null) { + return thing; + } + } + return null; + } + final public UUID hostForThing(UUID thingId) { + for (HashMap.Entry entry : m_nymeaHosts.entrySet()) { + Thing thing = entry.getValue().things.get(thingId); + if (thing != null) { + return entry.getKey(); } } return null; } - public void onReady() {} + public void onReadyChanged(UUID nymeaId, boolean ready) {} public void onError() {} - public void onUpdate(UUID thingId) {} + public void onUpdate(UUID nymeaId, UUID thingId) {} - final public void executeAction(UUID thingId, UUID actionTypeId, String param) { + final public void executeAction(UUID nymeaId, UUID thingId, UUID actionTypeId, String paramValue) { try { - Parcel parcel = Parcel.obtain(); - parcel.writeByteArray(thingId.toString().getBytes()); - parcel.writeByteArray(actionTypeId.toString().getBytes()); - parcel.writeByteArray(param.getBytes()); + JSONObject params = new JSONObject(); + params.put("nymeaId", nymeaId.toString()); + params.put("thingId", thingId.toString()); + params.put("actionTypeId", actionTypeId.toString()); + JSONArray actionParams = new JSONArray(); + JSONObject param = new JSONObject(); + param.put("paramTypeId", actionTypeId.toString()); + param.put("value", paramValue); + actionParams.put(param); + params.put("params", actionParams); + Parcel parcel = createRequest("ExecuteAction", params); Parcel retParcel = Parcel.obtain(); - m_service.transact(2, parcel, retParcel, 0); -// thingsList = retParcel.readString(); + m_service.transact(1, parcel, retParcel, 0); } catch (Exception e) { Log.d(TAG, "Error calling executeAction on NymeaAppService"); } @@ -94,30 +100,44 @@ public class NymeaAppServiceConnection implements ServiceConnection { registerServiceBroadcastReceiver(); try { - Parcel parcel = Parcel.obtain(); + Parcel parcel = createRequest("GetInstances"); Parcel retParcel = Parcel.obtain(); - m_service.transact(0, parcel, retParcel, 0); - m_isReady = retParcel.readBoolean(); - if (!m_isReady) { - m_nymeaName = retParcel.readString(); - m_isConnectedToNymea = true; - Log.d(TAG, "Service is ready!"); - fetchThings(); - } else { - Log.d(TAG, "Service is not ready yet!"); + + m_service.transact(1, parcel, retParcel, 0); + + JSONObject reply = new JSONObject(retParcel.readString()); + Log.d(TAG, "Instaces received: " + reply.toString()); + JSONArray instances = reply.getJSONArray("instances"); + for (int i = 0; i < instances.length(); i++) { + JSONObject instanceMap = instances.getJSONObject(i); + NymeaHost nymeaHost = new NymeaHost(); + nymeaHost.id = UUID.fromString(instanceMap.getString("id")); + nymeaHost.name = instanceMap.getString("name"); + nymeaHost.isReady = instanceMap.getBoolean("isReady"); + m_nymeaHosts.put(nymeaHost.id, nymeaHost); + } - } catch (Exception e) { - Log.d(TAG, "Error while waiting for service to be connected to nymea"); - m_service = null; + } catch (JSONException e) { + Log.d(TAG, "Error while processing JSON in communication with NymeaAppService: " + e.toString()); + onError(); + return; + } catch (RemoteException e) { + Log.d(TAG, "Error communicating with NymeaAppService: " + e.toString()); onError(); return; } + + m_connected = true; + onConnectedChanged(m_connected); } @Override public void onServiceDisconnected(ComponentName arg0) { m_service = null; - m_isConnectedToNymea = false; - m_isReady = false; + for (int i = 0; i < m_nymeaHosts.size(); i++) { + m_nymeaHosts.get(i).isReady = false; + } + m_connected = false; + onConnectedChanged(m_connected); } public void registerServiceBroadcastReceiver() { @@ -130,7 +150,7 @@ 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"); + Log.d(TAG, "In OnReceive broadcast receiver"); if (NymeaAppService.NYMEA_APP_BROADCAST.equals(intent.getAction())) { String payload = intent.getStringExtra("data"); try { @@ -147,52 +167,57 @@ 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, params.toString()); if (data.getString("notification").equals("ThingStateChanged")) { + UUID nymeaId = UUID.fromString(params.getString("nymeaId")); 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); - for (int i = 0; i < m_things.size(); i++) { - if (m_things.get(i).id.equals(thingId)) { - m_things.get(i).stateById(stateTypeId).value = value; - onUpdate(thingId); - } + Thing thing = getThing(thingId); + if (thing != null) { + thing.stateById(stateTypeId).value = value; + onUpdate(nymeaId, thingId); + } else { + Log.d(TAG, "Got a state change notification for a thing we don't know!"); } } if (data.getString("notification").equals("ReadyStateChanged")) { - m_isReady = params.getBoolean("isReady"); - if (m_isReady) { - m_nymeaName = params.getString("systemName"); - fetchThings(); + UUID nymeaId = UUID.fromString(params.getString("nymeaId")); + NymeaHost host = m_nymeaHosts.get(nymeaId); + host.isReady = params.getBoolean("isReady"); + if (host.isReady) { + Log.d(TAG, "Host is ready. Fetching things..."); + fetchThings(nymeaId); + } else { + Log.d(TAG, "Host is not ready yet..."); } } } - private void fetchThings() { + private void fetchThings(UUID nymeaId) { + Log.d(TAG, "Fetching things"); String thingsList; try { - Log.d(TAG, "Fetching things"); - Parcel parcel = Parcel.obtain(); + JSONObject params = new JSONObject(); + params.put("nymeaId", nymeaId.toString()); + Parcel parcel = createRequest("GetThings", params); Parcel retParcel = Parcel.obtain(); m_service.transact(1, parcel, retParcel, 0); thingsList = retParcel.readString(); - Log.d(TAG, "Things fetched"); } catch (Exception e) { Log.d(TAG, "Error fetching things from NymeaAppService"); - m_service = null; - m_isConnectedToNymea = false; onError(); return; } try { - Log.d(TAG, "Parsing JSON"); - JSONArray arr = new JSONArray(thingsList); - for (int i = 0; i < arr.length(); i++) { - JSONObject entry = arr.getJSONObject(i); + JSONObject result = new JSONObject(thingsList); + for (int i = 0; i < result.getJSONArray("things").length(); i++) { + JSONObject entry = result.getJSONArray("things").getJSONObject(i); Thing thing = new Thing(); thing.id = UUID.fromString(entry.getString("id")); thing.name = entry.getString("name"); @@ -220,19 +245,34 @@ public class NymeaAppServiceConnection implements ServiceConnection { a.displayName = actionMap.getString("displayName"); thing.actions.add(a); } - m_things.add(thing); + m_nymeaHosts.get(nymeaId).things.put(thing.id, thing); } } catch (Exception e) { Log.d(TAG, "Error parsing JSON from NymeaAppService: " + e.toString()); + Log.d(TAG, thingsList); m_service = null; - m_isConnectedToNymea = false; onError(); return; } - Log.d(TAG, "Fetched things"); - m_isReady = true; - onReady(); + Log.d(TAG, "Things fetched: " + m_nymeaHosts.get(nymeaId).things.size()); + m_nymeaHosts.get(nymeaId).isReady = true; + onReadyChanged(nymeaId, true); + } + + private Parcel createRequest(String method) throws JSONException { + return createRequest(method, null); + } + private Parcel createRequest(String method, JSONObject params) throws JSONException { + Parcel ret = Parcel.obtain(); + JSONObject payload = new JSONObject(); + payload.put("method", method); + if (params != null) { + payload.put("params", params); + } + Log.d(TAG, "Parcel payload: " + payload.toString()); + ret.writeString(payload.toString()); + return ret; } } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaHost.java b/packaging/android/src/io/guh/nymeaapp/NymeaHost.java new file mode 100644 index 00000000..a568563c --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/NymeaHost.java @@ -0,0 +1,13 @@ +package io.guh.nymeaapp; + +import java.util.HashMap; +import java.util.ArrayList; +import java.util.UUID; + +public class NymeaHost { + + UUID id; + boolean isReady = false; + String name = ""; + HashMap things = new HashMap(); +}