brought ad hoc conferences back. fixed #688 fixed #367

iNPUTmice created

Change summary

src/main/java/eu/siacs/conversations/entities/Conversation.java          |  18 
src/main/java/eu/siacs/conversations/entities/MucOptions.java            | 160 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java     |   4 
src/main/java/eu/siacs/conversations/services/AvatarService.java         |   2 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 171 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java   |  49 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java        |  14 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |   2 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                |  59 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java            |   9 
src/main/java/eu/siacs/conversations/xmpp/forms/Data.java                |  75 
src/main/java/eu/siacs/conversations/xmpp/forms/Field.java               |  50 
src/main/res/values/strings.xml                                          |   4 
13 files changed, 480 insertions(+), 137 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Conversation.java πŸ”—

@@ -149,11 +149,19 @@ public class Conversation extends AbstractEntity {
 	}
 
 	public String getName() {
-		if (getMode() == MODE_MULTI && getMucOptions().getSubject() != null) {
-			return getMucOptions().getSubject();
-		} else if (getMode() == MODE_MULTI && bookmark != null
-				&& bookmark.getName() != null) {
-			return bookmark.getName();
+		if (getMode() == MODE_MULTI) {
+			if (getMucOptions().getSubject() != null) {
+				return getMucOptions().getSubject();
+			} else if (bookmark != null && bookmark.getName() != null) {
+				return bookmark.getName();
+			} else {
+				String generatedName = getMucOptions().createNameFromParticipants();
+				if (generatedName != null) {
+					return generatedName;
+				} else {
+					return getContactJid().getLocalpart();
+				}
+			}
 		} else {
 			return this.getContact().getDisplayName();
 		}

src/main/java/eu/siacs/conversations/entities/MucOptions.java πŸ”—

@@ -4,18 +4,20 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 import android.annotation.SuppressLint;
+import android.util.Log;
 
 @SuppressLint("DefaultLocale")
 public class MucOptions {
 	public static final int ERROR_NO_ERROR = 0;
 	public static final int ERROR_NICK_IN_USE = 1;
-	public static final int ERROR_ROOM_NOT_FOUND = 2;
+	public static final int ERROR_UNKNOWN = 2;
 	public static final int ERROR_PASSWORD_REQUIRED = 3;
 	public static final int ERROR_BANNED = 4;
 	public static final int ERROR_MEMBERS_ONLY = 5;
@@ -25,8 +27,17 @@ public class MucOptions {
 	public static final String STATUS_CODE_BANNED = "301";
 	public static final String STATUS_CODE_KICKED = "307";
 
-	public interface OnRenameListener {
-		public void onRename(boolean success);
+	private interface OnEventListener {
+		public void onSuccess();
+		public void onFailure();
+	}
+
+	public interface OnRenameListener extends OnEventListener {
+
+	}
+
+	public interface OnJoinListener extends OnEventListener {
+
 	}
 
 	public class User {
@@ -119,13 +130,13 @@ public class MucOptions {
 	private List<User> users = new CopyOnWriteArrayList<>();
 	private Conversation conversation;
 	private boolean isOnline = false;
-	private int error = ERROR_ROOM_NOT_FOUND;
-	private OnRenameListener renameListener = null;
-	private boolean aboutToRename = false;
+	private int error = ERROR_UNKNOWN;
+	private OnRenameListener onRenameListener = null;
+	private OnJoinListener onJoinListener = null;
 	private User self = new User();
 	private String subject = null;
-	private String joinnick;
 	private String password = null;
+	private boolean mNickChangingInProgress = false;
 
 	public MucOptions(Conversation conversation) {
 		this.account = conversation.getAccount();
@@ -155,10 +166,11 @@ public class MucOptions {
         final Jid from = packet.getFrom();
 		if (!from.isBareJid()) {
 			final String name = from.getResourcepart();
-			String type = packet.getAttribute("type");
+			final String type = packet.getAttribute("type");
+			final Element x = packet.findChild("x","http://jabber.org/protocol/muc#user");
+			final List<String> codes = getStatusCodes(x);
 			if (type == null) {
 				User user = new User();
-				Element x = packet.findChild("x","http://jabber.org/protocol/muc#user");
 				if (x != null) {
 					Element item = x.findChild("item");
 					if (item != null) {
@@ -166,16 +178,16 @@ public class MucOptions {
 						user.setAffiliation(item.getAttribute("affiliation"));
 						user.setRole(item.getAttribute("role"));
 						user.setJid(item.getAttributeAsJid("jid"));
-						user.setName(name);
-						if (name.equals(this.joinnick)) {
+						if (codes.contains("110")) {
 							this.isOnline = true;
 							this.error = ERROR_NO_ERROR;
 							self = user;
-							if (aboutToRename) {
-								if (renameListener != null) {
-									renameListener.onRename(true);
-								}
-								aboutToRename = false;
+							if (mNickChangingInProgress) {
+								onRenameListener.onSuccess();
+								mNickChangingInProgress = false;
+							} else if (this.onJoinListener != null) {
+								this.onJoinListener.onSuccess();
+								this.onJoinListener = null;
 							}
 						} else {
 							addUser(user);
@@ -196,46 +208,63 @@ public class MucOptions {
 						}
 					}
 				}
-			} else if (type.equals("unavailable") && name.equals(this.joinnick)) {
-				Element x = packet.findChild("x",
-						"http://jabber.org/protocol/muc#user");
-				if (x != null) {
-					Element status = x.findChild("status");
-					if (status != null) {
-						String code = status.getAttribute("code");
-						if (STATUS_CODE_KICKED.equals(code)) {
-							this.isOnline = false;
-							this.error = KICKED_FROM_ROOM;
-						} else if (STATUS_CODE_BANNED.equals(code)) {
-							this.isOnline = false;
-							this.error = ERROR_BANNED;
-						}
-					}
+			} else if (type.equals("unavailable") && codes.contains("110")) {
+				if (codes.contains("303")) {
+					this.mNickChangingInProgress = true;
+				} else if (codes.contains(STATUS_CODE_KICKED)) {
+					setError(KICKED_FROM_ROOM);
+				} else if (codes.contains(STATUS_CODE_BANNED)) {
+					setError(ERROR_BANNED);
+				} else {
+					setError(ERROR_UNKNOWN);
 				}
 			} else if (type.equals("unavailable")) {
-				deleteUser(packet.getAttribute("from").split("/", 2)[1]);
+				deleteUser(name);
 			} else if (type.equals("error")) {
 				Element error = packet.findChild("error");
 				if (error != null && error.hasChild("conflict")) {
-					if (aboutToRename) {
-						if (renameListener != null) {
-							renameListener.onRename(false);
+					if (isOnline) {
+						if (onRenameListener != null) {
+							onRenameListener.onFailure();
 						}
-						aboutToRename = false;
-						this.setJoinNick(getActualNick());
 					} else {
-						this.error = ERROR_NICK_IN_USE;
+						setError(ERROR_NICK_IN_USE);
 					}
 				} else if (error != null && error.hasChild("not-authorized")) {
-					this.error = ERROR_PASSWORD_REQUIRED;
+					setError(ERROR_PASSWORD_REQUIRED);
 				} else if (error != null && error.hasChild("forbidden")) {
-					this.error = ERROR_BANNED;
-				} else if (error != null
-						&& error.hasChild("registration-required")) {
-					this.error = ERROR_MEMBERS_ONLY;
+					setError(ERROR_BANNED);
+				} else if (error != null && error.hasChild("registration-required")) {
+					setError(ERROR_MEMBERS_ONLY);
+				} else {
+					setError(ERROR_UNKNOWN);
+				}
+			}
+		}
+	}
+
+	private void setError(int error) {
+		this.isOnline = false;
+		this.error = error;
+		if (onJoinListener != null) {
+			onJoinListener.onFailure();
+			onJoinListener = null;
+		}
+	}
+
+	private List<String> getStatusCodes(Element x) {
+		List<String> codes = new ArrayList<String>();
+		if (x != null) {
+			for(Element child : x.getChildren()) {
+				if (child.getName().equals("status")) {
+					String code = child.getAttribute("code");
+					if (code!=null) {
+						codes.add(code);
+					}
 				}
 			}
 		}
+		return codes;
 	}
 
 	public List<User> getUsers() {
@@ -263,10 +292,6 @@ public class MucOptions {
 		}
 	}
 
-	public void setJoinNick(String nick) {
-		this.joinnick = nick;
-	}
-
 	public boolean online() {
 		return this.isOnline;
 	}
@@ -276,11 +301,11 @@ public class MucOptions {
 	}
 
 	public void setOnRenameListener(OnRenameListener listener) {
-		this.renameListener = listener;
+		this.onRenameListener = listener;
 	}
 
-	public OnRenameListener getOnRenameListener() {
-		return this.renameListener;
+	public void setOnJoinListener(OnJoinListener listener) {
+		this.onJoinListener = listener;
 	}
 
 	public void setOffline() {
@@ -301,8 +326,28 @@ public class MucOptions {
 		return this.subject;
 	}
 
-	public void flagAboutToRename() {
-		this.aboutToRename = true;
+	public String createNameFromParticipants() {
+		if (users.size() >= 2) {
+			List<String> names = new ArrayList<String>();
+				for (User user : users) {
+					Contact contact = user.getContact();
+					if (contact != null && !contact.getDisplayName().isEmpty()) {
+						names.add(contact.getDisplayName().split("\\s+")[0]);
+					} else {
+						names.add(user.getName());
+					}
+				}
+			StringBuilder builder = new StringBuilder();
+			for (int i = 0; i < names.size(); ++i) {
+				builder.append(names.get(i));
+				if (i != names.size() - 1) {
+					builder.append(", ");
+				}
+			}
+			return builder.toString();
+		} else {
+			return null;
+		}
 	}
 
 	public long[] getPgpKeyIds() {
@@ -337,10 +382,9 @@ public class MucOptions {
 		return true;
 	}
 
-	public Jid getJoinJid() {
+	public Jid createJoinJid(String nick) {
         try {
-            return Jid.fromString(this.conversation.getContactJid().toBareJid().toString() + "/"
-+ this.joinnick);
+            return Jid.fromString(this.conversation.getContactJid().toBareJid().toString() + "/"+nick);
         } catch (final InvalidJidException e) {
             return null;
         }
@@ -356,8 +400,7 @@ public class MucOptions {
 	}
 
 	public String getPassword() {
-		this.password = conversation
-				.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
+		this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
 		if (this.password == null && conversation.getBookmark() != null
 				&& conversation.getBookmark().getPassword() != null) {
 			return conversation.getBookmark().getPassword();
@@ -372,8 +415,7 @@ public class MucOptions {
 		} else {
 			this.password = password;
 		}
-		conversation
-				.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
+		conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
 	}
 
 	public Conversation getConversation() {

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java πŸ”—

@@ -153,14 +153,14 @@ public class MessageGenerator extends AbstractGenerator {
 		return packet;
 	}
 
-	public MessagePacket invite(Conversation conversation, String contact) {
+	public MessagePacket invite(Conversation conversation, Jid contact) {
 		MessagePacket packet = new MessagePacket();
 		packet.setTo(conversation.getContactJid().toBareJid());
 		packet.setFrom(conversation.getAccount().getJid());
 		Element x = new Element("x");
 		x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
 		Element invite = new Element("invite");
-		invite.setAttribute("to", contact);
+		invite.setAttribute("to", contact.toBareJid().toString());
 		x.addChild(invite);
 		packet.addChild(x);
 		return packet;

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -35,6 +35,7 @@ import org.openintents.openpgp.util.OpenPgpServiceConnection;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.math.BigInteger;
 import java.security.SecureRandom;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -273,7 +274,6 @@ public class XmppConnectionService extends Service {
 		}
 	};
 	private LruCache<String, Bitmap> mBitmapCache;
-	private OnRenameListener renameListener = null;
 	private IqGenerator mIqGenerator = new IqGenerator(this);
 
 	public PgpEngine getPgpEngine() {
@@ -1055,10 +1055,6 @@ public class XmppConnectionService extends Service {
 		updateConversationUi();
 	}
 
-	public int getConversationCount() {
-		return this.databaseBackend.getConversationCount();
-	}
-
 	public void createAccount(Account account) {
 		account.initOtrEngine(this);
 		databaseBackend.createAccount(account);
@@ -1276,10 +1272,9 @@ public class XmppConnectionService extends Service {
 			Log.d(Config.LOGTAG,
 					"joining conversation " + conversation.getContactJid());
 			String nick = conversation.getMucOptions().getProposedNick();
-			conversation.getMucOptions().setJoinNick(nick);
+			Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
 			PresencePacket packet = new PresencePacket();
-			final Jid joinJid = conversation.getMucOptions().getJoinJid();
-			packet.setTo(conversation.getMucOptions().getJoinJid());
+			packet.setTo(joinJid);
 			Element x = new Element("x");
 			x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
 			if (conversation.getMucOptions().getPassword() != null) {
@@ -1311,10 +1306,6 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
-	public void setOnRenameListener(OnRenameListener listener) {
-		this.renameListener = listener;
-	}
-
 	public void providePasswordForMuc(Conversation conversation, String password) {
 		if (conversation.getMode() == Conversation.MODE_MULTI) {
 			conversation.getMucOptions().setPassword(password);
@@ -1327,33 +1318,33 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
-	public void renameInMuc(final Conversation conversation, final String nick) {
+	public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
 		final MucOptions options = conversation.getMucOptions();
-		options.setJoinNick(nick);
+		final Jid joinJid = options.createJoinJid(nick);
 		if (options.online()) {
 			Account account = conversation.getAccount();
 			options.setOnRenameListener(new OnRenameListener() {
 
 				@Override
-				public void onRename(boolean success) {
-					if (renameListener != null) {
-						renameListener.onRename(success);
-					}
-					if (success) {
-						conversation.setContactJid(conversation.getMucOptions()
-								.getJoinJid());
-						databaseBackend.updateConversation(conversation);
-						Bookmark bookmark = conversation.getBookmark();
-						if (bookmark != null) {
-							bookmark.setNick(nick);
-							pushBookmarks(bookmark.getAccount());
-						}
+				public void onSuccess() {
+					conversation.setContactJid(joinJid);
+					databaseBackend.updateConversation(conversation);
+					Bookmark bookmark = conversation.getBookmark();
+					if (bookmark != null) {
+						bookmark.setNick(nick);
+						pushBookmarks(bookmark.getAccount());
 					}
+					callback.success(conversation);
+				}
+
+				@Override
+				public void onFailure() {
+					callback.error(R.string.nick_in_use,conversation);
 				}
 			});
-			options.flagAboutToRename();
+
 			PresencePacket packet = new PresencePacket();
-			packet.setTo(options.getJoinJid());
+			packet.setTo(joinJid);
 			packet.setFrom(conversation.getAccount().getJid());
 
 			String sig = account.getPgpSignature();
@@ -1363,7 +1354,7 @@ public class XmppConnectionService extends Service {
 			}
 			sendPresencePacket(account, packet);
 		} else {
-			conversation.setContactJid(options.getJoinJid());
+			conversation.setContactJid(joinJid);
 			databaseBackend.updateConversation(conversation);
 			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
 				Bookmark bookmark = conversation.getBookmark();
@@ -1382,7 +1373,7 @@ public class XmppConnectionService extends Service {
 		account.pendingConferenceLeaves.remove(conversation);
 		if (account.getStatus() == Account.State.ONLINE) {
 			PresencePacket packet = new PresencePacket();
-			packet.setTo(conversation.getMucOptions().getJoinJid());
+			packet.setTo(conversation.getContactJid());
 			packet.setFrom(conversation.getAccount().getJid());
 			packet.setAttribute("type", "unavailable");
 			sendPresencePacket(conversation.getAccount(), packet);
@@ -1395,6 +1386,117 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
+	private String findConferenceServer(final Account account) {
+		String server;
+		if (account.getXmppConnection() != null) {
+			server = account.getXmppConnection().getMucServer();
+			if (server != null) {
+				return server;
+			}
+		}
+		for(Account other : getAccounts()) {
+			if (other != account && other.getXmppConnection() != null) {
+				server = other.getXmppConnection().getMucServer();
+				if (server != null) {
+					return server;
+				}
+			}
+		}
+		return null;
+	}
+
+	public void createAdhocConference(final Account account, final List<Jid> jids, final UiCallback<Conversation> callback) {
+		Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": creating adhoc conference with "+ jids.toString());
+		if (account.getStatus() == Account.State.ONLINE) {
+			try {
+				String server = findConferenceServer(account);
+				if (server == null) {
+					if (callback != null) {
+						callback.error(R.string.no_conference_server_found,null);
+					}
+					return;
+				}
+				String name = new BigInteger(75,getRNG()).toString(32);
+				Jid jid = Jid.fromParts(name,server,null);
+				final Conversation conversation = findOrCreateConversation(account, jid, true);
+				joinMuc(conversation);
+				Bundle options = new Bundle();
+				options.putString("muc#roomconfig_persistentroom", "1");
+				options.putString("muc#roomconfig_membersonly", "1");
+				options.putString("muc#roomconfig_publicroom", "0");
+				options.putString("muc#roomconfig_whois", "anyone");
+				pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
+					@Override
+					public void onPushSucceeded() {
+						for(Jid invite : jids) {
+							invite(conversation,invite);
+						}
+						if (callback != null) {
+							callback.success(conversation);
+						}
+					}
+
+					@Override
+					public void onPushFailed() {
+						if (callback != null) {
+							callback.error(R.string.conference_creation_failed, conversation);
+						}
+					}
+				});
+
+			} catch (InvalidJidException e) {
+				if (callback != null) {
+					callback.error(R.string.conference_creation_failed, null);
+				}
+			}
+		} else {
+			if (callback != null) {
+				callback.error(R.string.not_connected_try_again,null);
+			}
+		}
+	}
+
+	public void pushConferenceConfiguration(final Conversation conversation,final Bundle options, final OnConferenceOptionsPushed callback) {
+		IqPacket request = new IqPacket(IqPacket.TYPE_GET);
+		request.setTo(conversation.getContactJid().toBareJid());
+		request.query("http://jabber.org/protocol/muc#owner");
+		sendIqPacket(conversation.getAccount(),request,new OnIqPacketReceived() {
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				if (packet.getType() != IqPacket.TYPE_ERROR) {
+					Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
+					for (Field field : data.getFields()) {
+						if (options.containsKey(field.getName())) {
+							field.setValue(options.getString(field.getName()));
+						}
+					}
+					data.submit();
+					IqPacket set = new IqPacket(IqPacket.TYPE_SET);
+					set.setTo(conversation.getContactJid().toBareJid());
+					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
+					sendIqPacket(account, set, new OnIqPacketReceived() {
+						@Override
+						public void onIqPacketReceived(Account account, IqPacket packet) {
+							if (packet.getType() == IqPacket.TYPE_RESULT) {
+								if (callback != null) {
+									callback.onPushSucceeded();
+								}
+							} else {
+								if (callback != null) {
+									callback.onPushFailed();
+								}
+							}
+						}
+					});
+				} else {
+					if (callback != null) {
+						callback.onPushFailed();
+					}
+				}
+			}
+		});
+	}
+
 	public void disconnect(Account account, boolean force) {
 		if ((account.getStatus() == Account.State.ONLINE)
 				|| (account.getStatus() == Account.State.DISABLED)) {
@@ -1726,7 +1828,7 @@ public class XmppConnectionService extends Service {
 		}).start();
 	}
 
-	public void invite(Conversation conversation, String contact) {
+	public void invite(Conversation conversation, Jid contact) {
 		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
 		sendMessagePacket(conversation.getAccount(), packet);
 	}
@@ -2023,6 +2125,11 @@ public class XmppConnectionService extends Service {
 		public void onRosterUpdate();
 	}
 
+	private interface OnConferenceOptionsPushed {
+		public void onPushSucceeded();
+		public void onPushFailed();
+	}
+
 	public class XmppConnectionBinder extends Binder {
 		public XmppConnectionService getService() {
 			return XmppConnectionService.this;

src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java πŸ”—

@@ -34,7 +34,7 @@ import eu.siacs.conversations.entities.MucOptions.User;
 import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
-public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnRenameListener {
+public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate {
 	public static final String ACTION_VIEW_MUC = "view_muc";
 	private Conversation mConversation;
 	private OnClickListener inviteListener = new OnClickListener() {
@@ -57,26 +57,34 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 	private List<User> users = new ArrayList<>();
 	private User mSelectedUser = null;
 
-	@Override
-	public void onRename(final boolean success) {
-		runOnUiThread(new Runnable() {
+	private UiCallback<Conversation> renameCallback = new UiCallback<Conversation>() {
+		@Override
+		public void success(Conversation object) {
+			runOnUiThread(new Runnable() {
+				@Override
+				public void run() {
+					Toast.makeText(ConferenceDetailsActivity.this,getString(R.string.your_nick_has_been_changed),Toast.LENGTH_SHORT).show();
+					populateView();
+				}
+			});
 
-			@Override
-			public void run() {
-				populateView();
-				if (success) {
-					Toast.makeText(
-							ConferenceDetailsActivity.this,
-							getString(R.string.your_nick_has_been_changed),
-							Toast.LENGTH_SHORT).show();
-				} else {
-					Toast.makeText(ConferenceDetailsActivity.this,
-							getString(R.string.nick_in_use),
-							Toast.LENGTH_SHORT).show();
+		}
+
+		@Override
+		public void error(final int errorCode, Conversation object) {
+			runOnUiThread(new Runnable() {
+				@Override
+				public void run() {
+					Toast.makeText(ConferenceDetailsActivity.this,getString(errorCode),Toast.LENGTH_SHORT).show();
 				}
-			}
-		});
-	}
+			});
+		}
+
+		@Override
+		public void userInputRequried(PendingIntent pi, Conversation object) {
+
+		}
+	};
 
 	@Override
 	public void onConversationUpdate() {
@@ -114,8 +122,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 
 							@Override
 							public void onValueEdited(String value) {
-								xmppConnectionService.renameInMuc(mConversation,
-										value);
+								xmppConnectionService.renameInMuc(mConversation,value,renameCallback);
 							}
 						});
 			}

src/main/java/eu/siacs/conversations/ui/ConversationActivity.java πŸ”—

@@ -225,6 +225,18 @@ public class ConversationActivity extends XmppActivity implements
 		}
 	}
 
+	@Override
+	public void switchToConversation(Conversation conversation) {
+		setSelectedConversation(conversation);
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation());
+				openConversation();
+			}
+		});
+	}
+
 	public void openConversation() {
 		ActionBar ab = getActionBar();
 		if (ab != null) {
@@ -286,7 +298,7 @@ public class ConversationActivity extends XmppActivity implements
 					menuAttach.setVisible(false);
 				} else {
 					menuMucDetails.setVisible(false);
-					menuInviteContact.setVisible(false);
+					menuInviteContact.setTitle(R.string.conference_with);
 				}
 				if (this.getSelectedConversation().isMuted()) {
 					menuMute.setVisible(false);

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java πŸ”—

@@ -553,7 +553,7 @@ public class ConversationFragment extends Fragment {
 							showSnackbar(R.string.nick_in_use, R.string.edit,
 									clickToMuc);
 							break;
-						case MucOptions.ERROR_ROOM_NOT_FOUND:
+						case MucOptions.ERROR_UNKNOWN:
 							showSnackbar(R.string.conference_not_found,
 									R.string.leave, leaveMuc);
 							break;

src/main/java/eu/siacs/conversations/ui/XmppActivity.java πŸ”—

@@ -42,6 +42,7 @@ import android.view.View;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 import android.widget.ImageView;
+import android.widget.Toast;
 
 import com.google.zxing.BarcodeFormat;
 import com.google.zxing.EncodeHintType;
@@ -54,6 +55,7 @@ import net.java.otr4j.session.SessionID;
 
 import java.io.FileNotFoundException;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.concurrent.RejectedExecutionException;
@@ -240,9 +242,6 @@ public abstract class XmppActivity extends Activity {
 		if (this instanceof XmppConnectionService.OnRosterUpdate) {
 			this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
 		}
-		if (this instanceof MucOptions.OnRenameListener) {
-			this.xmppConnectionService.setOnRenameListener((MucOptions.OnRenameListener) this);
-		}
 	}
 
 	protected void unregisterListeners() {
@@ -255,9 +254,6 @@ public abstract class XmppActivity extends Activity {
 		if (this instanceof XmppConnectionService.OnRosterUpdate) {
 			this.xmppConnectionService.removeOnRosterUpdateListener();
 		}
-		if (this instanceof MucOptions.OnRenameListener) {
-			this.xmppConnectionService.setOnRenameListener(null);
-		}
 	}
 
 	public boolean onOptionsItemSelected(MenuItem item) {
@@ -603,18 +599,53 @@ public abstract class XmppActivity extends Activity {
 		super.onActivityResult(requestCode, resultCode, data);
 		if (requestCode == REQUEST_INVITE_TO_CONVERSATION
 				&& resultCode == RESULT_OK) {
-			String contactJid = data.getStringExtra("contact");
-			String conversationUuid = data.getStringExtra("conversation");
-			Conversation conversation = xmppConnectionService
-					.findConversationByUuid(conversationUuid);
-			if (conversation.getMode() == Conversation.MODE_MULTI) {
-				xmppConnectionService.invite(conversation, contactJid);
+			try {
+				Jid jid = Jid.fromString(data.getStringExtra("contact"));
+				String conversationUuid = data.getStringExtra("conversation");
+				Conversation conversation = xmppConnectionService
+						.findConversationByUuid(conversationUuid);
+				if (conversation.getMode() == Conversation.MODE_MULTI) {
+					xmppConnectionService.invite(conversation, jid);
+				} else {
+					List<Jid> jids = new ArrayList<Jid>();
+					jids.add(conversation.getContactJid().toBareJid());
+					jids.add(jid);
+					xmppConnectionService.createAdhocConference(conversation.getAccount(), jids, adhocCallback);
+				}
+			} catch (final InvalidJidException ignored) {
+
 			}
-			Log.d(Config.LOGTAG, "inviting " + contactJid + " to "
-					+ conversation.getName());
 		}
 	}
 
+	private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
+		@Override
+		public void success(final Conversation conversation) {
+			switchToConversation(conversation);
+			runOnUiThread(new Runnable() {
+				@Override
+				public void run() {
+					Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
+				}
+			});
+		}
+
+		@Override
+		public void error(final int errorCode, Conversation object) {
+			runOnUiThread(new Runnable() {
+				@Override
+				public void run() {
+					Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
+				}
+			});
+		}
+
+		@Override
+		public void userInputRequried(PendingIntent pi, Conversation object) {
+
+		}
+	};
+
 	public int getSecondaryTextColor() {
 		return this.mSecondaryTextColor;
 	}

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java πŸ”—

@@ -1049,7 +1049,14 @@ public class XmppConnection implements Runnable {
 	}
 
 	public String getMucServer() {
-		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
+		final List<String> items = new ArrayList<>();
+		for (Entry<String, List<String>> cursor : disco.entrySet()) {
+			final List<String> value = cursor.getValue();
+			if (value.contains("http://jabber.org/protocol/muc") && !value.contains("jabber:iq:gateway")) {
+				return cursor.getKey();
+			}
+		}
+		return null;
 	}
 
 	public int getTimeToNextAttempt() {

src/main/java/eu/siacs/conversations/xmpp/forms/Data.java πŸ”—

@@ -0,0 +1,75 @@
+package eu.siacs.conversations.xmpp.forms;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Data extends Element {
+
+	public Data() {
+		super("x");
+		this.setAttribute("xmlns","jabber:x:data");
+	}
+
+	public List<Field> getFields() {
+		ArrayList<Field> fields = new ArrayList<Field>();
+		for(Element child : getChildren()) {
+			if (child.getName().equals("field")) {
+				fields.add(Field.parse(child));
+			}
+		}
+		return fields;
+	}
+
+	public Field getFieldByName(String needle) {
+		for(Element child : getChildren()) {
+			if (child.getName().equals("field") && needle.equals(child.getAttribute("var"))) {
+				return Field.parse(child);
+			}
+		}
+		return null;
+	}
+
+	public void put(String name, String value) {
+		Field field = getFieldByName(name);
+		if (field == null) {
+			field = new Field(name);
+		}
+		field.setValue(value);
+	}
+
+	public void put(String name, Collection<String> values) {
+		Field field = getFieldByName(name);
+		if (field == null) {
+			field = new Field(name);
+		}
+		field.setValues(values);
+	}
+
+	public void submit() {
+		this.setAttribute("type","submit");
+		removeNonFieldChildren();
+		for(Field field : getFields()) {
+			field.removeNonValueChildren();
+		}
+	}
+
+	private void removeNonFieldChildren() {
+		for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
+			Element element = iterator.next();
+			if (!element.getName().equals("field")) {
+				iterator.remove();
+			}
+		}
+	}
+
+	public static Data parse(Element element) {
+		Data data = new Data();
+		data.setAttributes(element.getAttributes());
+		data.setChildren(element.getChildren());
+		return data;
+	}
+}

src/main/java/eu/siacs/conversations/xmpp/forms/Field.java πŸ”—

@@ -0,0 +1,50 @@
+package eu.siacs.conversations.xmpp.forms;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Field extends Element {
+
+	public Field(String name) {
+		super("field");
+		this.setAttribute("var",name);
+	}
+
+	private Field() {
+		super("field");
+	}
+
+	public String getName() {
+		return this.getAttribute("var");
+	}
+
+	public void setValue(String value) {
+		this.children.clear();
+		this.addChild("value").setContent(value);
+	}
+
+	public void setValues(Collection<String> values) {
+		this.children.clear();
+		for(String value : values) {
+			this.addChild("value").setContent(value);
+		}
+	}
+
+	public void removeNonValueChildren() {
+		for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
+			Element element = iterator.next();
+			if (!element.getName().equals("value")) {
+				iterator.remove();
+			}
+		}
+	}
+
+	public static Field parse(Element element) {
+		Field field = new Field();
+		field.setAttributes(element.getAttributes());
+		field.setChildren(element.getChildren());
+		return field;
+	}
+}

src/main/res/values/strings.xml πŸ”—

@@ -352,4 +352,8 @@
     <string name="pref_show_dynamic_tags">Show dynamic tags</string>
     <string name="pref_show_dynamic_tags_summary">Display read-only tags underneath contacts</string>
     <string name="enable_notifications">Enable notifications</string>
+    <string name="conference_with">Create conference with…</string>
+    <string name="no_conference_server_found">No conference server found</string>
+    <string name="conference_creation_failed">Conference creation failed!</string>
+    <string name="conference_created">Conference created!</string>
 </resources>