From da091649e775b23a35eb88df1c2cf9ce3fb350f7 Mon Sep 17 00:00:00 2001
From: Raquel Medina <raquel.medina@konsulko.com>
Date: Fri, 29 Nov 2019 23:01:41 +0100
Subject: voice: add initial support for voice agents configuration

Bug-AGL: SPEC-2981
Signed-off-by: Raquel Medina <raquel.medina@konsulko.com>
Change-Id: I0195d914dc10f4fcdea1cb6df0e6a5859ad8269d
---
 CMakeLists.txt               |   1 +
 message.h                    |   9 +-
 messageengine.cpp            |  10 ++
 voice/CMakeLists.txt         |   4 +
 voice/voice.cpp              | 213 +++++++++++++++++++++++++++++++++++++++++++
 voice/voice.h                |  74 +++++++++++++++
 voice/voiceagentmodel.cpp    | 187 +++++++++++++++++++++++++++++++++++++
 voice/voiceagentmodel.h      |  63 +++++++++++++
 voice/voiceagentprofile.cpp  | 142 +++++++++++++++++++++++++++++
 voice/voiceagentprofile.h    |  78 ++++++++++++++++
 voice/voiceagentregistry.cpp | 145 +++++++++++++++++++++++++++++
 voice/voiceagentregistry.h   |  76 +++++++++++++++
 voice/voicemessage.cpp       |  43 +++++++++
 voice/voicemessage.h         | 112 +++++++++++++++++++++++
 14 files changed, 1153 insertions(+), 4 deletions(-)
 create mode 100644 voice/CMakeLists.txt
 create mode 100644 voice/voice.cpp
 create mode 100644 voice/voice.h
 create mode 100644 voice/voiceagentmodel.cpp
 create mode 100644 voice/voiceagentmodel.h
 create mode 100644 voice/voiceagentprofile.cpp
 create mode 100644 voice/voiceagentprofile.h
 create mode 100644 voice/voiceagentregistry.cpp
 create mode 100644 voice/voiceagentregistry.h
 create mode 100644 voice/voicemessage.cpp
 create mode 100644 voice/voicemessage.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6421e36..c9b77f9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -55,6 +55,7 @@ set (SUBDIRS
 	pbap
 	radio
 	telephony
+	voice
 	weather)
 
 add_headers(message.h messageengine.h responsemessage.h)
diff --git a/message.h b/message.h
index c39b107..229ba80 100644
--- a/message.h
+++ b/message.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017, 2018 Konsulko Group
+ * Copyright (C) 2017, 2018, 2019 Konsulko Group
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@ enum MessageType {
 	RadioEventMessage,
 	MapEventMessage,
 	NavigationEventMessage,
+	VoiceEventMessage,
 };
 
 class Message : public QObject
@@ -87,17 +88,17 @@ class Message : public QObject
 			return m_reply_data;
 		}
 
- 		inline bool isEvent() const
+		inline bool isEvent() const
 		{
 			return m_event;
 		}
 
- 		inline bool isReply() const
+		inline bool isReply() const
 		{
 			return m_reply;
 		}
 
- 		inline bool isValid() const
+		inline bool isValid() const
 		{
 			return m_init;
 		}
diff --git a/messageengine.cpp b/messageengine.cpp
index d057d11..4920043 100644
--- a/messageengine.cpp
+++ b/messageengine.cpp
@@ -26,6 +26,7 @@
 #include "responsemessage.h"
 #include "telephonymessage.h"
 #include "weathermessage.h"
+#include "voicemessage.h"
 
 #include <QJsonArray>
 
@@ -137,6 +138,15 @@ void MessageEngine::onTextMessageReceived(QString jsonStr)
 		} else if (api == "bluetooth-map") {
 			message = new MapMessage;
 			type = MapEventMessage;
+		} else if (api == "vshl-core" ) {
+			message = new VshlCoreVoiceMessage;
+			type = VoiceEventMessage;
+		} else if (api == "vshl-capabilities") {
+			message = new VshlCpbltsVoiceMessage;
+			type = VoiceEventMessage;
+		} else if (api == "alexa-voiceagent") {
+			message = new AlexaVoiceMessage;
+			type = VoiceEventMessage;
 		} else {
 			message = new Message;
 			type = GenericMessage;
diff --git a/voice/CMakeLists.txt b/voice/CMakeLists.txt
new file mode 100644
index 0000000..61b8fe2
--- /dev/null
+++ b/voice/CMakeLists.txt
@@ -0,0 +1,4 @@
+add_headers(voice.h voicemessage.h
+            voiceagentregistry.h voiceagentprofile.h voiceagentmodel.h)
+add_sources(voice.cpp voicemessage.cpp
+            voiceagentregistry.cpp voiceagentprofile.cpp voiceagentmodel.cpp)
diff --git a/voice/voice.cpp b/voice/voice.cpp
new file mode 100644
index 0000000..71c93d2
--- /dev/null
+++ b/voice/voice.cpp
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QStringList>
+#include "voice.h"
+#include "message.h"
+#include "messageengine.h"
+#include "responsemessage.h"
+#include "voicemessage.h"
+#include "voiceagentregistry.h"
+
+Voice::Voice (QUrl &url, QQmlContext *context, QObject *parent) :
+	QObject(parent),
+	m_loop(nullptr)
+{
+	m_loop = new MessageEngine(url);
+	m_var = new VoiceAgentRegistry(this, context, parent);
+
+	QObject::connect(m_loop, &MessageEngine::connected,
+			 this, &Voice::onConnected);
+	QObject::connect(m_loop, &MessageEngine::disconnected,
+			 this, &Voice::onDisconnected);
+	QObject::connect(m_loop, &MessageEngine::messageReceived,
+			 this, &Voice::onMessageReceived);
+}
+
+Voice::~Voice()
+{
+	delete m_loop;
+	delete m_var;
+}
+
+void Voice::scan()
+{
+	VoiceMessage *vmsg = new VshlCoreVoiceMessage();
+	QJsonObject parameter;
+
+	vmsg->createRequest("enumerateVoiceAgents", parameter);
+	m_loop->sendMessage(vmsg);
+	delete vmsg;
+}
+
+void Voice::getCBLpair(QString id)
+{
+	VoiceMessage *vmsg = new AlexaVoiceMessage();
+	QJsonObject parameter;
+
+	vmsg->createRequest("subscribeToCBLEvents", parameter);
+	m_loop->sendMessage(vmsg);
+	delete vmsg;
+}
+
+void Voice::subscribeAgentToVshlEvents(QString id)
+{
+	QJsonArray events = QJsonArray::fromStringList(vshl_events);
+	VoiceMessage *vmsg = new VshlCoreVoiceMessage();
+	QJsonObject parameter;
+
+	parameter.insert("va_id", id);
+	parameter.insert("events", events);
+	vmsg->createRequest("subscribe", parameter);
+	m_loop->sendMessage(vmsg);
+	delete vmsg;
+}
+
+void Voice::unsubscribeAgentFromVshlEvents(QString id)
+{
+	QJsonArray events = QJsonArray::fromStringList(vshl_events);
+	VoiceMessage *vmsg = new VshlCoreVoiceMessage();
+	QJsonObject parameter;
+
+	parameter.insert("va_id", id);
+	parameter.insert("events", events);
+	vmsg->createRequest("unsubscribe", parameter);
+	m_loop->sendMessage(vmsg);
+	delete vmsg;
+}
+
+void Voice::subscribeAgentToCblEvents(QString id)
+{
+	QJsonArray events = QJsonArray::fromStringList(cbl_events);
+	VoiceMessage *vmsg = new AlexaVoiceMessage();
+	QJsonObject parameter;
+
+	parameter.insert("va_id", id);
+	parameter.insert("events", events);
+	vmsg->createRequest("subscribeToCBLEvent", parameter);
+	m_loop->sendMessage(vmsg);
+	delete vmsg;
+}
+
+void Voice::parseAgentsList(QJsonArray agents)
+{
+	for (auto value : agents) {
+		QJsonObject a = value.toObject();
+		QString id = m_var->addAgent(a);
+		subscribeAgentToVshlEvents(id);
+	}
+}
+
+void Voice::processVshlEvent(VoiceMessage *vmsg)
+{
+	const QString str = vmsg->eventName();
+	const QJsonObject obj = vmsg->eventData();
+	QStringList strlist;
+
+	if (str.contains('#'))
+		strlist = str.split('#');
+	QString agentId = (strlist.isEmpty())?  m_var->getDefaultId() :
+						strlist.takeLast();
+	if (vmsg->isAuthStateEvent()) {
+		const QString authstate = obj.value("state").toString();
+		m_var->setAuthState(
+			agentId,
+			static_cast<VoiceAgentRegistry::ServiceAuthState>(
+				m_var->stringToEnum(authstate, "ServiceAuthState")));
+	} else if (vmsg->isConnectionStateEvent()) {
+		const QString connstate = obj.value("state").toString();
+		m_var->setConnectionState(
+			agentId,
+			static_cast<VoiceAgentRegistry::AgentConnectionState>(
+				m_var->stringToEnum(connstate, "AgentConnectionState")));
+	} else if (vmsg->isDialogStateEvent()) {
+		const QString dialogstate = obj.value("state").toString();
+		m_var->setDialogState(
+			agentId,
+			static_cast<VoiceAgentRegistry::VoiceDialogState>(
+			m_var->stringToEnum(dialogstate, "VoiceDialogState")));
+	} else
+		qWarning() << "Discarding vshl event:" << str;
+}
+
+void Voice::processCblEvent(VoiceMessage *vmsg)
+{
+	const QString str = vmsg->eventName();
+	const QJsonObject obj = vmsg->eventData();
+	QStringList strlist;
+
+	if (str.contains('#'))
+		strlist = str.split('#');
+	QString cblevent = (strlist.isEmpty())? QString() : strlist.takeFirst();
+	QString agentId =  (strlist.isEmpty())? m_var->getDefaultId() :
+						strlist.takeLast();
+	if (cblevent == "voice_cbl_codepair_received_event") {
+		QString code = obj.value("code").toString();
+		QString url = obj.value("url").toString();
+		m_var->updateCblPair(agentId, code, url, false);
+	} else if (cblevent == "voice_cbl_codepair_expired_event") {
+		QString code = obj.value("code").toString();
+		QString url = obj.value("url").toString();
+		m_var->updateCblPair(agentId, code, url, true);
+	} else
+		qWarning() << "Discarding cbl event:" << str;
+}
+
+void Voice::processEvent(VoiceMessage *vmsg)
+{
+	const QString api = vmsg->eventApi();
+	if (api == "vshl-core")
+		processVshlEvent(vmsg);
+	else if (api == "alexa-voiceagent")
+		processCblEvent(vmsg);
+	else
+		qWarning() << "Unknown api:" << api;
+}
+
+void Voice::processReply(ResponseMessage *rmsg)
+{
+	if (rmsg->requestVerb() == "enumerateVoiceAgents") {
+		parseAgentsList(rmsg->replyData().value("agents").toArray());
+		m_var->setDefaultId(
+				rmsg->replyData().value("default").toString());
+	} else
+		qWarning() << "Reply received for unknown verb:" <<
+							rmsg->requestVerb();
+}
+
+void Voice::onConnected()
+{
+	scan();
+}
+
+void Voice::onDisconnected()
+{
+	QStringList mvarlist = m_var->getAgentsListById();
+	QStringList::iterator it;
+	for (it = mvarlist.begin(); it != mvarlist.end(); ++it)
+		unsubscribeAgentFromVshlEvents(*it);
+}
+
+void Voice::onMessageReceived(MessageType type, Message *msg)
+{
+	if (msg->isEvent() && type == VoiceEventMessage) {
+		processEvent(qobject_cast<VoiceMessage*>(msg));
+	} else if (msg->isReply() && (type == ResponseRequestMessage)) {
+		processReply(qobject_cast<ResponseMessage*>(msg));
+	} else
+		qWarning() << "Received unknown message type:" << type;
+	msg->deleteLater();
+}
diff --git a/voice/voice.h b/voice/voice.h
new file mode 100644
index 0000000..1da7ca6
--- /dev/null
+++ b/voice/voice.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef VOICE_H
+#define VOICE_H
+
+#include <QDebug>
+#include <QObject>
+#include <QJsonArray>
+#include <QtQml/QQmlContext>
+#include "message.h"
+
+class MessageEngine;
+class ResponseMessage;
+class VoiceMessage;
+class VoiceAgentRegistry;
+
+class Voice : public QObject
+{
+	Q_OBJECT
+
+	public:
+		explicit Voice(QUrl &url, QQmlContext *context,
+			       QObject * parent = Q_NULLPTR);
+		virtual ~Voice();
+
+		// controls
+		Q_INVOKABLE void scan();
+		Q_INVOKABLE void getCBLpair(QString id);
+
+	private:
+		MessageEngine *m_loop;
+		VoiceAgentRegistry *m_var;
+
+		void subscribeAgentToVshlEvents(QString id);
+		void unsubscribeAgentFromVshlEvents(QString id);
+		void subscribeAgentToCblEvents(QString id);
+		void parseAgentsList(QJsonArray agents);
+		void processVshlEvent(VoiceMessage *vmsg);
+		void processCblEvent(VoiceMessage *vmsg);
+
+		void processEvent(VoiceMessage *vmsg);
+		void processReply(ResponseMessage *rmsg);
+
+		// slots
+		void onConnected();
+		void onDisconnected();
+		void onMessageReceived(MessageType type, Message *msg);
+
+		const QStringList vshl_events {
+			"voice_authstate_event",
+			"voice_dialogstate_event",
+			"voice_connectionstate_event",
+		};
+		const QStringList cbl_events {
+			"voice_cbl_codepair_received_event",
+			"voice_cbl_codepair_expired_event",
+		};
+};
+
+#endif // VOICE_H
diff --git a/voice/voiceagentmodel.cpp b/voice/voiceagentmodel.cpp
new file mode 100644
index 0000000..e0f9bfb
--- /dev/null
+++ b/voice/voiceagentmodel.cpp
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "voiceagentmodel.h"
+#include "voiceagentprofile.h"
+#include <QVector>
+#include <QDebug>
+
+VoiceAgentModel::VoiceAgentModel(QObject *parent)
+	: QAbstractListModel(parent)
+{
+}
+
+QVariant VoiceAgentModel::data(const QModelIndex &index, int role) const
+{
+	QVariant ret;
+
+	if (!index.isValid())
+		return ret;
+
+	if (index.row() < 0 || index.row() >= m_agents.count())
+		return ret;
+
+	const VoiceAgentProfile *vap = m_agents[index.row()];
+	switch (role) {
+		case IdRole:
+			return vap->vaid();
+		case NameRole:
+			return vap->name();
+		case WuwRole:
+			return vap->activewuw();
+		case AuthStateRole:
+			return vap->authstate();
+		case ConnStateRole:
+			return vap->connstate();
+		case DialogStateRole:
+			return vap->dialogstate();
+		case LoginParamsRole:
+			return readLoginParams(index);
+		case ActiveRole:
+			return vap->isactive()? "active" : "inactive";
+	}
+	return ret;
+}
+
+int VoiceAgentModel::rowCount(const QModelIndex &parent) const
+{
+	Q_UNUSED(parent);
+	return m_agents.count();
+}
+
+QVariantList VoiceAgentModel::readLoginParams(const QModelIndex &index) const
+{
+	QVariantList ret;
+
+	if (!index.isValid())
+		return ret;
+
+	if (index.row() < 0 || index.row() >= this->m_agents.count())
+		return ret;
+
+	const VoiceAgentProfile *vap = this->m_agents[index.row()];
+	ret.append(vap->loginurl());
+	ret.append(vap->logincode());
+	ret.append(vap->isloginpairexpired()? "expired" : "valid");
+	return ret;
+}
+
+void VoiceAgentModel::addAgent(VoiceAgentProfile *vap)
+{
+	beginInsertRows(QModelIndex(), rowCount(), rowCount());
+	m_agents.insert(rowCount(), vap);
+	endInsertRows();
+}
+
+void VoiceAgentModel::removeAgent(VoiceAgentProfile *vap)
+{
+	if (m_agents.isEmpty())
+		return;
+
+	int row = m_agents.indexOf(vap);
+	beginRemoveRows(QModelIndex(), row, row);
+	m_agents.removeAt(row);
+	endRemoveRows();
+	delete vap;
+}
+
+void VoiceAgentModel::removeAll()
+{
+	if (m_agents.isEmpty())
+		return;
+
+	beginRemoveRows(QModelIndex(), 0, m_agents.count() -1);
+	qDeleteAll(m_agents.begin(), m_agents.end());
+	endRemoveRows();
+	m_agents.clear();
+}
+
+bool VoiceAgentModel::agentExists(QString name, QString id, QString api) const
+{
+	VoiceAgentProfile *vap = getAgentFromName(name);
+	if (!vap)
+		return false;
+	bool sameid = id == vap->vaid();
+	bool sameapi = api == vap->vaapi();
+	return sameapi && (sameid || id != "UNKNOWN");
+}
+
+VoiceAgentProfile* VoiceAgentModel::getAgentFromName(QString name) const
+{
+	if (m_agents.isEmpty())
+		return nullptr;
+
+	for (auto agent : m_agents) {
+		if (agent->name() == name)
+			return agent;
+	}
+	return nullptr;
+}
+
+VoiceAgentProfile* VoiceAgentModel::getAgentFromId(QString id) const
+{
+	if (m_agents.isEmpty())
+		return nullptr;
+
+	for (auto agent : m_agents) {
+		if (agent->vaid() == id)
+			return agent;
+	}
+	return nullptr;
+}
+
+void VoiceAgentModel::updateAgentProperties(QString name, QString id, QString api,
+					    bool active, QString wuw)
+{
+	QVector<int> vroles;
+	VoiceAgentProfile *vap = getAgentFromName(name);
+	if (!vap) {
+		qWarning() << "Unknown agent";
+		return;
+	}
+	if ((vap->vaapi() == api) && (vap->vaid() != id) && (id != "UNKNOWN")) {
+		vap->setVaid(id);
+		vroles.push_back(IdRole);
+	}
+	vap->setActive(active);
+	vroles.push_back(ActiveRole);
+	if (!wuw.isEmpty()) {
+		vap->setWuw(wuw);
+		vroles.push_back(WuwRole);
+	}
+	if (!vroles.isEmpty())
+		emit dataChanged(indexOf(vap), indexOf(vap), vroles);
+}
+
+QModelIndex VoiceAgentModel::indexOf(VoiceAgentProfile *vap)
+{
+	int row = m_agents.indexOf(vap);
+	return index(row);
+}
+
+QHash<int, QByteArray> VoiceAgentModel::roleNames() const
+{
+	QHash<int, QByteArray> roles;
+	roles[NameRole] = "name";
+	roles[IdRole] = "id";
+	roles[WuwRole] = "wuw";
+	roles[AuthStateRole] = "authstate";
+	roles[ConnStateRole] = "connstate";
+	roles[DialogStateRole] = "dialogstate";
+	roles[LoginParamsRole] = "usrauth";
+	roles[ActiveRole] = "active";
+	return roles;
+}
diff --git a/voice/voiceagentmodel.h b/voice/voiceagentmodel.h
new file mode 100644
index 0000000..30d295b
--- /dev/null
+++ b/voice/voiceagentmodel.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef VOICEAGENTMODEL_H
+#define VOICEAGENTMODEL_H
+
+#include <QAbstractListModel>
+#include <QStringList>
+#include <QtQml/QQmlContext>
+#include <QJsonObject>
+
+#include "voiceagentprofile.h"
+
+class VoiceAgentModel : public QAbstractListModel
+{
+	Q_OBJECT
+
+	public:
+		enum VoiceAgentRoles {
+			IdRole = Qt::UserRole + 1,
+			NameRole,
+			WuwRole,
+			AuthStateRole,
+			ConnStateRole,
+			DialogStateRole,
+			LoginParamsRole,
+			ActiveRole,
+		};
+
+		VoiceAgentModel(QObject *parent = Q_NULLPTR);
+
+		QVariant data(const QModelIndex &index,
+			      int role = Qt::DisplayRole) const;
+		int rowCount(const QModelIndex &parent = QModelIndex()) const;
+		QVariantList readLoginParams(const QModelIndex &index) const;
+		void addAgent(VoiceAgentProfile *vap);
+		void removeAgent(VoiceAgentProfile* vap);
+		void removeAll();
+		bool agentExists(QString name, QString id, QString api) const;
+		VoiceAgentProfile *getAgentFromName(QString name) const;
+		VoiceAgentProfile *getAgentFromId(QString id) const;
+		void updateAgentProperties(QString name, QString id,
+					   QString api, bool active, QString wuw);
+
+	private:
+		QList<VoiceAgentProfile *> m_agents;
+		QModelIndex indexOf(VoiceAgentProfile *agent);
+		QHash<int, QByteArray> roleNames() const;
+};
+#endif // VOICEAGENTMODEL_H
diff --git a/voice/voiceagentprofile.cpp b/voice/voiceagentprofile.cpp
new file mode 100644
index 0000000..05e3839
--- /dev/null
+++ b/voice/voiceagentprofile.cpp
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "voiceagentprofile.h"
+
+VoiceAgentProfile::VoiceAgentProfile(const QString &name,
+				     const QString &id,
+				     const QString &api,
+				     bool active,
+				     const QString &wuw,
+				     const QString &vendor,
+				     const QString &wuws)
+	: m_name(name), m_vaid(id), m_vaapi(api), m_active(active),
+	  m_activewuw(wuw), m_vendor(vendor), m_wuws(wuws),
+	  m_authstate("UNITIALIZED"), m_connstate("DISCONNECTED"),
+	  m_dialogstate("MICROPHONEOFF"), m_logincode(QString()),
+	  m_loginurl(QString()), m_expired(true)
+{
+}
+
+QString VoiceAgentProfile::name() const
+{
+	return m_name;
+}
+
+QString VoiceAgentProfile::vaid() const
+{
+	return m_vaid;
+}
+
+QString VoiceAgentProfile::vaapi() const
+{
+	return m_vaapi;
+}
+
+bool VoiceAgentProfile::isactive() const
+{
+	return m_active;
+}
+
+QString VoiceAgentProfile::activewuw() const
+{
+	return m_activewuw;
+}
+
+QString VoiceAgentProfile::vendor() const
+{
+	return m_vendor;
+}
+
+QString VoiceAgentProfile::wuws() const
+{
+	return m_wuws;
+}
+
+QString VoiceAgentProfile::authstate() const
+{
+	return m_authstate;
+}
+
+QString VoiceAgentProfile::connstate() const
+{
+	return m_connstate;
+}
+
+QString VoiceAgentProfile::dialogstate() const
+{
+	return m_dialogstate;
+}
+
+QString VoiceAgentProfile::logincode() const
+{
+	return m_logincode;
+}
+
+QString VoiceAgentProfile::loginurl() const
+{
+	return m_loginurl;
+}
+
+bool VoiceAgentProfile::isloginpairexpired() const
+{
+	return m_expired;
+}
+
+void VoiceAgentProfile::setVaid(const QString id)
+{
+	m_vaid = id;
+}
+
+void VoiceAgentProfile::setActive(bool active)
+{
+	m_active = active;
+}
+
+void VoiceAgentProfile::setAuthState(const QString state)
+{
+	m_authstate = state;
+}
+
+void VoiceAgentProfile::setConnState(const QString state)
+{
+	m_connstate = state;
+}
+
+void VoiceAgentProfile::setDialogState(const QString state)
+{
+	m_dialogstate = state;
+}
+
+void VoiceAgentProfile::setLoginCode(const QString usrcode)
+{
+	m_logincode = usrcode;
+}
+
+void VoiceAgentProfile::setLoginUrl(const QString usrurl)
+{
+	m_loginurl = usrurl;
+}
+
+void VoiceAgentProfile::setLoginPairExpired(bool expired)
+{
+	m_expired = expired;
+}
+
+void VoiceAgentProfile::setWuw(const QString newwuw)
+{
+	m_activewuw = newwuw;
+}
diff --git a/voice/voiceagentprofile.h b/voice/voiceagentprofile.h
new file mode 100644
index 0000000..dda96c5
--- /dev/null
+++ b/voice/voiceagentprofile.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef VOICEAGENTPROFILE_H
+#define VOICEAGENTPROFILE_H
+
+#include <QString>
+
+class VoiceAgentProfile
+{
+	public:
+		VoiceAgentProfile(const QString &name,
+				  const QString &id,
+				  const QString &api,
+				  bool active,
+				  const QString &wuw,
+				  const QString &vendor,
+				  const QString &wuws);
+
+		QString name() const;
+		QString vaid() const;
+		QString vaapi() const;
+		bool isactive() const;
+		QString activewuw() const;
+		QString vendor() const;
+		QString wuws() const;
+		QString authstate() const;
+		QString connstate() const;
+		QString dialogstate() const;
+		QString logincode() const;
+		QString loginurl() const;
+		bool isloginpairexpired() const;
+
+		void setVaid(const QString newid);
+		void setActive(bool activemode);
+		void setAuthState(const QString newauthstate);
+		void setConnState(const QString newconnstate);
+		void setDialogState(const QString newdialogstate);
+		void setLoginCode(const QString newtoken);
+		void setLoginUrl(const QString newurl);
+		void setLoginPairExpired(bool expired);
+		void setWuw(const QString newwuw);
+
+		bool operator==(const VoiceAgentProfile& rhs) {
+			return (m_name == rhs.name() &&
+				m_vaid == rhs.vaid() &&
+				m_vaapi == rhs.vaapi()); };
+
+	private:
+		QString m_name;
+		QString m_vaid;
+		QString m_vaapi;
+		bool m_active;
+		QString m_activewuw;
+		QString m_vendor;
+		QString m_wuws;
+		QString m_authstate;
+		QString m_connstate;
+		QString m_dialogstate;
+		QString m_logincode;
+		QString m_loginurl;
+		bool m_expired;
+};
+
+#endif // VOICEAGENTPROFILE_H
diff --git a/voice/voiceagentregistry.cpp b/voice/voiceagentregistry.cpp
new file mode 100644
index 0000000..22c7051
--- /dev/null
+++ b/voice/voiceagentregistry.cpp
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QMetaEnum>
+#include <QSortFilterProxyModel>
+#include <QtQml/QQmlEngine>
+
+#include "voice.h"
+#include "voiceagentregistry.h"
+#include "voiceagentmodel.h"
+#include "voiceagentprofile.h"
+
+VoiceAgentRegistry::VoiceAgentRegistry(Voice *voice, QQmlContext *context, QObject *parent) :
+	QObject(parent),
+	m_model(nullptr),
+	m_voice(voice)
+{
+	m_model = new VoiceAgentModel();
+	QSortFilterProxyModel *model = new QSortFilterProxyModel();
+	model->setSourceModel(m_model);
+	model->setSortRole(VoiceAgentModel::VoiceAgentRoles::IdRole);
+	model->setSortCaseSensitivity(Qt::CaseInsensitive);
+	model->sort(0);
+
+	context->setContextProperty("VoiceAgentModel", m_model);
+	context->setContextProperty("VoiceAgent", this);
+}
+
+VoiceAgentRegistry::~VoiceAgentRegistry()
+{
+	delete m_model;
+}
+
+QString VoiceAgentRegistry::addAgent(QJsonObject va)
+{
+	bool active = va.value("active").toBool();
+	QString wuw = va.value("activewakeword").toString();
+	QString api = va.value("api").toString();
+	QString desc = va.value("description").toString();
+	QString id = va.value("id").toString();
+	QString name = va.value("name").toString();
+	QString vendor = va.value("vendor").toString();
+	QString wuws = va.value("wakewords").toString();
+
+	if (!m_model->agentExists(name, id, api)) {
+		VoiceAgentProfile *vap = new VoiceAgentProfile(name, id, api,
+							       active, wuw,
+							       vendor, wuws);
+		m_model->addAgent(vap);
+		m_regids.append(id);
+	}
+	else
+		m_model->updateAgentProperties(name, id, api, active, wuw);
+	return id;
+}
+
+bool VoiceAgentRegistry::removeAgent(QString id)
+{
+	VoiceAgentProfile *vap = m_model->getAgentFromId(id);
+	if (!vap)
+		return false;
+	m_model->removeAgent(vap);
+	return true;
+}
+
+void VoiceAgentRegistry::clearRegistry()
+{
+	m_default_aid.clear();
+	m_regids.clear();
+	m_model->removeAll();
+}
+
+QStringList VoiceAgentRegistry::getAgentsListById() const
+{
+	return m_regids;
+}
+
+QString VoiceAgentRegistry::getDefaultId() const
+{
+	return m_default_aid.isEmpty()? "UNKNOWN" : m_default_aid;
+}
+void VoiceAgentRegistry::setDefaultId(QString id)
+{
+	m_default_aid = id;
+}
+
+void VoiceAgentRegistry::setAuthState(QString id, ServiceAuthState state)
+{
+	const auto stateStr =
+		QMetaEnum::fromType<VoiceAgentRegistry::ServiceAuthState>().valueToKey(state);
+	VoiceAgentProfile *vap = m_model->getAgentFromId(id);
+	if (!vap)
+		vap->setAuthState(stateStr);
+}
+
+void VoiceAgentRegistry::setConnectionState(QString id, AgentConnectionState state)
+{
+	const auto stateStr =
+		QMetaEnum::fromType<VoiceAgentRegistry::AgentConnectionState>().valueToKey(state);
+	VoiceAgentProfile *vap = m_model->getAgentFromId(id);
+	if (!vap)
+		vap->setConnState(stateStr);
+}
+
+void VoiceAgentRegistry::setDialogState(QString id, VoiceDialogState state)
+{
+	const auto stateStr =
+	QMetaEnum::fromType<VoiceAgentRegistry::VoiceDialogState>().valueToKey(state);
+	VoiceAgentProfile *vap = m_model->getAgentFromId(id);
+	if (!vap)
+		vap->setDialogState(stateStr);
+}
+
+void VoiceAgentRegistry::updateCblPair(QString id, QString code, QString url,
+				       bool expired)
+{
+	VoiceAgentProfile *vap = m_model->getAgentFromId(id);
+	if (!vap) {
+		vap->setLoginCode(code);
+		vap->setLoginUrl(code);
+		vap->setLoginPairExpired(expired);
+	};
+}
+
+int VoiceAgentRegistry::stringToEnum(const QString key, const QString enumtype)
+{
+	const QMetaObject *metaObject = VoiceAgentRegistry::metaObject();
+	int enumIndex = metaObject->indexOfEnumerator(enumtype.toUtf8().data());
+	QMetaEnum metaEnum = metaObject->enumerator(enumIndex);
+	int value = metaEnum.keyToValue(key.toUtf8().data());
+	return (value < 0)? 0 : value;
+}
diff --git a/voice/voiceagentregistry.h b/voice/voiceagentregistry.h
new file mode 100644
index 0000000..2a69294
--- /dev/null
+++ b/voice/voiceagentregistry.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef VOICEAGENTREGISTRY_H
+#define VOICEAGENTREGISTRY_H
+
+#include <QDebug>
+#include <QObject>
+#include <QJsonArray>
+#include <QtQml/QQmlContext>
+
+class Voice;
+class VoiceAgentModel;
+
+class VoiceAgentRegistry : public QObject
+{
+	Q_OBJECT
+	public:
+		explicit VoiceAgentRegistry(Voice *voice, QQmlContext *context,
+					    QObject *parent);
+		virtual ~VoiceAgentRegistry();
+
+		enum AgentConnectionState {
+			DISCONNECTED = 0,
+			CONNECTED,
+		};
+		Q_ENUM(AgentConnectionState)
+
+		enum VoiceDialogState {
+			IDLE = 0,
+			LISTENING,
+			THINKING,
+			SPEAKING,
+			MICROPHONEOFF,
+		};
+		Q_ENUM(VoiceDialogState)
+
+		enum ServiceAuthState {
+			UNITIALIZED = 0,
+			REFRESHED,
+		};
+		Q_ENUM(ServiceAuthState)
+
+		QString addAgent(QJsonObject va);
+		bool removeAgent(QString id);
+		void clearRegistry();
+		QStringList getAgentsListById() const;
+		QString getDefaultId() const;
+		void setDefaultId(QString id);
+		void setAuthState(QString id, ServiceAuthState state);
+		void setConnectionState(QString id, AgentConnectionState state);
+		void setDialogState(QString id, VoiceDialogState state);
+		void updateCblPair(QString id, QString code, QString url,
+				   bool expired);
+		int stringToEnum(QString value, QString enumtype);
+	private:
+		VoiceAgentModel *m_model;
+		Voice *m_voice;
+		QString m_default_aid;
+		QStringList m_regids;
+};
+
+#endif // VOICEAGENTREGISTRY_H
diff --git a/voice/voicemessage.cpp b/voice/voicemessage.cpp
new file mode 100644
index 0000000..c338879
--- /dev/null
+++ b/voice/voicemessage.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDebug>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include "voicemessage.h"
+
+bool VshlCoreVoiceMessage::createRequest(QString verb, QJsonObject parameter)
+{
+	if (!verbs.contains(verb))
+		return false;
+	return Message::createRequest("vshl-core", verb, parameter);
+}
+
+bool VshlCpbltsVoiceMessage::createRequest(QString verb, QJsonObject parameter)
+{
+	if (!verbs.contains(verb))
+		return false;
+	return Message::createRequest("vshl-capabilities", verb, parameter);
+}
+
+bool AlexaVoiceMessage::createRequest(QString verb, QJsonObject parameter)
+{
+	if (!verbs.contains(verb))
+		return false;
+	return Message::createRequest("alexa-voiceagent", verb, parameter);
+}
diff --git a/voice/voicemessage.h b/voice/voicemessage.h
new file mode 100644
index 0000000..1de5dd2
--- /dev/null
+++ b/voice/voicemessage.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2019 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef VOICE_MESSAGE_H
+#define VOICE_MESSAGE_H
+
+#include "message.h"
+
+class VoiceMessage : public Message
+{
+	Q_OBJECT
+	public:
+		virtual ~VoiceMessage() {};
+		virtual bool isAuthStateEvent() const = 0;
+		virtual bool isConnectionStateEvent() const = 0;
+		virtual bool isDialogStateEvent() const = 0;
+		virtual bool createRequest(QString verb, QJsonObject parameter) = 0;
+};
+
+class VshlCoreVoiceMessage : public VoiceMessage
+{
+	Q_OBJECT
+	public:
+		virtual ~VshlCoreVoiceMessage() {};
+		bool isAuthStateEvent() const override {
+			return (this->eventName() == "voice_authstate_event"); };
+		bool isConnectionStateEvent() const override {
+			return (this->eventName() == "voice_connectionstate_event"); };
+		bool isDialogStateEvent() const override {
+			return (this->eventName() == "voice_dialogstate_event"); };
+		bool createRequest(QString verb, QJsonObject parameter) override;
+
+	private:
+		QStringList verbs {
+			"startListening",
+			"cancelListening",
+			"subscribe",
+			"unsubscribe",
+			"enumerateVoiceAgents",
+			"setDefaultVoiceAgent",
+		};
+		QStringList events {
+			"",
+		};
+};
+
+class VshlCpbltsVoiceMessage : public VoiceMessage
+{
+	Q_OBJECT
+	public:
+		virtual ~VshlCpbltsVoiceMessage() {};
+		bool isAuthStateEvent() const override { return false; };
+		bool isConnectionStateEvent() const override { return false; };
+		bool isDialogStateEvent() const override { return false; };
+		bool createRequest(QString verb, QJsonObject parameter) override;
+
+	private:
+		QStringList verbs {
+			"guiMetadataSubscribe",
+			"guiMetadataPublish",
+			"phonecontrolSubscribe",
+			"phonecontrolPublish",
+			"navigationSubscribe",
+			"navigationPublish",
+			"playbackControllerSubscribe",
+			"playbackControllerPublish",
+		};
+		QStringList events {
+			"voice_authstate_event",
+			"voice_dialogstate_event",
+			"voice_connectionstate_event",
+		};
+};
+
+/* We shouldnt access an agent directly, but CBL events
+ * are not abstracted/forwarded by vshl bindings.
+ */
+class AlexaVoiceMessage : public VoiceMessage
+{
+	Q_OBJECT
+	public:
+		virtual ~AlexaVoiceMessage() {};
+		bool isAuthStateEvent() const override {
+			return (!events.contains(this->eventName())); };
+		bool isConnectionStateEvent() const override { return false; };
+		bool isDialogStateEvent() const override { return false; };
+		bool createRequest(QString verb, QJsonObject parameter) override;
+
+	private:
+		QStringList verbs {
+			"subscribeToCBLEvents",
+		};
+		QStringList events {
+			"voice_cbl_codepair_received_event",
+			"voice_cbl_codepair_expired_event",
+		};
+};
+
+#endif // VOICE_MESSAGE_H
-- 
cgit