/*
* Copyright (C) 2013-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Authors:
* Charles Kerr
*/
#include "device.h"
#include
#include // qWarning()
#include
#include
#include "dbus-shared.h"
Device::Device(const QString &path, QDBusConnection &bus) :
m_strength(Device::None)
{
initDevice(path, bus);
}
void Device::initDevice(const QString &path, QDBusConnection &bus)
{
/* whenever any of the properties changes,
trigger the catch-all deviceChanged() signal */
QObject::connect(this, SIGNAL(nameChanged()), this, SIGNAL(deviceChanged()));
QObject::connect(this, SIGNAL(iconNameChanged()), this, SIGNAL(deviceChanged()));
QObject::connect(this, SIGNAL(addressChanged()), this, SIGNAL(deviceChanged()));
QObject::connect(this, SIGNAL(pairedChanged()), this, SIGNAL(deviceChanged()));
QObject::connect(this, SIGNAL(trustedChanged()), this, SIGNAL(deviceChanged()));
QObject::connect(this, SIGNAL(typeChanged()), this, SIGNAL(deviceChanged()));
QObject::connect(this, SIGNAL(connectionChanged()), this, SIGNAL(deviceChanged()));
QObject::connect(this, SIGNAL(strengthChanged()), this, SIGNAL(deviceChanged()));
m_bluezDevice.reset(new BluezDevice1(BLUEZ_SERVICE, path, bus));
/* Give our calls a bit more time than the default 25 seconds to
* complete whatever they are doing. In some situations (e.g. with
* specific devices) the default doesn't seem to be enough to. */
m_bluezDevice->setTimeout(60 * 1000 /* 60 seconds */);
m_bluezDeviceProperties.reset(new FreeDesktopProperties(BLUEZ_SERVICE, path, bus));
QObject::connect(m_bluezDeviceProperties.data(), SIGNAL(PropertiesChanged(const QString&, const QVariantMap&, const QStringList&)),
this, SLOT(slotPropertiesChanged(const QString&, const QVariantMap&, const QStringList&)));
Q_EMIT(pathChanged());
watchCall(m_bluezDeviceProperties->GetAll(BLUEZ_DEVICE_IFACE), [=](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply reply = *watcher;
if (reply.isError()) {
qWarning() << "Failed to retrieve properties for device" << m_bluezDevice->path();
watcher->deleteLater();
return;
}
auto properties = reply.argumentAt<0>();
setProperties(properties);
watcher->deleteLater();
});
}
void Device::slotPropertiesChanged(const QString &interface, const QVariantMap &changedProperties,
const QStringList &invalidatedProperties)
{
Q_UNUSED(invalidatedProperties);
if (interface != BLUEZ_DEVICE_IFACE)
return;
setProperties(changedProperties);
}
void Device::setProperties(const QMap &properties)
{
QMapIterator it(properties);
while (it.hasNext()) {
it.next();
updateProperty(it.key(), it.value());
}
}
void Device::setConnectAfterPairing(bool value)
{
if (m_connectAfterPairing == value)
return;
m_connectAfterPairing = value;
}
void Device::disconnect()
{
setConnection(Device::Disconnecting);
QDBusPendingCall call = m_bluezDevice->Disconnect();
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply reply = *watcher;
if (reply.isError()) {
qWarning() << "Could not disconnect device:"
<< reply.error().message();
// Make sure we switch the connection indicator back to
// a sane state
updateConnection();
}
watcher->deleteLater();
});
}
void Device::connectAfterPairing()
{
if (!m_connectAfterPairing)
return;
connect();
}
void Device::pair()
{
if (m_paired) {
// If we are already paired we just have to make sure we
// trigger the connection process if we have to
connectAfterPairing();
return;
}
setConnection(Device::Connecting);
m_isPairing = true;
auto call = m_bluezDevice->asyncCall("Pair");
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply reply = *watcher;
bool success = true;
if (reply.isError()) {
qWarning() << "Failed to pair with device:"
<< reply.error().message();
updateConnection();
success = false;
}
m_isPairing = false;
Q_EMIT(pairingDone(success));
watcher->deleteLater();
});
}
void Device::cancelPairing()
{
if (!m_isPairing)
return;
auto call = m_bluezDevice->asyncCall("CancelPairing");
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply reply = *watcher;
if (reply.isError()) {
qWarning() << "Failed to cancel pairing attempt with device:"
<< reply.error().message();
updateConnection();
} else {
// Only mark us a not pairing when call succeeded
m_isPairing = false;
}
watcher->deleteLater();
});
}
void Device::connect()
{
// If we have just paired then the device switched to connected = true for
// a short moment as BlueZ opened up a RFCOMM channel to perform SDP. If
// we should connect with the device on specific profiles now we go ahead
// here even if we're marked as connected as this still doesn't mean we're
// connected on any profile. Calling org.bluez.Device1.Connect multiple
// times doesn't hurt an will not fail.
if (m_isConnected && !m_connectAfterPairing)
return;
setConnection(Device::Connecting);
QDBusPendingCall call = m_bluezDevice->asyncCall("Connect");
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply reply = *watcher;
if (reply.isError()) {
qWarning() << "Could not connect device:"
<< reply.error().message();
} else {
makeTrusted(true);
}
// Regardless if the Connected property has changed or not we update
// the connection state here as the connection process is over now
// and we should have received any state change already at this
// point.
updateConnection();
watcher->deleteLater();
});
}
void Device::slotMakeTrustedDone(QDBusPendingCallWatcher *call)
{
QDBusPendingReply reply = *call;
if (reply.isError()) {
qWarning() << "Could not mark device as trusted:"
<< reply.error().message();
}
call->deleteLater();
}
void Device::makeTrusted(bool trusted)
{
auto call = m_bluezDeviceProperties->Set(BLUEZ_DEVICE_IFACE, "Trusted", QDBusVariant(trusted));
auto watcher = new QDBusPendingCallWatcher(call, this);
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
this, SLOT(slotMakeTrustedDone(QDBusPendingCallWatcher*)));
}
void Device::setName(const QString &name)
{
if (m_name != name) {
m_name = name;
Q_EMIT(nameChanged());
}
}
void Device::setIconName(const QString &iconName)
{
if (m_iconName != iconName) {
m_iconName = iconName;
Q_EMIT(iconNameChanged());
}
}
void Device::setAddress(const QString &address)
{
if (m_address != address) {
m_address = address;
Q_EMIT(addressChanged());
}
}
void Device::setType(Type type)
{
if (m_type != type) {
m_type = type;
Q_EMIT(typeChanged());
updateIcon();
}
}
void Device::setPaired(bool paired)
{
if (m_paired != paired) {
m_paired = paired;
Q_EMIT(pairedChanged());
}
}
void Device::setTrusted(bool trusted)
{
if (m_trusted != trusted) {
m_trusted = trusted;
Q_EMIT(trustedChanged());
}
}
void Device::setConnection(Connection connection)
{
if (m_connection != connection) {
m_connection = connection;
Q_EMIT(connectionChanged());
}
}
void Device::updateIcon()
{
/* bluez-provided icon is unreliable? In testing I'm getting
an "audio-card" icon from bluez for my NoiseHush N700 headset.
Try to guess the icon from the device type,
and use the bluez-provided icon as a fallback */
const auto type = getType();
switch (type) {
case Type::Headset:
setIconName("image://theme/audio-headset-symbolic");
break;
case Type::Headphones:
setIconName("image://theme/audio-headphones-symbolic");
break;
case Type::Carkit:
setIconName("image://theme/audio-carkit-symbolic");
break;
case Type::Speakers:
case Type::OtherAudio:
setIconName("image://theme/audio-speakers-symbolic");
break;
case Type::Mouse:
setIconName("image://theme/input-mouse-symbolic");
break;
case Type::Keyboard:
setIconName("image://theme/input-keyboard-symbolic");
break;
case Type::Cellular:
setIconName("image://theme/phone-cellular-symbolic");
break;
case Type::Smartphone:
setIconName("image://theme/phone-smartphone-symbolic");
break;
case Type::Phone:
setIconName("image://theme/phone-uncategorized-symbolic");
break;
case Type::Computer:
setIconName("image://theme/computer-symbolic");
break;
default:
setIconName(QString("image://theme/%1").arg(m_fallbackIconName));
}
}
void Device::updateConnection()
{
Connection c;
c = m_isConnected ? Connection::Connected : Connection::Disconnected;
setConnection(c);
}
void Device::updateProperty(const QString &key, const QVariant &value)
{
if (key == "Name") {
setName(value.toString());
} else if (key == "Address") {
setAddress(value.toString());
} else if (key == "Connected") {
m_isConnected = value.toBool();
updateConnection();
} else if (key == "Class") {
setType(getTypeFromClass(value.toUInt()));
} else if (key == "Paired") {
setPaired(value.toBool());
if (m_paired && m_connectAfterPairing) {
connectAfterPairing();
return;
}
updateConnection();
} else if (key == "Trusted") {
setTrusted(value.toBool());
} else if (key == "Icon") {
m_fallbackIconName = value.toString();
updateIcon ();
} else if (key == "RSSI") {
m_strength = getStrengthFromRssi(value.toInt());
Q_EMIT(strengthChanged());
}
}
/* Determine the Type from the bits in the Class of Device (CoD) field.
https://www.bluetooth.org/en-us/specification/assigned-numbers/baseband */
Device::Type Device::getTypeFromClass (quint32 c)
{
switch ((c & 0x1f00) >> 8) {
case 0x01:
return Type::Computer;
case 0x02:
switch ((c & 0xfc) >> 2) {
case 0x01:
return Type::Cellular;
case 0x03:
return Type::Smartphone;
case 0x04:
return Type::Modem;
default:
return Type::Phone;
}
break;
case 0x03:
return Type::Network;
case 0x04:
switch ((c & 0xfc) >> 2) {
case 0x01:
case 0x02:
return Type::Headset;
case 0x05:
return Type::Speakers;
case 0x06:
return Type::Headphones;
case 0x08:
return Type::Carkit;
case 0x0b: // vcr
case 0x0c: // video camera
case 0x0d: // camcorder
return Type::Video;
default:
return Type::OtherAudio;
}
break;
case 0x05:
switch ((c & 0xc0) >> 6) {
case 0x00:
switch ((c & 0x1e) >> 2) {
case 0x01:
case 0x02:
return Type::Joypad;
}
break;
case 0x01:
return Type::Keyboard;
case 0x02:
switch ((c & 0x1e) >> 2) {
case 0x05:
return Type::Tablet;
default:
return Type::Mouse;
}
}
break;
case 0x06:
if ((c & 0x80) != 0)
return Type::Printer;
if ((c & 0x20) != 0)
return Type::Camera;
break;
case 0x07:
if ((c & 0x4) != 0)
return Type::Watch;
break;
}
return Type::Other;
}
Device::Strength Device::getStrengthFromRssi(int rssi)
{
/* Modelled similar to what Mac OS X does.
* See http://www.cnet.com/how-to/how-to-check-bluetooth-connection-strength-in-os-x/ */
if (rssi >= -60)
return Excellent;
else if (rssi < -60 && rssi >= -70)
return Good;
else if (rssi < -70 && rssi >= -90)
return Fair;
else if (rssi < -90)
return Poor;
return None;
}