From d2ba1ab8a36333523bf794c23226bd044e1717c2 Mon Sep 17 00:00:00 2001
From: Tobias Markmann <tm@ayena.de>
Date: Thu, 21 Apr 2016 11:02:07 +0200
Subject: Use FeatureOracle to detect file-transfer support in roster

The FeatureOracle provides tri-state feature lookup
functionality for bare JIDs. It returns Yes if a feature is
supported by all resources of the bare JID, Maybe if some
support it, and No if none of the resources support it.
If passed a full JID, it returns the specific features supported
by that end-point.

Sending a file to a bare JID, will send a file to the resource
of the bare JID with the highest availability by presence, show
status and priority and which supports the features required
for a Jingle file-transfer.

Test-Information:

Added unit test verifying new behavior.

All tests pass on OS X 10.11.6. Added new unit tests for
FeatureOracle.

Manually verified that the roster and chat window both use
the same mechanism to detect support for file-transfers.

Manually verified that file-transfers via the contact list
goes to already bound full JIDs if there is an existing
ChatController.

Change-Id: I0175ac42ecb73f1d54f9c96ffbba773eb5e24296

diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp
index 232d903..b9e2cf4 100644
--- a/Swift/Controllers/Chat/ChatsManager.cpp
+++ b/Swift/Controllers/Chat/ChatsManager.cpp
@@ -24,6 +24,7 @@
 #include <Swiften/Client/NickResolver.h>
 #include <Swiften/Client/StanzaChannel.h>
 #include <Swiften/Disco/DiscoServiceWalker.h>
+#include <Swiften/Disco/FeatureOracle.h>
 #include <Swiften/Elements/CarbonsReceived.h>
 #include <Swiften/Elements/CarbonsSent.h>
 #include <Swiften/Elements/ChatState.h>
@@ -59,6 +60,7 @@
 #include <Swift/Controllers/UIEvents/RemoveMUCBookmarkUIEvent.h>
 #include <Swift/Controllers/UIEvents/RequestChatUIEvent.h>
 #include <Swift/Controllers/UIEvents/RequestJoinMUCUIEvent.h>
+#include <Swift/Controllers/UIEvents/SendFileUIEvent.h>
 #include <Swift/Controllers/UIInterfaces/ChatListWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/JoinMUCWindow.h>
 #include <Swift/Controllers/UIInterfaces/JoinMUCWindowFactory.h>
@@ -199,6 +201,7 @@ ChatsManager::~ChatsManager() {
     roster_->onJIDRemoved.disconnect(boost::bind(&ChatsManager::handleJIDRemovedFromRoster, this, _1));
     roster_->onJIDUpdated.disconnect(boost::bind(&ChatsManager::handleJIDUpdatedInRoster, this, _1));
     roster_->onRosterCleared.disconnect(boost::bind(&ChatsManager::handleRosterCleared, this));
+    ftOverview_->onNewFileTransferController.disconnect(boost::bind(&ChatsManager::handleNewFileTransferController, this, _1));
     delete joinMUCWindow_;
     for (JIDChatControllerPair controllerPair : chatControllers_) {
         delete controllerPair.second;
@@ -562,6 +565,24 @@ void ChatsManager::handleUIEvent(std::shared_ptr<UIEvent> event) {
         mucBookmarkManager_->addBookmark(addMUCBookmarkEvent->getBookmark());
         return;
     }
+    std::shared_ptr<SendFileUIEvent> sendFileEvent = std::dynamic_pointer_cast<SendFileUIEvent>(event);
+    if (sendFileEvent) {
+        JID fileReceiver = sendFileEvent->getJID();
+        if (fileReceiver.isBare()) {
+            // See if there is a chat controller for a conversation with a bound
+            // full JID. Check if this JID supports file transfer and use it instead
+            // of the bare JID.
+            ChatController* controller = getChatControllerIfExists(fileReceiver, false);
+            if (controller) {
+                JID controllerJID = controller->getToJID();
+                if (!controllerJID.isBare() && (FeatureOracle(entityCapsProvider_, presenceOracle_).isFileTransferSupported(controllerJID) == Yes)) {
+                    fileReceiver = controllerJID;
+                }
+            }
+        }
+        ftOverview_->sendFile(fileReceiver, sendFileEvent->getFilename());
+        return;
+    }
 
     std::shared_ptr<CreateImpromptuMUCUIEvent> createImpromptuMUCEvent = std::dynamic_pointer_cast<CreateImpromptuMUCUIEvent>(event);
     if (createImpromptuMUCEvent) {
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index a9d3f5c..0d9f1b8 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -15,7 +15,6 @@
 #include <Swiften/Base/Algorithm.h>
 #include <Swiften/Base/Log.h>
 #include <Swiften/Base/String.h>
-#include <Swiften/Base/foreach.h>
 #include <Swiften/Base/format.h>
 #include <Swiften/Client/Client.h>
 #include <Swiften/Client/ClientBlockListManager.h>
@@ -180,7 +179,7 @@ MainController::MainController(
     ClientOptions cachedOptions;
     bool eagle = settings_->getSetting(SettingConstants::FORGET_PASSWORDS);
     if (!eagle) {
-        foreach (std::string profile, settings->getAvailableProfiles()) {
+        for (auto&& profile : settings->getAvailableProfiles()) {
             ProfileSettingsProvider profileSettings(profile, settings);
             std::string password = profileSettings.getStringSetting("pass");
             std::string certificate = profileSettings.getStringSetting("certificate");
@@ -349,7 +348,7 @@ void MainController::handleConnected() {
         showProfileController_ = new ShowProfileController(client_->getVCardManager(), uiFactory_, uiEventStream_);
         ftOverview_ = new FileTransferOverview(client_->getFileTransferManager());
         fileTransferListController_->setFileTransferOverview(ftOverview_);
-        rosterController_ = new RosterController(boundJID_, client_->getRoster(), client_->getAvatarManager(), uiFactory_, client_->getNickManager(), client_->getNickResolver(), client_->getPresenceOracle(), client_->getSubscriptionManager(), eventController_, uiEventStream_, client_->getIQRouter(), settings_, client_->getEntityCapsProvider(), ftOverview_, client_->getClientBlockListManager(), client_->getVCardManager());
+        rosterController_ = new RosterController(boundJID_, client_->getRoster(), client_->getAvatarManager(), uiFactory_, client_->getNickManager(), client_->getNickResolver(), client_->getPresenceOracle(), client_->getSubscriptionManager(), eventController_, uiEventStream_, client_->getIQRouter(), settings_, client_->getEntityCapsProvider(), client_->getClientBlockListManager(), client_->getVCardManager());
         rosterController_->onChangeStatusRequest.connect(boost::bind(&MainController::handleChangeStatusRequest, this, _1, _2));
         rosterController_->onSignOutRequest.connect(boost::bind(&MainController::signOut, this));
         rosterController_->getWindow()->onShowCertificateRequest.connect(boost::bind(&MainController::handleShowCertificateRequest, this));
diff --git a/Swift/Controllers/Roster/RosterController.cpp b/Swift/Controllers/Roster/RosterController.cpp
index 116ef2e..1d20c4a 100644
--- a/Swift/Controllers/Roster/RosterController.cpp
+++ b/Swift/Controllers/Roster/RosterController.cpp
@@ -17,6 +17,7 @@
 #include <Swiften/Client/NickManager.h>
 #include <Swiften/Client/NickResolver.h>
 #include <Swiften/Disco/EntityCapsManager.h>
+#include <Swiften/Disco/FeatureOracle.h>
 #include <Swiften/Elements/DiscoInfo.h>
 #include <Swiften/FileTransfer/FileTransferManager.h>
 #include <Swiften/JID/JID.h>
@@ -30,7 +31,6 @@
 #include <Swiften/Roster/XMPPRosterItem.h>
 #include <Swiften/VCards/VCardManager.h>
 
-#include <Swift/Controllers/FileTransfer/FileTransferOverview.h>
 #include <Swift/Controllers/Intl.h>
 #include <Swift/Controllers/Roster/GroupRosterItem.h>
 #include <Swift/Controllers/Roster/ItemOperations/AppearOffline.h>
@@ -49,7 +49,6 @@
 #include <Swift/Controllers/UIEvents/RemoveRosterItemUIEvent.h>
 #include <Swift/Controllers/UIEvents/RenameGroupUIEvent.h>
 #include <Swift/Controllers/UIEvents/RenameRosterItemUIEvent.h>
-#include <Swift/Controllers/UIEvents/SendFileUIEvent.h>
 #include <Swift/Controllers/UIInterfaces/MainWindow.h>
 #include <Swift/Controllers/UIInterfaces/MainWindowFactory.h>
 #include <Swift/Controllers/XMPPEvents/ErrorEvent.h>
@@ -61,9 +60,8 @@ namespace Swift {
 /**
  * The controller does not gain ownership of these parameters.
  */
-RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsManager, FileTransferOverview* fileTransferOverview, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager)
-    : myJID_(jid), xmppRoster_(xmppRoster), mainWindowFactory_(mainWindowFactory), mainWindow_(mainWindowFactory_->createMainWindow(uiEventStream)), roster_(new Roster()), offlineFilter_(new OfflineRosterFilter()), vcardManager_(vcardManager), avatarManager_(avatarManager), nickManager_(nickManager), nickResolver_(nickResolver), presenceOracle_(presenceOracle), uiEventStream_(uiEventStream), entityCapsManager_(entityCapsManager), ftOverview_(fileTransferOverview), clientBlockListManager_(clientBlockListManager) {
-    assert(fileTransferOverview);
+RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsManager, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager)
+    : myJID_(jid), xmppRoster_(xmppRoster), mainWindowFactory_(mainWindowFactory), mainWindow_(mainWindowFactory_->createMainWindow(uiEventStream)), roster_(new Roster()), offlineFilter_(new OfflineRosterFilter()), vcardManager_(vcardManager), avatarManager_(avatarManager), nickManager_(nickManager), nickResolver_(nickResolver), presenceOracle_(presenceOracle), uiEventStream_(uiEventStream), entityCapsManager_(entityCapsManager), clientBlockListManager_(clientBlockListManager) {
     iqRouter_ = iqRouter;
     subscriptionManager_ = subscriptionManager;
     eventController_ = eventController;
@@ -81,6 +79,8 @@ RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, Avata
     subscriptionManager_->onPresenceSubscriptionRequest.connect(boost::bind(&RosterController::handleSubscriptionRequest, this, _1, _2));
     uiEventConnection_ = uiEventStream->onUIEvent.connect(boost::bind(&RosterController::handleUIEvent, this, _1));
 
+    featureOracle_ = std::unique_ptr<FeatureOracle>(new FeatureOracle(entityCapsManager_, presenceOracle_));
+
     vcardManager_->onOwnVCardChanged.connect(boost::bind(&RosterController::handleOwnVCardChanged, this, _1));
     avatarManager_->onAvatarChanged.connect(boost::bind(&RosterController::handleAvatarChanged, this, _1));
     presenceOracle_->onPresenceChange.connect(boost::bind(&RosterController::handlePresenceChanged, this, _1));
@@ -267,9 +267,6 @@ void RosterController::handleUIEvent(std::shared_ptr<UIEvent> event) {
             }
         }
     }
-    else if (std::shared_ptr<SendFileUIEvent> sendFileEvent = std::dynamic_pointer_cast<SendFileUIEvent>(event)) {
-        ftOverview_->sendFile(sendFileEvent->getJID(), sendFileEvent->getFilename());
-    }
 }
 
 void RosterController::setContactGroups(const JID& jid, const std::vector<std::string>& groups) {
@@ -396,17 +393,14 @@ std::set<std::string> RosterController::getGroups() const {
 }
 
 void RosterController::handleOnCapsChanged(const JID& jid) {
-    DiscoInfo::ref info = entityCapsManager_->getCaps(jid);
-    if (info) {
-        std::set<ContactRosterItem::Feature> features;
-        if (FileTransferManager::isSupportedBy(info)) {
-            features.insert(ContactRosterItem::FileTransferFeature);
-        }
-        if (info->hasFeature(DiscoInfo::WhiteboardFeature)) {
-            features.insert(ContactRosterItem::WhiteboardFeature);
-        }
-        roster_->applyOnItems(SetAvailableFeatures(jid, features));
+    std::set<ContactRosterItem::Feature> features;
+    if (featureOracle_->isFileTransferSupported(jid.toBare()) == Tristate::Yes || featureOracle_->isFileTransferSupported(jid.toBare()) == Tristate::Maybe) {
+        features.insert(ContactRosterItem::FileTransferFeature);
+    }
+    if (featureOracle_->isWhiteboardSupported(jid.toBare()) == Tristate::Yes) {
+        features.insert(ContactRosterItem::WhiteboardFeature);
     }
+    roster_->applyOnItems(SetAvailableFeatures(jid, features));
 }
 
 }
diff --git a/Swift/Controllers/Roster/RosterController.h b/Swift/Controllers/Roster/RosterController.h
index 980c545..ca2ecdc 100644
--- a/Swift/Controllers/Roster/RosterController.h
+++ b/Swift/Controllers/Roster/RosterController.h
@@ -27,8 +27,8 @@ namespace Swift {
     class ClientBlockListManager;
     class EntityCapsProvider;
     class EventController;
+    class FeatureOracle;
     class FileTransferManager;
-    class FileTransferOverview;
     class IQRouter;
     class MainWindow;
     class MainWindowFactory;
@@ -49,7 +49,7 @@ namespace Swift {
 
     class RosterController {
         public:
-            RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsProvider, FileTransferOverview* fileTransferOverview, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager);
+            RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings, EntityCapsProvider* entityCapsProvider, ClientBlockListManager* clientBlockListManager, VCardManager* vcardManager);
             ~RosterController();
             void showRosterWindow();
             void setJID(const JID& jid) { myJID_ = jid; }
@@ -111,10 +111,10 @@ namespace Swift {
             SettingsProvider* settings_;
             UIEventStream* uiEventStream_;
             EntityCapsProvider* entityCapsManager_;
-            FileTransferOverview* ftOverview_;
             ClientBlockListManager* clientBlockListManager_;
             RosterVCardProvider* rosterVCardProvider_;
             std::shared_ptr<ContactRosterItem> ownContact_;
+            std::unique_ptr<FeatureOracle> featureOracle_;
 
             boost::signals2::scoped_connection blockingOnStateChangedConnection_;
             boost::signals2::scoped_connection blockingOnItemAddedConnection_;
diff --git a/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp b/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp
index 0cd4080..ddbd7d3 100644
--- a/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp
+++ b/Swift/Controllers/Roster/UnitTest/RosterControllerTest.cpp
@@ -12,13 +12,16 @@
 #include <Swiften/Client/ClientBlockListManager.h>
 #include <Swiften/Client/DummyNickManager.h>
 #include <Swiften/Client/DummyStanzaChannel.h>
+#include <Swiften/Client/MemoryStorages.h>
 #include <Swiften/Client/NickResolver.h>
 #include <Swiften/Crypto/CryptoProvider.h>
 #include <Swiften/Crypto/PlatformCryptoProvider.h>
+#include <Swiften/Disco/CapsInfoGenerator.h>
+#include <Swiften/Disco/CapsManager.h>
 #include <Swiften/Disco/CapsProvider.h>
+#include <Swiften/Disco/ClientDiscoManager.h>
 #include <Swiften/Disco/EntityCapsManager.h>
 #include <Swiften/EventLoop/DummyEventLoop.h>
-#include <Swiften/FileTransfer/UnitTest/DummyFileTransferManager.h>
 #include <Swiften/Jingle/JingleSessionManager.h>
 #include <Swiften/MUC/MUCRegistry.h>
 #include <Swiften/Presence/PresenceOracle.h>
@@ -29,7 +32,6 @@
 #include <Swiften/VCards/VCardManager.h>
 #include <Swiften/VCards/VCardMemoryStorage.h>
 
-#include <Swift/Controllers/FileTransfer/FileTransferOverview.h>
 #include <Swift/Controllers/Roster/ContactRosterItem.h>
 #include <Swift/Controllers/Roster/GroupRosterItem.h>
 #include <Swift/Controllers/Roster/Roster.h>
@@ -42,8 +44,6 @@
 
 using namespace Swift;
 
-#define CHILDREN mainWindow_->roster->getRoot()->getChildren()
-
 class DummyCapsProvider : public CapsProvider {
         DiscoInfo::ref getCaps(const std::string&) const {return DiscoInfo::ref(new DiscoInfo());}
 };
@@ -61,6 +61,7 @@ class RosterControllerTest : public CppUnit::TestFixture {
         CPPUNIT_TEST(testUnavailablePresence);
         CPPUNIT_TEST(testRemoveResultsInUnavailablePresence);
         CPPUNIT_TEST(testOwnContactInRosterPresence);
+		CPPUNIT_TEST(testMultiResourceFileTransferFeature);
         CPPUNIT_TEST_SUITE_END();
 
     public:
@@ -70,6 +71,8 @@ class RosterControllerTest : public CppUnit::TestFixture {
             avatarManager_ = new NullAvatarManager();
             mainWindowFactory_ = new MockMainWindowFactory();
             mucRegistry_ = new MUCRegistry();
+            crypto_ = PlatformCryptoProvider::create();
+            storages_ = std::unique_ptr<MemoryStorages>(new MemoryStorages(crypto_));
             nickResolver_ = new NickResolver(jid_.toBare(), xmppRoster_, nullptr, mucRegistry_);
             channel_ = new DummyIQChannel();
             router_ = new IQRouter(channel_);
@@ -80,18 +83,16 @@ class RosterControllerTest : public CppUnit::TestFixture {
             uiEventStream_ = new UIEventStream();
             settings_ = new DummySettingsProvider();
             nickManager_ = new DummyNickManager();
-            capsProvider_ = new DummyCapsProvider();
-            entityCapsManager_ = new EntityCapsManager(capsProvider_, stanzaChannel_);
+            capsManager_ = std::unique_ptr<CapsManager>(new CapsManager(storages_->getCapsStorage(), stanzaChannel_, router_, crypto_));
+            entityCapsManager_ = new EntityCapsManager(capsManager_.get(), stanzaChannel_);
             jingleSessionManager_ = new JingleSessionManager(router_);
 
-            ftManager_ = new DummyFileTransferManager();
-            ftOverview_ = new FileTransferOverview(ftManager_);
             clientBlockListManager_ = new ClientBlockListManager(router_);
-            crypto_ = PlatformCryptoProvider::create();
             vcardStorage_ = new VCardMemoryStorage(crypto_);
             vcardManager_ = new VCardManager(jid_, router_, vcardStorage_);
-            rosterController_ = new RosterController(jid_, xmppRoster_, avatarManager_, mainWindowFactory_, nickManager_, nickResolver_, presenceOracle_, subscriptionManager_, eventController_, uiEventStream_, router_, settings_, entityCapsManager_, ftOverview_, clientBlockListManager_, vcardManager_);
+            rosterController_ = new RosterController(jid_, xmppRoster_, avatarManager_, mainWindowFactory_, nickManager_, nickResolver_, presenceOracle_, subscriptionManager_, eventController_, uiEventStream_, router_, settings_, entityCapsManager_, clientBlockListManager_, vcardManager_);
             mainWindow_ = mainWindowFactory_->last;
+            capsInfoGenerator_ = std::unique_ptr<CapsInfoGenerator>(new CapsInfoGenerator("", crypto_));
         }
 
         void tearDown() {
@@ -100,11 +101,8 @@ class RosterControllerTest : public CppUnit::TestFixture {
             delete vcardStorage_;
             delete crypto_;
             delete clientBlockListManager_;
-            delete ftOverview_;
-            delete ftManager_;
             delete jingleSessionManager_;
             delete entityCapsManager_;
-            delete capsProvider_;
             delete nickManager_;
             delete nickResolver_;
             delete mucRegistry_;
@@ -122,7 +120,7 @@ class RosterControllerTest : public CppUnit::TestFixture {
         }
 
     GroupRosterItem* groupChild(size_t i) {
-        return dynamic_cast<GroupRosterItem*>(CHILDREN[i]);
+        return dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[i]);
     }
 
     JID withResource(const JID& jid, const std::string& resource) {
@@ -140,10 +138,10 @@ class RosterControllerTest : public CppUnit::TestFixture {
         presence->setPriority(2);
         presence->setStatus("So totally here");
         stanzaChannel_->onPresenceReceived(presence);
-        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[0])->getChildren()[0]);
+        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
         CPPUNIT_ASSERT(item);
         CPPUNIT_ASSERT_EQUAL(presence->getStatus(), item->getStatusText());
-        ContactRosterItem* item2 = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[1])->getChildren()[0]);
+        ContactRosterItem* item2 = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[1])->getChildren()[0]);
         CPPUNIT_ASSERT(item2);
         CPPUNIT_ASSERT_EQUAL(presence->getStatus(), item2->getStatusText());
     }
@@ -163,7 +161,7 @@ class RosterControllerTest : public CppUnit::TestFixture {
         highPresence->setStatus("So totally here");
         stanzaChannel_->onPresenceReceived(lowPresence);
         stanzaChannel_->onPresenceReceived(highPresence);
-        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[0])->getChildren()[0]);
+        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
         CPPUNIT_ASSERT(item);
         CPPUNIT_ASSERT_EQUAL(highPresence->getStatus(), item->getStatusText());
     }
@@ -183,7 +181,7 @@ class RosterControllerTest : public CppUnit::TestFixture {
         highPresence->setStatus("So totally here");
         stanzaChannel_->onPresenceReceived(highPresence);
         stanzaChannel_->onPresenceReceived(lowPresence);
-        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[0])->getChildren()[0]);
+        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
         CPPUNIT_ASSERT(item);
         CPPUNIT_ASSERT_EQUAL(highPresence->getStatus(), item->getStatusText());
     }
@@ -223,7 +221,7 @@ class RosterControllerTest : public CppUnit::TestFixture {
         stanzaChannel_->onPresenceReceived(highPresenceOffline);
 
         // After this, the roster should show the low presence.
-        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[0])->getChildren()[0]);
+        ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
         CPPUNIT_ASSERT(item);
 
         Presence::ref low = presenceOracle_->getAccountPresence(from);
@@ -233,7 +231,7 @@ class RosterControllerTest : public CppUnit::TestFixture {
         CPPUNIT_ASSERT_EQUAL(lowPresence->getShow(), item->getStatusShow());
         CPPUNIT_ASSERT_EQUAL(lowPresence->getStatus(), item->getStatusText());
         stanzaChannel_->onPresenceReceived(lowPresenceOffline);
-        item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[0])->getChildren()[0]);
+        item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
         CPPUNIT_ASSERT(item);
         /* A verification that if the test fails, it's the RosterController, not the PresenceOracle. */
         low = presenceOracle_->getHighestPriorityPresence(from);
@@ -249,7 +247,7 @@ class RosterControllerTest : public CppUnit::TestFixture {
             groups.push_back("testGroup2");
             xmppRoster_->addContact(JID("test@testdomain.com/bob"), "name", groups, RosterItemPayload::Both);
 
-            CPPUNIT_ASSERT_EQUAL(2, static_cast<int>(CHILDREN.size()));
+            CPPUNIT_ASSERT_EQUAL(2, static_cast<int>(getUIRosterChildren().size()));
             //CPPUNIT_ASSERT_EQUAL(std::string("Bob"), xmppRoster_->getNameForJID(JID("foo@bar.com")));
         }
 
@@ -258,14 +256,14 @@ class RosterControllerTest : public CppUnit::TestFixture {
             JID jid("test@testdomain.com");
             xmppRoster_->addContact(jid, "name", groups, RosterItemPayload::None);
 
-            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
             CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
             xmppRoster_->addContact(jid, "name", groups, RosterItemPayload::To);
-            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
             CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
 
             xmppRoster_->addContact(jid, "name", groups, RosterItemPayload::Both);
-            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
             CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
 
         }
@@ -275,11 +273,11 @@ class RosterControllerTest : public CppUnit::TestFixture {
             JID jid("test@testdomain.com");
             xmppRoster_->addContact(jid, "name", groups, RosterItemPayload::Both);
 
-            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
             CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
             CPPUNIT_ASSERT_EQUAL(std::string("name"), groupChild(0)->getChildren()[0]->getDisplayName());
             xmppRoster_->addContact(jid, "NewName", groups, RosterItemPayload::Both);
-            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+            CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
             CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
             CPPUNIT_ASSERT_EQUAL(std::string("NewName"), groupChild(0)->getChildren()[0]->getDisplayName());
         }
@@ -293,18 +291,18 @@ class RosterControllerTest : public CppUnit::TestFixture {
         JID jid("test@testdomain.com");
         xmppRoster_->addContact(jid, "", oldGroups, RosterItemPayload::Both);
 
-        CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+        CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
         CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
         CPPUNIT_ASSERT_EQUAL(jid.toString(), groupChild(0)->getChildren()[0]->getDisplayName());
 
         xmppRoster_->addContact(jid, "new name", newGroups, RosterItemPayload::Both);
-        CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+        CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
         CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
         CPPUNIT_ASSERT_EQUAL(std::string("new name"), groupChild(0)->getChildren()[0]->getDisplayName());
         CPPUNIT_ASSERT_EQUAL(std::string("A Group"), groupChild(0)->getDisplayName());
 
         xmppRoster_->addContact(jid, "new name", newestGroups, RosterItemPayload::Both);
-        CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(CHILDREN.size()));
+        CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(getUIRosterChildren().size()));
         CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(groupChild(0)->getChildren().size()));
         CPPUNIT_ASSERT_EQUAL(std::string("new name"), groupChild(0)->getChildren()[0]->getDisplayName());
         CPPUNIT_ASSERT_EQUAL(std::string("Best Group"), groupChild(0)->getDisplayName());
@@ -364,14 +362,79 @@ class RosterControllerTest : public CppUnit::TestFixture {
             presence->setPriority(2);
             presence->setStatus("So totally here");
             stanzaChannel_->onPresenceReceived(presence);
-            ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[0])->getChildren()[0]);
+            ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
             CPPUNIT_ASSERT(item);
             CPPUNIT_ASSERT_EQUAL(presence->getStatus(), item->getStatusText());
-            ContactRosterItem* item2 = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(CHILDREN[1])->getChildren()[0]);
+            ContactRosterItem* item2 = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[1])->getChildren()[0]);
             CPPUNIT_ASSERT(item2);
             CPPUNIT_ASSERT_EQUAL(presence->getStatus(), item2->getStatusText());
         }
 
+         // This tests a scenario of a contact having a resource supporting Jingle File Transfer and
+        // one resource not supporting it, and the contact features being set correctly.
+        void testMultiResourceFileTransferFeature() {
+            JID contact("test@testdomain.com");
+            xmppRoster_->addContact(contact, "Name", {}, RosterItemPayload::Both);
+
+            auto sendPresenceAndAnswerCaps = [=](const JID& from, const DiscoInfo& discoInfo) {
+                auto capsInfo = capsInfoGenerator_->generateCapsInfo(discoInfo);
+
+                auto ftClientPresence = std::make_shared<Presence>();
+                ftClientPresence->setFrom(from);
+                ftClientPresence->setPriority(0);
+                ftClientPresence->setShow(StatusShow::Online);
+                ftClientPresence->addPayload(std::make_shared<CapsInfo>(capsInfo));
+                stanzaChannel_->onPresenceReceived(ftClientPresence);
+
+                // disco reply
+                auto discoRequest = channel_->iqs_.back();
+                CPPUNIT_ASSERT(discoRequest);
+                auto discoReply = IQ::createResult(discoRequest->getFrom(), ftClientPresence->getFrom(), discoRequest->getID(), std::make_shared<DiscoInfo>(discoInfo));
+                channel_->onIQReceived(discoReply);
+            };
+
+            auto ftDiscoInfo = DiscoInfo();
+            ftDiscoInfo.addFeature(DiscoInfo::JingleFeature);
+            ftDiscoInfo.addFeature(DiscoInfo::JingleFTFeature);
+            ftDiscoInfo.addFeature(DiscoInfo::JingleTransportsIBBFeature);
+
+            sendPresenceAndAnswerCaps(contact.withResource("ft-supported"), ftDiscoInfo);
+
+            auto* item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
+            CPPUNIT_ASSERT(item);
+            CPPUNIT_ASSERT_EQUAL(contact, item->getJID());
+            CPPUNIT_ASSERT_EQUAL(true, item->supportsFeature(ContactRosterItem::FileTransferFeature));
+
+            sendPresenceAndAnswerCaps(contact.withResource("ft-unsupported"), DiscoInfo());
+
+            item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
+            CPPUNIT_ASSERT(item);
+            CPPUNIT_ASSERT_EQUAL(contact, item->getJID());
+            CPPUNIT_ASSERT_EQUAL(true, item->supportsFeature(ContactRosterItem::FileTransferFeature));
+
+            auto unavailablePresence = std::make_shared<Presence>();
+            unavailablePresence->setFrom(contact.withResource("ft-unsupported"));
+            unavailablePresence->setPriority(0);
+            unavailablePresence->setType(Presence::Unavailable);
+            stanzaChannel_->onPresenceReceived(unavailablePresence);
+
+            item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
+            CPPUNIT_ASSERT(item);
+            CPPUNIT_ASSERT_EQUAL(contact, item->getJID());
+            CPPUNIT_ASSERT_EQUAL(true, item->supportsFeature(ContactRosterItem::FileTransferFeature));
+
+            unavailablePresence = std::make_shared<Presence>();
+            unavailablePresence->setFrom(contact.withResource("ft-supported"));
+            unavailablePresence->setPriority(0);
+            unavailablePresence->setType(Presence::Unavailable);
+            stanzaChannel_->onPresenceReceived(unavailablePresence);
+
+            item = dynamic_cast<ContactRosterItem*>(dynamic_cast<GroupRosterItem*>(getUIRosterChildren()[0])->getChildren()[0]);
+            CPPUNIT_ASSERT(item);
+            CPPUNIT_ASSERT_EQUAL(contact, item->getJID());
+            CPPUNIT_ASSERT_EQUAL(false, item->supportsFeature(ContactRosterItem::FileTransferFeature));
+        }
+
         void assertVectorsEqual(const std::vector<std::string>& v1, const std::vector<std::string>& v2, int line) {
             for (const auto& entry : v1) {
                 if (std::find(v2.begin(), v2.end(), entry) == v2.end()) {
@@ -382,8 +445,13 @@ class RosterControllerTest : public CppUnit::TestFixture {
             }
         }
 
+        const std::vector<RosterItem*>& getUIRosterChildren() const {
+            return mainWindow_->roster->getRoot()->getChildren();
+        }
+
     private:
         JID jid_;
+        std::unique_ptr<MemoryStorages> storages_;
         XMPPRosterImpl* xmppRoster_;
         MUCRegistry* mucRegistry_;
         AvatarManager* avatarManager_;
@@ -400,15 +468,14 @@ class RosterControllerTest : public CppUnit::TestFixture {
         UIEventStream* uiEventStream_;
         MockMainWindow* mainWindow_;
         DummySettingsProvider* settings_;
-        DummyCapsProvider* capsProvider_;
+        std::unique_ptr<CapsManager> capsManager_;
         EntityCapsManager* entityCapsManager_;
         JingleSessionManager* jingleSessionManager_;
-        FileTransferManager* ftManager_;
-        FileTransferOverview* ftOverview_;
         ClientBlockListManager* clientBlockListManager_;
         CryptoProvider* crypto_;
         VCardStorage* vcardStorage_;
         VCardManager* vcardManager_;
+        std::unique_ptr<CapsInfoGenerator> capsInfoGenerator_;
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION(RosterControllerTest);
diff --git a/Swiften/Base/SConscript b/Swiften/Base/SConscript
index 92e3da8..de502c2 100644
--- a/Swiften/Base/SConscript
+++ b/Swiften/Base/SConscript
@@ -1,23 +1,23 @@
 Import("swiften_env")
 
 objects = swiften_env.SwiftenObject([
+            "BoostRandomGenerator.cpp",
             "ByteArray.cpp",
             "DateTime.cpp",
-            "SafeByteArray.cpp",
-            "SafeAllocator.cpp",
             "Error.cpp",
+            "FileSize.cpp",
+            "IDGenerator.cpp",
             "Log.cpp",
             "LogSerializers.cpp",
             "Path.cpp",
             "Paths.cpp",
-            "String.cpp",
-            "IDGenerator.cpp",
-            "SimpleIDGenerator.cpp",
             "RandomGenerator.cpp",
-            "BoostRandomGenerator.cpp",
-            "sleep.cpp",
-            "URL.cpp",
             "Regex.cpp",
-            "FileSize.cpp"
+            "SafeAllocator.cpp",
+            "SafeByteArray.cpp",
+            "SimpleIDGenerator.cpp",
+            "String.cpp",
+            "URL.cpp",
+            "sleep.cpp",
         ])
 swiften_env.Append(SWIFTEN_OBJECTS = [objects])
diff --git a/Swiften/Disco/FeatureOracle.cpp b/Swiften/Disco/FeatureOracle.cpp
index 8328984..2baf87c 100644
--- a/Swiften/Disco/FeatureOracle.cpp
+++ b/Swiften/Disco/FeatureOracle.cpp
@@ -8,10 +8,12 @@
 
 #include <algorithm>
 #include <iterator>
+#include <unordered_set>
 #include <vector>
 
-#include <Swiften/Base/foreach.h>
+#include <Swiften/Base/Log.h>
 #include <Swiften/Disco/EntityCapsProvider.h>
+#include <Swiften/Elements/Idle.h>
 #include <Swiften/Elements/Presence.h>
 #include <Swiften/FileTransfer/FileTransferManager.h>
 #include <Swiften/JID/JID.h>
@@ -20,78 +22,178 @@
 namespace Swift {
 
 FeatureOracle::FeatureOracle(EntityCapsProvider* capsProvider, PresenceOracle* presenceOracle) : capsProvider_(capsProvider), presenceOracle_(presenceOracle) {
-
 }
 
 Tristate FeatureOracle::isFileTransferSupported(const JID& jid) {
-    DiscoInfo::ref discoInfo = getDiscoResultForJID(jid);
-    if (discoInfo) {
-        return FileTransferManager::isSupportedBy(discoInfo) ? Yes : No;
+    Tristate fileTransferSupported = No;
+
+    auto isYesOrMaybe = [](Tristate tristate) { return tristate == Yes || tristate == Maybe; };
+    auto isYes = [](Tristate tristate) { return tristate == Yes; };
+
+    auto supportedFeatures = getFeaturesForJID(jid);
+
+    auto jingleSupported = isFeatureSupported(supportedFeatures, DiscoInfo::JingleFeature);
+    auto jingleFTSupported = isFeatureSupported(supportedFeatures, DiscoInfo::JingleFTFeature);
+    auto jingleTransportIBBSupported = isFeatureSupported(supportedFeatures, DiscoInfo::JingleTransportsIBBFeature);
+    auto jingleTransportS5BSupported = isFeatureSupported(supportedFeatures, DiscoInfo::JingleTransportsS5BFeature);
+
+    if (isYes(jingleSupported) && isYes(jingleFTSupported) && (isYes(jingleTransportIBBSupported) || isYes(jingleTransportS5BSupported))) {
+        fileTransferSupported = Yes;
     }
-    else {
-        return Maybe;
+    else if (isYesOrMaybe(jingleSupported) && isYesOrMaybe(jingleFTSupported) && (isYesOrMaybe(jingleTransportIBBSupported) || isYesOrMaybe(jingleTransportS5BSupported))) {
+        fileTransferSupported = Maybe;
     }
+
+    return fileTransferSupported;
 }
 
 Tristate FeatureOracle::isMessageReceiptsSupported(const JID& jid) {
-    return isFeatureSupported(jid, DiscoInfo::MessageDeliveryReceiptsFeature);
+    return isFeatureSupported(getFeaturesForJID(jid), DiscoInfo::MessageDeliveryReceiptsFeature);
 }
 
 Tristate FeatureOracle::isMessageCorrectionSupported(const JID& jid) {
-    return isFeatureSupported(jid, DiscoInfo::MessageCorrectionFeature);
+    return isFeatureSupported(getFeaturesForJID(jid), DiscoInfo::MessageCorrectionFeature);
+}
+
+Tristate FeatureOracle::isWhiteboardSupported(const JID& jid) {
+    return isFeatureSupported(getFeaturesForJID(jid), DiscoInfo::WhiteboardFeature);
 }
 
-DiscoInfo::ref FeatureOracle::getDiscoResultForJID(const JID& jid) {
-    DiscoInfo::ref discoInfo;
+class PresenceFeatureAvailablityComparator {
+    public:
+        static int preferenceFromStatusShow(StatusShow::Type showType) {
+            switch (showType) {
+                case StatusShow::FFC:
+                    return 5;
+                case StatusShow::Online:
+                    return 4;
+                case StatusShow::DND:
+                    return 3;
+                case StatusShow::Away:
+                    return 2;
+                case StatusShow::XA:
+                    return 1;
+                case StatusShow::None:
+                    return 0;
+            }
+            assert(false);
+            return -1;
+        }
+
+        static int compareWithoutResource(const Presence::ref& a, const Presence::ref& b) {
+            int aPreference = preferenceFromStatusShow(a->getShow());
+            int bPreference = preferenceFromStatusShow(b->getShow());
+
+            if (aPreference != bPreference) {
+                return aPreference < bPreference ? 1 : -1;
+            }
+
+            Idle::ref aIdle = a->getPayload<Idle>();
+            Idle::ref bIdle = b->getPayload<Idle>();
+
+            if (aIdle && !bIdle) {
+                return -1;
+            }
+            else if (!aIdle && bIdle) {
+                return 1;
+            }
+
+            if (a->getPriority() != b->getPriority()) {
+                return a->getPriority() < b->getPriority() ? 1 : -1;
+            }
+
+            return 0;
+        }
+
+        /*
+         * This method returns true, if \ref Presence \p b is more available than
+         * \ref Presence \p a.
+         * Going by \ref StatusShow::Type first, then by idle state, then by
+         * presence priority and finally by resource name.
+         */
+        bool operator()(const Presence::ref& a, const Presence::ref& b) {
+            int aMoreAvailableThanB = compareWithoutResource(a, b);
+            if (aMoreAvailableThanB < 0) {
+                return true;
+            }
+            else if (aMoreAvailableThanB > 0) {
+                return false;
+            }
+
+            return a->getFrom().getResource() < b->getFrom().getResource();
+        }
+};
+
+JID FeatureOracle::getMostAvailableClientForFileTrasfer(const JID& bareJID) {
+    JID fullJID;
+    assert(bareJID.isBare());
+
+    std::vector<Presence::ref> allPresences =  presenceOracle_->getAllPresence(bareJID);
+    std::sort(allPresences.begin(), allPresences.end(), PresenceFeatureAvailablityComparator());
+
+    for (const auto& presence : allPresences) {
+        if (presence->isAvailable()) {
+            if (isFileTransferSupported(presence->getFrom()) == Yes) {
+                fullJID = presence->getFrom();
+                break;
+            }
+        }
+    }
+
+    SWIFT_LOG_ASSERT(!fullJID.isBare(), error);
+    return fullJID;
+}
+
+std::unordered_map<std::string, Tristate> FeatureOracle::getFeaturesForJID(const JID& jid) {
+    std::unordered_map<std::string, Tristate> supportedFeatures;
     if (jid.isBare()) {
-        // Calculate the common subset of disco features of all available results and return that.
-        std::vector<Presence::ref> availablePresences =  presenceOracle_->getAllPresence(jid);
-
-        bool commonFeaturesInitialized = false;
-        std::vector<std::string> commonFeatures;
-        foreach(Presence::ref presence, availablePresences) {
-            DiscoInfo::ref presenceDiscoInfo = capsProvider_->getCaps(presence->getFrom());
-            if (presenceDiscoInfo) {
-                std::vector<std::string> features = presenceDiscoInfo->getFeatures();
-                if (!commonFeaturesInitialized) {
-                    commonFeatures = features;
-                    commonFeaturesInitialized = true;
-                }
-                else {
-                    std::vector<std::string> featuresToRemove;
-                    foreach(const std::string& feature, commonFeatures) {
-                        if (std::find(features.begin(), features.end(), feature) == features.end()) {
-                            featuresToRemove.push_back(feature);
-                        }
-                    }
-                    foreach(const std::string& featureToRemove, featuresToRemove) {
-                        commonFeatures.erase(std::remove(commonFeatures.begin(), commonFeatures.end(), featureToRemove), commonFeatures.end());
-                    }
+        // Calculate the union of disco features of all most available results and return that.
+        std::vector<DiscoInfo::ref> onlineDiscoInfos;
+        std::unordered_set<std::string> features;
+
+        // Collect relevant disco info results and the set of features.
+        for (auto&& presence : presenceOracle_->getAllPresence(jid)) {
+            if (presence->getType() == Presence::Available) {
+                DiscoInfo::ref presenceDiscoInfo = capsProvider_->getCaps(presence->getFrom());
+                if (presenceDiscoInfo) {
+                    onlineDiscoInfos.push_back(presenceDiscoInfo);
+                    features.insert(presenceDiscoInfo->getFeatures().begin(), presenceDiscoInfo->getFeatures().end());
                 }
             }
         }
-        discoInfo = std::make_shared<DiscoInfo>();
 
-        foreach(const std::string& commonFeature, commonFeatures) {
-            discoInfo->addFeature(commonFeature);
+        // Calculate supportedFeaturesMap.
+        for (auto&& feature : features) {
+            Tristate supported = Yes;
+            for (auto&& discoInfo : onlineDiscoInfos) {
+                if (!discoInfo->hasFeature(feature)) {
+                    supported = Maybe;
+                    break;
+                }
+            }
+            supportedFeatures[feature] = supported;
         }
     }
     else {
         // Return the disco result of the full JID.
-        discoInfo = capsProvider_->getCaps(jid);
+        auto discoInfo = capsProvider_->getCaps(jid);
+        if (discoInfo) {
+            for (auto&& feature : discoInfo->getFeatures()) {
+                supportedFeatures[feature] = Yes;
+            }
+        }
     }
 
-    return discoInfo;
+    return supportedFeatures;
 }
 
-Tristate FeatureOracle::isFeatureSupported(const JID& jid, const std::string& feature) {
-    DiscoInfo::ref discoInfo = getDiscoResultForJID(jid);
-    if (discoInfo) {
-        return discoInfo->hasFeature(feature) ? Yes : No;
-    }
-    else {
-        return Maybe;
+Tristate FeatureOracle::isFeatureSupported(const std::unordered_map<std::string, Tristate>& supportedFeatures, const std::string& feature) {
+    Tristate supported = No;
+    auto lookupResult = supportedFeatures.find(feature);
+    if (lookupResult != supportedFeatures.end()) {
+        supported = lookupResult->second;
     }
+    return supported;
 }
 
 }
diff --git a/Swiften/Disco/FeatureOracle.h b/Swiften/Disco/FeatureOracle.h
index d434e86..be0cd6f 100644
--- a/Swiften/Disco/FeatureOracle.h
+++ b/Swiften/Disco/FeatureOracle.h
@@ -1,11 +1,14 @@
 /*
- * Copyright (c) 2015 Isode Limited.
+ * Copyright (c) 2015-2016 Isode Limited.
  * All rights reserved.
  * See the COPYING file for more information.
  */
 
 #pragma once
 
+#include <unordered_map>
+#include <unordered_set>
+
 #include <Swiften/Base/API.h>
 #include <Swiften/Base/Tristate.h>
 #include <Swiften/Elements/DiscoInfo.h>
@@ -27,16 +30,19 @@ class SWIFTEN_API FeatureOracle {
         Tristate isFileTransferSupported(const JID& jid);
         Tristate isMessageReceiptsSupported(const JID& jid);
         Tristate isMessageCorrectionSupported(const JID& jid);
+        Tristate isWhiteboardSupported(const JID& jid);
+
+        JID getMostAvailableClientForFileTrasfer(const JID& bareJID);
 
     private:
         /**
          * @brief getDiscoResultForJID returns a  shared reference to a DiscoInfo representing features supported by the jid.
-         * @param jid The JID to return the DiscoInfo::ref for.
-         * @return DiscoResult::ref
+         * @param jid The JID to return an std::unordered_map<std::string, Tristate> for.
+         * @return std::unordered_map<std::string, Tristate>
          */
-        DiscoInfo::ref getDiscoResultForJID(const JID& jid);
+        std::unordered_map<std::string, Tristate> getFeaturesForJID(const JID& jid);
 
-        Tristate isFeatureSupported(const JID& jid, const std::string& feature);
+        Tristate isFeatureSupported(const std::unordered_map<std::string, Tristate>& supportedFeatures, const std::string& feature);
 
     private:
         EntityCapsProvider* capsProvider_;
diff --git a/Swiften/Disco/UnitTest/FeatureOracleTest.cpp b/Swiften/Disco/UnitTest/FeatureOracleTest.cpp
new file mode 100644
index 0000000..e5ff09b
--- /dev/null
+++ b/Swiften/Disco/UnitTest/FeatureOracleTest.cpp
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2016 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#include <vector>
+
+#include <boost/bind.hpp>
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include <Swiften/Base/Tristate.h>
+#include <Swiften/Client/DummyStanzaChannel.h>
+#include <Swiften/Crypto/CryptoProvider.h>
+#include <Swiften/Crypto/PlatformCryptoProvider.h>
+#include <Swiften/Disco/CapsInfoGenerator.h>
+#include <Swiften/Disco/CapsProvider.h>
+#include <Swiften/Disco/EntityCapsManager.h>
+#include <Swiften/Disco/FeatureOracle.h>
+#include <Swiften/Elements/CapsInfo.h>
+#include <Swiften/Presence/PresenceOracle.h>
+#include <Swiften/Roster/XMPPRosterImpl.h>
+
+using namespace Swift;
+
+class FeatureOracleTest : public CppUnit::TestFixture {
+        CPPUNIT_TEST_SUITE(FeatureOracleTest);
+        CPPUNIT_TEST(testMergeAvailableResourcesForFeatures);
+        CPPUNIT_TEST(testMostAvailableFileTransferClient);
+        CPPUNIT_TEST_SUITE_END();
+
+    public:
+        void setUp() {
+            crypto_ = std::shared_ptr<CryptoProvider>(PlatformCryptoProvider::create());
+            dummyStanzaChannel_ = new DummyStanzaChannel();
+            xmppRosterImpl_ = new XMPPRosterImpl();
+            dummyCapsProvider_ = new DummyCapsProvider();
+            entityCapsManager_ = new EntityCapsManager(dummyCapsProvider_, dummyStanzaChannel_);
+            presenceOracle_ = new PresenceOracle(dummyStanzaChannel_, xmppRosterImpl_);
+            featureOracle_ = new FeatureOracle(entityCapsManager_, presenceOracle_);
+        }
+
+        void tearDown() {
+            delete featureOracle_;
+            delete presenceOracle_;
+            delete entityCapsManager_;
+            delete dummyCapsProvider_;
+            delete xmppRosterImpl_;
+            delete dummyStanzaChannel_;
+        }
+
+        void simulateIncomingPresence(const JID& from, Presence::Type type, StatusShow::Type status, const DiscoInfo::ref& disco, const std::vector<Payload::ref>& additionalPayloads = {}) {
+             auto capsInfo = std::make_shared<CapsInfo>(CapsInfoGenerator("http://example.com", crypto_.get()).generateCapsInfo(*disco.get()));
+             dummyCapsProvider_->caps[capsInfo->getVersion()] = disco;
+
+             Presence::ref capsNotifyPresence = std::make_shared<Presence>();
+             capsNotifyPresence->setType(type);
+             capsNotifyPresence->setFrom(from);
+             capsNotifyPresence->setShow(status);
+             capsNotifyPresence->addPayload(capsInfo);
+
+             capsNotifyPresence->addPayloads(additionalPayloads);
+
+             xmppRosterImpl_->addContact(from, "Foo", {}, RosterItemPayload::Both);
+             dummyStanzaChannel_->onPresenceReceived(capsNotifyPresence);
+        }
+
+        DiscoInfo::ref fileTransferSupportingDisco() {
+            DiscoInfo::ref discoInfo = std::make_shared<DiscoInfo>();
+            discoInfo->addFeature(DiscoInfo::JingleFeature);
+            discoInfo->addFeature(DiscoInfo::JingleFTFeature);
+            discoInfo->addFeature(DiscoInfo::JingleTransportsS5BFeature);
+            discoInfo->addFeature(DiscoInfo::JingleTransportsIBBFeature);
+            return discoInfo;
+        }
+
+        DiscoInfo::ref noFileTransferSupportingDisco() {
+            DiscoInfo::ref discoInfo = std::make_shared<DiscoInfo>();
+            discoInfo->addFeature(DiscoInfo::JingleFeature);
+            return discoInfo;
+        }
+
+        void testMergeAvailableResourcesForFeatures() {
+            CPPUNIT_ASSERT_EQUAL(No, featureOracle_->isFileTransferSupported(baseJID));
+
+            simulateIncomingPresence(noFileTransferJID, Presence::Available, StatusShow::Online, noFileTransferSupportingDisco());
+
+            CPPUNIT_ASSERT_EQUAL(size_t(1), presenceOracle_->getAllPresence(baseJID).size());
+            CPPUNIT_ASSERT_EQUAL(No, featureOracle_->isFileTransferSupported(baseJID));
+
+            simulateIncomingPresence(fileTransferJID, Presence::Available, StatusShow::Online, fileTransferSupportingDisco());
+
+            CPPUNIT_ASSERT_EQUAL(size_t(2), presenceOracle_->getAllPresence(baseJID).size());
+            CPPUNIT_ASSERT_EQUAL(Maybe, featureOracle_->isFileTransferSupported(baseJID));
+
+            simulateIncomingPresence(noFileTransferJID, Presence::Unavailable, StatusShow::None, noFileTransferSupportingDisco());
+
+            CPPUNIT_ASSERT_EQUAL(size_t(1), presenceOracle_->getAllPresence(baseJID).size());
+            CPPUNIT_ASSERT_EQUAL(Yes, featureOracle_->isFileTransferSupported(baseJID));
+
+            simulateIncomingPresence(fileTransferJID, Presence::Unavailable, StatusShow::None, fileTransferSupportingDisco());
+
+            CPPUNIT_ASSERT_EQUAL(size_t(1), presenceOracle_->getAllPresence(baseJID).size());
+            CPPUNIT_ASSERT_EQUAL(No, featureOracle_->isFileTransferSupported(baseJID));
+        }
+
+        void testMostAvailableFileTransferClient() {
+            simulateIncomingPresence(fileTransferJID, Presence::Available, StatusShow::DND, fileTransferSupportingDisco());
+
+            CPPUNIT_ASSERT_EQUAL(fileTransferJID, featureOracle_->getMostAvailableClientForFileTrasfer(baseJID));
+
+            simulateIncomingPresence(noFileTransferJID, Presence::Available, StatusShow::Online, noFileTransferSupportingDisco());
+
+            CPPUNIT_ASSERT_EQUAL(fileTransferJID, featureOracle_->getMostAvailableClientForFileTrasfer(baseJID));
+
+            auto moreAvailableJID = baseJID.withResource("moreAvailableFt");
+            simulateIncomingPresence(moreAvailableJID, Presence::Available, StatusShow::Online, fileTransferSupportingDisco());
+
+            CPPUNIT_ASSERT_EQUAL(moreAvailableJID, featureOracle_->getMostAvailableClientForFileTrasfer(baseJID));
+        }
+
+    private:
+        struct DummyCapsProvider : public CapsProvider {
+            virtual DiscoInfo::ref getCaps(const std::string& hash) const {
+                std::map<std::string, DiscoInfo::ref>::const_iterator i = caps.find(hash);
+                if (i != caps.end()) {
+                    return i->second;
+                }
+                return DiscoInfo::ref();
+            }
+
+            std::map<std::string, DiscoInfo::ref> caps;
+        };
+
+    private:
+        JID baseJID = "test@example.com";
+        JID fileTransferJID = baseJID.withResource("fileTransfer");
+        JID noFileTransferJID = baseJID.withResource("noFileTransfer");
+
+        std::shared_ptr<CryptoProvider> crypto_;
+        DummyCapsProvider* dummyCapsProvider_;
+        DummyStanzaChannel* dummyStanzaChannel_;
+        EntityCapsManager* entityCapsManager_;
+        FeatureOracle* featureOracle_;
+        PresenceOracle* presenceOracle_;
+        XMPPRosterImpl* xmppRosterImpl_;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(FeatureOracleTest);
diff --git a/Swiften/Elements/Stanza.h b/Swiften/Elements/Stanza.h
index 9a69696..9284bc3 100644
--- a/Swiften/Elements/Stanza.h
+++ b/Swiften/Elements/Stanza.h
@@ -68,6 +68,11 @@ namespace Swift {
                 payloads_.insert(payloads_.end(), begin, end);
             }
 
+            template<typename Container>
+            void addPayloads(const Container& container) {
+                payloads_.insert(payloads_.end(), std::begin(container), std::end(container));
+            }
+
             void updatePayload(std::shared_ptr<Payload> payload);
 
             void removePayloadOfSameType(std::shared_ptr<Payload>);
diff --git a/Swiften/FileTransfer/FileTransferManager.cpp b/Swiften/FileTransfer/FileTransferManager.cpp
index 94f4ab7..a5d7313 100644
--- a/Swiften/FileTransfer/FileTransferManager.cpp
+++ b/Swiften/FileTransfer/FileTransferManager.cpp
@@ -4,6 +4,12 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2016 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
 #include <Swiften/FileTransfer/FileTransferManager.h>
 
 namespace Swift {
@@ -11,13 +17,4 @@ namespace Swift {
 FileTransferManager::~FileTransferManager() {
 }
 
-bool FileTransferManager::isSupportedBy(const DiscoInfo::ref info) {
-    if (info) {
-        return info->hasFeature(DiscoInfo::JingleFeature)
-                        && info->hasFeature(DiscoInfo::JingleFTFeature)
-                        && (info->hasFeature(DiscoInfo::JingleTransportsIBBFeature) || info->hasFeature(DiscoInfo::JingleTransportsS5BFeature));
-    }
-    return false;
-}
-
 }
diff --git a/Swiften/FileTransfer/FileTransferManager.h b/Swiften/FileTransfer/FileTransferManager.h
index e315c67..07cfc90 100644
--- a/Swiften/FileTransfer/FileTransferManager.h
+++ b/Swiften/FileTransfer/FileTransferManager.h
@@ -47,8 +47,6 @@ namespace Swift {
                     std::shared_ptr<ReadBytestream> bytestream,
                     const FileTransferOptions& = FileTransferOptions()) = 0;
 
-            static bool isSupportedBy(const DiscoInfo::ref info);
-
             boost::signals2::signal<void (IncomingFileTransfer::ref)> onIncomingFileTransfer;
     };
 }
diff --git a/Swiften/FileTransfer/FileTransferManagerImpl.cpp b/Swiften/FileTransfer/FileTransferManagerImpl.cpp
index d449179..05dd3bb 100644
--- a/Swiften/FileTransfer/FileTransferManagerImpl.cpp
+++ b/Swiften/FileTransfer/FileTransferManagerImpl.cpp
@@ -19,8 +19,8 @@
 #include <Swiften/Base/BoostFilesystemVersion.h>
 #include <Swiften/Base/Log.h>
 #include <Swiften/Base/Path.h>
-#include <Swiften/Base/foreach.h>
 #include <Swiften/Disco/EntityCapsProvider.h>
+#include <Swiften/Disco/FeatureOracle.h>
 #include <Swiften/Elements/JingleFileTransferFileInfo.h>
 #include <Swiften/Elements/Presence.h>
 #include <Swiften/FileTransfer/DefaultFileTransferTransporterFactory.h>
@@ -99,28 +99,6 @@ void FileTransferManagerImpl::stop() {
     s5bServerManager->stop();
 }
 
-boost::optional<JID> FileTransferManagerImpl::highestPriorityJIDSupportingFileTransfer(const JID& bareJID) {
-    JID fullReceipientJID;
-    int priority = INT_MIN;
-
-    //getAllPresence(bareJID) gives you all presences for the bare JID (i.e. all resources) Isode Limited. @ 11:11
-    std::vector<Presence::ref> presences = presenceOracle->getAllPresence(bareJID);
-
-    //iterate over them
-    foreach(Presence::ref pres, presences) {
-        if (pres->getPriority() > priority) {
-            // look up caps from the jid
-            DiscoInfo::ref info = capsProvider->getCaps(pres->getFrom());
-            if (isSupportedBy(info)) {
-                priority = pres->getPriority();
-                fullReceipientJID = pres->getFrom();
-            }
-        }
-    }
-
-    return fullReceipientJID.isValid() ? boost::optional<JID>(fullReceipientJID) : boost::optional<JID>();
-}
-
 OutgoingFileTransfer::ref FileTransferManagerImpl::createOutgoingFileTransfer(
         const JID& to,
         const boost::filesystem::path& filepath,
@@ -155,9 +133,10 @@ OutgoingFileTransfer::ref FileTransferManagerImpl::createOutgoingFileTransfer(
     JID receipient = to;
 
     if(receipient.isBare()) {
-        boost::optional<JID> fullJID = highestPriorityJIDSupportingFileTransfer(receipient);
-        if (fullJID.is_initialized()) {
-            receipient = fullJID.get();
+        auto featureOracle = FeatureOracle(capsProvider, presenceOracle);
+        JID fullJID = featureOracle.getMostAvailableClientForFileTrasfer(receipient);
+        if (!fullJID.toString().empty()) {
+            receipient = fullJID;
         } else {
             return OutgoingFileTransfer::ref();
         }
diff --git a/Swiften/Presence/PresenceOracle.cpp b/Swiften/Presence/PresenceOracle.cpp
index c2c1152..1c9d0ea 100644
--- a/Swiften/Presence/PresenceOracle.cpp
+++ b/Swiften/Presence/PresenceOracle.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2015 Isode Limited.
+ * Copyright (c) 2010-2016 Isode Limited.
  * All rights reserved.
  * See the COPYING file for more information.
  */
@@ -10,7 +10,6 @@
 
 #include <boost/bind.hpp>
 
-#include <Swiften/Base/foreach.h>
 #include <Swiften/Client/StanzaChannel.h>
 #include <Swiften/Elements/StatusShow.h>
 #include <Swiften/Roster/XMPPRoster.h>
@@ -101,11 +100,10 @@ std::vector<Presence::ref> PresenceOracle::getAllPresence(const JID& bareJID) co
     if (i == entries_.end()) {
         return results;
     }
-    PresenceMap presenceMap = i->second;
-    PresenceMap::const_iterator j = presenceMap.begin();
-    for (; j != presenceMap.end(); ++j) {
-        Presence::ref current = j->second;
-        results.push_back(current);
+    for (const auto& jidPresence : i->second) {
+        if (jidPresence.second) {
+            results.push_back(jidPresence.second);
+        }
     }
     return results;
 }
@@ -153,7 +151,7 @@ Presence::ref PresenceOracle::getActivePresence(const std::vector<Presence::ref>
     PresenceAccountPriorityQueue away;
     PresenceAccountPriorityQueue offline;
 
-    foreach(Presence::ref presence, presences) {
+    for (auto&& presence : presences) {
         switch (presence->getShow()) {
             case StatusShow::Online:
                 online.push(presence);
@@ -200,18 +198,15 @@ Presence::ref PresenceOracle::getHighestPriorityPresence(const JID& bareJID) con
     if (i == entries_.end()) {
         return Presence::ref();
     }
-    PresenceMap presenceMap = i->second;
-    PresenceMap::const_iterator j = presenceMap.begin();
     Presence::ref highest;
-    for (; j != presenceMap.end(); ++j) {
-        Presence::ref current = j->second;
+    for (const auto& jidPresence : i->second) {
+        Presence::ref current = jidPresence.second;
         if (!highest
                 || current->getPriority() > highest->getPriority()
                 || (current->getPriority() == highest->getPriority()
                         && StatusShow::typeToAvailabilityOrdering(current->getShow()) > StatusShow::typeToAvailabilityOrdering(highest->getShow()))) {
             highest = current;
         }
-
     }
     return highest;
 }
diff --git a/Swiften/SConscript b/Swiften/SConscript
index eb7ae19..a8fb88a 100644
--- a/Swiften/SConscript
+++ b/Swiften/SConscript
@@ -387,9 +387,10 @@ if env["SCONS_STAGE"] == "build" :
             File("Component/UnitTest/ComponentSessionTest.cpp"),
             File("Disco/UnitTest/CapsInfoGeneratorTest.cpp"),
             File("Disco/UnitTest/CapsManagerTest.cpp"),
+            File("Disco/UnitTest/DiscoInfoResponderTest.cpp"),
             File("Disco/UnitTest/EntityCapsManagerTest.cpp"),
+            File("Disco/UnitTest/FeatureOracleTest.cpp"),
             File("Disco/UnitTest/JIDDiscoInfoResponderTest.cpp"),
-            File("Disco/UnitTest/DiscoInfoResponderTest.cpp"),
             File("Elements/UnitTest/IQTest.cpp"),
             File("Elements/UnitTest/StanzaTest.cpp"),
             File("Elements/UnitTest/FormTest.cpp"),
-- 
cgit v0.10.2-6-g49f6