diff options
author | Naveen Bobbili <nbobbili@amazon.com> | 2019-04-28 20:51:16 -0700 |
---|---|---|
committer | Jan-Simon Möller <jsmoeller@linuxfoundation.org> | 2019-11-12 15:32:46 +0100 |
commit | 0349f05f5885987952a2d8de03983b36722b264e (patch) | |
tree | 95c2f3a30447831deda72625c8a60f077ac671ee | |
parent | 0bdd39b247661c1a0406d450d578d4ff3fd171b0 (diff) |
Add push to talk support to homescreen
Reworked version of Alexa specific changes from ICS to add push to
talk button for voice services to homescreen media area.
v2: change config.xml to audiomixer
v3: reworked to not be Alexa specific:
- Now use the default voiceagent if available, instead of hard-coding
Alexa usage
- The Alexa logo for the button has been replaced with a generic
microphone icon derived from the radio application's launcher icon.
This is a placeholder until a new icon is provided by LF graphics
team. Meeting any Amazon requirements around Alexa chrome is now
envisioned as being provided for with a TBD voiceagent API
enhancement.
- The QML for the PTT button has been moved to MediaAreaBlank.qml,
which seems a more logical location for it ATM. It is likely that
the MediaArea QML should be simplified in a future change, as it
currently contains a signficant amount of unused code.
- The PTT button has been moved to the left hand side of the media
area, as this seems more sensible if demonstrating driver usage.
- The delay on fade-out of the master volume slider has been lowered
to 3 seconds from 5, with the PTT button present it started seeming
excessive during testing.
- Some extra debug messages have been added to make tracking the
voiceagent state more straightforward.
Bug-AGL: SPEC-2764,
Signed-off-by: Naveen Bobbili <nbobbili@amazon.com>
Signed-off-by: Jan-Simon Moeller <jsmoeller@linuxfoundation.org>
Signed-off-by: Scott Murray <scott.murray@konsulko.com>
Change-Id: I398bf7aebc5c9b459b1fce94511eee3698c08347
-rw-r--r-- | homescreen/homescreen.pro | 12 | ||||
-rw-r--r-- | homescreen/qml/MediaArea.qml | 4 | ||||
-rw-r--r-- | homescreen/qml/MediaAreaBlank.qml | 18 | ||||
-rw-r--r-- | homescreen/qml/SpeechChrome.qml | 120 | ||||
-rw-r--r-- | homescreen/qml/images/SpeechChrome/bar.png | bin | 0 -> 23826 bytes | |||
-rw-r--r-- | homescreen/qml/images/SpeechChrome/push_to_talk.svg | 322 | ||||
-rw-r--r-- | homescreen/qml/images/SpeechChrome/speechchrome.qrc | 6 | ||||
-rw-r--r-- | homescreen/qml/main.qml | 2 | ||||
-rw-r--r-- | homescreen/qml/qml.qrc | 1 | ||||
-rw-r--r-- | homescreen/src/aglsocketwrapper.cpp | 90 | ||||
-rw-r--r-- | homescreen/src/aglsocketwrapper.h | 35 | ||||
-rw-r--r-- | homescreen/src/chromecontroller.cpp | 159 | ||||
-rw-r--r-- | homescreen/src/chromecontroller.h | 42 | ||||
-rw-r--r-- | homescreen/src/constants.h | 42 | ||||
-rw-r--r-- | homescreen/src/main.cpp | 4 | ||||
-rw-r--r-- | package/config.xml | 1 |
16 files changed, 848 insertions, 10 deletions
diff --git a/homescreen/homescreen.pro b/homescreen/homescreen.pro index 8baa90d..773271e 100644 --- a/homescreen/homescreen.pro +++ b/homescreen/homescreen.pro @@ -30,14 +30,19 @@ SOURCES += \ src/statusbarserver.cpp \ src/applicationlauncher.cpp \ src/mastervolume.cpp \ - src/homescreenhandler.cpp + src/homescreenhandler.cpp \ + src/aglsocketwrapper.cpp \ + src/chromecontroller.cpp HEADERS += \ src/statusbarmodel.h \ src/statusbarserver.h \ src/applicationlauncher.h \ src/mastervolume.h \ - src/homescreenhandler.h + src/homescreenhandler.h \ + src/aglsocketwrapper.h \ + src/chromecontroller.h \ + src/constants.h OTHER_FILES += \ README.md @@ -49,4 +54,5 @@ RESOURCES += \ qml/images/Shortcut/shortcut.qrc \ qml/images/Status/status.qrc \ qml/images/images.qrc \ - qml/qml.qrc + qml/qml.qrc \ + qml/images/SpeechChrome/speechchrome.qrc
\ No newline at end of file diff --git a/homescreen/qml/MediaArea.qml b/homescreen/qml/MediaArea.qml index 0447589..3b6d18a 100644 --- a/homescreen/qml/MediaArea.qml +++ b/homescreen/qml/MediaArea.qml @@ -20,8 +20,8 @@ import QtQuick.Controls 2.0 StackView { id: root - width: 1080 - height: 215 + width: parent.width + height: parent.height initialItem: blank diff --git a/homescreen/qml/MediaAreaBlank.qml b/homescreen/qml/MediaAreaBlank.qml index ebddb0c..60d0c92 100644 --- a/homescreen/qml/MediaAreaBlank.qml +++ b/homescreen/qml/MediaAreaBlank.qml @@ -22,8 +22,8 @@ import AGL.Demo.Controls 1.0 import MasterVolume 1.0 Image { - width: 1080 - height: 215 + width: parent.width + height: parent.height source: './images/Utility_Logo_Background-01.svg' property bool displayVolume: false; @@ -40,14 +40,14 @@ Image { } Image { - id: logo_image + id: logo_image anchors.centerIn: parent source: './images/Utility_Logo_Grey-01.svg' } Timer { id: volume_timer - interval: 5000; running: false; repeat: false + interval: 3000; running: false; repeat: false onTriggered: displayVolume = false } @@ -56,11 +56,13 @@ Image { PropertyChanges { target: master_volume; opacity: 1.0 } PropertyChanges { target: slider; enabled: true } PropertyChanges { target: logo_image; opacity: 0.0 } + PropertyChanges { target: speech_chrome; visible: false } }, State { when: !displayVolume; PropertyChanges { target: master_volume; opacity: 0.0 } PropertyChanges { target: slider; enabled: false } PropertyChanges { target: logo_image; opacity: 1.0 } + PropertyChanges { target: speech_chrome; visible: speech_chrome.agentPresent } } ] @@ -121,4 +123,12 @@ Image { } } } + + SpeechChrome { + id: speech_chrome + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height + } } diff --git a/homescreen/qml/SpeechChrome.qml b/homescreen/qml/SpeechChrome.qml new file mode 100644 index 0000000..911d481 --- /dev/null +++ b/homescreen/qml/SpeechChrome.qml @@ -0,0 +1,120 @@ +import QtQuick 2.0 +import SpeechChrome 1.0 + +Item { + id: root + + clip: true + + property bool agentPresent: speechChromeController.agentPresent + + visible: agentPresent + + Image { + id: chromeBarImage + + anchors.top: parent.top + source: "./images/SpeechChrome/bar.png" + + Behavior on x { + NumberAnimation { duration: 250 } + } + Behavior on opacity { + NumberAnimation { duration: 250 } + } + } + + Image { + id: pushToTalk + + height: parent.height * 0.80 + width: height + + anchors.left: parent.left + anchors.leftMargin: parent.width / 128 + anchors.verticalCenter: parent.verticalCenter + source: "./images/SpeechChrome/push_to_talk.svg" + + MouseArea { + anchors.fill: parent + onPressed: speechChromeController.pushToTalk() + } + + Behavior on opacity { + NumberAnimation { duration: 250 } + } + } + + states: [ + State { + name: "Idle" + when: speechChromeController.chromeState == SpeechChromeController.Idle + PropertyChanges { + target: chromeBarImage + opacity: 0.0 + x: 0 + } + PropertyChanges { + target: pushToTalk + opacity: 1.0 + enabled: true + } + }, + State { + name: "Listening" + when: speechChromeController.chromeState == SpeechChromeController.Listening + PropertyChanges { + target: chromeBarImage + opacity: 1.0 + x: 0 + } + PropertyChanges { + target: pushToTalk + opacity: 0.0 + enabled: false + } + }, + State { + name: "Thinking" + when: speechChromeController.chromeState == SpeechChromeController.Thinking + PropertyChanges { + target: chromeBarImage + opacity: 1.0 + x: root.width - chromeBarImage.width + } + PropertyChanges { + target: pushToTalk + opacity: 0.0 + enabled: false + } + }, + State { + name: "Speaking" + when: speechChromeController.chromeState == SpeechChromeController.Speaking + PropertyChanges { + target: chromeBarImage + opacity: 1.0 + x: (root.width - chromeBarImage.width) * 0.5 + } + PropertyChanges { + target: pushToTalk + opacity: 0.0 + enabled: false + } + }, + State { + name: "MicrophoneOff" + when: speechChromeController.chromeState == SpeechChromeController.MicrophoneOff + PropertyChanges { + target: chromeBarImage + opacity: 0.0 + x: 0 + } + PropertyChanges { + target: pushToTalk + opacity: 1.0 + enabled: true + } + } + ] +} diff --git a/homescreen/qml/images/SpeechChrome/bar.png b/homescreen/qml/images/SpeechChrome/bar.png Binary files differnew file mode 100644 index 0000000..caabde1 --- /dev/null +++ b/homescreen/qml/images/SpeechChrome/bar.png diff --git a/homescreen/qml/images/SpeechChrome/push_to_talk.svg b/homescreen/qml/images/SpeechChrome/push_to_talk.svg new file mode 100644 index 0000000..0c775a1 --- /dev/null +++ b/homescreen/qml/images/SpeechChrome/push_to_talk.svg @@ -0,0 +1,322 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> + +<svg + xmlns:i="&#38;ns_ai;" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Radio_Inactive" + x="0px" + y="0px" + viewBox="0 0 280 280" + xml:space="preserve" + inkscape:version="0.92.4 (unknown)" + sodipodi:docname="mic2.svg" + width="280" + height="280"><metadata + id="metadata5319"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs5317" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1910" + inkscape:window-height="899" + id="namedview5315" + showgrid="false" + inkscape:zoom="2" + inkscape:cx="166.88636" + inkscape:cy="140" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="0" + inkscape:current-layer="Radio_Inactive" /><style + type="text/css" + id="style5192"> + .st0{fill:#FFFFFF;} + .st1{font-family:'Roboto-Regular';} + .st2{font-size:25px;} + .st3{letter-spacing:6;} + .st4{fill:url(#SVGID_1_);} + .st5{fill:url(#SVGID_2_);} + .st6{fill:url(#SVGID_3_);} + .st7{fill:url(#SVGID_4_);} + .st8{fill:url(#SVGID_5_);} + .st9{fill:url(#SVGID_6_);} + .st10{fill:url(#SVGID_7_);} + .st11{fill:url(#SVGID_8_);} + .st12{fill:url(#SVGID_9_);} + .st13{fill:url(#SVGID_10_);} + .st14{fill:url(#SVGID_11_);} + .st15{fill:url(#SVGID_12_);} + .st16{fill:url(#SVGID_13_);} +</style><switch + id="switch5194" + transform="matrix(1.3307804,0,0,1.3314313,-72.924861,-37.945792)"><g + i:extraneous="self" + id="g5196"><g + id="g5198"><linearGradient + id="SVGID_1_" + gradientUnits="userSpaceOnUse" + x1="4.0481" + y1="287.94919" + x2="320.4859" + y2="-15.4029" + gradientTransform="matrix(1,0.00546456,-0.00546456,1,-2.0192,-3.0212)"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5201" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5203" /></linearGradient><path + class="st4" + d="m 160,238.8 c -0.2,0 -0.4,0 -0.6,0 C 101.4,238.5 54.5,191.1 54.8,133.1 55.2,75.3 102.3,28.5 160,28.5 c 0.2,0 0.4,0 0.6,0 58,0.3 104.9,47.7 104.6,105.7 v 0 C 264.8,192 217.7,238.8 160,238.8 Z m 0,-206.6 c -55.7,0 -101.2,45.2 -101.5,100.9 -0.3,55.9 45,101.7 100.9,102 0.2,0 0.4,0 0.6,0 55.7,0 101.2,-45.2 101.5,-100.9 0.3,-55.9 -45,-101.7 -100.9,-102 -0.2,0 -0.4,0 -0.6,0 z" + id="path5205" + style="fill:url(#SVGID_1_)" + inkscape:connector-curvature="0" /><g + id="g5207"><linearGradient + id="SVGID_2_" + gradientUnits="userSpaceOnUse" + x1="-11.0561" + y1="273.63409" + x2="354.8013" + y2="-51.979"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5210" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5212" /></linearGradient><path + class="st5" + d="m 168.2,162.4 -1.2,-3.5 c 7.9,-2.6 13.3,-9.6 13.3,-17.3 v -40.5 c 0,-10.2 -9.1,-18.4 -20.2,-18.4 -11.1,0 -20.2,8.3 -20.2,18.4 v 40.5 c 0,7.7 5.3,14.6 13.2,17.3 l -1.2,3.5 c -9.4,-3.2 -15.7,-11.5 -15.7,-20.8 v -40.5 c 0,-12.2 10.7,-22.1 23.9,-22.1 13.2,0 23.9,9.9 23.9,22.1 v 40.5 c 0,9.3 -6.4,17.6 -15.8,20.8 z" + id="path5214" + style="fill:url(#SVGID_2_)" + inkscape:connector-curvature="0" /></g><g + id="g5216"><linearGradient + id="SVGID_3_" + gradientUnits="userSpaceOnUse" + x1="3.6219001" + y1="290.12631" + x2="369.4794" + y2="-35.486801"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5219" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5221" /></linearGradient><path + class="st6" + d="m 160,172.9 c -18.3,0 -33.1,-12.2 -33.1,-27.3 h 3.7 c 0,13 13.2,23.6 29.5,23.6 16.3,0 29.5,-10.6 29.5,-23.6 h 3.7 c -0.2,15.1 -15,27.3 -33.3,27.3 z" + id="path5223" + style="fill:url(#SVGID_3_)" + inkscape:connector-curvature="0" /></g><g + id="g5225"><linearGradient + id="SVGID_4_" + gradientUnits="userSpaceOnUse" + x1="19.325199" + y1="307.77039" + x2="385.18259" + y2="-17.8428"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5228" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5230" /></linearGradient><rect + x="158.2" + y="178.5" + class="st7" + width="3.7" + height="8" + id="rect5232" + style="fill:url(#SVGID_4_)" /></g><g + id="g5234"><linearGradient + id="SVGID_5_" + gradientUnits="userSpaceOnUse" + x1="-22.1502" + y1="261.16879" + x2="343.70721" + y2="-64.444397"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5237" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5239" /></linearGradient><rect + x="138.10001" + y="110.3" + class="st8" + width="14.3" + height="3.7" + id="rect5241" + style="fill:url(#SVGID_5_)" /></g><g + id="g5243"><linearGradient + id="SVGID_6_" + gradientUnits="userSpaceOnUse" + x1="-27.6269" + y1="255.0152" + x2="338.23059" + y2="-70.5979"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5246" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5248" /></linearGradient><rect + x="138.10001" + y="99.300003" + class="st9" + width="14.3" + height="3.7" + id="rect5250" + style="fill:url(#SVGID_6_)" /></g><g + id="g5252"><linearGradient + id="SVGID_7_" + gradientUnits="userSpaceOnUse" + x1="-16.6164" + y1="267.3866" + x2="349.241" + y2="-58.226601"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5255" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5257" /></linearGradient><rect + x="138.10001" + y="121.4" + class="st10" + width="14.3" + height="3.7" + id="rect5259" + style="fill:url(#SVGID_7_)" /></g><g + id="g5261"><linearGradient + id="SVGID_8_" + gradientUnits="userSpaceOnUse" + x1="-11.1393" + y1="273.54059" + x2="354.71811" + y2="-52.072498"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5264" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5266" /></linearGradient><rect + x="138.10001" + y="132.5" + class="st11" + width="14.3" + height="3.7" + id="rect5268" + style="fill:url(#SVGID_8_)" /></g><g + id="g5270"><linearGradient + id="SVGID_9_" + gradientUnits="userSpaceOnUse" + x1="-9.1322002" + y1="275.7959" + x2="356.72531" + y2="-49.817299"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5273" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5275" /></linearGradient><rect + x="167.60001" + y="110.3" + class="st12" + width="14.3" + height="3.7" + id="rect5277" + style="fill:url(#SVGID_9_)" /></g><g + id="g5279"><linearGradient + id="SVGID_10_" + gradientUnits="userSpaceOnUse" + x1="-14.6088" + y1="269.6423" + x2="351.2486" + y2="-55.970798"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5282" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5284" /></linearGradient><rect + x="167.60001" + y="99.300003" + class="st13" + width="14.3" + height="3.7" + id="rect5286" + style="fill:url(#SVGID_10_)" /></g><g + id="g5288"><linearGradient + id="SVGID_11_" + gradientUnits="userSpaceOnUse" + x1="-3.5984001" + y1="282.01361" + x2="362.25909" + y2="-43.599499"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5291" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5293" /></linearGradient><rect + x="167.60001" + y="121.4" + class="st14" + width="14.3" + height="3.7" + id="rect5295" + style="fill:url(#SVGID_11_)" /></g><g + id="g5297"><linearGradient + id="SVGID_12_" + gradientUnits="userSpaceOnUse" + x1="1.8788" + y1="288.16769" + x2="367.73621" + y2="-37.445499"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5300" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5302" /></linearGradient><rect + x="167.60001" + y="132.5" + class="st15" + width="14.3" + height="3.7" + id="rect5304" + style="fill:url(#SVGID_12_)" /></g><g + id="g5306"><linearGradient + id="SVGID_13_" + gradientUnits="userSpaceOnUse" + x1="24.376101" + y1="313.44559" + x2="390.23361" + y2="-12.1676"><stop + offset="0" + style="stop-color:#00ADDC" + id="stop5309" /><stop + offset="1" + style="stop-color:#6BFBFF" + id="stop5311" /></linearGradient><path + class="st16" + d="m 182.1,195 h -3.7 c 0,-4.6 -2.3,-5.4 -8.8,-5.4 h -19.2 c -6.5,0 -8.8,0.8 -8.8,5.4 h -3.7 c 0,-9.1 7.8,-9.1 12.5,-9.1 h 19.2 c 4.7,0 12.5,0 12.5,9.1 z" + id="path5313" + style="fill:url(#SVGID_13_)" + inkscape:connector-curvature="0" /></g></g></g></switch></svg>
\ No newline at end of file diff --git a/homescreen/qml/images/SpeechChrome/speechchrome.qrc b/homescreen/qml/images/SpeechChrome/speechchrome.qrc new file mode 100644 index 0000000..42357f1 --- /dev/null +++ b/homescreen/qml/images/SpeechChrome/speechchrome.qrc @@ -0,0 +1,6 @@ +<RCC> + <qresource prefix="/images/SpeechChrome"> + <file>bar.png</file> + <file>push_to_talk.svg</file> + </qresource> +</RCC> diff --git a/homescreen/qml/main.qml b/homescreen/qml/main.qml index 7d40276..233ee4f 100644 --- a/homescreen/qml/main.qml +++ b/homescreen/qml/main.qml @@ -99,7 +99,7 @@ Window { } } - Timer { + Timer { id:notificationTimer interval: 3000 running: false diff --git a/homescreen/qml/qml.qrc b/homescreen/qml/qml.qrc index e60ea63..d901481 100644 --- a/homescreen/qml/qml.qrc +++ b/homescreen/qml/qml.qrc @@ -10,5 +10,6 @@ <file>StatusArea.qml</file> <file>TopArea.qml</file> <file>IconItem.qml</file> + <file>SpeechChrome.qml</file> </qresource> </RCC> diff --git a/homescreen/src/aglsocketwrapper.cpp b/homescreen/src/aglsocketwrapper.cpp new file mode 100644 index 0000000..8352660 --- /dev/null +++ b/homescreen/src/aglsocketwrapper.cpp @@ -0,0 +1,90 @@ +#include "aglsocketwrapper.h" +#include "constants.h" + +#include <QWebSocket> +#include <QUuid> +#include <QJsonArray> +#include <QJsonObject> +#include <QJsonDocument> + +#include <QDebug> + +namespace { +enum MessageTypes { + Call = 2, + Success = 3, + Error = 4, + Event = 5 +}; +} + +AglSocketWrapper::AglSocketWrapper(QObject *parent) : + QObject(parent) + , m_socket(new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this)) +{ + connect(m_socket, &QWebSocket::connected, this, &AglSocketWrapper::connected); + connect(m_socket, &QWebSocket::disconnected, this, &AglSocketWrapper::disconnected); + connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), + [](QAbstractSocket::SocketError error) -> void { + qWarning() << "AglSocketWrapper internal socket error" << error; + }); + connect(m_socket, &QWebSocket::textMessageReceived, + this, [this](const QString &msg) -> void { + const QJsonDocument doc = QJsonDocument::fromJson(msg.toUtf8()); + if (doc.isArray()) { + const QJsonArray msgArray = doc.array(); + if (msgArray.count() >= 3) { + const int msgType = msgArray.at(0).toInt(); + switch (msgType) { + case Success: + case Error: { + auto callbackIt = m_callbacks.find( msgArray.at(1).toString()); + if (callbackIt != m_callbacks.constEnd()) { + (*callbackIt)(msgType == Success, msgArray.at(2)); + m_callbacks.erase(callbackIt); + } + } + break; + case Event: { + const QJsonObject eventObj = msgArray.at(2).toObject(); + emit eventReceived(msgArray.at(1).toString(), eventObj.value(vshl::DATA_TAG)); + } + break; + default: + break; + } + return; + } + } + qWarning() << "Unsupported message format:" << msg; + }); +} + +void AglSocketWrapper::open(const QUrl &url) +{ + m_socket->open(url); +} + +void AglSocketWrapper::close() +{ + m_socket->close(); +} + +void AglSocketWrapper::apiCall(const QString &api, const QString &verb, const QJsonValue &args, + AglSocketWrapper::ApiCallback callback) +{ + const QString id = QUuid::createUuid().toString(); + if (callback) + m_callbacks.insert(id, callback); + + QJsonArray callData; + callData.append(Call); + callData.append(id); + callData.append(api + QLatin1String("/") + verb); + callData.append(args); + + const QString msg = QLatin1String(QJsonDocument(callData).toJson(QJsonDocument::Compact)); + m_socket->sendTextMessage(msg); + + qDebug() << Q_FUNC_INFO << "Data sent:" << msg; +} diff --git a/homescreen/src/aglsocketwrapper.h b/homescreen/src/aglsocketwrapper.h new file mode 100644 index 0000000..4807cd5 --- /dev/null +++ b/homescreen/src/aglsocketwrapper.h @@ -0,0 +1,35 @@ +#ifndef AGLSOCKETWRAPPER_H +#define AGLSOCKETWRAPPER_H + +#include <QUrl> +#include <QMap> +#include <QObject> +#include <QJsonValue> + +#include <functional> + +class QWebSocket; +class AglSocketWrapper : public QObject +{ + Q_OBJECT +public: + explicit AglSocketWrapper(QObject *parent = nullptr); + + void open(const QUrl &url); + void close(); + + using ApiCallback = std::function<void(bool, const QJsonValue&)>; + void apiCall(const QString &api, const QString &verb, const QJsonValue &args = QJsonValue(), + ApiCallback callback = nullptr); + +signals: + void connected(); + void disconnected(); + void eventReceived(const QString &eventName, const QJsonValue &data); + +private: + QWebSocket *m_socket; + QMap<QString, ApiCallback> m_callbacks; +}; + +#endif // AGLSOCKETWRAPPER_H diff --git a/homescreen/src/chromecontroller.cpp b/homescreen/src/chromecontroller.cpp new file mode 100644 index 0000000..b604dae --- /dev/null +++ b/homescreen/src/chromecontroller.cpp @@ -0,0 +1,159 @@ +#include "chromecontroller.h" +#include "aglsocketwrapper.h" +#include "constants.h" + +#include <QTimer> +#include <QDebug> +#include <QJsonDocument> + +ChromeController::ChromeController(const QUrl &bindingUrl, QObject *parent) : + QObject(parent) + , m_aglSocket(new AglSocketWrapper(this)) +{ + //Alexa voice agent subscription---------------------------------------------------------------- + { + connect(m_aglSocket, &AglSocketWrapper::connected, + this, [this]() -> void { + m_aglSocket->apiCall(vshl::API, vshl::VOICE_AGENT_ENUMERATION_VERB, QJsonValue(), + [this](bool result, const QJsonValue &data) -> void { + qDebug() << (vshl::API + QLatin1String(":") + vshl::VOICE_AGENT_ENUMERATION_VERB) + << "result: " << result << " val: " << data; + if (!result) { + qWarning() << "Failed to enumerate voice agents"; + return; + } + + QJsonObject dataObj = data.toObject(); + auto objIt = dataObj.find(vshl::RESPONSE_TAG); + if (objIt == dataObj.constEnd()) { + qWarning() << "Voice agent enumeration response tag missing." + << dataObj; + return; + } + + // Get default voice agent + dataObj = objIt.value().toObject(); + QJsonObject responseObj = dataObj; + objIt = dataObj.find(vshl::DEFAULT_TAG); + if (objIt == dataObj.constEnd()) { + qWarning() << "Voice agent enumeration default agent tag missing." + << dataObj; + return; + } + QString agentId = objIt.value().toString(); + if (agentId.isEmpty()) { + qWarning() << "Default voice agent not found"; + return; + } + qDebug() << (vshl::API + QLatin1String(":") + vshl::VOICE_AGENT_ENUMERATION_VERB) << "default: " << agentId; + + objIt = dataObj.find(vshl::AGENTS_TAG); + if (objIt == dataObj.constEnd()) { + qWarning() << "Voice agent enumeration agents tag missing." + << dataObj; + return; + } + + // Sanity check that the default agent is actually listed + bool agentFound = false; + const QJsonArray agents = objIt.value().toArray(); + for (const QJsonValue &agent : agents) { + const QJsonObject agentObj = agent.toObject(); + auto agentIt = agentObj.find(vshl::ID_TAG); + if (agentIt == agentObj.constEnd()) + continue; + if (agentId.compare(agentIt.value().toString()) == 0) { + agentFound = true; + break; + } + } + if (!agentFound) { + qWarning() << "Default voice agent configuration not found"; + return; + } + m_agentPresent = true; + emit agentPresentChanged(); + + //Voice agent subscription------------------------------------------------------ + { + m_voiceAgentId = agentId; + const QJsonObject args { + { vshl::VOICE_AGENT_ID_ARG, agentId }, + { vshl::VOICE_AGENT_EVENTS_ARG, vshl::VOICE_AGENT_EVENTS_ARRAY } + }; + m_aglSocket->apiCall(vshl::API, vshl::SUBSCRIBE_VERB, args, + [](bool result, const QJsonValue &data) -> void { + qDebug() << (vshl::API + QLatin1String(":") + vshl::SUBSCRIBE_VERB) + << "result: " << result << " val: " << data; + }); + } + //------------------------------------------------------------------------------ + }); + }); + } + //----------------------------------------------------------------------------------------------< + + //Socket connection management------------------------------------------------------------------ + { + auto connectToBinding = [bindingUrl, this]() -> void { + m_aglSocket->open(bindingUrl); + qDebug() << "Connecting to:" << bindingUrl; + }; + connect(m_aglSocket, &AglSocketWrapper::disconnected, this, [connectToBinding]() -> void { + QTimer::singleShot(2500, connectToBinding); + }); + connectToBinding(); + } + //---------------------------------------------------------------------------------------------- + + //Speech chrome state change event handling----------------------------------------------------- + { + connect(m_aglSocket, &AglSocketWrapper::eventReceived, + this, [this](const QString &eventName, const QJsonValue &data) -> void { + if (eventName.compare(vshl::VOICE_DIALOG_STATE_EVENT + m_voiceAgentId) == 0) { + const QJsonObject dataObj = QJsonDocument::fromJson(data.toString().toUtf8()).object(); + auto objIt = dataObj.find(vshl::STATE_TAG); + if (objIt == dataObj.constEnd()) { + qWarning() << "Voice dialog state event state missing."; + return; + } + const QString stateStr = objIt.value().toString(); + if (stateStr.compare(vshl::VOICE_DIALOG_IDLE) == 0) { + setChromeState(Idle); + } else if (stateStr.compare(vshl::VOICE_DIALOG_LISTENING) == 0) { + setChromeState(Listening); + } else if (stateStr.compare(vshl::VOICE_DIALOG_THINKING) == 0) { + setChromeState(Thinking); + } else if (stateStr.compare(vshl::VOICE_DIALOG_SPEAKING) == 0) { + setChromeState(Speaking); + } else if (stateStr.compare(vshl::VOICE_DIALOG_MICROPHONEOFF) == 0) { + setChromeState(MicrophoneOff); + } + } + }); + } + //---------------------------------------------------------------------------------------------- +} + +void ChromeController::pushToTalk() +{ + m_aglSocket->apiCall(vshl::API, vshl::TAP_TO_TALK_VERB, QJsonValue(), + [](bool result, const QJsonValue &data) -> void { + qDebug() << (vshl::API + QLatin1String(":") + vshl::TAP_TO_TALK_VERB) + << "result: " << result << " val: " << data; + }); +} + +void ChromeController::setChromeState(ChromeController::ChromeState state) +{ + const char* ChromeStateNames[MicrophoneOff + 1] = { "Idle", "Listening", "Thinking", "Speaking", "MicrophoneOff" }; + + if (m_chromeState != state) { + m_chromeState = state; + emit chromeStateChanged(); + if(state <= MicrophoneOff) + qDebug() << "new state = " << ChromeStateNames[state]; + else + qDebug() << "new state = " << state; + } +} diff --git a/homescreen/src/chromecontroller.h b/homescreen/src/chromecontroller.h new file mode 100644 index 0000000..2a76002 --- /dev/null +++ b/homescreen/src/chromecontroller.h @@ -0,0 +1,42 @@ +#pragma once + +#include <QObject> +#include <QUrl> + +class AglSocketWrapper; +class ChromeController : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool agentPresent READ agentPresent NOTIFY agentPresentChanged) + Q_PROPERTY(int chromeState READ chromeState NOTIFY chromeStateChanged) + +public: + enum ChromeState { + Idle = 0, + Listening, + Thinking, + Speaking, + MicrophoneOff + }; + Q_ENUM(ChromeState) + + explicit ChromeController(const QUrl &bindingUrl, QObject *parent = nullptr); + bool agentPresent() const { return m_agentPresent; } + int chromeState() const { return m_chromeState; } + +public slots: + void pushToTalk(); + +signals: + void agentPresentChanged(); + void chromeStateChanged(); + +private: + void setChromeState(ChromeState state); + + AglSocketWrapper *m_aglSocket; + QString m_voiceAgentId; + bool m_agentPresent = false; + ChromeState m_chromeState = Idle; +}; diff --git a/homescreen/src/constants.h b/homescreen/src/constants.h new file mode 100644 index 0000000..a43bf6d --- /dev/null +++ b/homescreen/src/constants.h @@ -0,0 +1,42 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#include <QString> +#include <QJsonArray> +#include <QJsonObject> + +namespace vshl { +const QString API = QLatin1String("vshl-core"); +const QString VOICE_AGENT_ENUMERATION_VERB = QLatin1String("enumerateVoiceAgents"); +const QString SUBSCRIBE_VERB = QLatin1String("subscribe"); +const QString TAP_TO_TALK_VERB = QLatin1String("startListening"); + +const QString ALEXA_AGENT_NAME = QLatin1String("Alexa"); + +const QString DATA_TAG = QLatin1String("data"); +const QString RESPONSE_TAG = QLatin1String("response"); +const QString AGENTS_TAG = QLatin1String("agents"); +const QString DEFAULT_TAG = QLatin1String("default"); +const QString NAME_TAG = QLatin1String("name"); +const QString ID_TAG = QLatin1String("id"); +const QString STATE_TAG = QLatin1String("state"); + +const QString VOICE_AGENT_ID_ARG = QLatin1String("va_id"); +const QString VOICE_AGENT_EVENTS_ARG = QLatin1String("events"); +const QString VOICE_AGENT_ACTIONS_ARG = QLatin1String("actions"); + +const QJsonArray VOICE_AGENT_EVENTS_ARRAY = { + QLatin1String("voice_authstate_event"), + QLatin1String("voice_dialogstate_event"), + QLatin1String("voice_connectionstate_event") +}; + +const QString VOICE_DIALOG_STATE_EVENT = QLatin1String("vshl-core/voice_dialogstate_event#"); +const QString VOICE_DIALOG_IDLE = QLatin1String("IDLE"); +const QString VOICE_DIALOG_LISTENING = QLatin1String("LISTENING"); +const QString VOICE_DIALOG_THINKING = QLatin1String("THINKING"); +const QString VOICE_DIALOG_SPEAKING = QLatin1String("SPEAKING"); +const QString VOICE_DIALOG_MICROPHONEOFF = QLatin1String("MICROPHONEOFF"); +} + +#endif // CONSTANTS_H diff --git a/homescreen/src/main.cpp b/homescreen/src/main.cpp index 5f283fb..5c819f9 100644 --- a/homescreen/src/main.cpp +++ b/homescreen/src/main.cpp @@ -32,6 +32,7 @@ #include "mastervolume.h" #include "homescreenhandler.h" #include "hmi-debug.h" +#include "chromecontroller.h" // XXX: We want this DBus connection to be shared across the different // QML objects, is there another way to do this, a nice way, perhaps? @@ -91,6 +92,8 @@ int main(int argc, char *argv[]) // qmlRegisterType<ApplicationLauncher>("HomeScreen", 1, 0, "ApplicationLauncher"); qmlRegisterType<StatusBarModel>("HomeScreen", 1, 0, "StatusBarModel"); qmlRegisterType<MasterVolume>("MasterVolume", 1, 0, "MasterVolume"); + qmlRegisterUncreatableType<ChromeController>("SpeechChrome", 1, 0, "SpeechChromeController", + QLatin1String("SpeechChromeController is uncreatable.")); ApplicationLauncher *launcher = new ApplicationLauncher(); QLibWindowmanager* layoutHandler = new QLibWindowmanager(); @@ -140,6 +143,7 @@ int main(int argc, char *argv[]) engine.rootContext()->setContextProperty("launcher", launcher); engine.rootContext()->setContextProperty("weather", new Weather(bindingAddress)); engine.rootContext()->setContextProperty("bluetooth", new Bluetooth(bindingAddress, engine.rootContext())); + engine.rootContext()->setContextProperty("speechChromeController", new ChromeController(bindingAddress, &engine)); engine.rootContext()->setContextProperty("screenInfo", &screenInfo); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); diff --git a/package/config.xml b/package/config.xml index 3ebe39b..441cc8d 100644 --- a/package/config.xml +++ b/package/config.xml @@ -13,6 +13,7 @@ <param name="Bluetooth-Manager" value="ws" /> <param name="windowmanager" value="ws" /> <param name="audiomixer" value="ws" /> + <param name="vshl-core" value="ws" /> </feature> <feature name="urn:AGL:widget:required-permission"> <param name="urn:AGL:permission::public:no-htdocs" value="required" /> |