From 0f9e9e41961a896ad2dc2bb05fcd7f5d9ee9e26b Mon Sep 17 00:00:00 2001 From: Matt Ranostay Date: Mon, 26 Jun 2017 18:34:20 -0700 Subject: binding: bluetooth: switch from dbus calls to system binding Use the system wide binding for Bluetooth access to get AVRCP metadata, and triggers controls. Bug-AGL: SPEC-610 SPEC-615 Change-Id: Ic894c75d663b797b8a331236ff756b3b94bc13c8 Signed-off-by: Matt Ranostay --- app/MediaPlayer.qml | 105 +++++++------------- app/api/BluetoothManager.qml | 183 +++++++++++++++++++++++++++++++++++ app/app.pri | 2 - app/dbus.cpp | 224 ------------------------------------------- app/dbus.h | 64 ------------- app/main.cpp | 14 --- app/mediaplayer.qrc | 1 + package/config.xml | 1 + 8 files changed, 217 insertions(+), 377 deletions(-) create mode 100644 app/api/BluetoothManager.qml delete mode 100644 app/dbus.cpp delete mode 100644 app/dbus.h diff --git a/app/MediaPlayer.qml b/app/MediaPlayer.qml index fbd889c..d37e097 100644 --- a/app/MediaPlayer.qml +++ b/app/MediaPlayer.qml @@ -25,63 +25,14 @@ import 'api' as API ApplicationWindow { id: root - Item { - id: bluetooth - property bool connected: false - property string state - - property string artist - property string title - property int duration: 0 - property int position: 0 - property int pos_offset: 0 - - function disableBluetooth() { - bluetooth.artist = '' - bluetooth.title = '' - bluetooth.duration = 0 - bluetooth.position = 0 - bluetooth.pos_offset = 0 - bluetooth.connected = false - } - } - API.LightMediaScanner { id: binding url: bindingAddress } - Connections { - target: dbus - - onProcessPlaylistHide: { - playlistview.visible = false - player.stop() - } - - onProcessPlaylistShow: { - playlistview.visible = true - bluetooth.disableBluetooth() - } - - onDisplayBluetoothMetadata: { - if (avrcp_artist) - bluetooth.artist = avrcp_artist - if (avrcp_title) - bluetooth.title = avrcp_title - bluetooth.duration = avrcp_duration - bluetooth.pos_offset = 0 - } - - onUpdatePlayerStatus: { - bluetooth.connected = true - bluetooth.state = status - } - - onUpdatePosition: { - slider.value = current_position - bluetooth.position = current_position - } + API.BluetoothManager { + id: bluetooth + url: bindingAddress } MediaPlayer { @@ -90,20 +41,18 @@ ApplicationWindow { autoLoad: true playlist: playlist - property bool is_bluetooth: false function time2str(value) { return Qt.formatTime(new Date(value), 'mm:ss') } - onPositionChanged: slider.value = player.position } Timer { id: timer interval: 250 - running: (bluetooth.connected && bluetooth.state == "playing") + running: (bluetooth.av_connected && bluetooth.state == "playing") repeat: true onTriggered: { - bluetooth.position = dbus.getCurrentPosition() - bluetooth.pos_offset + bluetooth.position = bluetooth.position + 250 slider.value = bluetooth.position } } @@ -169,13 +118,13 @@ ApplicationWindow { Label { id: title Layout.alignment: Layout.Center - text: bluetooth.title ? bluetooth.title : (player.metaData.title ? player.metaData.title : '') + text: bluetooth.av_connected ? bluetooth.title : (player.metaData.title ? player.metaData.title : '') horizontalAlignment: Label.AlignHCenter verticalAlignment: Label.AlignVCenter } Label { Layout.alignment: Layout.Center - text: bluetooth.artist ? bluetooth.artist : (player.metaData.contributingArtist ? player.metaData.contributingArtist : '') + text: bluetooth.av_connected ? bluetooth.artist : (player.metaData.contributingArtist ? player.metaData.contributingArtist : '') horizontalAlignment: Label.AlignHCenter verticalAlignment: Label.AlignVCenter font.pixelSize: title.font.pixelSize * 0.6 @@ -185,10 +134,11 @@ ApplicationWindow { Slider { id: slider Layout.fillWidth: true - to: bluetooth.connected ? bluetooth.duration : player.duration - enabled: bluetooth.connected == false + to: bluetooth.av_connected ? bluetooth.duration : player.duration + enabled: bluetooth.av_connected == false + value: bluetooth.av_connected ? bluetooth.position : player.position function getPosition() { - if (bluetooth.connected && bluetooth.position) { + if (bluetooth.av_connected) { return player.time2str(bluetooth.position) } return player.time2str(player.position) @@ -205,7 +155,7 @@ ApplicationWindow { anchors.right: parent.right anchors.bottom: parent.top font.pixelSize: 32 - text: bluetooth.connected ? player.time2str(bluetooth.duration) : player.time2str(player.duration) + text: bluetooth.av_connected ? player.time2str(bluetooth.duration) : player.time2str(player.duration) } onPressedChanged: player.seek(value) } @@ -222,9 +172,9 @@ ApplicationWindow { id: previous offImage: './images/AGL_MediaPlayer_BackArrow.svg' onClicked: { - if (bluetooth.connected) { - bluetooth.pos_offset = dbus.getCurrentPosition() - dbus.processQMLEvent("Previous") + if (bluetooth.av_connected) { + bluetooth.sendMediaCommand("Previous") + bluetooth.position = 0 } else { playlist.previous() } @@ -234,8 +184,8 @@ ApplicationWindow { id: play offImage: './images/AGL_MediaPlayer_Player_Play.svg' onClicked: { - if (bluetooth.connected) { - dbus.processQMLEvent("Play") + if (bluetooth.av_connected) { + bluetooth.sendMediaCommand("Play") } else { player.play() } @@ -250,11 +200,11 @@ ApplicationWindow { } }, State { - when: bluetooth.connected && bluetooth.state == "playing" + when: bluetooth.av_connected && bluetooth.state == "playing" PropertyChanges { target: play offImage: './images/AGL_MediaPlayer_Player_Pause.svg' - onClicked: dbus.processQMLEvent("Pause") + onClicked: bluetooth.sendMediaCommand("Pause") } } @@ -264,8 +214,8 @@ ApplicationWindow { id: forward offImage: './images/AGL_MediaPlayer_ForwardArrow.svg' onClicked: { - if (bluetooth.connected) { - dbus.processQMLEvent("Next") + if (bluetooth.av_connected) { + bluetooth.sendMediaCommand("Next") } else { playlist.next() } @@ -275,10 +225,18 @@ ApplicationWindow { Item { Layout.fillWidth: true } ToggleButton { - enabled: false - checked: bluetooth.connected + enabled: bluetooth.connected + checked: bluetooth.av_connected offImage: './images/AGL_MediaPlayer_Bluetooth_Inactive.svg' onImage: './images/AGL_MediaPlayer_Bluetooth_Active.svg' + + onClicked: { + if (bluetooth.av_connected) { + bluetooth.disconnect_profiles() + } else { + bluetooth.connect_profiles() + } + } } } } @@ -297,6 +255,7 @@ ApplicationWindow { ListView { anchors.fill: parent id: playlistview + visible: bluetooth.av_connected == false clip: true header: Label { x: 50 diff --git a/app/api/BluetoothManager.qml b/app/api/BluetoothManager.qml new file mode 100644 index 0000000..bb273a5 --- /dev/null +++ b/app/api/BluetoothManager.qml @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 The Qt Company Ltd. + * Copyright (C) 2017 Konsulko Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import QtQuick 2.6 +import QtWebSockets 1.0 + +WebSocket { + id: root + active: true + url: bindingAddress + + property string statusString: "waiting..." + property string apiString: "Bluetooth-Manager" + property var verbs: [] + property string payloadLength: "9999" + + property string deviceAddress: "" + property bool connected: false + property bool av_connected: false + + property int position: 0 + property int duration: 0 + + property string artist: "" + property string title: "" + property string state: "stopped" + + // AVRCP Target UUID + property string avrcp_uuid: "0000110e-0000-1000-8000-00805f9b34fb" + + // A2DP Source + property string a2dp_uuid: "0000110a-0000-1000-8000-00805f9b34fb" + + readonly property var msgid: { + "call": 2, + "retok": 3, + "reterr": 4, + "event": 5 + } + + onTextMessageReceived: { + var json = JSON.parse(message) + console.debug("Raw response: " + message) + var request = json[2].request + var response = json[2].response + console.debug("response: " + JSON.stringify(response)) + switch (json[0]) { + case msgid.call: + break + case msgid.retok: + root.statusString = request.status + var address = "" + + if (request.info == "BT - Scan Result is Displayed") { + for (var i = 0; i < response.length; i++) { + var data = response[i] + if (data.Connected == "True" && data.UUIDs.indexOf(avrcp_uuid) >= 0) { + address = response[i].Address + console.debug("Connected Device: " + address) + + root.connected = true + player.pause() + + //NOTE: This hack is here for when MediaPlayer is started + // with an existing connection. + if (data.AVPConnected == "True") { + root.av_connected = true + } + } + } + root.deviceAddress = address + if (!address) { + root.connected = false + playlistview.visible = true + } + } + break + case msgid.reterr: + root.statusString = "Bad return value, binding probably not installed" + break + case msgid.event: + var payload = JSON.parse(JSON.stringify(json[2])) + var event = payload.event + + if (event == "Bluetooth-Manager/connection") { + sendSocketMessage("discovery_result", 'None') + } else if (event == "Bluetooth-Manager/device_updated") { + var data = payload.data + var metadata = data.Metadata + + if (root.deviceAddress != data.Address) + break + + if (data.Connected == "False") { + console.debug("Device Disconnected") + sendSocketMessage("discovery_result", 'None') + break + } + root.av_connected = data.AVPConnected == "True" + + if ('Position' in metadata) { + console.debug("Position " + metadata.Position) + root.position = metadata.Position + } + + if ('Duration' in metadata) { + console.debug("Duration " + metadata.Duration) + root.duration = metadata.Duration + } + + if ('Status' in metadata) { + console.debug("Status " + metadata.Status) + root.state = metadata.Status + } + + if ('Artist' in metadata) { + console.debug("Artist " + metadata.Artist) + root.artist = metadata.Artist + } + + if ('Title' in metadata) { + console.debug("Title " + metadata.Title) + root.title = metadata.Title + } + } + break + } + } + + onStatusChanged: { + switch (status) { + case WebSocket.Open: + console.debug("onStatusChanged: Open") + sendSocketMessage("eventadd", { "tag" : "device_updated", "name" : "device_updated" }) + sendSocketMessage("eventsub", { "tag" : "device_updated" }) + sendSocketMessage("eventadd", { "tag" : "connection", "name" : "connection" }) + sendSocketMessage("eventsub", { "tag" : "connection" }) + sendSocketMessage("discovery_result", 'None') + break + case WebSocket.Error: + root.statusString = "WebSocket error: " + root.errorString + break + } + } + + function sendSocketMessage(verb, parameter) { + var requestJson = [ msgid.call, payloadLength, apiString + '/' + + verb, parameter ] + console.debug("sendSocketMessage: " + JSON.stringify(requestJson)) + verbs.push(verb) + root.sendTextMessage(JSON.stringify(requestJson)) + } + + function sendMediaCommand(state) { + var parameters = { "Address": deviceAddress, "value": state } + sendSocketMessage("set_avrcp_controls", parameters) + } + + function connect_profiles() { + sendSocketMessage("connect", { "value": root.deviceAddress, "uuid": a2dp_uuid }) + sendSocketMessage("connect", { "value": root.deviceAddress, "uuid": avrcp_uuid }) + root.av_connected = true + } + + function disconnect_profiles() { + sendSocketMessage("disconnect", { "value": root.deviceAddress, "uuid": a2dp_uuid }) + sendSocketMessage("disconnect", { "value": root.deviceAddress, "uuid": avrcp_uuid }) + } +} diff --git a/app/app.pri b/app/app.pri index 56bc949..7ec39b1 100644 --- a/app/app.pri +++ b/app/app.pri @@ -14,8 +14,6 @@ packagesExist(sqlite3 lightmediascanner) { } packagesExist(dbus-1) { - HEADERS += dbus.h - SOURCES += dbus.cpp DEFINES += HAVE_DBUS QT += dbus } diff --git a/app/dbus.cpp b/app/dbus.cpp deleted file mode 100644 index a562989..0000000 --- a/app/dbus.cpp +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2017 Konsulko Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "dbus.h" - - -DbusService::DbusService(QObject *parent) : QObject(parent) -{ -} - -/* - * Bluetooth - */ - -void DbusService::setBluezPath(const QString& path) -{ - this->bluezPath = path; -} - -QString DbusService::getBluezPath() const -{ - return this->bluezPath; -} - -bool DbusService::enableBluetooth() -{ - QDBusConnection system_bus = QDBusConnection::systemBus(); - QString interface = "org.freedesktop.DBus.ObjectManager"; - bool ret; - - if (!system_bus.isConnected()) - return false; - - if (deviceConnected(system_bus)) - initialBluetoothData(system_bus); - - ret = system_bus.connect(QString("org.bluez"), QString("/"), interface, "InterfacesAdded", this, SLOT(newBluetoothDevice(QDBusObjectPath,QVariantMap))); - - if (!ret) - return false; - - ret = system_bus.connect(QString("org.bluez"), QString("/"), interface, "InterfacesRemoved", this, SLOT(removeBluetoothDevice(QDBusObjectPath,QStringList))); - - /* - * Unregister InterfacesAdded on error condition - */ - if (!ret) - system_bus.disconnect(QString("org.bluez"), QString("/"), interface, "InterfacesAdded", this, SLOT(newBluetoothDevice(QString,QVariantMap))); - - return ret; -} - -bool DbusService::checkIfPlayer(const QString& path) const -{ - QRegExp regex("^.*/player\\d$"); - if (regex.exactMatch(path)) - return true; - - return false; -} - -bool DbusService::deviceConnected(const QDBusConnection& system_bus) -{ - QDBusInterface interface("org.bluez", "/", "org.freedesktop.DBus.ObjectManager", system_bus); - QDBusMessage result = interface.call("GetManagedObjects"); - const QDBusArgument argument = result.arguments().at(0).value(); - bool ret = false; - - if (argument.currentType() != QDBusArgument::MapType) - return false; - - argument.beginMap(); - - while (!argument.atEnd()) { - QString key; - - argument.beginMapEntry(); - argument >> key; - argument.endMapEntry(); - - ret = checkIfPlayer(key); - - if (ret) { - newBluetoothDevice(QDBusObjectPath(key), QVariantMap()); - break; - } - } - - argument.endMap(); - - return ret; -} - -void DbusService::initialBluetoothData(const QDBusConnection& system_bus) -{ - QDBusInterface interface("org.bluez", getBluezPath(), "org.freedesktop.DBus.Properties", system_bus); - - QDBusMessage result = interface.call("GetAll", "org.bluez.MediaPlayer1"); - const QDBusArgument argument = result.arguments().at(0).value(); - QString status, artist, title; - int position = 0, duration = 0; - - if (argument.currentType() != QDBusArgument::MapType) - return; - - argument.beginMap(); - - while (!argument.atEnd()) { - QString key; - QVariant value; - - argument.beginMapEntry(); - argument >> key >> value; - - if (key == "Position") { - position = value.toInt(); - } else if (key == "Status") { - status = value.toString(); - } else if (key == "Track") { - const QDBusArgument argument1 = qvariant_cast(value); - QString key1; - QVariant value1; - - argument1.beginMap(); - - while (!argument1.atEnd()) { - argument1.beginMapEntry(); - argument1 >> key1 >> value1; - if (key1 == "Artist") - artist = value1.toString(); - else if (key1 == "Title") - title = value1.toString(); - else if (key1 == "Duration") - duration = value1.toInt(); - argument1.endMapEntry(); - } - argument1.endMap(); - } - argument.endMapEntry(); - } - argument.endMap(); - - emit processPlaylistHide(); - emit displayBluetoothMetadata(artist, title, duration); - emit updatePlayerStatus(status); - emit updatePosition(position); -} - -void DbusService::newBluetoothDevice(const QDBusObjectPath& item, const QVariantMap&) -{ - QString path = item.path(); - if (!checkIfPlayer(path)) - return; - - if (!getBluezPath().isEmpty()) { - qWarning() << "Another Bluetooth Player already connected"; - return; - } - - emit processPlaylistHide(); - - QDBusConnection system_bus = QDBusConnection::systemBus(); - system_bus.connect(QString("org.bluez"), path, "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(processBluetoothEvent(QString,QVariantMap,QStringList))); - - setBluezPath(path); -} - -void DbusService::removeBluetoothDevice(const QDBusObjectPath& item, const QStringList&) -{ - QString path = item.path(); - if (!checkIfPlayer(path)) - return; - - if (getBluezPath().isEmpty()) { - qWarning() << "No Bluetooth Player connected"; - return; - } - - QDBusConnection system_bus = QDBusConnection::systemBus(); - system_bus.disconnect(QString("org.bluez"), path, "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(processBluetoothEvent(QString,QVariantMap,QStringList))); - - setBluezPath(QString()); - emit processPlaylistShow(); -} - -void DbusService::processBluetoothEvent(const QString&, const QVariantMap& map, const QStringList&) -{ - if (map.contains("Track")) { - QVariantMap track; - map["Track"].value() >> track; - emit displayBluetoothMetadata(track["Artist"].toString(), track["Title"].toString(), track["Duration"].toInt()); - } - - if (map.contains("Position")) - emit updatePosition(map["Position"].toInt()); - - if (map.contains("Status")) - emit updatePlayerStatus(map["Status"].toString()); -} - -void DbusService::processQMLEvent(const QString& state) -{ - QDBusInterface interface("org.bluez", getBluezPath(), "org.bluez.MediaPlayer1", QDBusConnection::systemBus()); - interface.call(state); -} - -long DbusService::getCurrentPosition() -{ - QDBusInterface interface("org.bluez", getBluezPath(), "org.bluez.MediaPlayer1", QDBusConnection::systemBus()); - return interface.property("Position").toInt(); -} diff --git a/app/dbus.h b/app/dbus.h deleted file mode 100644 index bdc2f4f..0000000 --- a/app/dbus.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2017 Konsulko Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef DBUS_H -#define DBUS_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class DbusService : public QObject { - Q_OBJECT -public: - explicit DbusService(QObject *parent = 0); - - bool enableBluetooth(); - Q_INVOKABLE void processQMLEvent(const QString&); - Q_INVOKABLE long getCurrentPosition(); - -private: - void setBluezPath(const QString& path); - QString getBluezPath() const; - bool checkIfPlayer(const QString& path) const; - bool deviceConnected(const QDBusConnection& system_bus); - void initialBluetoothData(const QDBusConnection& system_bus); - QString bluezPath; - -signals: - void processPlaylistUpdate(const QVariantList& mediaFiles); - void processPlaylistHide(); - void processPlaylistShow(); - - void displayBluetoothMetadata(const QString& avrcp_artist, const QString& avrcp_title, const int avrcp_duration); - void stopPlayback(); - void updatePosition(const int current_position); - void updatePlayerStatus(const QString status); - -private slots: - void newBluetoothDevice(const QDBusObjectPath&, const QVariantMap&); - void removeBluetoothDevice(const QDBusObjectPath&, const QStringList&); - void processBluetoothEvent(const QString&, const QVariantMap&, const QStringList&); -}; - -#endif diff --git a/app/main.cpp b/app/main.cpp index a5bab67..5ad9577 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -29,10 +29,6 @@ #include #endif -#ifdef HAVE_DBUS -#include "dbus.h" -#endif - #include "playlistwithmetadata.h" #ifndef HAVE_LIGHTMEDIASCANNER @@ -105,17 +101,7 @@ int main(int argc, char *argv[]) context->setContextProperty(QStringLiteral("bindingAddress"), bindingAddress); } -#if defined(HAVE_DBUS) - DbusService dbus_service; - context->setContextProperty("dbus", &dbus_service); -#endif - engine.load(QUrl(QStringLiteral("qrc:/MediaPlayer.qml"))); -#if defined(HAVE_DBUS) - if (!dbus_service.enableBluetooth()) - qWarning() << "Cannot run enableBluetooth"; -#endif - return app.exec(); } diff --git a/app/mediaplayer.qrc b/app/mediaplayer.qrc index 22b52e1..15e5288 100644 --- a/app/mediaplayer.qrc +++ b/app/mediaplayer.qrc @@ -2,5 +2,6 @@ MediaPlayer.qml api/LightMediaScanner.qml + api/BluetoothManager.qml diff --git a/package/config.xml b/package/config.xml index 7112452..1091f17 100644 --- a/package/config.xml +++ b/package/config.xml @@ -8,6 +8,7 @@ APL 2.0 + -- cgit 1.2.3-korg