summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNaveen Bobbili <nbobbili@amazon.com>2019-04-28 20:51:16 -0700
committerScott Murray <scott.murray@konsulko.com>2019-12-20 19:16:39 +0000
commit6c2177c0125fd78c94aa0970dc3488492cd1a319 (patch)
treebd431652ea3048f69c2e9f7ebc54ed9643aa6ee4
parent0f52dc1537fd50cbbd179ee17cc6203900c833b6 (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 (cherry picked from commit 0349f05f5885987952a2d8de03983b36722b264e)
-rw-r--r--homescreen/homescreen.pro12
-rw-r--r--homescreen/qml/MediaArea.qml4
-rw-r--r--homescreen/qml/MediaAreaBlank.qml18
-rw-r--r--homescreen/qml/SpeechChrome.qml120
-rw-r--r--homescreen/qml/images/SpeechChrome/bar.pngbin0 -> 23826 bytes
-rw-r--r--homescreen/qml/images/SpeechChrome/push_to_talk.svg322
-rw-r--r--homescreen/qml/images/SpeechChrome/speechchrome.qrc6
-rw-r--r--homescreen/qml/main.qml2
-rw-r--r--homescreen/qml/qml.qrc1
-rw-r--r--homescreen/src/aglsocketwrapper.cpp90
-rw-r--r--homescreen/src/aglsocketwrapper.h35
-rw-r--r--homescreen/src/chromecontroller.cpp159
-rw-r--r--homescreen/src/chromecontroller.h42
-rw-r--r--homescreen/src/constants.h42
-rw-r--r--homescreen/src/main.cpp4
-rw-r--r--package/config.xml1
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
new file mode 100644
index 0000000..caabde1
--- /dev/null
+++ b/homescreen/qml/images/SpeechChrome/bar.png
Binary files differ
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="&amp;#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" />