diff --git a/androidservice/androidbinder.cpp b/androidservice/androidbinder.cpp new file mode 100644 index 00000000..029f17e3 --- /dev/null +++ b/androidservice/androidbinder.cpp @@ -0,0 +1,97 @@ +#include "androidbinder.h" +#include "engine.h" +#include "types/device.h" + +#include +#include +#include +#include +#include + +AndroidBinder::AndroidBinder(Engine * engine): + m_engine(engine) +{ + 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 + bool isReady = m_engine->jsonRpcClient()->connected() && !m_engine->thingManager()->fetchingData(); + reply.handle().callMethod("writeBoolean", "(Z)V", isReady); + } 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().toString()); + 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().toString()); + 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().toString()); + 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 true; +} diff --git a/androidservice/androidbinder.h b/androidservice/androidbinder.h new file mode 100644 index 00000000..0647cff4 --- /dev/null +++ b/androidservice/androidbinder.h @@ -0,0 +1,19 @@ +#ifndef ANDROIDBINDER_H +#define ANDROIDBINDER_H + +#include + +#include "engine.h" + +class AndroidBinder : public QAndroidBinder +{ +public: + explicit AndroidBinder(Engine *engine); + + bool onTransact(int code, const QAndroidParcel &data, const QAndroidParcel &reply, QAndroidBinder::CallType flags) override; + +private: + Engine *m_engine = nullptr; +}; + +#endif // ANDROIDBINDER_H diff --git a/androidservice/androidservice.pro b/androidservice/androidservice.pro new file mode 100644 index 00000000..62c2cb68 --- /dev/null +++ b/androidservice/androidservice.pro @@ -0,0 +1,34 @@ +TEMPLATE = lib +TARGET = service +CONFIG += dll +QT += core androidextras +QT += network qml quick quickcontrols2 svg websockets bluetooth charts + +include(../config.pri) +include(../android_openssl/openssl.pri) + + +INCLUDEPATH += $$top_srcdir/libnymea-app/ + +# https://bugreports.qt.io/browse/QTBUG-83165 +LIBS += -L$${top_builddir}/libnymea-app/$${ANDROID_TARGET_ARCH} + +LIBS += -L$$top_builddir/libnymea-app/ -lnymea-app +PRE_TARGETDEPS += ../libnymea-app + +SOURCES += \ + androidbinder.cpp \ + service_main.cpp + +#HEADERS += servicemessenger.h + +HEADERS += \ + androidbinder.h + +DISTFILES += \ + ../packaging/android/src/io/guh/nymeaapp/Action.java \ + ../packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java \ + ../packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java \ + ../packaging/android/src/io/guh/nymeaapp/Thing.java \ + ../packaging/android/src/io/guh/nymeaapp/State.java + diff --git a/androidservice/service_main.cpp b/androidservice/service_main.cpp new file mode 100644 index 00000000..9a63316e --- /dev/null +++ b/androidservice/service_main.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include + +#include "androidbinder.h" + +#include "engine.h" +#include "connection/discovery/nymeadiscovery.h" +#include "connection/nymeahosts.h" + +int main(int argc, char *argv[]) +{ + qWarning() << "Service starting from a separate .so file"; + + + Engine *engine = new Engine(); +// engine->jsonRpcClient()->connectToHost() + + + QAndroidService app(argc, argv, [=](const QAndroidIntent &) { + qDebug() << "Android service onBind()"; + return new AndroidBinder{engine}; + }); + + app.setApplicationName("nymea-app"); + app.setOrganizationName("nymea"); + + qDebug() << "Starting nymea app service"; + + QSettings settings; + settings.beginGroup("tabSettings0"); + QUuid lastConnected = settings.value("lastConnectedHost").toUuid(); + settings.endGroup(); + + NymeaDiscovery *discovery = new NymeaDiscovery(); + + NymeaHost *host = discovery->nymeaHosts()->find(lastConnected); + qDebug() << "**** Tab settings" << lastConnected << host; + if (host) { + engine->jsonRpcClient()->connectToHost(host); + } + + QObject::connect(engine->thingManager(), &DeviceManager::thingStateChanged, [=](const QUuid &thingId, const QUuid &stateTypeId, const QVariant &value){ + qDebug() << "**** State changed" << thingId << stateTypeId << value; + qDebug() << "Sending broadcast"; + QtAndroid::androidService().callMethod("sendBroadcast", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + QAndroidJniObject::fromString(thingId.toString()).object(), + QAndroidJniObject::fromString(stateTypeId.toString()).object(), + QAndroidJniObject::fromString(value.toString()).object()); + }); + + return app.exec(); +} diff --git a/libnymea-app/devicemanager.cpp b/libnymea-app/devicemanager.cpp index a2eaafd1..b2ce20d7 100644 --- a/libnymea-app/devicemanager.cpp +++ b/libnymea-app/devicemanager.cpp @@ -175,10 +175,11 @@ void DeviceManager::notificationReceived(const QVariantMap &data) qWarning() << "Device state change notification received for an unknown device"; return; } - QUuid stateTyoeId = data.value("params").toMap().value("stateTypeId").toUuid(); + QUuid stateTypeId = data.value("params").toMap().value("stateTypeId").toUuid(); QVariant value = data.value("params").toMap().value("value"); -// qDebug() << "Device state changed for:" << dev->name() << "State name:" << dev->thingClass()->stateTypes()->getStateType(stateTyoeId) << "value:" << value; - dev->setStateValue(stateTyoeId, value); +// qDebug() << "Device state changed for:" << dev->name() << "State name:" << dev->thingClass()->stateTypes()->getStateType(stateTypeId) << "value:" << value; + dev->setStateValue(stateTypeId, value); + emit thingStateChanged(dev->id(), stateTypeId, value); } else if (notification == "Devices.DeviceAdded") { Device *dev = JsonTypes::unpackDevice(this, data.value("params").toMap().value("device").toMap(), m_thingClasses); if (!dev) { diff --git a/libnymea-app/devicemanager.h b/libnymea-app/devicemanager.h index b09d3ddc..1afcc01d 100644 --- a/libnymea-app/devicemanager.h +++ b/libnymea-app/devicemanager.h @@ -151,7 +151,8 @@ signals: void fetchingDataChanged(); void notificationReceived(const QString &deviceId, const QString &eventTypeId, const QVariantList ¶ms); - void eventTriggered(const QString &deviceId, const QString &eventTypeId, const QVariantMap params); + void eventTriggered(const QUuid &deviceId, const QUuid &eventTypeId, const QVariantMap params); + void thingStateChanged(const QUuid &deviceId, const QUuid &stateTypeId, const QVariant &value); private: Vendors *m_vendors; diff --git a/libnymea-app/engine.h b/libnymea-app/engine.h index 300b7fb4..07b38c6a 100644 --- a/libnymea-app/engine.h +++ b/libnymea-app/engine.h @@ -61,9 +61,6 @@ class Engine : public QObject public: explicit Engine(QObject *parent = nullptr); - bool connected() const; - QString connectedHost() const; - DeviceManager *deviceManager() const; DeviceManager *thingManager() const; RuleManager *ruleManager() const; diff --git a/libnymea-common/libnymea-common.h b/libnymea-common/libnymea-common.h deleted file mode 100644 index 1fd15938..00000000 --- a/libnymea-common/libnymea-common.h +++ /dev/null @@ -1,7 +0,0 @@ -#include - -#if defined(LIBNYMEA_COMMON) -# define LIBNYMEA_COMMON_EXPORT Q_DECL_EXPORT -#else -# define LIBNYMEA_COMMON_EXPORT Q_DECL_IMPORT -#endif diff --git a/libnymea-common/libnymea-common.pro b/libnymea-common/libnymea-common.pro deleted file mode 100644 index a7e17797..00000000 --- a/libnymea-common/libnymea-common.pro +++ /dev/null @@ -1,15 +0,0 @@ -include(../config.pri) - -TARGET = nymea-common -TEMPLATE = lib -CONFIG += staticlib - -QT -= gui -QT += network - -HEADERS += \ - - -SOURCES += \ - - diff --git a/nymea-app.pro b/nymea-app.pro index 24e9cefe..63a6c6be 100644 --- a/nymea-app.pro +++ b/nymea-app.pro @@ -82,6 +82,11 @@ icons.path = /usr/share/ INSTALLS += desktopfile icons } +# Android service +android: { +SUBDIRS += androidservice +} + # Linux desktop (snap package) snap: { desktopfile.files = packaging/linux/nymea-app.desktop diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index 480a2503..c4ae3f28 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -78,6 +78,7 @@ android { $$ANDROID_PACKAGE_SOURCE_DIR/src/io/guh/nymeaapp/NymeaAppActivity.java \ $$ANDROID_PACKAGE_SOURCE_DIR/src/io/guh/nymeaapp/NymeaAppNotificationService.java \ $$ANDROID_PACKAGE_SOURCE_DIR/src/io/guh/nymeaapp/NymeaAppControlService.java \ + $$ANDROID_PACKAGE_SOURCE_DIR/src/io/guh/nymeaapp/NymeaAppService.java \ $$ANDROID_PACKAGE_SOURCE_DIR/LICENSE # https://bugreports.qt.io/browse/QTBUG-83165 diff --git a/nymea-app/platformhelper.cpp b/nymea-app/platformhelper.cpp index 27a97d4d..e3226514 100644 --- a/nymea-app/platformhelper.cpp +++ b/nymea-app/platformhelper.cpp @@ -98,3 +98,8 @@ QString PlatformHelper::fromClipBoard() { return QApplication::clipboard()->text(); } + +void PlatformHelper::syncThings() +{ + // no-op by default +} diff --git a/nymea-app/platformhelper.h b/nymea-app/platformhelper.h index d0074ef7..de32c3a2 100644 --- a/nymea-app/platformhelper.h +++ b/nymea-app/platformhelper.h @@ -88,6 +88,8 @@ public: Q_INVOKABLE virtual void toClipBoard(const QString &text); Q_INVOKABLE virtual QString fromClipBoard(); + Q_INVOKABLE virtual void syncThings(); + signals: void permissionsRequestFinished(); void screenTimeoutChanged(); diff --git a/nymea-app/platformintegration/android/platformhelperandroid.cpp b/nymea-app/platformintegration/android/platformhelperandroid.cpp index 7332b8e9..e0861943 100644 --- a/nymea-app/platformintegration/android/platformhelperandroid.cpp +++ b/nymea-app/platformintegration/android/platformhelperandroid.cpp @@ -33,6 +33,7 @@ #include #include #include +#include // WindowManager.LayoutParams @@ -47,13 +48,16 @@ static PlatformHelperAndroid *m_instance; static QAndroidJniObject getAndroidWindow() { QAndroidJniObject window = QtAndroid::androidActivity().callObjectMethod("getWindow", "()Landroid/view/Window;"); - window.callMethod("addFlags", "(I)V", FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.callMethod("clearFlags", "(I)V", FLAG_TRANSLUCENT_STATUS); return window; } PlatformHelperAndroid::PlatformHelperAndroid(QObject *parent) : PlatformHelper(parent) { +// QAndroidIntent serviceIntent(QtAndroid::androidActivity().object(), "io.guh.nymeaapp.NymeaAppControlService"); + +// m_serviceConnection = new DeviceControlServiceConnection(); +// QtAndroid::bindService(serviceIntent, *m_serviceConnection, QtAndroid::BindFlag::AutoCreate); + m_instance = this; } @@ -123,6 +127,33 @@ void PlatformHelperAndroid::vibrate(PlatformHelper::HapticsFeedback feedbackType QtAndroid::androidActivity().callMethod("vibrate","(I)V", duration); } +void PlatformHelperAndroid::syncThings() +{ + + QAndroidIntent serviceIntent(QtAndroid::androidActivity().object(), + "io/guh/nymeaapp/NymeaAppService"); + QAndroidJniObject result = QtAndroid::androidActivity().callObjectMethod( + "startService", + "(Landroid/content/Intent;)Landroid/content/ComponentName;", + serviceIntent.handle().object()); + + +// QtAndroid::androidService() + +// QAndroidIntent serviceIntent(QtAndroid::androidActivity().object(), +// "io/guh/nymeaapp/NymeaAppControlService"); +// serviceIntent.putExtra("name", QByteArray("foobar")); + + +// m_serviceConnection->handle().callMethod("syncThings", "(Ljava/lang/String;)V", "bla"); + + +// QAndroidJniObject result = QtAndroid::androidActivity().callObjectMethod( +// "syncThings", +// "(Landroid/content/Intent;)Landroid/content/ComponentName;", +// m_serviceConnection->handle().object()); +} + void PlatformHelperAndroid::setTopPanelColor(const QColor &color) { PlatformHelper::setTopPanelColor(color); @@ -132,6 +163,8 @@ void PlatformHelperAndroid::setTopPanelColor(const QColor &color) QtAndroid::runOnAndroidThread([=]() { QAndroidJniObject window = getAndroidWindow(); + window.callMethod("addFlags", "(I)V", FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.callMethod("clearFlags", "(I)V", FLAG_TRANSLUCENT_STATUS); window.callMethod("setStatusBarColor", "(I)V", color.rgba()); }); diff --git a/nymea-app/platformintegration/android/platformhelperandroid.h b/nymea-app/platformintegration/android/platformhelperandroid.h index b648b755..d393d549 100644 --- a/nymea-app/platformintegration/android/platformhelperandroid.h +++ b/nymea-app/platformintegration/android/platformhelperandroid.h @@ -31,9 +31,11 @@ #ifndef PLATFORMHELPERANDROID_H #define PLATFORMHELPERANDROID_H -#include #include "platformhelper.h" + +#include #include +#include class PlatformHelperAndroid : public PlatformHelper { @@ -55,6 +57,7 @@ public: QString deviceManufacturer() const override; Q_INVOKABLE void vibrate(HapticsFeedback feedbackType) override; + Q_INVOKABLE void syncThings() override; void setTopPanelColor(const QColor &color) override; void setTopPanelTheme(Theme theme); @@ -62,7 +65,6 @@ public: private: static void permissionRequestFinished(const QtAndroid::PermissionResultMap &); - }; #endif // PLATFORMHELPERANDROID_H diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index e38bc904..8fb50f6f 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -132,6 +132,12 @@ Item { initialItem: Page {} } + Button { + anchors.centerIn: parent + text: "bla" + onClicked: PlatformHelper.syncThings() + } + Component.onCompleted: { setupPushNotifications(); if (autoConnectHost.length > 0) { diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml index 33c84d4d..85aabd73 100644 --- a/packaging/android/AndroidManifest.xml +++ b/packaging/android/AndroidManifest.xml @@ -62,8 +62,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/android/src/io/guh/nymeaapp/Action.java b/packaging/android/src/io/guh/nymeaapp/Action.java new file mode 100644 index 00000000..7cd4735d --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/Action.java @@ -0,0 +1,7 @@ +package io.guh.nymeaapp; + +public class Action { + public String typeId; + public String name; + public String displayName; +} diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java index 1a923e5e..0abd2ec5 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppActivity.java @@ -8,9 +8,6 @@ import android.telephony.TelephonyManager; import android.provider.Settings.Secure; import android.os.Vibrator; -//import com.google.firebase.messaging.MessageForwardingService; - - public class NymeaAppActivity extends org.qtproject.qt5.android.bindings.QtActivity { public String deviceSerial() diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java index e5727f07..68afe3cd 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java @@ -2,129 +2,212 @@ package io.guh.nymeaapp; import android.util.Log; import android.content.Intent; +import android.content.ServiceConnection; +import android.content.ComponentName; import android.app.PendingIntent; import android.net.Uri; import android.content.Context; import android.service.controls.ControlsProviderService; -import android.service.controls.actions.ControlAction; -import android.service.controls.actions.BooleanAction; +import android.service.controls.actions.*; import android.service.controls.Control; import android.service.controls.DeviceTypes; +import android.service.controls.templates.*; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; import java.util.concurrent.Flow.Publisher; import java.util.function.Consumer; import java.util.List; import java.util.ArrayList; +import java.util.HashMap; import io.reactivex.Flowable; import io.reactivex.processors.ReplayProcessor; import org.reactivestreams.FlowAdapters; +import org.json.*; public class NymeaAppControlService extends ControlsProviderService { + private String TAG = "nymea-app: NymeaAppControlService"; + private NymeaAppServiceConnection m_serviceConnection; - private ReplayProcessor updatePublisher; + private ReplayProcessor m_publisherForAll; + private ReplayProcessor m_updatePublisher; + private List m_activeControlIds; - @Override - public Publisher createPublisherForAllAvailable() { - Log.d("********************************* Creating publishers for all ****************************", "fff"); - Context context = getBaseContext(); - Intent i = new Intent(); - PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT); -// pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT); - List controls = new ArrayList<>(); - Control control = new Control.StatelessBuilder("e24b0d95-9982-4f9b-ad8b-2aa6b9aba8fd", pi) - // Required: The name of the control - .setTitle("TestControl") - // Required: Usually the room where the control is located - .setSubtitle("TestSubtitle") - // Optional: Structure where the control is located, an example would be a house - .setStructure("TestLocation") - // Required: Type of device, i.e., thermostat, light, switch - .setDeviceType(DeviceTypes.TYPE_GENERIC_ON_OFF) // For example, DeviceTypes.TYPE_THERMOSTAT - .build(); - controls.add(control); - // Create more controls here if needed and add it to the ArrayList + private void ensureServiceConnection() { + if (m_serviceConnection == null) { + m_serviceConnection = new NymeaAppServiceConnection(getBaseContext()) { + @Override public void onReady() { + process(); + } + @Override public void onUpdate(String thingId) { + if (m_updatePublisher != null && m_activeControlIds.contains(thingId)) { + Thing thing = m_serviceConnection.getThing(thingId); + Log.d(TAG, "Updating publisher for thing: " + thing.name + " id: " + thing.id); + m_updatePublisher.onNext(thingToControl(thing)); +// m_updatePublisher.onComplete(); + } + } + }; + } + if (!m_serviceConnection.isConnected()) { + Intent serviceIntent = new Intent(this, NymeaAppService.class); + bindService(serviceIntent, m_serviceConnection, Context.BIND_AUTO_CREATE); + } + } - // Uses the RxJava 2 library - return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls)); + private void process() { + Log.d(TAG, "Processing..."); + ensureServiceConnection(); + if (!m_serviceConnection.isReady()) { + Log.d(TAG, "Service connection is not ready yet..."); + return; + } + +// ArrayList things = m_serviceConnection.getThings(); + + for (Thing thing : m_serviceConnection.getThings()) { + Log.d(TAG, "Processing thing: " + thing.name); + + if (m_publisherForAll != null) { + Log.d(TAG, "Adding stateless"); + m_publisherForAll.onNext(thingToControl(thing)); + } + + if (m_updatePublisher != null) { + if (m_activeControlIds.contains(thing.id)) { + Log.d(TAG, "Adding stateful"); + m_updatePublisher.onNext(thingToControl(thing)); + } + } + } + + // The publisher for all needs to be completed when done + if (m_publisherForAll != null) { + Log.d(TAG, "Completing all publisher"); + m_publisherForAll.onComplete(); + } + + Log.d(TAG, "Done processing"); + // We never close the update publisher as we need that one to send updates } + @Override + public Publisher createPublisherForAllAvailable() { + Log.d(TAG, "Creating publishers for all"); + m_publisherForAll = ReplayProcessor.create(); + process(); + return FlowAdapters.toFlowPublisher(m_publisherForAll); + } + @Override public Publisher createPublisherFor(List controlIds) { - Log.d("********************************* Creating publishers for one ****************************", ".."); -// for(int i = 0; i < controlIds.size(); i++) { -// Log.d("requested control id:", controlIds.get(i)); -// } - Context context = getBaseContext(); - /* Fill in details for the activity related to this device. On long press, - * this Intent will be launched in a bottomsheet. Please design the activity - * accordingly to fit a more limited space (about 2/3 screen height). - */ - Intent i = new Intent(); - PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT); - - updatePublisher = ReplayProcessor.create(); - - // For each controlId in controlIds - - if (controlIds.contains("e24b0d95-9982-4f9b-ad8b-2aa6b9aba8fd")) { - Log.d("**", "control asked"); - Control control = new Control.StatefulBuilder("e24b0d95-9982-4f9b-ad8b-2aa6b9aba8fd", pi) - // Required: The name of the control - .setTitle("TestTitle") - // Required: Usually the room where the control is located - .setSubtitle("TestSubTitle") - // Optional: Structure where the control is located, an example would be a house - .setStructure("TestStructure") - // Required: Type of device, i.e., thermostat, light, switch - .setDeviceType(DeviceTypes.TYPE_GENERIC_ON_OFF) // For example, DeviceTypes.TYPE_THERMOSTAT - // Required: Current status of the device - .setStatus(Control.STATUS_OK) // For example, Control.STATUS_OK - .build(); - - updatePublisher.onNext(control); - } - // Uses the Reactive Streams API - return FlowAdapters.toFlowPublisher(updatePublisher); + Log.d(TAG, "Creating publishers for " + Integer.toString(controlIds.size())); + m_updatePublisher = ReplayProcessor.create(); + m_activeControlIds = controlIds; + process(); + return FlowAdapters.toFlowPublisher(m_updatePublisher); } @Override public void performControlAction(String controlId, ControlAction action, Consumer consumer) { - /* First, locate the control identified by the controlId. Once it is located, you can - * interpret the action appropriately for that specific device. For instance, the following - * assumes that the controlId is associated with a light, and the light can be turned on - * or off. - */ - if (action instanceof BooleanAction) { + Log.d(TAG, "Performing control action: " + controlId); +//// PendingAction pendingAction = new PendingAction(); +//// pendingAction.thingId = controlId; +//// pendingAction.actionTypeId = ""; +//// pendingAction.consumer = consumer; +//// m_pendingActions.put( - // Inform SystemUI that the action has been received and is being processed - consumer.accept(ControlAction.RESPONSE_OK); - - BooleanAction bAction = (BooleanAction) action; - // In this example, action.getNewState() will have the requested action: true for “On”, - // false for “Off”. - - /* This is where application logic/network requests would be invoked to update the state of - * the device. - * After updating, the application should use the publisher to update SystemUI with the new - * state. - */ -// Control control = new Control.StatefulBuilder("123", pi) -// // Required: The name of the control -// .setTitle("TestControl") -// // Required: Usually the room where the control is located -// .setSubtitle("TestSubTitle") -// // Optional: Structure where the control is located, an example would be a house -// .setStructure("TestStructure") -// // Required: Type of device, i.e., thermostat, light, switch -// .setDeviceType(DeviceTypes.TYPE_GENERIC_ON_OFF) // For example, DeviceTypes.TYPE_THERMOSTAT -// // Required: Current status of the device -// .setStatus(Control.STATUS_OK) // For example, Control.STATUS_OK -// .build(); - -// // This is the publisher the application created during the call to createPublisherFor() -// updatePublisher.onNext(control); + Thing thing = m_serviceConnection.getThing(controlId); + if (thing == null) { + Log.d(TAG, "Thing not found for id: " + controlId); + consumer.accept(ControlAction.RESPONSE_FAIL); + return; } + + String actionTypeId; + String param; + if (thing.interfaces.contains("dimmablelight") && action instanceof FloatAction) { + actionTypeId = thing.stateByName("brightness").typeId; + FloatAction fAction = (FloatAction) action; + param = String.valueOf(Math.round(fAction.getNewValue())); + } else if (thing.interfaces.contains("power") && action instanceof BooleanAction) { + actionTypeId = thing.stateByName("power").typeId; + BooleanAction bAction = (BooleanAction) action; + param = bAction.getNewState() == true ? "true" : "false"; + } else if (thing.interfaces.contains("closable") && action instanceof BooleanAction) { + BooleanAction bAction = (BooleanAction) action; + if (bAction.getNewState()) { + Log.d(TAG, "executing open"); + actionTypeId = thing.actionByName("open").typeId; + } else { + Log.d(TAG, "executing close"); + actionTypeId = thing.actionByName("close").typeId; + } + param = ""; + } else { + Log.d(TAG, "Unhandled action for: " + thing.name); + consumer.accept(ControlAction.RESPONSE_FAIL); + return; + } + + m_serviceConnection.executeAction(thing.id, actionTypeId, param); + consumer.accept(ControlAction.RESPONSE_OK); + + } + + private Control thingToControl(Thing thing) { + Log.d(TAG, "Creating control for thing: " + thing.name + " id: " + thing.id); + + + // TODO: Create Intent to launch control view + Context context = getBaseContext(); + Intent intent = new Intent(this, NymeaAppActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent m_pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + Control.StatefulBuilder builder = new Control.StatefulBuilder(thing.id, m_pi) + .setTitle(thing.name) + .setSubtitle(thing.className) + .setStructure("TestLocation"); + + if (thing.interfaces.contains("impulsebasedgaragedoor")) { + builder.setDeviceType(DeviceTypes.TYPE_GARAGE); + builder.setControlTemplate(new StatelessTemplate(thing.id)); + } else if (thing.interfaces.contains("statefulgaragedoor")) { + builder.setDeviceType(DeviceTypes.TYPE_GARAGE); + State stateState = thing.stateByName("state"); + ControlButton controlButton = new ControlButton(stateState.value.equals("open"), stateState.displayName); + builder.setControlTemplate(new ToggleTemplate(thing.id, controlButton)); + +// } else if (thing.interfaces.contains("extendedstatefulgaragedoor")) { +// builder.setDeviceTyoe(DeviceTypes.TYPE_GARAGE); + + } else if (thing.interfaces.contains("light")) { + builder.setDeviceType(DeviceTypes.TYPE_LIGHT); + State powerState = thing.stateByName("power"); + ControlButton controlButton = new ControlButton(powerState.value.equals("true"), powerState.displayName); + + if (thing.interfaces.contains("dimmablelight")) { + State brightnessState = thing.stateByName("brightness"); + RangeTemplate rangeTemplate = new RangeTemplate(thing.id, 0, 100, Float.parseFloat(brightnessState.value), 1, brightnessState.displayName); + builder.setControlTemplate(new ToggleRangeTemplate(thing.id, controlButton, rangeTemplate)); + } else { + builder.setControlTemplate(new ToggleTemplate(thing.id, controlButton)); + } + } else if (thing.interfaces.contains("powersocket")) { + builder.setDeviceType(DeviceTypes.TYPE_OUTLET); + State powerState = thing.stateByName("power"); + ControlButton controlButton = new ControlButton(powerState.value.equals("true"), powerState.displayName); + builder.setControlTemplate(new ToggleTemplate(thing.id, controlButton)); + } else { + builder.setDeviceType(DeviceTypes.TYPE_GENERIC_ON_OFF); + } + builder.setStatus(Control.STATUS_OK); + + Log.d(TAG, "Created control for thing: " + thing.name + " id: " + thing.id); + return builder.build(); } } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java new file mode 100644 index 00000000..64335642 --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java @@ -0,0 +1,13 @@ +package io.guh.nymeaapp; +import android.util.Log; +import android.content.Intent; +import android.content.Context; +import android.os.Bundle; +import android.os.Build; +import android.telephony.TelephonyManager; +import android.provider.Settings.Secure; +import android.os.Vibrator; + +public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings.QtActivity +{ +} diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppService.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppService.java new file mode 100644 index 00000000..f5b8cef5 --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppService.java @@ -0,0 +1,49 @@ +package io.guh.nymeaapp; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.qtproject.qt5.android.bindings.QtService; + +public class NymeaAppService extends QtService +{ + public static final String BROADCAST_STATE_CHANGE = "io.guh.nymeaapp.NymeaAppService.broadcast.stateChanged"; + + private static final String TAG = "nymea-app: NymeaAppService"; + + @Override + public void onCreate() { + super.onCreate(); + Log.i(TAG, "Creating Service"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.i(TAG, "Destroying Service"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + int ret = super.onStartCommand(intent, flags, startId); + + // Do some work + + Log.d(TAG, "*************** Service started"); + + return ret; + } + + public void sendBroadcast(String thingId, String stateTypeId, String value) { +// String name = new String(intent.getByteArrayExtra("name")); + Intent sendToUiIntent = new Intent(); + sendToUiIntent.setAction(BROADCAST_STATE_CHANGE); + sendToUiIntent.putExtra("name", "io.guh.nymeaapp.NymeaAppService"); + sendToUiIntent.putExtra("thingId", thingId); + sendToUiIntent.putExtra("stateTypeId", stateTypeId); + sendToUiIntent.putExtra("value", value); + Log.d(TAG, "Service sending broadcast"); + sendBroadcast(sendToUiIntent); + } +} diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java new file mode 100644 index 00000000..06ef24b1 --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java @@ -0,0 +1,205 @@ +package io.guh.nymeaapp; + +import java.util.List; +import java.util.ArrayList; + +import android.util.Log; + +import android.os.IBinder; +import android.os.Parcel; + +import android.content.Intent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.ServiceConnection; +import android.content.ComponentName; +import android.content.Context; + +import android.service.controls.Control; +import android.service.controls.DeviceTypes; + +import io.reactivex.processors.ReplayProcessor; + +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 ArrayList m_things = new ArrayList<>(); + + public NymeaAppServiceConnection(Context context) { + super(); + m_context = context; + } + + final public boolean isConnected() { + return m_service != null; + } + + final public boolean isConnectedToNymea() { + return m_isConnectedToNymea; + } + + final public boolean isReady() { + return m_isReady; + } + + final public ArrayList getThings() { + return m_things; + } + final public Thing getThing(String thingId) { + for (int i = 0; i < m_things.size(); i++) { + if (m_things.get(i).id.equals(thingId)) { + return m_things.get(i); + } + } + return null; + } + + public void onReady() {} + public void onError() {} + public void onUpdate(String thingId) {} + + final public void executeAction(String thingId, String actionTypeId, String param) { + try { + Parcel parcel = Parcel.obtain(); + parcel.writeByteArray(thingId.getBytes()); + parcel.writeByteArray(actionTypeId.getBytes()); + parcel.writeByteArray(param.getBytes()); + Parcel retParcel = Parcel.obtain(); + m_service.transact(2, parcel, retParcel, 0); +// thingsList = retParcel.readString(); + } catch (Exception e) { + Log.d(TAG, "Error calling executeAction on NymeaAppService"); + } + } + + @Override public void onServiceConnected(ComponentName className, IBinder service) { + Log.d(TAG, "Connected to NymeaAppService"); + m_service = service; + + try { + boolean ready = false; + Log.d(TAG, "Waiting for service to be connected to nymea..."); + do { + Parcel parcel = Parcel.obtain(); + Parcel retParcel = Parcel.obtain(); + m_service.transact(0, parcel, retParcel, 0); + ready = retParcel.readBoolean(); + if (!ready) { + Thread.sleep(100); + } + } while (!ready); + Log.d(TAG, "Service connected to nymea!"); + m_isConnectedToNymea = true; + } catch (Exception e) { + Log.d(TAG, "Error while waiting for service to be connected to nymea"); + m_service = null; + onError(); + return; + } + + String thingsList; + try { + Log.d(TAG, "Fetching things"); + Parcel parcel = Parcel.obtain(); + 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); + Thing thing = new Thing(); + thing.id = entry.getString("id"); + thing.name = entry.getString("name"); + thing.className = entry.getString("className"); + JSONArray ifaces = entry.getJSONArray("interfaces"); + for (int j = 0; j < ifaces.length(); j++) { + thing.interfaces.add(ifaces.get(j)); + } + JSONArray states = entry.getJSONArray("states"); + for (int j = 0; j < states.length(); j++) { + JSONObject stateMap = states.getJSONObject(j); + State s = new State(); + s.typeId = stateMap.getString("stateTypeId"); + s.name = stateMap.getString("name"); + s.displayName = stateMap.getString("displayName"); + s.value = stateMap.getString("value"); + thing.states.add(s); + } + JSONArray actions = entry.getJSONArray("actions"); + for (int j = 0; j < actions.length(); j++) { + JSONObject actionMap = actions.getJSONObject(j); + Action a = new Action(); + a.typeId = actionMap.getString("actionTypeId"); + a.name = actionMap.getString("name"); + a.displayName = actionMap.getString("displayName"); + thing.actions.add(a); + } + m_things.add(thing); + } + + } catch (Exception e) { + Log.d(TAG, "Error parsing JSON from NymeaAppService: " + thingsList); + m_service = null; + m_isConnectedToNymea = false; + onError(); + return; + } + + Log.d(TAG, "Fetched things"); + m_isReady = true; + onReady(); + + registerServiceBroadcastReceiver(); + } + + @Override public void onServiceDisconnected(ComponentName arg0) { + m_service = null; + m_isConnectedToNymea = false; + m_isReady = false; + } + + public void registerServiceBroadcastReceiver() { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(NymeaAppService.BROADCAST_STATE_CHANGE); + m_context.registerReceiver(serviceMessageReceiver, intentFilter); + Log.d(TAG, "Registered broadcast receiver"); + } + private BroadcastReceiver serviceMessageReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "In OnReceive broadcast receiver"); + if (NymeaAppService.BROADCAST_STATE_CHANGE.equals(intent.getAction())) { + String name = intent.getStringExtra("name"); + String thingId = intent.getStringExtra("thingId"); + String stateTypeId = intent.getStringExtra("stateTypeId"); + String value = intent.getStringExtra("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); + } + } + } + } + }; +} diff --git a/packaging/android/src/io/guh/nymeaapp/State.java b/packaging/android/src/io/guh/nymeaapp/State.java new file mode 100644 index 00000000..c106700f --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/State.java @@ -0,0 +1,8 @@ +package io.guh.nymeaapp; + +public class State { + public String typeId; + public String name; + public String displayName; + public String value; +} diff --git a/packaging/android/src/io/guh/nymeaapp/Thing.java b/packaging/android/src/io/guh/nymeaapp/Thing.java new file mode 100644 index 00000000..1211212c --- /dev/null +++ b/packaging/android/src/io/guh/nymeaapp/Thing.java @@ -0,0 +1,54 @@ +package io.guh.nymeaapp; + +import android.util.Log; + +import java.util.List; +import java.util.ArrayList; + +public class Thing { + static final public String TAG = "nymea-app: Thing"; + public String id; + public String name; + public String className; + public List interfaces = new ArrayList(); + + public ArrayList states = new ArrayList(); + public ArrayList actions = new ArrayList(); + + public State stateByName(String name) { + for (int i = 0; i < states.size(); i++) { + if (states.get(i).name.equals(name)) { + return states.get(i); + } + } + return null; + } + + public State stateById(String stateTypeId) { + for (int i = 0; i < states.size(); i++) { + if (states.get(i).typeId.equals(stateTypeId)) { + return states.get(i); + } + } + return null; + } + + public Action actionByName(String name) { + for (int i = 0; i < actions.size(); i++) { + Log.d(TAG, "Thing has action: " + actions.get(i).name); + if (actions.get(i).name.equals(name)) { + return actions.get(i); + } + } + return null; + } + + public Action actionById(String actionTypeId) { + for (int i = 0; i < actions.size(); i++) { + if (actions.get(i).typeId.equals(actionTypeId)) { + return actions.get(i); + } + } + return null; + } +}