Add push to talk support to homescreen 82/21182/3
authorNaveen Bobbili <nbobbili@amazon.com>
Mon, 29 Apr 2019 03:51:16 +0000 (20:51 -0700)
committerScott Murray <scott.murray@konsulko.com>
Wed, 16 Oct 2019 22:58:55 +0000 (18:58 -0400)
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,

Change-Id: I2052e345baaf4306e8e3f27a01bc6940f4d27d88
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>
16 files changed:
homescreen/homescreen.pro
homescreen/qml/MediaArea.qml
homescreen/qml/MediaAreaBlank.qml
homescreen/qml/SpeechChrome.qml [new file with mode: 0644]
homescreen/qml/images/SpeechChrome/bar.png [new file with mode: 0644]
homescreen/qml/images/SpeechChrome/push_to_talk.svg [new file with mode: 0644]
homescreen/qml/images/SpeechChrome/speechchrome.qrc [new file with mode: 0644]
homescreen/qml/main.qml
homescreen/qml/qml.qrc
homescreen/src/aglsocketwrapper.cpp [new file with mode: 0644]
homescreen/src/aglsocketwrapper.h [new file with mode: 0644]
homescreen/src/chromecontroller.cpp [new file with mode: 0644]
homescreen/src/chromecontroller.h [new file with mode: 0644]
homescreen/src/constants.h [new file with mode: 0644]
homescreen/src/main.cpp
package/config.xml

index 8baa90d..773271e 100644 (file)
@@ -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
index 0447589..3b6d18a 100644 (file)
@@ -20,8 +20,8 @@ import QtQuick.Controls 2.0
 
 StackView {
     id: root
-    width: 1080
-    height: 215
+    width: parent.width
+    height: parent.height
 
     initialItem: blank
 
index ebddb0c..60d0c92 100644 (file)
@@ -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 (file)
index 0000000..911d481
--- /dev/null
@@ -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 (file)
index 0000000..caabde1
Binary files /dev/null and b/homescreen/qml/images/SpeechChrome/bar.png 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 (file)
index 0000000..0c775a1
--- /dev/null
@@ -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 (file)
index 0000000..42357f1
--- /dev/null
@@ -0,0 +1,6 @@
+<RCC>
+    <qresource prefix="/images/SpeechChrome">
+        <file>bar.png</file>
+        <file>push_to_talk.svg</file>
+    </qresource>
+</RCC>
index 7d40276..233ee4f 100644 (file)
@@ -99,7 +99,7 @@ Window {
         }
     }
 
-       Timer {
+    Timer {
         id:notificationTimer
         interval: 3000
         running: false
index e60ea63..d901481 100644 (file)
@@ -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 (file)
index 0000000..8352660
--- /dev/null
@@ -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 (file)
index 0000000..4807cd5
--- /dev/null
@@ -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 (file)
index 0000000..b604dae
--- /dev/null
@@ -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 (file)
index 0000000..2a76002
--- /dev/null
@@ -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 (file)
index 0000000..a43bf6d
--- /dev/null
@@ -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
index 5f283fb..5c819f9 100644 (file)
@@ -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")));
 
index 3ebe39b..441cc8d 100644 (file)
@@ -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" />