This commit is contained in:
Julien Wadel 2021-04-16 20:05:24 +02:00
parent 3ae5b6284d
commit 65689adb34
22 changed files with 211 additions and 998 deletions

View file

@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Options to audio codec can be used and stored
- Opus can now use `packetlosspercentage` and `useinbandfec` configuration
- A silence file have been added : `silence.mkv` and can be used to switch off some musics (hold_music)
- MSYS2 support for Windows
- OpenLDAP support
### Fixed

View file

@ -34,7 +34,7 @@ You will need the tools :
- `yasm` : https://yasm.tortall.net/Download.html
- `nasm` : https://www.nasm.us/pub/nasm/releasebuilds/
- `doxygen` (required for the Cxx Wrapper)
- `Perl` : (can be downloaded at http://strawberryperl.com/ for Windows. Set your Path to Perl binaries)
- `Perl`
- `Pystache` : use 'pip install pystache --user'
- `six` : use 'pip install six --user'
- `git`
@ -45,7 +45,7 @@ For Desktop : you will need [Qt5](https://www.qt.io/download-thank-you) (_5.12 o
1. It's necessary to install the `pip` command and to execute:
pip install pystache
pip install pystache six
2. You have to set the environment variable `Qt5_DIR` to point to the path containing the cmake folders of Qt5, and the `PATH` to the Qt5 `bin`. Example:
@ -167,29 +167,34 @@ OR
## Specific instructions for the Windows platform
1. Ensure that you have downloaded the `Qt msvc2015 version` or `Qt msvc2017 version` (32-bit). (64-bit version is not supported at this moment by Linphone Desktop.)
- `MinGW` : [download](https://sourceforge.net/projects/mingw/)
- Select all installer options except Ada and Fortran
- Install it in the default location (C:/Mingw), this is important as there are hard-links on it.
- The gcc version should be 6.3.0. It wasn't tested for other versions. It seems that MinGW from osdn.net try to install gcc 9 that breaks the build.
- `Yasm`
- download yasm-1.3.0-win32.exe
- copy it to a bin directory of your user directory
- rename yasm-1.3.0-win32.exe as yasm.exe
- `nasm` : [download](https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/win64/)
- `git` : [download](https://git-scm.com/download/win)
64-bit version is not fully supported at this moment by Linphone Desktop and wasn't tested.
If a build for 64bits is needed, replace all `mingw32` by `mingw64`, `i686` by `x86_64`, `-A Win32` by `-A x64` or simply remove it.
Visual Studio must also be properly configured with addons. Under "Tools"->"Obtain tools and features", make sure that the following components are installed:
- Tasks: Select Windows Universal Platform development, Desktop C++ Development, .NET Development
- Under "Installation details". Go to "Desktop C++ Development" and add "SDK Windows 8.1 and SDK UCRT"
- Individual component: Windows 8.1 SDK
1. Install main tools:
- `MinGW/MSYS2` : [download](https://www.msys2.org/)
- Follow instructions on their "Getting Started" page.
- Install toolchains and prepare python:
- `pacman -Sy --needed base-devel mingw-w64-i686-toolchain`
- `pacman -S python3-pip` in `MSYS2 MSYS` console
- `python3 -m pip install pystache six` in `cmd`
- In this order, add `C:\msys64\`, `C:\msys64\usr\bin` and `C:\msys64\mingw32\bin` in your PATH (the last one is needed by cmake to know where gcc is) to the PATH environement variable from windows advanced settings.
When building the SDK, it will install automatically from MSYS2 : `perl`, `yasm`, `gawk`, `bzip2`, `nasm, `sed`, `patch`, `pkg-config`, `gettext`, `glib2` and `intltool` (if needed)
2. Or open a Command line with Visual Studio `Developer Command Prompt for VS 2017` and call qtenv2.bat that is in your qt binaries eg: `C:\Qt\<version>\msvc2017\bin\qtenv2.bat`
- `git` : use MSYS2 : `pacman -S git` or [download](https://git-scm.com/download/win)
- Visual Studio must also be properly configured with addons. Under "Tools"->"Obtain tools and features", make sure that the following components are installed:
- Tasks: Select Windows Universal Platform development, Desktop C++ Development, .NET Development
- Under "Installation details". Go to "Desktop C++ Development" and add "SDK Windows 8.1 and SDK UCRT"
- Individual component: Windows 8.1 SDK
3. Install msys-coreutils : `mingw-get install msys-coreutils`
2. Ensure that you have downloaded the `Qt msvc2015 version` or `Qt msvc2017 version` (32-bit).
3. Or open a Command line with Visual Studio `Developer Command Prompt for VS 2017` and call qtenv2.bat that is in your qt binaries eg: `C:\Qt\<version>\msvc2017\bin\qtenv2.bat`
4. Build as usual with adding `-A Win32` to `cmake ..` (General Steps) :
- `cmake .. -DCMAKE_BUILD_PARALLEL_LEVEL=10 -DCMAKE_BUILD_TYPE=RelWithDebInfo -A Win32`
The default build is very long. It is prefered to use the Ninja generator `-G "Ninja"`
- `cmake --build . --target ALL_BUILD --parallel 10 --config RelWithDebInfo`
5. The project folder will be in the build directory and binaries should be in the OUTPUT folder.

View file

@ -489,9 +489,6 @@
<file>ui/dev-modules/Colors/Colors.qml</file>
<file>ui/dev-modules/Units/Units.qml</file>
<file>assets/icon.ico</file>
<file>ui/views/App/Settings/SettingsLdap.qml</file>
<file>ui/views/App/Settings/SettingsLdapDescription.qml</file>
<file>ui/views/App/Settings/Dialogs/SettingsLdapEdit.qml</file>
<file>ui/views/App/Settings/Dialogs/SettingsLdapEdit.js</file>
</qresource>
</RCC>

View file

@ -35,11 +35,7 @@
#include "utils/MediastreamerUtils.hpp"
#include "utils/Utils.hpp"
//#include "linphone/api/c-magic-search.h"
#include "linphone/api/c-search-result.h"
//#include "linphone/api/friends.h"
// =============================================================================
@ -49,33 +45,10 @@ namespace {
constexpr char AutoAnswerObjectName[] = "auto-answer-timer";
}
void CallModel::searchReceived(std::list<std::shared_ptr<linphone::SearchResult>> results){
bool found = false;
for(auto it = results.begin() ; it != results.end() && !found ; ++it){
if((*it)->getFriend()){
if((*it)->getFriend()->getAddress()->weakEqual(mRemoteAddress)){
setRemoteDisplayName((*it)->getFriend()->getName());
found = true;
}
}else{
if((*it)->getAddress()->weakEqual(mRemoteAddress)){
setRemoteDisplayName((*it)->getAddress()->getDisplayName());
found = true;
}
}
}
}
void CallModel::setRemoteDisplayName(const std::string& name){
mRemoteAddress->setDisplayName(name);
emit fullPeerAddressChanged();
}
CallModel::CallModel (shared_ptr<linphone::Call> call){
Q_CHECK_PTR(call);
mCall = call;
mCall->setData("call-model", *this);
updateIsInConference();
@ -105,7 +78,8 @@ CallModel::CallModel (shared_ptr<linphone::Call> call){
coreHandlers, &CoreHandlers::callEncryptionChanged,
this, &CallModel::handleCallEncryptionChanged
);
// Update fields
// Update fields and make a search to know to who the call belong
mMagicSearch = CoreManager::getInstance()->getCore()->createMagicSearch();
mSearch = std::make_shared<SearchHandler>(this);
QObject::connect(mSearch.get(), SIGNAL(searchReceived(std::list<std::shared_ptr<linphone::SearchResult>> )), this, SLOT(searchReceived(std::list<std::shared_ptr<linphone::SearchResult>>)));
@ -113,8 +87,6 @@ CallModel::CallModel (shared_ptr<linphone::Call> call){
mRemoteAddress = mCall->getRemoteAddress()->clone();
mMagicSearch->getContactListFromFilterAsync(mRemoteAddress->getUsername(),mRemoteAddress->getDomain());
}
CallModel::~CallModel () {
@ -656,6 +628,31 @@ void CallModel::updateStreams () {
void CallModel::toggleSpeakerMute(){
setSpeakerMuted(!getSpeakerMuted());
}
// -----------------------------------------------------------------------------
// Set remote display name when a search has been done
void CallModel::searchReceived(std::list<std::shared_ptr<linphone::SearchResult>> results){
bool found = false;
for(auto it = results.begin() ; it != results.end() && !found ; ++it){
if((*it)->getFriend()){
if((*it)->getFriend()->getAddress()->weakEqual(mRemoteAddress)){
setRemoteDisplayName((*it)->getFriend()->getName());
found = true;
}
}else{
if((*it)->getAddress()->weakEqual(mRemoteAddress)){
setRemoteDisplayName((*it)->getAddress()->getDisplayName());
found = true;
}
}
}
}
void CallModel::setRemoteDisplayName(const std::string& name){
mRemoteAddress->setDisplayName(name);
emit fullPeerAddressChanged();
}
// -----------------------------------------------------------------------------
CallModel::CallEncryption CallModel::getEncryption () const {

View file

@ -142,6 +142,7 @@ public:
std::shared_ptr<linphone::MagicSearch> mMagicSearch;
public slots:
// Set remote display name when a search has been done
void searchReceived(std::list<std::shared_ptr<linphone::SearchResult>> results);
signals:
@ -164,8 +165,6 @@ signals:
private:
void handleCallEncryptionChanged (const std::shared_ptr<linphone::Call> &call);
void handleCallStateChanged (const std::shared_ptr<linphone::Call> &call, linphone::Call::State state);
void accept (bool withVideo);
@ -244,7 +243,6 @@ private:
QVariantList mAudioStats;
QVariantList mVideoStats;
std::shared_ptr<SearchHandler> mSearch;
};
#endif // CALL_MODEL_H_

View file

@ -110,10 +110,7 @@ void CoreManager::initCoreManager(){
QObject::connect(mEventCountNotifier, &EventCountNotifier::eventCountChanged,this, &CoreManager::eventCountChanged);
migrate();
mStarted = true;
//std::list<std::string> dns;
//dns.push_back("10.0.3.50");
//mCore->setDnsServers(dns);
qInfo() << QStringLiteral("CoreManager initialized");
emit coreManagerInitialized();
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
@ -98,6 +98,7 @@ void LdapListModel::initLdap () {
CoreManager *coreManager = CoreManager::getInstance();
auto lConfig = coreManager->getCore()->getConfig();
auto bcSections = lConfig->getSectionsNamesList();
// Loop on all sections and load configuration. If this is not a LDAP configuration, the model is discarded.
for(auto itSections = bcSections.begin(); itSections != bcSections.end(); ++itSections) {
LdapModel * ldap = new LdapModel();
if(ldap->load(*itSections)){
@ -107,29 +108,28 @@ void LdapListModel::initLdap () {
}
}
// Save if valid
void LdapListModel::enable(int id, bool status){
if( mServers[id]->isValid()){
QVariantMap config = mServers[id]->getConfig();
config["enable"] = status;
mServers[id]->setConfig(config);
mServers[id]->save();
}
emit dataChanged(index(id, 0), index(id, 0));
}
// Create a new LdapModel and put it in the list
void LdapListModel::add(){
int row = mServers.count();
beginInsertRows(QModelIndex(), row, row);
auto ldap= new LdapModel(row);
ldap->init();
mServers << ldap;
endInsertRows();
//emit dataChanged(index(row, 0), index(row, 0));
resetInternalData();
}
void LdapListModel::remove (LdapModel *ldap) {
int index = mServers.indexOf(ldap);
if (index >=0){

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
@ -30,33 +30,35 @@
class CoreHandlers;
class LdapListModel : public QAbstractListModel {
Q_OBJECT
Q_OBJECT
public:
LdapListModel (QObject *parent = Q_NULLPTR);
void reset();
int rowCount (const QModelIndex &index = QModelIndex()) const override;
QHash<int, QByteArray> roleNames () const override;
QVariant data (const QModelIndex &index, int role = Qt::DisplayRole) const override;
Q_INVOKABLE void enable(int id, bool status);
Q_INVOKABLE void add();
Q_INVOKABLE void remove (LdapModel *importer);
LdapListModel (QObject *parent = Q_NULLPTR);
void reset();
int rowCount (const QModelIndex &index = QModelIndex()) const override;
QHash<int, QByteArray> roleNames () const override;
QVariant data (const QModelIndex &index, int role = Qt::DisplayRole) const override;
// Enable the Server and save it if it is valid
Q_INVOKABLE void enable(int id, bool status);
// Create a Server
Q_INVOKABLE void add();
// Remove a Server
Q_INVOKABLE void remove (LdapModel *importer);
private:
bool removeRow (int row, const QModelIndex &parent = QModelIndex());
bool removeRows (int row, int count, const QModelIndex &parent = QModelIndex()) override;
// ---------------------------------------------------------------------------
void initLdap ();
QList<LdapModel*> mServers;
bool removeRow (int row, const QModelIndex &parent = QModelIndex());
bool removeRows (int row, int count, const QModelIndex &parent = QModelIndex()) override;
// ---------------------------------------------------------------------------
void initLdap ();
QList<LdapModel*> mServers;
};
#endif // LDAP_LIST_MODEL_H_

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
@ -30,8 +30,8 @@
class CoreHandlers;
class LdapModel : public QObject {
Q_OBJECT
Q_PROPERTY(QVariantMap config READ getConfig WRITE setConfig NOTIFY configChanged)
Q_OBJECT
Q_PROPERTY(QVariantMap config READ getConfig WRITE setConfig NOTIFY configChanged)
Q_PROPERTY(bool isValid MEMBER mIsValid NOTIFY isValidChanged)
Q_PROPERTY(QString server MEMBER mServer WRITE setServer NOTIFY serverChanged)
@ -68,20 +68,20 @@ class LdapModel : public QObject {
Q_PROPERTY(QString sipDomain MEMBER mSipDomain WRITE setSipDomain NOTIFY sipDomainChanged)
Q_PROPERTY(QString sipDomainFieldError MEMBER mSipDomainFieldError NOTIFY sipDomainFieldErrorChanged)
Q_PROPERTY(bool debug MEMBER mDebug NOTIFY debugChanged)
Q_PROPERTY(int verifyServerCertificates MEMBER mVerifyServerCertificates NOTIFY verifyServerCertificatesChanged)
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged)
public:
LdapModel (const int& id = 0,QObject *parent = nullptr);
QVariantMap mConfig;
LdapModel (const int& id = 0,QObject *parent = nullptr);
QVariantMap mConfig;
bool mIsValid;
int mId; // "ldap_mId" from section name
QString mServer;
int mId; // "ldap_mId" from section name
QString mServer;
QString mServerFieldError;
void setServer(const QString& server);
void testServerField();
@ -91,85 +91,86 @@ public:
bool mUseSal;
bool mUseTls;
int mMaxResults;
int mMaxResults;
QString mMaxResultsFieldError;
void setMaxResults(const int& data);
void testMaxResultsField();
QString mPassword;
QString mPassword;
QString mPasswordFieldError;
void setPassword(const QString& data);
void testPasswordField();
QString mBindDn;
QString mBindDn;
QString mBindDnFieldError;
void setBindDn(const QString& data);
void testBindDnField();
QString mBaseObject;
QString mBaseObject;
QString mBaseObjectFieldError;
void setBaseObject(const QString& data);
void testBaseObjectField();
QString mFilter;
QString mFilter;
QString mFilterFieldError;
void setFilter(const QString& data);
void testFilterField();
QString mNameAttributes;
QString mNameAttributes;
QString mNameAttributesFieldError;
void setNameAttributes(const QString& data);
void testNameAttributesField();
QString mSipAttributes;
QString mSipAttributes;
QString mSipAttributesFieldError;
void setSipAttributes(const QString& data);
void testSipAttributesField();
QString mSipScheme;
QString mSipScheme;
QString mSipSchemeFieldError;
void setSipScheme(const QString& data);
void testSipSchemeField();
QString mSipDomain;
QString mSipDomain;
QString mSipDomainFieldError;
void setSipDomain(const QString& data);
void testSipDomainField();
bool mDebug;
int mVerifyServerCertificates;
bool isValid();
void init();// init by default value
Q_INVOKABLE void save();
void unsave();
bool load(const std::string& sectionName);
void set();
Q_INVOKABLE void unset();
QVariantMap getConfig();
void setConfig(const QVariantMap& config);
// Test if the configuration is valid
bool isValid();
void init();// init configuration by default value
Q_INVOKABLE void save(); // Save configuration to linphonerc
void unsave(); // Remove configuration from linphonerc
bool load(const std::string& sectionName);// Load a configuration : ldap_x where x is a unique number
void set(); // Fix Configuration from variables
Q_INVOKABLE void unset(); // Set variables from Configuration
QVariantMap getConfig();
void setConfig(const QVariantMap& config);
bool isEnabled();
void setEnabled(const bool& data);
signals:
void configChanged();
void configChanged();
void isValidChanged();
void serverChanged();
void serverChanged();
void displayNameChanged();
void useTlsChanged();
void useSalChanged();
void isServerValidChanged();
void maxResultsChanged();
void passwordChanged();
void bindDnChanged();
void baseObjectChanged();
void filterChanged();
void nameAttributesChanged();
void sipAttributesChanged();
void sipSchemeChanged();
void sipDomainChanged();
void maxResultsChanged();
void passwordChanged();
void bindDnChanged();
void baseObjectChanged();
void filterChanged();
void nameAttributesChanged();
void sipAttributesChanged();
void sipSchemeChanged();
void sipDomainChanged();
void debugChanged();
void verifyServerCertificatesChanged();

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2021 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
@ -26,14 +26,13 @@
// =============================================================================
class LdapProxyModel : public QSortFilterProxyModel {
Q_OBJECT
Q_OBJECT
public:
LdapProxyModel (QObject *parent = Q_NULLPTR);
LdapProxyModel (QObject *parent = Q_NULLPTR);
protected:
bool filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const override;
bool lessThan (const QModelIndex &left, const QModelIndex &right) const override;
bool filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const override;
bool lessThan (const QModelIndex &left, const QModelIndex &right) const override;
};
#endif // LDAP_PROXY_MODEL_H_

View file

@ -42,16 +42,7 @@
using namespace std;
// -----------------------------------------------------------------------------
/*
static inline QVariantMap buildVariantMap (const SearchSipAddressesModel::SipAddressEntry &sipAddressEntry) {
return QVariantMap{
{ "sipAddress", sipAddressEntry.sipAddress },
{ "contact", QVariant::fromValue(sipAddressEntry.contact) },
{ "presenceStatus", sipAddressEntry.presenceStatus },
{ "__localToConferenceEntry", QVariant::fromValue(&sipAddressEntry.localAddressToConferenceEntry) }
};
}
*/
SearchSipAddressesModel::SearchSipAddressesModel (QObject *parent) : QAbstractListModel(parent) {
mMagicSearch = CoreManager::getInstance()->getCore()->createMagicSearch();
@ -60,67 +51,60 @@ SearchSipAddressesModel::SearchSipAddressesModel (QObject *parent) : QAbstractLi
mMagicSearch->addListener(mSearch);
}
SearchSipAddressesModel::~SearchSipAddressesModel(){
mMagicSearch->removeListener(mSearch);
}
// -----------------------------------------------------------------------------
int SearchSipAddressesModel::rowCount (const QModelIndex &) const {
return mAddresses.count()-1;
return mAddresses.count()-1;
}
QHash<int, QByteArray> SearchSipAddressesModel::roleNames () const {
QHash<int, QByteArray> roles;
roles[Qt::DisplayRole] = "$sipAddress";
return roles;
QHash<int, QByteArray> roles;
roles[Qt::DisplayRole] = "$sipAddress";
return roles;
}
QVariant SearchSipAddressesModel::data (const QModelIndex &index, int role) const {
int row = index.row();
if (!index.isValid() || row < 0 || row >= mAddresses.count())
return QVariant();
if (role == Qt::DisplayRole)
return QVariantMap{{"sipAddress", mAddresses[row]}};
return QVariant();
int row = index.row();
if (!index.isValid() || row < 0 || row >= mAddresses.count())
return QVariant();
if (role == Qt::DisplayRole)
return QVariantMap{{"sipAddress", mAddresses[row]}};
return QVariant();
}
// -----------------------------------------------------------------------------
bool SearchSipAddressesModel::removeRow (int row, const QModelIndex &parent) {
return removeRows(row, 1, parent);
return removeRows(row, 1, parent);
}
bool SearchSipAddressesModel::removeRows (int row, int count, const QModelIndex &parent) {
int limit = row + count - 1;
if (row < 0 || count < 0 || limit >= mAddresses.count())
return false;
beginRemoveRows(parent, row, limit);
for (int i = 0; i < count; ++i)
mAddresses.removeAt(row);
endRemoveRows();
return true;
int limit = row + count - 1;
if (row < 0 || count < 0 || limit >= mAddresses.count())
return false;
beginRemoveRows(parent, row, limit);
for (int i = 0; i < count; ++i)
mAddresses.removeAt(row);
endRemoveRows();
return true;
}
static std::list<std::pair<std::shared_ptr<linphone::MagicSearch>, std::shared_ptr<SearchHandler> > > searches;
class DeleteMagic{
public:
std::shared_ptr<linphone::MagicSearch> magic;
std::shared_ptr<SearchHandler> search;
};
void SearchSipAddressesModel::setFilter(const QString& filter){
mMagicSearch->getContactListFromFilterAsync(filter.toStdString(),"");
//searchReceived(mMagicSearch->getContactListFromFilter(filter.toStdString(),"")); // Just to show how to use sync method
}
void SearchSipAddressesModel::searchReceived(std::list<std::shared_ptr<linphone::SearchResult>> results){
@ -128,24 +112,10 @@ void SearchSipAddressesModel::searchReceived(std::list<std::shared_ptr<linphone:
mAddresses.clear();
for(auto it = results.begin() ; it != results.end() ; ++it){
if((*it)->getFriend()){
//QString username = QString::fromStdString((*it)->getFriend()->getName());
//auto f = (*it)->getFriend();
//auto vcard = f->getVcard();
//if(vcard)
// qDebug() << QString::fromStdString(vcard->asVcard4String());
mAddresses << QString::fromStdString((*it)->getFriend()->getAddress()->asString());
//qDebug() << username << " " << QString::fromStdString((*it)->getFriend()->getAddress()->getDisplayName());
}else{
//QString username = QString::fromStdString((*it)->getAddress()->getDisplayName());
mAddresses << QString::fromStdString((*it)->getAddress()->asString());
//qDebug() << username;
}
}
//invalidate();
endResetModel();
/*
mMagicSearch->removeListener(mSearch);
mMagicSearch = nullptr;
mSearch = nullptr;*/
}

View file

@ -33,31 +33,32 @@
class SearchSipAddressesModel : public QAbstractListModel {
Q_OBJECT;
Q_OBJECT;
public:
SearchSipAddressesModel (QObject *parent = Q_NULLPTR);
~SearchSipAddressesModel();
int rowCount (const QModelIndex &index = QModelIndex()) const override;
QHash<int, QByteArray> roleNames () const override;
QVariant data (const QModelIndex &index, int role = Qt::DisplayRole) const override;
Q_INVOKABLE void setFilter (const QString &pattern);
QStringList mAddresses;
std::shared_ptr<linphone::MagicSearch> mMagicSearch;
std::shared_ptr<SearchHandler> mSearch;
SearchSipAddressesModel (QObject *parent = Q_NULLPTR);
~SearchSipAddressesModel();
int rowCount (const QModelIndex &index = QModelIndex()) const override;
QHash<int, QByteArray> roleNames () const override;
QVariant data (const QModelIndex &index, int role = Qt::DisplayRole) const override;
Q_INVOKABLE void setFilter (const QString &pattern);
QStringList mAddresses;
// And instance of Magic search
std::shared_ptr<linphone::MagicSearch> mMagicSearch;
// Callback when searching
std::shared_ptr<SearchHandler> mSearch;
public slots:
void searchReceived(std::list<std::shared_ptr<linphone::SearchResult>> results);
void searchReceived(std::list<std::shared_ptr<linphone::SearchResult>> results);
private:
bool removeRow (int row, const QModelIndex &parent = QModelIndex());
bool removeRows (int row, int count, const QModelIndex &parent = QModelIndex()) override;
bool removeRow (int row, const QModelIndex &parent = QModelIndex());
bool removeRows (int row, int count, const QModelIndex &parent = QModelIndex()) override;
};
Q_DECLARE_METATYPE(SearchSipAddressesModel *);

View file

@ -1,130 +0,0 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "components/contact/ContactModel.hpp"
#include "components/contact/VcardModel.hpp"
#include "components/core/CoreManager.hpp"
#include "SearchSipAddressesModel.hpp"
#include "SearchSipAddressesProxyModel.hpp"
// =============================================================================
namespace {
constexpr int WeightPos0 = 5;
constexpr int WeightPos1 = 4;
constexpr int WeightPos2 = 3;
constexpr int WeightPos3 = 2;
constexpr int WeightPosOther = 1;
}
const QRegExp SearchSipAddressesProxyModel::SearchSeparators("^[^_.-;@ ][_.-;@ ]");
// -----------------------------------------------------------------------------
SearchSipAddressesProxyModel::SearchSipAddressesProxyModel (QObject *parent) : QSortFilterProxyModel(parent) {
setSourceModel(CoreManager::getInstance()->getSipAddressesModel());
sort(0);
}
// -----------------------------------------------------------------------------
void SearchSipAddressesProxyModel::setFilter (const QString &pattern) {
mFilter = pattern;
invalidate();
}
// -----------------------------------------------------------------------------
bool SearchSipAddressesProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const {
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
return computeEntryWeight(index.data().toMap()) > 0;
}
bool SearchSipAddressesProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const {
const QVariantMap mapA = sourceModel()->data(left).toMap();
const QVariantMap mapB = sourceModel()->data(right).toMap();
const QString sipAddressA = mapA["sipAddress"].toString();
const QString sipAddressB = mapB["sipAddress"].toString();
// TODO: Use a cache, do not compute the same value as `filterAcceptsRow`.
int weightA = computeEntryWeight(mapA);
int weightB = computeEntryWeight(mapB);
// 1. Not the same weight.
if (weightA != weightB)
return weightA > weightB;
const ContactModel *contactA = mapA.value("contact").value<ContactModel *>();
const ContactModel *contactB = mapB.value("contact").value<ContactModel *>();
// 2. No contacts.
if (!contactA && !contactB)
return sipAddressA <= sipAddressB;
// 3. No contact for a or b.
if (!contactA || !contactB)
return !!contactA;
// 4. Same contact (address).
if (contactA == contactB)
return sipAddressA <= sipAddressB;
// 5. Not the same contact name.
int diff = contactA->mLinphoneFriend->getName().compare(contactB->mLinphoneFriend->getName());
if (diff)
return diff <= 0;
// 6. Same contact name, so compare sip addresses.
return sipAddressA <= sipAddressB;
}
int SearchSipAddressesProxyModel::computeEntryWeight (const QVariantMap &entry) const {
int weight = computeStringWeight(entry["sipAddress"].toString().mid(4));
const ContactModel *contact = entry.value("contact").value<ContactModel *>();
if (contact)
weight += computeStringWeight(contact->getVcardModel()->getUsername());
return weight;
}
int SearchSipAddressesProxyModel::computeStringWeight (const QString &string) const {
int index = -1;
int offset = -1;
while ((index = string.indexOf(mFilter, index + 1, Qt::CaseInsensitive)) != -1) {
int tmpOffset = index - string.lastIndexOf(SearchSeparators, index) - 1;
if ((tmpOffset != -1 && tmpOffset < offset) || offset == -1)
if ((offset = tmpOffset) == 0) break;
}
switch (offset) {
case -1: return 0;
case 0: return WeightPos0;
case 1: return WeightPos1;
case 2: return WeightPos2;
case 3: return WeightPos3;
default: break;
}
return WeightPosOther;
}

View file

@ -1,49 +0,0 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SEARCH_SIP_ADDRESSES_PROXY_MODEL_H_
#define SEARCH_SIP_ADDRESSES_PROXY_MODEL_H_
#include <QSortFilterProxyModel>
// =============================================================================
class SearchSipAddressesProxyModel : public QSortFilterProxyModel {
Q_OBJECT;
public:
SearchSipAddressesProxyModel (QObject *parent = Q_NULLPTR);
Q_INVOKABLE void setFilter (const QString &pattern);
protected:
bool filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const override;
bool lessThan (const QModelIndex &left, const QModelIndex &right) const override;
private:
int computeEntryWeight (const QVariantMap &entry) const;
int computeStringWeight (const QString &string) const;
QString mFilter;
static const QRegExp SearchSeparators;
};
#endif // SIP_ADDRESSES_PROXY_MODEL_H_

View file

@ -1,149 +0,0 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 <http://www.gnu.org/licenses/>.
*/
// =============================================================================
// `SettingsSipAccounts.qml` Logic.
// =============================================================================
.import Linphone 1.0 as Linphone
.import 'qrc:/ui/scripts/Utils/utils.js' as Utils
// =============================================================================
var proxyConfig
function initForm (ldap) {
/*
var AccountSettingsModel = Linphone.AccountSettingsModel
proxyConfig = account
? account.proxyConfig
: AccountSettingsModel.createProxyConfig()
var config = AccountSettingsModel.getProxyConfigDescription(proxyConfig)
sipAddress.text = config.sipAddress
serverAddress.text = config.serverAddress
registrationDuration.text = config.registrationDuration
var currentTransport = config.transport.toUpperCase()
transport.currentIndex = Number(
Utils.findIndex(transport.model, function (value) {
return value === currentTransport
})
)
route.text = config.route
contactParams.text = config.contactParams
avpfInterval.text = config.avpfInterval
registerEnabled.checked = config.registerEnabled
publishPresence.checked = config.publishPresence
avpfEnabled.checked = config.avpfEnabled
iceEnabled.checked = config.iceEnabled
turnEnabled.checked = config.turnEnabled
stunServer.text = config.stunServer
turnPassword.text = config.turnPassword
turnUser.text = config.turnUser
if (account) {
dialog._sipAddressOk = true
dialog._serverAddressOk = true
}
dialog._routeOk = true
*/
}
function formIsValid () {
//return dialog._sipAddressOk && dialog._serverAddressOk && dialog._routeOk
}
// -----------------------------------------------------------------------------
function validProxyConfig () {
/*
if (Linphone.AccountSettingsModel.addOrUpdateProxyConfig(proxyConfig, {
sipAddress: sipAddress.text,
serverAddress: serverAddress.text,
registrationDuration: registrationDuration.text,
transport: transport.currentText,
route: route.text,
contactParams: contactParams.text,
avpfInterval: avpfInterval.text,
registerEnabled: registerEnabled.checked,
publishPresence: publishPresence.checked,
avpfEnabled: avpfEnabled.checked,
iceEnabled: iceEnabled.checked,
turnEnabled: turnEnabled.checked,
stunServer: stunServer.text,
turnUser: turnUser.text,
turnPassword: turnPassword.text
})) {
dialog.exit(1)
} else {
// TODO: Display errors on the form (if necessary).
}
*/
}
// -----------------------------------------------------------------------------
function handleRouteChanged (route) {
// dialog._routeOk = route.length === 0 || Linphone.SipAddressesModel.addressIsValid(route)
}
function handleServerAddressChanged (address) {
/*
if (address.length === 0) {
dialog._serverAddressOk = false
return
}
var newTransport = Linphone.SipAddressesModel.getTransportFromSipAddress(address)
if (newTransport.length > 0) {
transport.currentIndex = Utils.findIndex(transport.model, function (value) {
return value === newTransport
})
dialog._serverAddressOk = true
} else {
dialog._serverAddressOk = false
}*/
}
function handleSipAddressChanged (address) {
/*
dialog._sipAddressOk = address.length > 0 &&
Linphone.SipAddressesModel.sipAddressIsValid(address)*/
}
function handleTransportChanged (transport) {
/*
var newServerAddress = Linphone.SipAddressesModel.addTransportToSipAddress(serverAddress.text, transport)
if (newServerAddress.length > 0) {
serverAddress.text = newServerAddress
dialog._serverAddressOk = true
} else {
dialog._serverAddressOk = false
}*/
}
// -----------------------------------------------------------------------------

View file

@ -5,8 +5,6 @@ import Linphone 1.0
import App.Styles 1.0
import 'SettingsLdapEdit.js' as Logic
// =============================================================================
DialogPlus {
@ -39,10 +37,6 @@ DialogPlus {
// ---------------------------------------------------------------------------
//Component.onCompleted: Logic.initForm(ldapData)
// ---------------------------------------------------------------------------
TabContainer {
anchors.fill: parent

View file

@ -100,9 +100,9 @@ TabContainer {
}
onVisibleChanged: sendLogsBlock.setText('')
// -------------------------------------------------------------------------
// LDAP
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// LDAP
// -------------------------------------------------------------------------
Form {
title: 'LDAP'
width: parent.width
@ -113,11 +113,10 @@ TabContainer {
width: parent.width
}
}
// -------------------------------------------------------------------------
// ADDRESS BOOK
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// ADDRESS BOOK
// -------------------------------------------------------------------------
Form {
title: qsTr('contactsTitle')
@ -150,7 +149,7 @@ TabContainer {
id:importerLine
property var fields : modelData.fields
property int identity : modelData.identity
property var pluginDescription : ContactsImporterPluginsManager.getContactsImporterPluginDescription(fields["pluginID"]) // Plugin definition
property var pluginDescription : ContactsImporterPluginsManager.getContactsImporterPluginDescription(fields["pluginID"]) // Plugin definition
FormTableEntry {
Row{
@ -273,14 +272,14 @@ TabContainer {
iconSize:CallsStyle.entry.iconActionSize
onClicked:{
ContactsImporterPluginsManager.openNewPlugin();
pluginChoice.model = ContactsImporterPluginsManager.getPlugins();
pluginChoice.model = ContactsImporterPluginsManager.getPlugins();
}
}
ComboBox{
id: pluginChoice
model:ContactsImporterPluginsManager.getPlugins()
model:ContactsImporterPluginsManager.getPlugins()
textRole: "pluginTitle"
displayText: currentIndex === -1 ? 'No Plugins to load' : currentText
displayText: currentIndex === -1 ? 'No Plugins to load' : currentText
Text{// Hack, combobox show empty text when empty
anchors.fill:parent
visible:pluginChoice.currentIndex===-1
@ -295,7 +294,7 @@ TabContainer {
}
Connections{
target:SettingsModel
onContactImporterChanged:pluginChoice.model=ContactsImporterPluginsManager.getPlugins()
onContactImporterChanged:pluginChoice.model=ContactsImporterPluginsManager.getPlugins()
}
}
ActionButton {
@ -304,7 +303,7 @@ TabContainer {
visible:pluginChoice.currentIndex>=0
onClicked:{
if( pluginChoice.currentIndex >= 0)
ContactsImporterListModel.createContactsImporter({"pluginID":pluginChoice.model[pluginChoice.currentIndex]["pluginID"]})
ContactsImporterListModel.createContactsImporter({"pluginID":pluginChoice.model[pluginChoice.currentIndex]["pluginID"]})
}
}
}
@ -316,7 +315,7 @@ TabContainer {
Form {
title: qsTr('developerSettingsTitle')
// visible: SettingsModel.developerSettingsEnabled
visible: SettingsModel.developerSettingsEnabled
width: parent.width
FormLine {

View file

@ -27,6 +27,7 @@ Column {
id: ldapList
model:LdapProxyModel{id:ldapProxy}
delegate:Item{
// LDAP line description : Summary (remove + name + activation)
id: swipeView
anchors.left: parent.left
anchors.right: parent.right
@ -69,7 +70,6 @@ Column {
modelData.enabled = !checked
}
}
}
}
}

View file

@ -1,421 +0,0 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
//import QtQuick.Controls 2.15 // SwipeView : Qt 5.7
import QtQuick.Controls 1.4 // TabView
import Common 1.0
import Linphone 1.0
import App.Styles 1.0
import Linphone.Styles 1.0
import Common.Styles 1.0
// =============================================================================
//Qt *View override childs geometry. Do not use them
Item{
id: swipeView
anchors.left: parent.left
anchors.right: parent.right
property LdapModel ldapData
property int currentIndex: ldapData.isValid?0:1
clip:true
Component.onCompleted:updateHeight()
onCurrentIndexChanged:updateHeight()
function updateHeight(){
if( currentIndex==0)
swipeView.height=summaryRowItem.height
else if( currentIndex==1)
swipeView.height=mainColumn.height
}
Item{
id: summaryRow
anchors.fill:parent
visible:currentIndex == 0
Row{
id:summaryRowItem
anchors.horizontalCenter: parent.horizontalCenter
spacing:10
ActionButton {
id:removeldap
anchors.verticalCenter: parent.verticalCenter
icon: 'cancel'
iconSize:CallsStyle.entry.iconActionSize
scale:0.8
}
Text {
id: summaryTitle
color: FormStyle.header.title.color
text: serverUrl.text?serverUrl.text:'New server'
font {
bold: true
pointSize: FormStyle.header.title.pointSize
}
anchors.verticalCenter: parent.verticalCenter
MouseArea{
anchors.fill:parent
onClicked:swipeView.currentIndex = 1
}
}
Switch {
id: ldapActivation
anchors.verticalCenter: parent.verticalCenter
checked: false
onClicked: {
checked = !checked
}
}
}
}
Item {
id: page2
anchors.fill:parent
visible:currentIndex == 1
Column {
id: mainColumn
property bool dealWithErrors: false
property int orientation: Qt.Horizontal
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 0
anchors.rightMargin: 0
height:centerRow.height+titleRow.height+spacing*2
// ---------------------------------------------------------------------------
spacing: FormStyle.spacing
// ---------------------------------------------------------------------------
Column{
id:titleRow
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 0
anchors.rightMargin: 0
spacing: FormStyle.header.spacing
Text {
id: title
text:"LDAP Server settings :"+serverUrl.text
color: FormStyle.header.title.color
font {
bold: true
pointSize: FormStyle.header.title.pointSize
}
}
Rectangle {
anchors.left:parent.left
anchors.right:parent.right
color: FormStyle.header.separator.color
}
}
Item{
id: centerRow
anchors.left:parent.left
anchors.right:parent.right
transformOrigin: Item.Center
layer.wrapMode: ShaderEffectSource.ClampToEdge
height:detailsRow.height
ActionButton {
id:back
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 0
icon: 'edit'
iconSize:CallsStyle.entry.iconActionSize
onClicked:swipeView.currentIndex = 0
}
RowLayout{// Details row have its size from children
id:detailsRow
anchors.left: back.right
anchors.right: deleteLdap.left
anchors.rightMargin: 10
anchors.leftMargin: 10
ColumnLayout{
Layout.fillHeight: true
Layout.fillWidth:true
TextField {
id:serverUrl
Layout.fillWidth: true
placeholderText :"Server"
TooltipArea{
text : 'LDAP Server. eg: ldap:/// for a localhost server or ldap://ldap.example.org/'
}
}
TextField {
Layout.fillWidth: true
placeholderText :"Bind DN"
TooltipArea{
text : 'The bindDN DN is the credential that is used to authenticate against an LDAP.\n eg: cn=ausername,ou=people,dc=bc,dc=com'
}
}
PasswordField {
Layout.fillWidth: true
placeholderText :"Password"
}
Switch {
id: useTlsLdap
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 0
checked: false
onClicked: {
checked = !checked
}
}
}
ColumnLayout{
Layout.fillHeight: true
Layout.fillWidth:true
TextField {
Layout.fillWidth: true
placeholderText :"Base Object"
TooltipArea{
text : ''
}
}
TextField {
text: "Filter"
Layout.fillWidth: true
placeholderText :"Filter"
TooltipArea{
text : 'The search is base on this filter to search friends. Default value : (sn=%s)'
}
}
NumericField {
text: "MaxResults"
Layout.fillWidth: true
TooltipArea{
text : 'The max results when requesting searches'
}
}
}
ColumnLayout{
Layout.fillHeight: true
Layout.fillWidth:true
TextField {
Layout.fillWidth: true
placeholderText :"Names Attributes"
TooltipArea{
text : 'Check these attributes To build Name Friend, separated by a comma and the first is the highest priority. The default value is: sn'
}
}
TextField {
Layout.fillWidth: true
placeholderText :"Sip Attributes"
TooltipArea{
text : 'Check these attributes To build the SIP username in address of Friend, separated by a comma and the first is the highest priority. The default value is: mobile,telephoneNumber,homePhone,sn'
}
}
TextField {
Layout.fillWidth: true
placeholderText :"Scheme"
TooltipArea{
text : 'Add the scheme to the sip address(scheme:username@domain). The default value is sip'
}
}
TextField {
Layout.fillWidth: true
placeholderText :"Domain"
TooltipArea{
text : 'Add the domain to the sip address(scheme:username@domain). The default value is the ldap server url'
}
}
}
}
Switch {
id: deleteLdap
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 0
checked: false
onClicked: {
checked = !checked
}
}
}
}
}
/*
Column{
id:summaryRow
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 0
anchors.rightMargin: 0
spacing: FormStyle.header.spacing
//visible: parent.title.length > 0
height:summaryTitle.height
Text {
id: summaryTitle
color: FormStyle.header.title.color
text: "Summary of "// +serverUrl.text
font {
bold: true
pointSize: FormStyle.header.title.pointSize
}
}
MouseArea{
onClicked: swipeView.currentIndex = 2
anchors.fill:parent
Rectangle{
anchors.fill:parent
color:"red"
}
}
}
Column {
id: mainColumn
property alias title: title.text
property bool dealWithErrors: false
property int orientation: Qt.Horizontal
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 0
anchors.rightMargin: 0
height:centerRow.height+titleRow.height+spacing*2
// ---------------------------------------------------------------------------
spacing: FormStyle.spacing
// ---------------------------------------------------------------------------
Column{
id:titleRow
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 0
anchors.rightMargin: 0
spacing: FormStyle.header.spacing
//visible: parent.title.length > 0
Text {
id: title
text:"LDAP Server :"+serverUrl.text
color: FormStyle.header.title.color
font {
bold: true
pointSize: FormStyle.header.title.pointSize
}
}
Rectangle {
anchors.left:parent.left
anchors.right:parent.right
color: FormStyle.header.separator.color
}
}
Item{
id: centerRow
anchors.left:parent.left
anchors.right:parent.right
transformOrigin: Item.Center
layer.wrapMode: ShaderEffectSource.ClampToEdge
height:detailsRow.height
ActionButton {
id:removeldap
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 0
icon: 'cancel'
iconSize:CallsStyle.entry.iconActionSize-2
}
RowLayout{// Details row have its size from children
id:detailsRow
anchors.left: removeldap.right
anchors.right: deleteLdap.left
anchors.rightMargin: 10
anchors.leftMargin: 10
ColumnLayout{
Layout.fillHeight: true
Layout.fillWidth:true
TextField {
id:serverUrl
text: "Server"
Layout.fillWidth: true
placeholderText :"Server"
}
TextField {
text: "Bind DN"
Layout.fillWidth: true
}
TextField {
text: "Password"
Layout.fillWidth: true
}
}
ColumnLayout{
Layout.fillHeight: true
Layout.fillWidth:true
TextField {
text: "Base Object"
Layout.fillWidth: true
}
TextField {
text: "Filter"
Layout.fillWidth: true
}
TextField {
text: "MaxResults"
Layout.fillWidth: true
}
}
ColumnLayout{
Layout.fillHeight: true
Layout.fillWidth:true
TextField {
text: "Names Attributes"
Layout.fillWidth: true
}
TextField {
text: "Sip Attributes"
Layout.fillWidth: true
}
TextField {
text: "Scheme"
Layout.fillWidth: true
}
TextField {
text: "Domain"
Layout.fillWidth: true
}
}
}
Switch {
id: deleteLdap
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 0
checked: false
onClicked: {
checked = !checked
}
}
}
}
states: [
State {
name: "Summary"
when: swipeView.index==1
},
State {
name: "Details"
when: swipeView.index==2
}
]
*/
}
/*##^##
Designer {
D{i:0;autoSize:true;formeditorZoom:0.75;height:480;width:640}
}
##^##*/

@ -1 +1 @@
Subproject commit b95fee70fc3456b024a8dc9c00d6adb694c58092
Subproject commit e4c051fa9b3c9efb631047edd8c0a961e1487c82