@@ -3,31 +3,42 @@ package eu.siacs.conversations.entities;
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.utils.LanguageUtils;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
 
 public class Room implements AvatarService.Avatarable, Comparable<Room> {
 
-    public String address;
-    public String name;
-    public String description;
-    public String language;
-    public int nusers;
+    public final String address;
+    public final String name;
+    public final String description;
+    public final String language;
+    public final int numberOfUsers;
 
-    public Room(String address, String name, String description, String language, int nusers) {
+    public Room(
+            final String address,
+            final String name,
+            final String description,
+            final String language,
+            final Integer numberOfUsers) {
         this.address = address;
         this.name = name;
         this.description = description;
         this.language = language;
-        this.nusers = nusers;
+        this.numberOfUsers = numberOfUsers == null ? 0 : numberOfUsers;
     }
 
-    public Room() {}
-
     public String getName() {
-        return name;
+        if (Strings.isNullOrEmpty(name)) {
+            final var jid = Jid.ofOrInvalid(address);
+            return jid.getLocal();
+        } else {
+            return name;
+        }
     }
 
     public String getDescription() {
@@ -81,9 +92,28 @@ public class Room implements AvatarService.Avatarable, Comparable<Room> {
     @Override
     public int compareTo(Room o) {
         return ComparisonChain.start()
-                .compare(o.nusers, nusers)
+                .compare(o.numberOfUsers, numberOfUsers)
                 .compare(Strings.nullToEmpty(name), Strings.nullToEmpty(o.name))
                 .compare(Strings.nullToEmpty(address), Strings.nullToEmpty(o.address))
                 .result();
     }
+
+    public static Room of(final Jid address, InfoQuery query) {
+        final var identity = Iterables.getFirst(query.getIdentities(), null);
+        final var ri =
+                query.getServiceDiscoveryExtension("http://jabber.org/protocol/muc#roominfo");
+        final String name = identity == null ? null : identity.getIdentityName();
+        String roomName = ri == null ? null : ri.getValue("muc#roomconfig_roomname");
+        String description = ri == null ? null : ri.getValue("muc#roominfo_description");
+        String language = ri == null ? null : ri.getValue("muc#roominfo_lang");
+        String occupants = ri == null ? null : ri.getValue("muc#roominfo_occupants");
+        final Integer numberOfUsers = Ints.tryParse(Strings.nullToEmpty(occupants));
+
+        return new Room(
+                address.toString(),
+                Strings.isNullOrEmpty(roomName) ? name : roomName,
+                description,
+                language,
+                numberOfUsers);
+    }
 }
  
  
  
    
    @@ -1,6 +1,5 @@
 package eu.siacs.conversations.parser;
 
-import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 import androidx.annotation.NonNull;
@@ -10,13 +9,11 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.entities.Room;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
-import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.manager.DiscoManager;
 import im.conversations.android.xmpp.model.disco.info.InfoQuery;
 import im.conversations.android.xmpp.model.stanza.Iq;
@@ -61,38 +58,6 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
         return items;
     }
 
-    public static Room parseRoom(Iq packet) {
-        final Element query = packet.findChild("query", Namespace.DISCO_INFO);
-        if (query == null) {
-            return null;
-        }
-        final Element x = query.findChild("x");
-        if (x == null) {
-            return null;
-        }
-        final Element identity = query.findChild("identity");
-        Data data = Data.parse(x);
-        String address = packet.getFrom().toString();
-        String name = identity == null ? null : identity.getAttribute("name");
-        String roomName = data.getValue("muc#roomconfig_roomname");
-        String description = data.getValue("muc#roominfo_description");
-        String language = data.getValue("muc#roominfo_lang");
-        String occupants = data.getValue("muc#roominfo_occupants");
-        int nusers;
-        try {
-            nusers = occupants == null ? 0 : Integer.parseInt(occupants);
-        } catch (NumberFormatException e) {
-            nusers = 0;
-        }
-
-        return new Room(
-                address,
-                TextUtils.isEmpty(roomName) ? name : roomName,
-                description,
-                language,
-                nusers);
-    }
-
     private void rosterItems(final Account account, final Element query) {
         final String version = query.getAttribute("ver");
         if (version != null) {
  
  
  
    
    @@ -5,24 +5,33 @@ import androidx.annotation.NonNull;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Ordering;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Room;
 import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.http.services.MuclumbusService;
-import eu.siacs.conversations.parser.IqParser;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.disco.items.Item;
+import im.conversations.android.xmpp.model.disco.items.ItemsQuery;
 import im.conversations.android.xmpp.model.stanza.Iq;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 import okhttp3.OkHttpClient;
 import okhttp3.ResponseBody;
 import retrofit2.Call;
@@ -164,7 +173,7 @@ public class ChannelDiscoveryService {
 
     private void discoverChannelsLocalServers(
             final String query, final OnChannelSearchResultsFound listener) {
-        final Map<Jid, Account> localMucService = getLocalMucServices();
+        final var localMucService = getLocalMucServices();
         Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
         if (localMucService.isEmpty()) {
             listener.onChannelSearchResultsFound(Collections.emptyList());
@@ -178,57 +187,104 @@ public class ChannelDiscoveryService {
                 listener.onChannelSearchResultsFound(results);
             }
         }
-        final AtomicInteger queriesInFlight = new AtomicInteger();
-        final List<Room> rooms = new ArrayList<>();
-        for (final Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
-            Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
-            queriesInFlight.incrementAndGet();
-            final var account = entry.getValue();
-            service.sendIqPacket(
-                    account,
-                    itemsRequest,
-                    (itemsResponse) -> {
-                        if (itemsResponse.getType() == Iq.Type.RESULT) {
-                            final List<Jid> items = IqParser.items(itemsResponse);
-                            for (final Jid item : items) {
-                                final Iq infoRequest =
-                                        service.getIqGenerator().queryDiscoInfo(item);
-                                queriesInFlight.incrementAndGet();
-                                service.sendIqPacket(
-                                        account,
-                                        infoRequest,
-                                        infoResponse -> {
-                                            if (infoResponse.getType() == Iq.Type.RESULT) {
-                                                final Room room = IqParser.parseRoom(infoResponse);
-                                                if (room != null) {
-                                                    rooms.add(room);
-                                                }
-                                                if (queriesInFlight.decrementAndGet() <= 0) {
-                                                    finishDiscoSearch(rooms, query, listener);
-                                                }
-                                            } else {
-                                                queriesInFlight.decrementAndGet();
-                                            }
-                                        });
+        final var roomsRoomsFuture =
+                Futures.successfulAsList(
+                        Collections2.transform(
+                                localMucService.entrySet(),
+                                e -> discoverRooms(e.getValue(), e.getKey())));
+        final var roomsFuture =
+                Futures.transform(
+                        roomsRoomsFuture,
+                        rooms -> {
+                            final var builder = new ImmutableList.Builder<Room>();
+                            for (final var inner : rooms) {
+                                if (inner == null) {
+                                    continue;
+                                }
+                                builder.addAll(inner);
                             }
-                        }
-                        if (queriesInFlight.decrementAndGet() <= 0) {
-                            finishDiscoSearch(rooms, query, listener);
-                        }
-                    });
-        }
+                            return builder.build();
+                        },
+                        MoreExecutors.directExecutor());
+        Futures.addCallback(
+                roomsFuture,
+                new FutureCallback<>() {
+                    @Override
+                    public void onSuccess(ImmutableList<Room> rooms) {
+                        finishDiscoSearch(rooms, query, listener);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable throwable) {
+                        Log.d(Config.LOGTAG, "could not perform room search", throwable);
+                    }
+                },
+                MoreExecutors.directExecutor());
+    }
+
+    private ListenableFuture<Collection<Room>> discoverRooms(
+            final XmppConnection connection, final Jid server) {
+        final var request = new Iq(Iq.Type.GET);
+        request.addExtension(new ItemsQuery());
+        request.setTo(server);
+        final ListenableFuture<Collection<Item>> itemsFuture =
+                Futures.transform(
+                        connection.sendIqPacket(request),
+                        iq -> {
+                            final var itemsQuery = iq.getExtension(ItemsQuery.class);
+                            if (itemsQuery == null) {
+                                return Collections.emptyList();
+                            }
+                            final var items = itemsQuery.getExtensions(Item.class);
+                            return Collections2.filter(items, i -> Objects.nonNull(i.getJid()));
+                        },
+                        MoreExecutors.directExecutor());
+        final var roomsFutures =
+                Futures.transformAsync(
+                        itemsFuture,
+                        items -> {
+                            final var infoFutures =
+                                    Collections2.transform(
+                                            items, i -> discoverRoom(connection, i.getJid()));
+                            return Futures.successfulAsList(infoFutures);
+                        },
+                        MoreExecutors.directExecutor());
+        return Futures.transform(
+                roomsFutures,
+                rooms -> Collections2.filter(rooms, Objects::nonNull),
+                MoreExecutors.directExecutor());
+    }
+
+    private ListenableFuture<Room> discoverRoom(final XmppConnection connection, final Jid room) {
+        final var request = new Iq(Iq.Type.GET);
+        request.addExtension(new InfoQuery());
+        request.setTo(room);
+        final var infoQueryResponseFuture = connection.sendIqPacket(request);
+        return Futures.transform(
+                infoQueryResponseFuture,
+                result -> {
+                    final var infoQuery = result.getExtension(InfoQuery.class);
+                    if (infoQuery == null) {
+                        return null;
+                    }
+                    return Room.of(room, infoQuery);
+                },
+                MoreExecutors.directExecutor());
     }
 
     private void finishDiscoSearch(
-            List<Room> rooms, String query, OnChannelSearchResultsFound listener) {
-        Collections.sort(rooms);
-        cache.put(key(Method.LOCAL_SERVER, ""), rooms);
+            final List<Room> rooms,
+            final String query,
+            final OnChannelSearchResultsFound listener) {
+        Log.d(Config.LOGTAG, "finishDiscoSearch with " + rooms.size() + " rooms");
+        final var sorted = Ordering.natural().sortedCopy(rooms);
+        cache.put(key(Method.LOCAL_SERVER, ""), sorted);
         if (query.isEmpty()) {
-            listener.onChannelSearchResultsFound(rooms);
+            listener.onChannelSearchResultsFound(sorted);
         } else {
-            List<Room> results = copyMatching(rooms, query);
+            List<Room> results = copyMatching(sorted, query);
             cache.put(key(Method.LOCAL_SERVER, query), results);
-            listener.onChannelSearchResultsFound(rooms);
+            listener.onChannelSearchResultsFound(sorted);
         }
     }
 
@@ -242,23 +298,21 @@ public class ChannelDiscoveryService {
         return result;
     }
 
-    private Map<Jid, Account> getLocalMucServices() {
-        final HashMap<Jid, Account> localMucServices = new HashMap<>();
-        for (Account account : service.getAccounts()) {
-            if (account.isEnabled()) {
-                final XmppConnection xmppConnection = account.getXmppConnection();
-                if (xmppConnection == null) {
-                    continue;
-                }
-                for (final String mucService : xmppConnection.getMucServers()) {
-                    final Jid jid = Jid.of(mucService);
-                    if (!localMucServices.containsKey(jid)) {
-                        localMucServices.put(jid, account);
+    private Map<Jid, XmppConnection> getLocalMucServices() {
+        final ImmutableMap.Builder<Jid, XmppConnection> localMucServices =
+                new ImmutableMap.Builder<>();
+        for (final var account : service.getAccounts()) {
+            final var connection = account.getXmppConnection();
+            if (connection != null && account.isEnabled()) {
+                for (final String mucService : connection.getMucServers()) {
+                    final Jid jid = Jid.ofOrInvalid(mucService);
+                    if (Jid.Invalid.isValid(jid)) {
+                        localMucServices.put(jid, connection);
                     }
                 }
             }
         }
-        return localMucServices;
+        return localMucServices.buildKeepingLast();
     }
 
     private static String key(Method method, String query) {
  
  
  
    
    @@ -4369,7 +4369,6 @@ public class XmppConnectionService extends Service {
 
     public void fetchConferenceConfiguration(
             final Conversation conversation, final OnConferenceConfigurationFetched callback) {
-        final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
         final var account = conversation.getAccount();
         final var connection = account.getXmppConnection();
         if (connection == null) {
@@ -4381,7 +4380,7 @@ public class XmppConnectionService extends Service {
                         .info(Entity.discoItem(conversation.getJid().asBareJid()), null);
         Futures.addCallback(
                 future,
-                new FutureCallback<InfoQuery>() {
+                new FutureCallback<>() {
                     @Override
                     public void onSuccess(InfoQuery result) {
                         final MucOptions mucOptions = conversation.getMucOptions();