diff --git a/linphone-app/assets/images/chat_room_custom.svg b/linphone-app/assets/images/chat_room_custom.svg index 408e19619..a9d140c6d 100644 --- a/linphone-app/assets/images/chat_room_custom.svg +++ b/linphone-app/assets/images/chat_room_custom.svg @@ -3,16 +3,19 @@ width="80" height="80" viewBox="0 0 80 80" + fill="none" version="1.1" - id="svg21" + id="svg8" sodipodi:docname="chat_room_custom.svg" - inkscape:version="1.1 (c68e22c387, 2021-05-23)" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + - - - - - + inkscape:current-layer="svg8" /> - - - - - - - - - - + id="g832" + transform="matrix(0.67345025,0,0,0.67345025,9.9999997,20)"> + + + diff --git a/linphone-app/assets/images/conference_merge_custom.svg b/linphone-app/assets/images/conference_merge_custom.svg new file mode 100644 index 000000000..8f195ab4b --- /dev/null +++ b/linphone-app/assets/images/conference_merge_custom.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/linphone-app/resources.qrc b/linphone-app/resources.qrc index d3d6ddeee..8cc39f5fa 100644 --- a/linphone-app/resources.qrc +++ b/linphone-app/resources.qrc @@ -62,6 +62,7 @@ assets/images/conference_audio_only_custom.svg assets/images/conference_layout_grid_custom.svg assets/images/conference_layout_active_speaker_custom.svg + assets/images/conference_merge_custom.svg assets/images/contact_add_custom.svg assets/images/contact_card_photo_custom.svg assets/images/contact_custom.svg diff --git a/linphone-app/src/components/call/CallModel.cpp b/linphone-app/src/components/call/CallModel.cpp index 093d3158d..63a8ed4a3 100644 --- a/linphone-app/src/components/call/CallModel.cpp +++ b/linphone-app/src/components/call/CallModel.cpp @@ -109,8 +109,10 @@ CallModel::CallModel (shared_ptr call){ if(mCall) { mRemoteAddress = mCall->getRemoteAddress()->clone(); - if(mCall->getConference()) + if(mCall->getConference()) { mConferenceModel = ConferenceModel::create(mCall->getConference()); + connect(mConferenceModel.get(), &ConferenceModel::participantAdminStatusChanged, this, &CallModel::onParticipantAdminStatusChanged); + } auto conferenceInfo = CoreManager::getInstance()->getCore()->findConferenceInformationFromUri(getConferenceAddress()); if( conferenceInfo ){ mConferenceInfoModel = ConferenceInfoModel::create(conferenceInfo); @@ -232,7 +234,8 @@ ConferenceInfoModel * CallModel::getConferenceInfoModel(){ QSharedPointer CallModel::getConferenceSharedModel(){ if(mCall->getConference() && !mConferenceModel){ - mConferenceModel = ConferenceModel::create(mCall->getConference()); + mConferenceModel = ConferenceModel::create(mCall->getConference()); + connect(mConferenceModel.get(), &ConferenceModel::participantAdminStatusChanged, this, &CallModel::onParticipantAdminStatusChanged); emit conferenceModelChanged(); } return mConferenceModel; @@ -977,6 +980,12 @@ void CallModel::onChatRoomInitialized(int state){ emit chatRoomModelChanged(); } +void CallModel::onParticipantAdminStatusChanged(const std::shared_ptr & participant){ + if(mConferenceModel && participant == mConferenceModel->getConference()->getMe()) { + emit meAdminChanged(); + } +} + void CallModel::setRemoteDisplayName(const std::string& name){ mRemoteAddress->setDisplayName(name); if(mCall) { diff --git a/linphone-app/src/components/call/CallModel.hpp b/linphone-app/src/components/call/CallModel.hpp index 7138a000b..5c8976f97 100644 --- a/linphone-app/src/components/call/CallModel.hpp +++ b/linphone-app/src/components/call/CallModel.hpp @@ -200,8 +200,10 @@ public slots: void endCall(); void onRemoteRecording(const std::shared_ptr & call, bool recording); void onChatRoomInitialized(int state); + void onParticipantAdminStatusChanged(const std::shared_ptr & participant); signals: + void meAdminChanged(); void callErrorChanged (const QString &callError); void callIdChanged(); void isInConferenceChanged (bool status); diff --git a/linphone-app/src/components/calls/CallsListModel.cpp b/linphone-app/src/components/calls/CallsListModel.cpp index 72c53acc5..924960e97 100644 --- a/linphone-app/src/components/calls/CallsListModel.cpp +++ b/linphone-app/src/components/calls/CallsListModel.cpp @@ -69,6 +69,7 @@ CallsListModel::CallsListModel (QObject *parent) : ProxyListModel(parent) { mCoreHandlers.get(), &CoreHandlers::callStateChanged, this, &CallsListModel::handleCallStateChanged ); + connect(this, &CallsListModel::countChanged, this, &CallsListModel::canMergeCallsChanged); } CallModel *CallsListModel::findCallModelFromPeerAddress (const QString &peerAddress) const { @@ -372,15 +373,100 @@ void CallsListModel::prepareConferenceCall(ConferenceInfoModel * model){ app->smartShowWindow(app->getCallsWindow()); emit callConferenceAsked(model); } + int CallsListModel::addAllToConference(){ return CoreManager::getInstance()->getCore()->addAllToConference(); } + +void CallsListModel::mergeAll(){ + auto core = CoreManager::getInstance()->getCore(); + auto currentCalls = CoreManager::getInstance()->getCore()->getCalls(); + shared_ptr conference = core->getConference(); + + // Search a managable conference from calls + if(!conference){ + for(auto call : currentCalls){ + auto dbConference = call->getConference(); + if(dbConference && dbConference->getMe()->isAdmin()){ + conference = dbConference; + break; + } + } + } + + auto currentCall = CoreManager::getInstance()->getCore()->getCurrentCall(); + bool enablingVideo = false; + if( currentCall ) + enablingVideo = currentCall->getCurrentParams()->videoEnabled(); + if(!conference){ + auto parameters = core->createConferenceParams(conference); + + if(!CoreManager::getInstance()->getSettingsModel()->getVideoConferenceEnabled()) { + parameters->enableVideo(false); + parameters->setConferenceFactoryAddress(nullptr);// Do a local conference + parameters->setSubject("Local meeting"); + }else{ + parameters->enableVideo(enablingVideo); + parameters->setSubject("Meeting"); + } + conference = core->createConferenceWithParams(parameters); + } + + list> allLinphoneAddresses; + list> newCalls; + list> runningCallsToAdd; + + for(auto call : currentCalls){ + if(!call->getConference()){ + runningCallsToAdd.push_back(call); + } + } + +// 1) Add running calls + if( runningCallsToAdd.size() > 0){ + conference->addParticipants(runningCallsToAdd); + } + /* +// 2) Put in pause and remove all calls that are not in the conference list + for(const auto &call : CoreManager::getInstance()->getCore()->getCalls()){ + const std::string callAddress = call->getRemoteAddress()->asStringUriOnly(); + auto address = allLinphoneAddresses.begin(); + while(address != allLinphoneAddresses.end() && (*address)->asStringUriOnly() != callAddress) + ++address; + if(address == allLinphoneAddresses.end()){// Not in conference list : put in pause and remove it from conference if it's the case + if( call->getParams()->getLocalConferenceMode() ){// Remove conference if it is not yet requested + CoreManager::getInstance()->getCore()->removeFromConference(call); + }else + call->pause(); + } + }*/ +} // ----------------------------------------------------------------------------- int CallsListModel::getRunningCallsNumber () const { return CoreManager::getInstance()->getCore()->getCallsNb(); } +bool CallsListModel::canMergeCalls()const{ + auto calls = CoreManager::getInstance()->getCore()->getCalls(); + + bool mergableConference = false; + int mergableCalls = 0; + bool mergable = false; + for(auto itCall = calls.begin(); !mergable && itCall != calls.end() ; ++itCall ) { + auto conference = (*itCall)->getConference(); + if(conference){ + if( !mergableConference ) + mergableConference = (conference && conference->getMe()->isAdmin()); + }else{ + ++mergableCalls; + } + mergable = (mergableConference && mergableCalls>0) // A call can be merged into the conference + || mergableCalls>1;// 2 calls can be merged + } + return mergable; +} + void CallsListModel::terminateAllCalls () const { CoreManager::getInstance()->getCore()->terminateAllCalls(); } @@ -479,6 +565,8 @@ void CallsListModel::addCall (const shared_ptr &call) { qInfo() << QStringLiteral("Add call:") << callModel->getFullLocalAddress() << callModel->getFullPeerAddress(); App::getInstance()->getEngine()->setObjectOwnership(callModel.get(), QQmlEngine::CppOwnership); + connect(callModel.get(), &CallModel::meAdminChanged, this, &CallsListModel::canMergeCallsChanged); + add(callModel); emit layoutChanged(); @@ -511,7 +599,7 @@ void CallsListModel::addDummyCall () { int id = findCallIndex(mList, *callModel); emit dataChanged(index(id, 0), index(id, 0)); }); - + connect(callModel.get(), &CallModel::meAdminChanged, this, &CallsListModel::canMergeCallsChanged); add(callModel); emit layoutChanged(); diff --git a/linphone-app/src/components/calls/CallsListModel.hpp b/linphone-app/src/components/calls/CallsListModel.hpp index c7ed56ce1..1c774c039 100644 --- a/linphone-app/src/components/calls/CallsListModel.hpp +++ b/linphone-app/src/components/calls/CallsListModel.hpp @@ -37,6 +37,9 @@ class CallsListModel : public ProxyListModel { Q_OBJECT public: + Q_PROPERTY(bool canMergeCalls READ canMergeCalls NOTIFY canMergeCallsChanged) + + CallsListModel (QObject *parent = Q_NULLPTR); CallModel *findCallModelFromPeerAddress (const QString &peerAddress) const; @@ -57,9 +60,11 @@ public: Q_INVOKABLE void prepareConferenceCall(ConferenceInfoModel * model); Q_INVOKABLE int addAllToConference(); + Q_INVOKABLE void mergeAll(); Q_INVOKABLE int getRunningCallsNumber () const; + bool canMergeCalls()const; Q_INVOKABLE void terminateAllCalls () const; Q_INVOKABLE void terminateCall (const QString& sipAddress) const; @@ -73,6 +78,7 @@ signals: void callConferenceAsked(ConferenceInfoModel * conferenceInfoModel); void callMissed (CallModel *callModel); + void canMergeCallsChanged(); private: diff --git a/linphone-app/src/components/participant/ParticipantDeviceListModel.cpp b/linphone-app/src/components/participant/ParticipantDeviceListModel.cpp index ebb5234b1..ea2e9b6c8 100644 --- a/linphone-app/src/components/participant/ParticipantDeviceListModel.cpp +++ b/linphone-app/src/components/participant/ParticipantDeviceListModel.cpp @@ -99,6 +99,8 @@ void ParticipantDeviceListModel::updateDevices(const std::list deviceToAdd){ + auto deviceToAddAddr = deviceToAdd->getAddress(); + int row = 0; qDebug() << "Adding device " << deviceToAdd->getAddress()->asString().c_str(); for(auto item : mList) { auto deviceModel = item.objectCast(); @@ -106,7 +108,12 @@ bool ParticipantDeviceListModel::add(std::shared_ptrupdateVideoEnabled(); return false; + }else if(deviceToAddAddr->equal(deviceModel->getDevice()->getAddress())){// Address is the same (same device) but the model is using another linphone object. Replace it. + deviceModel->updateVideoEnabled(); + removeRow(row); + break; } + ++row; } bool addMe = isMe(deviceToAdd); auto deviceModel = ParticipantDeviceModel::create(mCallModel, deviceToAdd, addMe); @@ -114,6 +121,12 @@ bool ParticipantDeviceListModel::add(std::shared_ptr(deviceModel); qDebug() << "Device added. Count=" << mList.count(); + QStringList debugDevices; + for(auto i : mList){ + auto item = i.objectCast(); + debugDevices.push_back( item->getAddress()); + } + qDebug() << debugDevices.join("\n"); if( addMe){ qDebug() << "Added a me device"; emit meChanged(); diff --git a/linphone-app/ui/modules/Linphone/Calls/CallControls.qml b/linphone-app/ui/modules/Linphone/Calls/CallControls.qml index 1797322f5..9687bdf4e 100644 --- a/linphone-app/ui/modules/Linphone/Calls/CallControls.qml +++ b/linphone-app/ui/modules/Linphone/Calls/CallControls.qml @@ -13,6 +13,7 @@ Rectangle { // --------------------------------------------------------------------------- default property alias _content: content.data + property alias isDarkMode: contact.isDarkMode property alias signIcon: signIcon.icon property alias subtitleColor: contact.subtitleColor diff --git a/linphone-app/ui/modules/Linphone/Calls/Calls.qml b/linphone-app/ui/modules/Linphone/Calls/Calls.qml index 4ce7f47f5..5004e78d5 100644 --- a/linphone-app/ui/modules/Linphone/Calls/Calls.qml +++ b/linphone-app/ui/modules/Linphone/Calls/Calls.qml @@ -144,20 +144,17 @@ ListView { id: _callControls // ------------------------------------------------------------------------- - - function useColorStatus () { - return calls.currentIndex === index && $modelData && $modelData.status !== CallModel.CallStatusEnded - } + isDarkMode: calls.currentIndex === index && $modelData!=undefined && $modelData.status!=undefined && $modelData.status !== CallModel.CallStatusEnded // ------------------------------------------------------------------------- - color: useColorStatus() + color: isDarkMode ? CallsStyle.entry.color.selected : CallsStyle.entry.color.normal - subtitleColor: useColorStatus() + subtitleColor: isDarkMode ? CallsStyle.entry.subtitleColor.selected : CallsStyle.entry.subtitleColor.normal - titleColor: useColorStatus() + titleColor: isDarkMode ? CallsStyle.entry.titleColor.selected : CallsStyle.entry.titleColor.normal diff --git a/linphone-app/ui/modules/Linphone/Contact/Avatar.qml b/linphone-app/ui/modules/Linphone/Contact/Avatar.qml index efe711055..a1a06c7a8 100644 --- a/linphone-app/ui/modules/Linphone/Contact/Avatar.qml +++ b/linphone-app/ui/modules/Linphone/Contact/Avatar.qml @@ -15,7 +15,8 @@ Item { // --------------------------------------------------------------------------- property alias presenceLevel: presenceLevelIcon.level - property color backgroundColor: AvatarStyle.backgroundColor + property bool isDarkMode: false + property color backgroundColor: isDarkMode ? AvatarStyle.backgroundDarkModeColor : AvatarStyle.backgroundColor property color foregroundColor: 'transparent' property string username property var image @@ -64,7 +65,7 @@ Item { Text { id: initialsText anchors.centerIn: parent - color: AvatarStyle.initials.color + color: isDarkMode ? AvatarStyle.initials.darkModeColor : AvatarStyle.initials.color font.pointSize: { var width @@ -82,7 +83,7 @@ Item { Icon { anchors.fill: parent icon: ContactStyle.groupChat.icon - overwriteColor: ContactStyle.groupChat.avatarColor + overwriteColor: isDarkMode ? ContactStyle.groupChat.avatarDarkModeColor : ContactStyle.groupChat.avatarColor iconSize: avatar.width //visible: entry!=undefined && entry.isOneToOne!=undefined && !entry.isOneToOne visible: !avatar.isOneToOne diff --git a/linphone-app/ui/modules/Linphone/Contact/Contact.qml b/linphone-app/ui/modules/Linphone/Contact/Contact.qml index e71f3a53a..630ee0ab0 100644 --- a/linphone-app/ui/modules/Linphone/Contact/Contact.qml +++ b/linphone-app/ui/modules/Linphone/Contact/Contact.qml @@ -21,6 +21,7 @@ Rectangle { property alias subtitleColor: description.subtitleColor property alias titleColor: description.titleColor property alias statusText : description.statusText + property alias isDarkMode: avatar.isDarkMode property bool displayUnreadMessageCount: false property bool showSubtitle : true diff --git a/linphone-app/ui/modules/Linphone/Styles/Calls/CallsStyle.qml b/linphone-app/ui/modules/Linphone/Styles/Calls/CallsStyle.qml index 3722411a7..7056af056 100644 --- a/linphone-app/ui/modules/Linphone/Styles/Calls/CallsStyle.qml +++ b/linphone-app/ui/modules/Linphone/Styles/Calls/CallsStyle.qml @@ -19,7 +19,7 @@ QtObject { } property QtObject burgerMenu: QtObject { property string name : 'burgerMenu' - property string icon : 'burger_menu_custom' + property string icon : 'menu_vdots_custom' property int iconSize: 35 property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'me_n_b_bg').color property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'me_h_b_bg').color @@ -30,7 +30,7 @@ QtObject { } property QtObject selectedBurgerMenu: QtObject { property string name : 'selectedBurgerMenu' - property string icon : 'burger_menu_custom' + property string icon : 'menu_vdots_custom' property int iconSize: 35 property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'me_n_b_inv_bg').color property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'me_h_b_inv_bg').color @@ -39,6 +39,7 @@ QtObject { property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'me_h_b_inv_fg').color property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'me_p_b_inv_fg').color } + property QtObject hangup: QtObject { property int iconSize: 35 property string icon : 'hangup_custom' diff --git a/linphone-app/ui/modules/Linphone/Styles/Contact/AvatarStyle.qml b/linphone-app/ui/modules/Linphone/Styles/Contact/AvatarStyle.qml index a7344b858..8f8f9c1d6 100644 --- a/linphone-app/ui/modules/Linphone/Styles/Contact/AvatarStyle.qml +++ b/linphone-app/ui/modules/Linphone/Styles/Contact/AvatarStyle.qml @@ -9,11 +9,14 @@ import ColorsList 1.0 QtObject { property string sectionName: 'Avatar' property color backgroundColor: ColorsList.add(sectionName+'_bg', 'd').color + property color backgroundDarkModeColor: ColorsList.add(sectionName+'_dark_bg', 'q').color + property string personImage : 'contact_custom' property QtObject initials: QtObject { property color color: ColorsList.add(sectionName+'_initials', 'q').color + property color darkModeColor: ColorsList.add(sectionName+'_dark_initials', 'd').color property int pointSize: Units.dp * 10 property int ratio: 30 } diff --git a/linphone-app/ui/modules/Linphone/Styles/Contact/ContactStyle.qml b/linphone-app/ui/modules/Linphone/Styles/Contact/ContactStyle.qml index 0e25378c6..29b751b5d 100644 --- a/linphone-app/ui/modules/Linphone/Styles/Contact/ContactStyle.qml +++ b/linphone-app/ui/modules/Linphone/Styles/Contact/ContactStyle.qml @@ -16,5 +16,6 @@ QtObject { property string icon: 'chat_room_custom' property color color: ColorsList.addImageColor(sectionName+'_groupChat', icon, 'g').color property color avatarColor: ColorsList.addImageColor(sectionName+'_groupChat_onAvatar', icon, 'q').color + property color avatarDarkModeColor: ColorsList.addImageColor(sectionName+'_groupChat_dark_onAvatar', icon, 'd').color } } diff --git a/linphone-app/ui/views/App/Calls/CallsWindow.qml b/linphone-app/ui/views/App/Calls/CallsWindow.qml index 38a188c9f..5a2b81b69 100644 --- a/linphone-app/ui/views/App/Calls/CallsWindow.qml +++ b/linphone-app/ui/views/App/Calls/CallsWindow.qml @@ -140,11 +140,12 @@ Window { ActionButton { isCustom: true backgroundRadius: 4 - colorSet: CallsWindowStyle.callsList.newConference + colorSet: CallsWindowStyle.callsList.mergeConference visible: SettingsModel.conferenceEnabled + enabled: CallsListModel.canMergeCalls onClicked: { - Logic.openConferenceManager() + CallsListModel.mergeAll() } } } diff --git a/linphone-app/ui/views/App/Main/Conversation.qml b/linphone-app/ui/views/App/Main/Conversation.qml index b2cf8b343..40fa59185 100644 --- a/linphone-app/ui/views/App/Main/Conversation.qml +++ b/linphone-app/ui/views/App/Main/Conversation.qml @@ -103,20 +103,9 @@ ColumnLayout { //username: Logic.getUsername() username: chatRoomModel?chatRoomModel.username:( conversation._sipAddressObserver ? UtilsCpp.getDisplayName(conversation._sipAddressObserver.peerAddress) : '') - visible: !groupChat.visible + isOneToOne: chatRoomModel==undefined || chatRoomModel.isOneToOne==undefined || chatRoomModel.isOneToOne } - Icon { - id: groupChat - - Layout.preferredHeight: ConversationStyle.bar.groupChatSize - Layout.preferredWidth: ConversationStyle.bar.groupChatSize - - icon: ConversationStyle.bar.groupChatIcon - overwriteColor: ConversationStyle.bar.groupChatColor - iconSize: ConversationStyle.bar.groupChatSize - visible: chatRoomModel && !chatRoomModel.isOneToOne - } Item{ Layout.fillHeight: true Layout.fillWidth: true @@ -127,7 +116,7 @@ ColumnLayout { ColumnLayout{ property int maximumContentWidth: contactBar.width - -(avatar.visible?avatar.width:0)-(groupChat.visible?groupChat.width:0) + -(avatar.visible?avatar.width:0) -actionBar.width - (secureIcon.visible?secureIcon.width :0) -3*ConversationStyle.bar.spacing Layout.fillHeight: true diff --git a/linphone-app/ui/views/App/Styles/Calls/CallsWindowStyle.qml b/linphone-app/ui/views/App/Styles/Calls/CallsWindowStyle.qml index f0bd8069e..d02d535fa 100644 --- a/linphone-app/ui/views/App/Styles/Calls/CallsWindowStyle.qml +++ b/linphone-app/ui/views/App/Styles/Calls/CallsWindowStyle.qml @@ -37,16 +37,18 @@ QtObject { property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'ma_h_b_fg').color property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'ma_p_b_fg').color } - property QtObject newConference: QtObject { + property QtObject mergeConference: QtObject { property int iconSize: 40 - property string name : 'newConference' - property string icon : 'conference_custom' + property string name : 'mergeConference' + property string icon : 'conference_merge_custom' property color backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'ma_n_b_bg').color property color backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'ma_h_b_bg').color property color backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'ma_p_b_bg').color + property color backgroundDisabledColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_d', icon, 'ma_d_b_bg').color property color foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'ma_n_b_fg').color property color foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'ma_h_b_fg').color property color foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'ma_p_b_fg').color + property color foregroundDisabledColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_d', icon, 'ma_d_b_fg').color } property QtObject closeButton: QtObject{ diff --git a/linphone-sdk b/linphone-sdk index 11f72d4e2..e5c95eaed 160000 --- a/linphone-sdk +++ b/linphone-sdk @@ -1 +1 @@ -Subproject commit 11f72d4e254a2a8305a9400e38cb44f125c18873 +Subproject commit e5c95eaed5a2113cbba1ab62e8baa826abe7609c