Drop androidservice

qt6-qmake-android
Simon Stürz 2025-10-17 11:47:27 +02:00
parent fa1f14f07f
commit fd11f30f01
18 changed files with 0 additions and 1448 deletions

View File

@ -1,56 +0,0 @@
TEMPLATE = lib
TARGET = service
CONFIG += dll
QT += core core-private
QT += network qml quick quickcontrols2 svg websockets bluetooth charts nfc
include(../shared.pri)
include(../3rdParty/android/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
RESOURCES += controlviews/controlviews.qrc \
../nymea-app/resources.qrc \
../nymea-app/images.qrc \
../nymea-app/styles.qrc
INCLUDEPATH += ../nymea-app/
SOURCES += \
controlviews/devicecontrolapplication.cpp \
nymeaappservice/nymeaappservice.cpp \
nymeaappservice/androidbinder.cpp \
../nymea-app/stylecontroller.cpp \
../nymea-app/platformhelper.cpp \
../nymea-app/nfchelper.cpp \
../nymea-app/nfcthingactionwriter.cpp \
../nymea-app/platformintegration/android/platformhelperandroid.cpp \
service_main.cpp
HEADERS += \
controlviews/devicecontrolapplication.h \
nymeaappservice/nymeaappservice.h \
nymeaappservice/androidbinder.h \
../nymea-app/stylecontroller.h \
../nymea-app/platformhelper.h \
../nymea-app/nfchelper.h \
../nymea-app/nfcthingactionwriter.h \
../nymea-app/platformintegration/android/platformhelperandroid.h \
DISTFILES += \
java/io/guh/nymeaapp/Action.java \
java/io/guh/nymeaapp/NymeaAppControlService.java \
java/io/guh/nymeaapp/NymeaAppService.java \
java/io/guh/nymeaapp/NymeaAppControlsActivity.java \
java/io/guh/nymeaapp/NymeaAppServiceConnection.java \
java/io/guh/nymeaapp/Thing.java \
java/io/guh/nymeaapp/State.java \
java/io/guh/nymeaapp/NymeaHost.java \
controlviews/Main.qml

View File

@ -1,66 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
import QtQuick.Layouts
import QtCore
import Nymea
import "qrc:/ui/devicepages/"
ApplicationWindow {
id: app
visible: true
visibility: ApplicationWindow.FullScreen
color: Material.background
title: Configuration.appName
Material.theme: NymeaUtils.isDark(Style.backgroundColor) ? Material.Dark : Material.Light
Material.background: Style.backgroundColor
Material.accent: Style.accentColor
Material.foreground: Style.foregroundColor
font.pixelSize: mediumFont
font.weight: Font.Normal
font.capitalization: Font.MixedCase
font.family: Style.fontFamily
property int margins: 16
property int bigMargins: 20
property int extraSmallFont: 10
property int smallFont: 13
property int mediumFont: 16
property int largeFont: 20
property int smallIconSize: 16
property int iconSize: 24
property int bigIconSize: 40
property int hugeIconSize: 64
property int delegateHeight: 60
readonly property bool landscape: app.width > app.height
ThingsProxy {
id: thingProxy
engine: _engine
filterThingId: controlledThingId
}
property Thing controlledThing: engine.thingManager.fetchingData ? null : engine.thingManager.things.getThing(controlledThingId)
onControlledThingChanged: (controlledThing) => {
loader.setSource("qrc:/ui/devicepages/" + NymeaUtils.interfaceListToDevicePage(controlledThing.thingClass.interfaces), {thing: controlledThing, header: null})
PlatformHelper.hideSplashScreen();
}
Loader {
id: loader
anchors.fill: parent
anchors.bottomMargin: app.margins // For some reason the bottom edge seems a bit off in the overlay
}
onClosing: {
print("************* Control View closing")
}
}

View File

@ -1,5 +0,0 @@
<RCC>
<qresource prefix="/">
<file>Main.qml</file>
</qresource>
</RCC>

View File

@ -1,234 +0,0 @@
#include "devicecontrolapplication.h"
#include "engine.h"
#include "connection/discovery/nymeadiscovery.h"
#include "connection/nymeahosts.h"
#include "libnymea-app-core.h"
#include "../nymea-app/stylecontroller.h"
#include "../nymea-app/platformhelper.h"
#include "../nymea-app/nfchelper.h"
#include "../nymea-app/nfcthingactionwriter.h"
#include "../nymea-app/platformintegration/android/platformhelperandroid.h"
#include <QQmlApplicationEngine>
#include <QtDebug>
#include <QtQml>
#include <QtAndroid>
#include <QJniObject>
#include <QAndroidIntent>
#include <QNdefNfcUriRecord>
QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
Q_UNUSED(scriptEngine)
return new PlatformHelperAndroid();
}
DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QApplication(argc, argv)
{
setApplicationName("nymea-app");
setOrganizationName("nymea");
QSettings settings;
m_discovery = new NymeaDiscovery(this);
m_engine = new Engine(this);
m_qmlEngine = new QQmlApplicationEngine(this);
Nymea::Core::registerQmlTypes();
qmlRegisterSingletonType<PlatformHelper>("Nymea", 1, 0, "PlatformHelper", platformHelperProvider);
qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" );
qmlRegisterType<NfcThingActionWriter>("Nymea", 1, 0, "NfcThingActionWriter");
qmlRegisterSingletonType<NfcHelper>("Nymea", 1, 0, "NfcHelper", NfcHelper::nfcHelperProvider);
StyleController *styleController = new StyleController("light", this);
QQmlFileSelector *styleSelector = new QQmlFileSelector(m_qmlEngine);
styleSelector->setExtraSelectors({styleController->currentStyle()});
foreach (const QFileInfo &fi, QDir(":/ui/fonts/").entryInfoList()) {
QFontDatabase::addApplicationFont(fi.absoluteFilePath());
}
foreach (const QFileInfo &fi, QDir(":/styles/" + styleController->currentStyle() + "/fonts/").entryInfoList()) {
qDebug() << "Adding style font:" << fi.absoluteFilePath();
QFontDatabase::addApplicationFont(fi.absoluteFilePath());
}
qmlRegisterSingletonType(QUrl("qrc:///styles/" + styleController->currentStyle() + "/Style.qml"), "Nymea", 1, 0, "Style" );
m_qmlEngine->rootContext()->setContextProperty("styleController", styleController);
m_qmlEngine->rootContext()->setContextProperty("engine", m_engine);
m_qmlEngine->rootContext()->setContextProperty("_engine", m_engine);
m_qmlEngine->rootContext()->setContextProperty("controlledThingId", ""); // Unknown at this point
m_qmlEngine->load(QUrl(QLatin1String("qrc:/Main.qml")));
jboolean startedByNfc = QtAndroid::androidActivity().callMethod<jboolean>("startedByNfc", "()Z");
if (startedByNfc) {
qDebug() << "**** Started by NFC";
qDebug() << "Registering NFC handler and waiting for message.";
QNearFieldManager *manager = new QNearFieldManager(this);
manager->registerNdefMessageHandler(this, SLOT(handleNdefMessage(QNdefMessage,QNearFieldTarget*)));
} else {
qDebug() << "*** Started by other intent";
qDebug() << "Expecing nymeaId and thingId in intent extras.";
QString nymeaId = QtAndroid::androidActivity().callObjectMethod<jstring>("nymeaId").toString();
QString thingId = QtAndroid::androidActivity().callObjectMethod<jstring>("thingId").toString();
connectToNymea(nymeaId);
m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId);
}
}
void DeviceControlApplication::handleNdefMessage(QNdefMessage message, QNearFieldTarget *target)
{
Q_UNUSED(target)
qDebug() << "************* NFC message!" << message.toByteArray();
if (message.count() < 1) {
qWarning() << "NFC message doesn't contain any records...";
return;
}
// NOTE: At this point we're only supporting one NDEF record per message
QNdefRecord record = message.first();
QNdefNfcUriRecord uriRecord(record);
QUrl url = uriRecord.uri();
if (url.scheme() != "nymea") {
qWarning() << "NDEF URI record scheme is not \"nymea://\"";
return;
}
QUuid nymeaId = QUuid(url.host());
if (nymeaId.isNull()) {
qWarning() << "Invalid nymea UUID in NDEF record.";
return;
}
QUuid thingId = QUuid(QUrlQuery(url).queryItemValue("t"));
if (thingId.isNull()) {
qWarning() << "Invalid thing in NDEF record";
return;
}
m_pendingNfcAction = url;
connectToNymea(nymeaId);
m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId);
connect(m_engine->thingManager(), &ThingManager::fetchingDataChanged, [this](){
if (m_engine->jsonRpcClient()->connected() && !m_engine->thingManager()->fetchingData()) {
qDebug() << "Ready to process commands";
runNfcAction();
}
});
}
void DeviceControlApplication::connectToNymea(const QUuid &nymeaId)
{
NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId);
if (!host) {
qWarning() << "No such nymea host:" << nymeaId;
// TODO: We could wait here until the discovery finds it... But it really should be cached already...
exit(1);
}
qDebug() << "Connecting to:" << host->name();
m_engine->jsonRpcClient()->connectToHost(host);
}
void DeviceControlApplication::runNfcAction()
{
if (!m_pendingNfcAction.isEmpty()) {
qDebug() << "NFC action:" << m_pendingNfcAction;
}
QUrl url = m_pendingNfcAction;
m_pendingNfcAction.clear();
if (url.scheme() != "nymea") {
qWarning() << "NDEF URI record scheme is not \"nymea://\" in" << url.toString();
return;
}
QUuid nymeaId = QUuid(url.host());
if (nymeaId.isNull()) {
qWarning() << "Invalid nymea UUID" << url.host() << "in NDEF record" << url.toString();
return;
}
QUuid thingId = QUuid(QUrlQuery(url).queryItemValue("t"));
Thing *thing = m_engine->thingManager()->things()->getThing(thingId);
if (!thing) {
qDebug() << "Thing" << thingId.toString() << "from" << url.toString() << "doesn't exist on nymea host" << nymeaId.toString();
return;
}
QList<QPair<QString, QString>> queryItems = QUrlQuery(url.query()).queryItems();
for (int i = 0; i < queryItems.count(); i++) {
QString entryName = queryItems.at(i).first;
if (entryName == "t") {
continue;
}
if (!entryName.startsWith("a")) {
qDebug() << "Only actions are supported. Skipping query item" << entryName;
continue;
}
QString actionString = queryItems.at(i).second;
QStringList parts = actionString.split("#");
if (parts.count() == 0) {
qDebug() << "Invalid action definition:" << actionString;
continue;
}
if (parts.count() > 2) {
// The parameters might contain a #, let's merge them again
parts[1] = parts.mid(1).join('#');
}
QString actionTypeName = parts.at(0);
ActionType *actionType = thing->thingClass()->actionTypes()->findByName(actionTypeName);
if (!actionType) {
qWarning() << "Invalid action name" << actionType << "in url:" << url.toString();
continue;
}
QHash<QString, QVariant> paramsInUri;
if (parts.count() > 1) {
QString paramsString = parts.at(1);
foreach (const QString &paramString, paramsString.split("+")) {
QStringList parts = paramString.split(":");
if (parts.count() != 2) {
qWarning() << "Invalid param format" << paramString << "in url:" << url.toString();
continue;
}
paramsInUri.insert(parts.at(0), parts.at(1));
}
}
qDebug() << "Parameters in NFC uri:" << paramsInUri;
QVariantList params;
for (int j = 0; j < actionType->paramTypes()->rowCount(); j++) {
ParamType *paramType = actionType->paramTypes()->get(j);
QVariantMap param;
param.insert("paramTypeId", paramType->id());
if (paramsInUri.contains(paramType->name())) {
param.insert("value", paramsInUri.value(paramType->name()));
} else {
param.insert("value", paramType->defaultValue());
}
params.append(param);
}
qDebug() << "Action parameters:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
m_engine->thingManager()->executeAction(thingId, actionType->id(), params);
}
}

View File

@ -1,37 +0,0 @@
#ifndef DEVICECONTROLAPPLICATION_H
#define DEVICECONTROLAPPLICATION_H
#include <QApplication>
#include <QNearFieldManager>
#include <QNdefMessage>
#include <QQmlApplicationEngine>
#include <QNdefMessage>
#include "types/ruleactions.h"
#include "connection/discovery/nymeadiscovery.h"
#include "engine.h"
class DeviceControlApplication : public QApplication
{
Q_OBJECT
public:
explicit DeviceControlApplication(int argc, char *argv[]);
private slots:
void handleNdefMessage(QNdefMessage message,QNearFieldTarget* target);
void connectToNymea(const QUuid &nymeaId);
void runNfcAction();
private:
NymeaDiscovery *m_discovery = nullptr;
Engine *m_engine = nullptr;
QQmlApplicationEngine *m_qmlEngine = nullptr;
QUrl m_pendingNfcAction;
};
#endif // DEVICECONTROLAPPLICATION_H

View File

@ -1,9 +0,0 @@
package io.guh.nymeaapp;
import java.util.UUID;
public class Action {
public UUID typeId;
public String name;
public String displayName;
}

View File

@ -1,289 +0,0 @@
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.*;
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.UUID;
import java.util.HashMap;
import io.reactivex.Flowable;
import io.reactivex.processors.ReplayProcessor;
import org.reactivestreams.FlowAdapters;
import org.json.*;
// Android device controls service
// This service is instantiated by the android device controls on demand. It will
// connect to the NymeaAppService and interact with nymea through that.
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<UUID> m_pendingForAll = new ArrayList<UUID>(); // pending nymea ids to query
private ReplayProcessor m_updatePublisher;
private List m_activeControlIds;
private void ensureServiceConnection() {
if (m_serviceConnection == null) {
m_serviceConnection = new NymeaAppServiceConnection(getBaseContext()) {
@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 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) {
if (m_updatePublisher != null && m_activeControlIds.contains(thingId.toString())) {
// Log.d(TAG, "Updating publisher for thing: " + thingId);
m_updatePublisher.onNext(thingToControl(nymeaId, thingId));
// m_updatePublisher.onComplete();
}
}
};
}
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()) {
if (m_serviceConnection.getHosts().get(nymeaId).isReady) {
process(nymeaId);
}
}
} else {
Log.d(TAG, "Not connected to NymeaAppService yet...");
}
}
private void process(UUID nymeaId) {
Log.d(TAG, "Processing...");
ensureServiceConnection();
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.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(nymeaId, thing.id));
}
if (m_updatePublisher != null) {
if (m_activeControlIds.contains(thing.id.toString())) {
Log.d(TAG, "Adding stateful");
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 && m_pendingForAll.isEmpty()) {
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();
processAll();
return FlowAdapters.toFlowPublisher(m_publisherForAll);
}
@Override
public Publisher createPublisherFor(List controlIds) {
Log.d(TAG, "Creating publishers for " + Integer.toString(controlIds.size()));
m_updatePublisher = ReplayProcessor.create();
m_activeControlIds = controlIds;
processAll();
return FlowAdapters.toFlowPublisher(m_updatePublisher);
}
@Override
public void performControlAction(String controlId, ControlAction action, Consumer consumer) {
Log.d(TAG, "Performing control action: " + controlId);
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);
consumer.accept(ControlAction.RESPONSE_FAIL);
return;
}
UUID 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 if (thing.interfaces.contains("volumecontroller") && thing.stateByName("volume") != null) {
actionTypeId = thing.stateByName("volume").typeId;
FloatAction fAction = (FloatAction) action;
param = String.valueOf(Math.round(fAction.getNewValue()));
} else {
Log.d(TAG, "Unhandled action for: " + thing.name);
consumer.accept(ControlAction.RESPONSE_FAIL);
return;
}
m_serviceConnection.executeAction(nymeaId, thing.id, actionTypeId, param);
consumer.accept(ControlAction.RESPONSE_OK);
}
private HashMap<UUID, Integer> m_intents = new HashMap<UUID, Integer>();
private Control thingToControl(UUID nymeaId, UUID thingId) {
// Log.d(TAG, "Creating control for thing: " + thing.name + " id: " + thing.id);
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)) {
intentId = m_intents.get(thing.id);
} else {
m_intents.put(thing.id, intentId);
}
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 | PendingIntent.FLAG_IMMUTABLE);
Log.d(TAG, "Created pendingintent for " + thing.name + " with id " + intentId + " and extra " + thing.id);
Control.StatefulBuilder builder = new Control.StatefulBuilder(thing.id.toString(), pi)
.setTitle(thing.name)
.setSubtitle(thing.className)
.setStructure(nymeaHost.name);
if (thing.interfaces.contains("impulsebasedgaragedoor")) {
builder.setDeviceType(DeviceTypes.TYPE_GARAGE);
builder.setControlTemplate(new StatelessTemplate(thing.id.toString()));
} 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.toString(), 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.toString(), 0, 100, Float.parseFloat(brightnessState.value), 1, brightnessState.displayName);
builder.setControlTemplate(new ToggleRangeTemplate(thing.id.toString(), controlButton, rangeTemplate));
} else {
builder.setControlTemplate(new ToggleTemplate(thing.id.toString(), 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.toString(), controlButton));
} else if (thing.interfaces.contains("mediaplayer")) {
if (thing.stateByName("playerType").value == "video") {
builder.setDeviceType(DeviceTypes.TYPE_TV);
} else {
// FIXME: There doesn't seem to be a speaker DeviceType!?!
builder.setDeviceType(DeviceTypes.TYPE_TV);
}
if (thing.interfaces.contains("volumecontroller")) {
State volumeState = thing.stateByName("volume");
if (volumeState != null) {
RangeTemplate rangeTemplate = new RangeTemplate(thing.id.toString(), 0, 100, Float.parseFloat(volumeState.value), 1, volumeState.displayName);
builder.setControlTemplate(rangeTemplate);
}
}
} 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

@ -1,55 +0,0 @@
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;
import android.os.Process;
import android.nfc.NfcAdapter;
import android.nfc.NdefMessage;
import android.os.Parcelable;
// An activity spawned by android device controls on demand.
public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings.QtActivity
{
private static final String TAG = "nymea-app: NymeaAppControlActivity";
@Override public void onPause() {
Log.d(TAG, "Pausing...");
System.exit(0);
}
@Override public void onResume() {
super.onResume();
Log.d(TAG, "Resuming...");
}
@Override public void onDestroy() {
Log.d(TAG, "Destroying...");
}
public boolean startedByNfc() {
return NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction());
}
public String nymeaId()
{
return getIntent().getStringExtra("nymeaId");
}
public String thingId()
{
return getIntent().getStringExtra("thingId");
}
public void vibrate(int duration)
{
Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(duration);
}
}

View File

@ -1,50 +0,0 @@
package io.guh.nymeaapp;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.qtproject.qt5.android.bindings.QtService;
// Background service establishing a connection to nymea and providing data on android specific interfaces
// such as IBinder and BroadcastListener
// This service loads the service_main Qt entry point and does most of its work in C++/Qt
public class NymeaAppService extends QtService
{
public static final String NYMEA_APP_BROADCAST = "io.guh.nymeaapp.NymeaAppService.broadcast";
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 payload) {
Intent sendToUiIntent = new Intent();
sendToUiIntent.setAction(NYMEA_APP_BROADCAST);
sendToUiIntent.putExtra("data", payload);
// Log.d(TAG, "Service sending broadcast");
sendBroadcast(sendToUiIntent);
}
}

View File

@ -1,283 +0,0 @@
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;
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.*;
// Helper class to establish a connection to the NymeaAppService and interact
// with that using IBinder and ServiceBroadcastListener
public class NymeaAppServiceConnection implements ServiceConnection {
private static final String TAG = "nymea-app: NymeaAppServiceConnection";
private IBinder m_service;
private Context m_context;
private boolean m_connected = false;
private HashMap<UUID, NymeaHost> m_nymeaHosts = new HashMap<UUID, NymeaHost>();
public NymeaAppServiceConnection(Context context) {
super();
m_context = context;
}
final public boolean connected() {
return m_connected;
}
public void onConnectedChanged(boolean connected) {};
final public boolean isReady(UUID nymeaId) {
return m_nymeaHosts.get(nymeaId).isReady;
}
public void onReadyChanged(UUID nymeaId, boolean ready) {}
public final HashMap<UUID, NymeaHost> getHosts() {
return m_nymeaHosts;
}
final public Thing getThing(UUID thingId) {
for (HashMap.Entry<UUID, NymeaHost> 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<UUID, NymeaHost> entry : m_nymeaHosts.entrySet()) {
Thing thing = entry.getValue().things.get(thingId);
if (thing != null) {
return entry.getKey();
}
}
return null;
}
public void onError() {}
public void onUpdate(UUID nymeaId, UUID thingId) {}
final public void executeAction(UUID nymeaId, UUID thingId, UUID actionTypeId, String paramValue) {
try {
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(1, parcel, retParcel, 0);
} 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;
registerServiceBroadcastReceiver();
try {
Parcel parcel = createRequest("GetInstances");
Parcel retParcel = Parcel.obtain();
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);
if (nymeaHost.isReady) {
fetchThings(nymeaHost.id);
}
}
} 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;
for (int i = 0; i < m_nymeaHosts.size(); i++) {
m_nymeaHosts.get(i).isReady = false;
}
m_connected = false;
onConnectedChanged(m_connected);
}
public void registerServiceBroadcastReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(NymeaAppService.NYMEA_APP_BROADCAST);
m_context.registerReceiver(serviceMessageReceiver, intentFilter);
Log.d(TAG, "Registered broadcast receiver");
}
private BroadcastReceiver serviceMessageReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (NymeaAppService.NYMEA_APP_BROADCAST.equals(intent.getAction())) {
String payload = intent.getStringExtra("data");
try {
processBroadcast(payload);
} catch(JSONException e) {
Log.d(TAG, "Error parsing broadcast JSON: " + e.toString());
}
}
}
};
private void processBroadcast(String payload) throws JSONException
{
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);
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")) {
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(UUID nymeaId) {
Log.d(TAG, "Fetching things");
String thingsList;
try {
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();
} catch (Exception e) {
Log.d(TAG, "Error fetching things from NymeaAppService: " + e.toString());
onError();
return;
}
try {
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");
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 = UUID.fromString(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 = UUID.fromString(actionMap.getString("actionTypeId"));
a.name = actionMap.getString("name");
a.displayName = actionMap.getString("displayName");
thing.actions.add(a);
}
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;
onError();
return;
}
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;
}
}

View File

@ -1,13 +0,0 @@
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<UUID, Thing> things = new HashMap<UUID, Thing>();
}

View File

@ -1,10 +0,0 @@
package io.guh.nymeaapp;
import java.util.UUID;
public class State {
public UUID typeId;
public String name;
public String displayName;
public String value;
}

View File

@ -1,56 +0,0 @@
package io.guh.nymeaapp;
import android.util.Log;
import java.util.List;
import java.util.ArrayList;
import java.util.UUID;
public class Thing {
static final public String TAG = "nymea-app: Thing";
public UUID 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(UUID 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(UUID actionTypeId) {
for (int i = 0; i < actions.size(); i++) {
if (actions.get(i).typeId.equals(actionTypeId)) {
return actions.get(i);
}
}
return null;
}
}

View File

@ -1,115 +0,0 @@
#include "androidbinder.h"
#include "engine.h"
#include "types/thing.h"
#include <QDebug>
#include <QAndroidParcel>
#include <QJniObject>
#include <QJsonDocument>
#include <QtAndroid>
AndroidBinder::AndroidBinder(NymeaAppService *service):
m_service(service)
{
}
bool AndroidBinder::onTransact(int code, const QAndroidParcel &data, const QAndroidParcel &reply, QAndroidBinder::CallType flags)
{
qDebug() << "onTransact: code " << code << ", flags " << int(flags);
// QString payload = data.readData();
QString payload = data.handle().callObjectMethod<jstring>("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;
}
QVariantList thingsList;
for (int i = 0; i < engine->thingManager()->things()->rowCount(); i++) {
Thing *thing = 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);
}
QVariantMap params;
params.insert("things", thingsList);
sendReply(reply, params);
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);
}
return false;
}
void AndroidBinder::sendReply(const QAndroidParcel &reply, const QVariantMap &params)
{
QString payload = QJsonDocument::fromVariant(params).toJson();
reply.handle().callMethod<void>("writeString", "(Ljava/lang/String;)V", QJniObject::fromString(payload).object<jstring>());
}

View File

@ -1,23 +0,0 @@
#ifndef ANDROIDBINDER_H
#define ANDROIDBINDER_H
#include <QAndroidBinder>
#include "nymeaappservice.h"
#include "engine.h"
class AndroidBinder : public QAndroidBinder
{
public:
explicit AndroidBinder(NymeaAppService *service);
bool onTransact(int code, const QAndroidParcel &data, const QAndroidParcel &reply, QAndroidBinder::CallType flags) override;
private:
void sendReply(const QAndroidParcel &reply, const QVariantMap &params);
private:
NymeaAppService *m_service = nullptr;
};
#endif // ANDROIDBINDER_H

View File

@ -1,83 +0,0 @@
#include "nymeaappservice.h"
#include "androidbinder.h"
#include <QtAndroid>
#include <QDebug>
#include <QSettings>
#include <QJsonDocument>
#include "connection/discovery/nymeadiscovery.h"
#include "connection/nymeahosts.h"
NymeaAppService::NymeaAppService(int argc, char **argv):
QAndroidService(argc, argv, [=](const QAndroidIntent &) {
return new AndroidBinder{this};
})
{
setApplicationName("nymea-app");
setOrganizationName("nymea");
QSettings settings;
NymeaDiscovery *discovery = new NymeaDiscovery(this);
settings.beginGroup("ConfiguredHosts");
foreach (const QString &childGroup, settings.childGroups()) {
settings.beginGroup(childGroup);
QUuid lastConnected = settings.value("uuid").toUuid();
QString cachedName = settings.value("cachedName").toString();
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(), &ThingManager::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(), &ThingManager::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);
});
}
settings.endGroup();
qDebug() << "NymeaAppService started.";
}
QHash<QUuid, Engine *> NymeaAppService::engines() const
{
return m_engines;
}
void NymeaAppService::sendNotification(const QString &notification, const QVariantMap &params)
{
QVariantMap data;
data.insert("notification", notification);
data.insert("params", params);
QString payload = QJsonDocument::fromVariant(data).toJson();
QtAndroid::androidService().callMethod<void>("sendBroadcast",
"(Ljava/lang/String;)V",
QJniObject::fromString(payload).object<jstring>());
}

View File

@ -1,27 +0,0 @@
#ifndef NYMEAAPPSERVICE_H
#define NYMEAAPPSERVICE_H
#include <QNearFieldManager>
#include <QNdefMessage>
#include <QtCore/private/qandroidextras_p.h>
#include "engine.h"
class NymeaAppService : public QAndroidService
{
Q_OBJECT
public:
explicit NymeaAppService(int argc, char** argv);
QHash<QUuid, Engine*> engines() const;
private:
void sendNotification(const QString &notification, const QVariantMap &params);
private:
QHash<QUuid, Engine*> m_engines;
};
#endif // NYMEAAPPSERVICE_H

View File

@ -1,37 +0,0 @@
#include <QDebug>
#include <QCoreApplication>
#include "nymeaappservice/nymeaappservice.h"
#include "controlviews/devicecontrolapplication.h"
#include <QCommandLineParser>
#include <QLoggingCategory>
int main(int argc, char *argv[])
{
qWarning() << "Service starting from a separate .so file";
QLoggingCategory::setFilterRules("qt.remoteobjects.debug=true\n");
QStringList args;
for (int i = 0; i < argc; i++) {
args.append(QByteArray(argv[i]));
qDebug() << "nymea-app: Added command line arg" << args.last();
}
QCommandLineParser parser;
QCommandLineOption controlActivityOption("controlActivity");
parser.addOption(controlActivityOption);
parser.parse(args);
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QCoreApplication *app;
if (parser.isSet(controlActivityOption)) {
qDebug() << "nymea-app: Starting Device Control Activity";
app = new DeviceControlApplication(argc, argv);
} else {
qDebug() << "nymea-app: Starting NymeaAppService background service";
app = new NymeaAppService(argc, argv);
}
return app->exec();
}