Commit eeefcd35 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik Committed by Kai Uwe Broulik

Add Solid backend using libimobiledevice for finding iOS devices

This allows Solid to find them and exposes them as PortableMediaPlayer with
known protocol "afc" similar to how it's done for MTP devices.
Devices get the device name and form factor icon. It also monitors pluggin in
and out devices.

Battery is not provided as UPower already reports this through libimobiledevice.
Also, there's no way for Solid to relate devices from different backends
so iPhones still also show up as PTP camera.

Right now it doesn't do much (apart showing up in `solid-hardware5 list`
and KInfoCenter's device browser) but this is preparing the necessary
infrastructure for improving iOS support throughout KDE.
Signed-off-by: default avatarEike Hein <eike.hein@mbition.io>
parent ab3ab0f3
......@@ -50,6 +50,18 @@ set_package_properties(BISON PROPERTIES
PURPOSE "Required for the Predicate parser"
)
find_package(IMobileDevice)
set_package_properties(IMobileDevice PROPERTIES
TYPE OPTIONAL
PURPOSE "Needed to build the iOS device support backend"
)
find_package(PList)
set_package_properties(PList PROPERTIES
TYPE OPTIONAL
PURPOSE "Needed to build the iOS device support backend"
)
include(ECMPoQmTools)
......@@ -117,6 +129,11 @@ elseif (NOT ANDROID)
add_device_backend(hal)
endif()
endif()
if(IMobileDevice_FOUND AND PList_FOUND)
add_device_backend(imobile)
endif()
add_device_backends_cmake()
add_subdirectory(src)
......
#.rst:
# FindIMobileDevice
# --------
#
# Try to find the imobiledevice library, once done this will define:
#
# ``IMobileDevice_FOUND``
# System has libimobiledevice.
#
# ``IMobileDevice_INCLUDE_DIRS``
# The libimobiledevice include directory.
#
# ``IMobileDevice_LIBRARIES``
# The libimobiledevice libraries.
#
# ``IMobileDevice_VERSION``
# The libimobiledevice version.
#
# If ``IMobileDevice_FOUND`` is TRUE, the following imported target
# will be available:
#
# ``IMobileDevice::IMobileDevice``
# The libimobiledevice library
#=============================================================================
# SPDX-FileCopyrightText: 2020 MBition GmbH
# SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
#
# SPDX-License-Identifier: BSD-3-Clause
#=============================================================================
find_package(PkgConfig QUIET)
pkg_check_modules(PC_libimobiledevice QUIET libimobiledevice-1.0)
find_path(IMobileDevice_INCLUDE_DIRS NAMES libimobiledevice/libimobiledevice.h HINTS ${PC_libimobiledevice_INCLUDE_DIRS})
find_library(IMobileDevice_LIBRARIES NAMES imobiledevice HINTS ${PC_libimobiledevice_LIBRARY_DIRS})
set(IMobileDevice_VERSION ${PC_libimobiledevice_VERSION})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(IMobileDevice
FOUND_VAR IMobileDevice_FOUND
REQUIRED_VARS IMobileDevice_INCLUDE_DIRS IMobileDevice_LIBRARIES
VERSION_VAR IMobileDevice_VERSION
)
mark_as_advanced(IMobileDevice_INCLUDE_DIRS IMobileDevice_LIBRARIES)
if(IMobileDevice_FOUND AND NOT TARGET IMobileDevice::IMobileDevice)
add_library(IMobileDevice::IMobileDevice UNKNOWN IMPORTED)
set_target_properties(IMobileDevice::IMobileDevice PROPERTIES
IMPORTED_LOCATION "${IMobileDevice_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${IMobileDevice_INCLUDE_DIRS}"
INTERFACE_COMPILE_DEFINITIONS "${PC_libimobiledevice_CFLAGS_OTHER}"
)
endif()
include(FeatureSummary)
set_package_properties(IMobileDevice PROPERTIES
DESCRIPTION "library to communicate with iOS devices"
URL "https://www.libimobiledevice.org/"
)
#.rst:
# FindPList
# --------
#
# Try to find the plist library, once done this will define:
#
# ``PList_FOUND``
# System has libplist.
#
# ``PList_INCLUDE_DIRS``
# The libplist include directory.
#
# ``PList_LIBRARIES``
# The libplist libraries.
#
# ``PList_VERSION``
# The libplist version.
#
# If ``PList_FOUND`` is TRUE, the following imported target
# will be available:
#
# ``PList::PList``
# The libplist library
#=============================================================================
# SPDX-FileCopyrightText: 2020 MBition GmbH
# SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
#
# SPDX-License-Identifier: BSD-3-Clause
#=============================================================================
find_package(PkgConfig QUIET)
pkg_check_modules(PC_libplist QUIET libplist-2.0 libplist)
find_path(PList_INCLUDE_DIRS NAMES plist/plist.h HINTS ${PC_libplist_INCLUDE_DIRS})
find_library(PList_LIBRARIES NAMES plist HINTS ${PC_libplist_LIBRARY_DIRS})
set(PList_VERSION ${PC_libplist_VERSION})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(PList
FOUND_VAR PList_FOUND
REQUIRED_VARS PList_INCLUDE_DIRS PList_LIBRARIES
VERSION_VAR PList_VERSION
)
mark_as_advanced(PList_INCLUDE_DIRS PList_LIBRARIES)
if(PList_FOUND AND NOT TARGET PList::PList)
add_library(PList::PList UNKNOWN IMPORTED)
set_target_properties(PList::PList PROPERTIES
IMPORTED_LOCATION "${PList_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${PList_INCLUDE_DIRS}"
INTERFACE_COMPILE_DEFINITIONS "${PC_libplist_CFLAGS_OTHER}"
)
endif()
include(FeatureSummary)
set_package_properties(PList PROPERTIES
DESCRIPTION "library to handle Apple property list format"
URL "https://www.libimobiledevice.org/"
)
set(backend_sources
imobile.cpp
imobiledevice.cpp
imobilemanager.cpp
imobiledeviceinterface.cpp
imobileportablemediaplayer.cpp
)
set(backend_libs IMobileDevice::IMobileDevice PList::PList)
ecm_qt_declare_logging_category(backend_sources
HEADER imobile_debug.h
IDENTIFIER Solid::Backends::IMobile::IMOBILE
DEFAULT_SEVERITY Warning
CATEGORY_NAME kf.solid.backends.imobile
DESCRIPTION "IMobileDevice (Solid)"
EXPORT SOLID
)
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "imobile.h"
QString Solid::Backends::IMobile::udiPrefix()
{
return QStringLiteral("/org/kde/solid/imobile");
}
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef SOLID_BACKENDS_IMOBILE_H
#define SOLID_BACKENDS_IMOBILE_H
#include <QString>
namespace Solid
{
namespace Backends
{
namespace IMobile
{
QString udiPrefix();
} // namespace IMobile
} // namespace Backends
} // namespace Solid
#endif // SOLID_BACKENDS_IMOBILE_H
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "imobiledevice.h"
#include <QCoreApplication>
#include <QScopeGuard>
#include "imobile_debug.h"
#include "imobile.h"
#include "imobileportablemediaplayer.h"
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
using namespace Solid::Backends::IMobile;
IMobileDevice::IMobileDevice(const QString &udi)
: Solid::Ifaces::Device()
, m_udi(udi)
{
const QString deviceId = udi.mid(udiPrefix().length() + 1);
idevice_t device;
idevice_new(&device, deviceId.toUtf8().constData());
if (!device) {
qCWarning(IMOBILE) << "Failed to create device instance for" << deviceId;
return;
}
auto deviceCleanup = qScopeGuard([device] {
idevice_free(device);
});
lockdownd_client_t lockdowndClient = nullptr;
auto ret = lockdownd_client_new(device, &lockdowndClient, "kde_solid_imobile");
if (ret != LOCKDOWN_E_SUCCESS || !lockdowndClient) {
qCWarning(IMOBILE) << "Failed to create lockdownd client for" << deviceId;
return;
}
auto lockdowndClientCleanup = qScopeGuard([lockdowndClient] {
lockdownd_client_free(lockdowndClient);
});
char *name = nullptr;
auto lockdownRet = lockdownd_get_device_name(lockdowndClient, &name);
if (lockdownRet != LOCKDOWN_E_SUCCESS) {
qCWarning(IMOBILE) << "Failed to get device name for" << deviceId << lockdownRet;
} else if (name) {
m_name = QString::fromUtf8(name);
free(name);
}
plist_t deviceClassEntry = nullptr;
lockdownRet = lockdownd_get_value(lockdowndClient, nullptr /*global domain*/, "DeviceClass", &deviceClassEntry);
if (lockdownRet != LOCKDOWN_E_SUCCESS) {
qCWarning(IMOBILE) << "Failed to get device class for" << deviceId << lockdownRet;
} else {
char *deviceClass = nullptr;
plist_get_string_val(deviceClassEntry, &deviceClass);
if (deviceClass) {
m_deviceClass = QString::fromUtf8(deviceClass);
free(deviceClass);
}
}
}
IMobileDevice::~IMobileDevice()
{
}
QString IMobileDevice::udi() const
{
return m_udi;
}
QString IMobileDevice::parentUdi() const
{
return udiPrefix();
}
QString IMobileDevice::vendor() const
{
return QCoreApplication::translate("imobiledevice", "Apple", "Company name");
}
QString IMobileDevice::product() const
{
// TODO would be nice to use actual product names, e.g. "iPhone 5S"
// but accessing device type requires doing a handshake with the device,
// which will fail if locked or not paired, and also would require us
// to maintain a giant mapping table
return m_deviceClass;
}
QString IMobileDevice::icon() const
{
if (m_deviceClass.contains(QLatin1String("iPod"))) {
return QStringLiteral("multimedia-player-apple-ipod-touch");
} else if (m_deviceClass.contains(QLatin1String("iPad"))) {
return QStringLiteral("computer-apple-ipad");
} else {
return QStringLiteral("phone-apple-iphone");
}
}
QStringList IMobileDevice::emblems() const
{
return {};
}
QString IMobileDevice::description() const
{
return m_name;
}
bool IMobileDevice::queryDeviceInterface(const Solid::DeviceInterface::Type &type) const
{
switch (type) {
// TOOD would be cool to support GenericInterface for reading
// arbitrary plist configuration, cf. what ideviceinfo tool does
case Solid::DeviceInterface::PortableMediaPlayer:
return true;
default:
return false;
}
}
QObject *IMobileDevice::createDeviceInterface(const Solid::DeviceInterface::Type &type)
{
if (!queryDeviceInterface(type)) {
return nullptr;
}
switch (type) {
case Solid::DeviceInterface::PortableMediaPlayer:
return new PortableMediaPlayer(this);
default:
Q_UNREACHABLE();
return nullptr;
}
}
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef SOLID_BACKENDS_IMOBILE_IMOBILEDEVICE_H
#define SOLID_BACKENDS_IMOBILE_IMOBILEDEVICE_H
#include <solid/devices/ifaces/device.h>
#include <QStringList>
namespace Solid
{
namespace Backends
{
namespace IMobile
{
class IMobileDevice : public Solid::Ifaces::Device
{
Q_OBJECT
public:
explicit IMobileDevice(const QString &udi);
~IMobileDevice() override;
QString udi() const override;
QString parentUdi() const override;
QString vendor() const override;
QString product() const override;
QString icon() const override;
QStringList emblems() const override;
QString description() const override;
bool queryDeviceInterface(const Solid::DeviceInterface::Type &type) const override;
QObject *createDeviceInterface(const Solid::DeviceInterface::Type &type) override;
private:
QString m_udi;
QString m_name;
QString m_deviceClass;
};
} // namespace IMobile
} // namespace Backends
} // namespace Solid
#endif // SOLID_BACKENDS_IMOBILE_IMOBILEDEVICE_H
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "imobiledeviceinterface.h"
#include "imobiledevice.h"
using namespace Solid::Backends::IMobile;
DeviceInterface::DeviceInterface(IMobileDevice *device)
: QObject(device)
, m_device(device)
{
}
DeviceInterface::~DeviceInterface() = default;
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef SOLID_BACKENDS_IMOBILE_IMOBILEDEVICEINTERFACE_H
#define SOLID_BACKENDS_IMOBILE_IMOBILEDEVICEINTERFACE_H
#include <solid/devices/ifaces/deviceinterface.h>
//#include "udevdevice.h"
#include <QObject>
namespace Solid
{
namespace Backends
{
namespace IMobile
{
class IMobileDevice;
class DeviceInterface : public QObject, virtual public Solid::Ifaces::DeviceInterface
{
Q_OBJECT
Q_INTERFACES(Solid::Ifaces::DeviceInterface)
public:
explicit DeviceInterface(IMobileDevice *device);
~DeviceInterface() override;
protected:
IMobileDevice *m_device;
};
}
}
}
#endif // SOLID_BACKENDS_IMOBILE_IMOBILEDEVICEINTERFACE_H
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "imobilemanager.h"
#include "imobile_debug.h"
#include "../shared/rootdevice.h"
#include "imobile.h"
#include "imobiledevice.h"
#include <libimobiledevice/libimobiledevice.h>
using namespace Solid::Backends::IMobile;
using namespace Solid::Backends::Shared;
Manager::Manager(QObject *parent)
: Solid::Ifaces::DeviceManager(parent)
{
auto ret = idevice_event_subscribe([](const idevice_event_t *event, void *user_data) {
static_cast<Manager *>(user_data)->onDeviceEvent(event);
}, this);
if (ret != IDEVICE_E_SUCCESS) {
qCWarning(IMOBILE) << "Failed to subscribe to device events";
}
char **devices = nullptr;
int count = 0;
ret = idevice_get_device_list(&devices, &count);
if (ret != IDEVICE_E_SUCCESS && ret != IDEVICE_E_NO_DEVICE) {
qCWarning(IMOBILE) << "Failed to get list of iOS devices" << ret;
return;
}
m_deviceUdis.reserve(count);
for (int i = 0; i < count; ++i) {
m_deviceUdis.append(udiPrefix() + QLatin1Char('/') + QString::fromLatin1(devices[i]));
}
if (devices) {
idevice_device_list_free(devices);
}
}
Manager::~Manager()
{
idevice_event_unsubscribe();
}
QObject *Manager::createDevice(const QString &udi)
{
if (udi == udiPrefix()) {
RootDevice *root = new RootDevice(udi);
root->setProduct(tr("iDevice"));
root->setDescription(tr("iOS devices"));
root->setIcon("phone-apple-iphone");
return root;
}
if (m_deviceUdis.contains(udi)) {
return new IMobileDevice(udi);
}
return nullptr;
}
QStringList Manager::devicesFromQuery(const QString &parentUdi, Solid::DeviceInterface::Type type)
{
QStringList devices;
if (!parentUdi.isEmpty() || type != Solid::DeviceInterface::Unknown) {
for (const QString &udi : m_deviceUdis) {
IMobileDevice device(udi);
if (!device.queryDeviceInterface(type)) {
continue;
}
if (!parentUdi.isEmpty() && device.parentUdi() != parentUdi) {
continue;
}
devices << udi;
}
}
return devices;
}
QStringList Manager::allDevices()
{
return m_deviceUdis;
}
QSet<Solid::DeviceInterface::Type> Manager::supportedInterfaces() const
{
return {
Solid::DeviceInterface::PortableMediaPlayer
};
}
QString Manager::udiPrefix() const
{
return Solid::Backends::IMobile::udiPrefix();
}
void Manager::onDeviceEvent(const idevice_event_t *event)
{
const QString udi = udiPrefix() + QLatin1Char('/') + QString::fromLatin1(event->udid);
switch (event->event) {
case IDEVICE_DEVICE_ADD:
if (!m_deviceUdis.contains(udi)) {
m_deviceUdis.append(udi);
emit deviceAdded(udi);
}
return;
case IDEVICE_DEVICE_REMOVE:
if (m_deviceUdis.removeOne(udi)) {
emit deviceRemoved(udi);
}
return;
case IDEVICE_DEVICE_PAIRED:
return;
}
qCDebug(IMOBILE) << "Unhandled device event" << event->event << "for" << event->udid;
}
/*
SPDX-FileCopyrightText: 2020 MBition GmbH
SPDX-FileContributor: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef SOLID_BACKENDS_IMOBILE_IMOBILEMANAGER_H
#define SOLID_BACKENDS_IMOBILE_IMOBILEMANAGER_H
#include <solid/devices/ifaces/devicemanager.h>
#include <libimobiledevice/libimobiledevice.h>