It's working (needs a lot more love still tho)

This commit is contained in:
Michael Zanetti 2020-09-14 13:23:14 +02:00
parent ee34f6a623
commit 09e259266b
25 changed files with 809 additions and 129 deletions

View File

@ -0,0 +1,97 @@
#include "androidbinder.h"
#include "engine.h"
#include "types/device.h"
#include <QDebug>
#include <QAndroidParcel>
#include <QAndroidJniObject>
#include <QJsonDocument>
#include <QtAndroid>
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<void>("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<void>("writeString", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(jsonDoc.toJson()).object<jstring>());
} break;
case 2: {// ExecuteAction
// QString thingId = data.handle().callMethod<QAndroidJniObject>("readString", "").toString();
// jstring atId = data.handle().callMethod<jstring>("readString", "");
// QString actionTypeId = QAndroidJniObject::fromLocalRef(atId).toString();
// jstring p = data.handle().callMethod<jstring>("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;
}

View File

@ -0,0 +1,19 @@
#ifndef ANDROIDBINDER_H
#define ANDROIDBINDER_H
#include <QAndroidBinder>
#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

View File

@ -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

View File

@ -0,0 +1,55 @@
#include <QDebug>
#include <QAndroidService>
#include <QSettings>
#include <QCoreApplication>
#include <QtAndroid>
#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<void>("sendBroadcast", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
QAndroidJniObject::fromString(thingId.toString()).object<jstring>(),
QAndroidJniObject::fromString(stateTypeId.toString()).object<jstring>(),
QAndroidJniObject::fromString(value.toString()).object<jstring>());
});
return app.exec();
}

View File

@ -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) {

View File

@ -151,7 +151,8 @@ signals:
void fetchingDataChanged();
void notificationReceived(const QString &deviceId, const QString &eventTypeId, const QVariantList &params);
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;

View File

@ -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;

View File

@ -1,7 +0,0 @@
#include <QtCore/QtGlobal>
#if defined(LIBNYMEA_COMMON)
# define LIBNYMEA_COMMON_EXPORT Q_DECL_EXPORT
#else
# define LIBNYMEA_COMMON_EXPORT Q_DECL_IMPORT
#endif

View File

@ -1,15 +0,0 @@
include(../config.pri)
TARGET = nymea-common
TEMPLATE = lib
CONFIG += staticlib
QT -= gui
QT += network
HEADERS += \
SOURCES += \

View File

@ -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

View File

@ -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

View File

@ -98,3 +98,8 @@ QString PlatformHelper::fromClipBoard()
{
return QApplication::clipboard()->text();
}
void PlatformHelper::syncThings()
{
// no-op by default
}

View File

@ -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();

View File

@ -33,6 +33,7 @@
#include <QAndroidJniObject>
#include <QtAndroid>
#include <QDebug>
#include <QAndroidIntent>
// WindowManager.LayoutParams
@ -47,13 +48,16 @@ static PlatformHelperAndroid *m_instance;
static QAndroidJniObject getAndroidWindow()
{
QAndroidJniObject window = QtAndroid::androidActivity().callObjectMethod("getWindow", "()Landroid/view/Window;");
window.callMethod<void>("addFlags", "(I)V", FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.callMethod<void>("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<void>("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<void>("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<void>("addFlags", "(I)V", FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.callMethod<void>("clearFlags", "(I)V", FLAG_TRANSLUCENT_STATUS);
window.callMethod<void>("setStatusBarColor", "(I)V", color.rgba());
});

View File

@ -31,9 +31,11 @@
#ifndef PLATFORMHELPERANDROID_H
#define PLATFORMHELPERANDROID_H
#include <QObject>
#include "platformhelper.h"
#include <QObject>
#include <QtAndroid>
#include <QAndroidServiceConnection>
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

View File

@ -132,6 +132,12 @@ Item {
initialItem: Page {}
}
Button {
anchors.centerIn: parent
text: "bla"
onClicked: PlatformHelper.syncThings()
}
Component.onCompleted: {
setupPushNotifications();
if (autoConnectHost.length > 0) {

View File

@ -62,8 +62,36 @@
<!-- extract android style -->
</activity>
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="io.guh.nymeaapp.NymeaAppControlsActivity" android:label="nymea:app" android:screenOrientation="unspecified" android:launchMode="singleTop">
<meta-data android:name="android.app.lib_name" android:value="service"/>
</activity>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
<service android:process=":qt_service" android:name="io.guh.nymeaapp.NymeaAppService">
<meta-data android:name="android.app.lib_name" android:value="service"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Run with local libs -->
<!-- Background running -->
<meta-data android:name="android.app.background_running" android:value="true"/>
<!-- Background running -->
</service>
<service android:name="com.google.firebase.messaging.MessageForwardingService" android:exported="false">
</service>

View File

@ -0,0 +1,7 @@
package io.guh.nymeaapp;
public class Action {
public String typeId;
public String name;
public String displayName;
}

View File

@ -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()

View File

@ -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<Thing> 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();
}
}

View File

@ -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
{
}

View File

@ -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);
}
}

View File

@ -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<Thing> 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<Thing> 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);
}
}
}
}
};
}

View File

@ -0,0 +1,8 @@
package io.guh.nymeaapp;
public class State {
public String typeId;
public String name;
public String displayName;
public String value;
}

View File

@ -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<State>();
public ArrayList<State> states = new ArrayList<State>();
public ArrayList<Action> actions = new ArrayList<Action>();
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;
}
}