added interface to edit nick

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/MucOptions.java            |    8 
src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java    |    2 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java          |   11 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           | 1606 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |   33 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java         | 2321 
src/main/java/eu/siacs/conversations/xml/Namespace.java                  |    1 
src/main/res/layout/activity_edit_account.xml                            |   46 
src/main/res/values/strings.xml                                          |    8 
src/main/res/values/styles.xml                                           |    4 
src/quicksy/AndroidManifest.xml                                          |   10 
src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java        |   54 
src/quicksy/java/eu/siacs/conversations/ui/TosActivity.java              |   80 
src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java           |    4 
src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java     |    3 
src/quicksy/java/eu/siacs/conversations/utils/SignupUtils.java           |   10 
src/quicksy/res/layout/activity_enter_name.xml                           |   60 
src/quicksy/res/layout/activity_tos.xml                                  |   77 
18 files changed, 2,373 insertions(+), 1,965 deletions(-)

Detailed changes

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

@@ -2,6 +2,7 @@ package eu.siacs.conversations.entities;
 
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.text.TextUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -380,7 +381,12 @@ public class MucOptions {
         } else if (!conversation.getJid().isBareJid()) {
             return conversation.getJid().getResource();
         } else {
-            return JidHelper.localPartOrFallback(account.getJid());
+            final String displayName = account.getDisplayName();
+            if (TextUtils.isEmpty(displayName)) {
+                return JidHelper.localPartOrFallback(account.getJid());
+            } else {
+                return displayName;
+            }
         }
     }
 

src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java ๐Ÿ”—

@@ -35,7 +35,7 @@ public abstract class AbstractGenerator {
 			"http://jabber.org/protocol/caps",
 			"http://jabber.org/protocol/disco#info",
 			"urn:xmpp:avatar:metadata+notify",
-			"http://jabber.org/protocol/nick+notify",
+			Namespace.NICK+"+notify",
 			Namespace.BOOKMARKS+"+notify",
 			"urn:xmpp:ping",
 			"jabber:iq:version",

src/main/java/eu/siacs/conversations/generator/IqGenerator.java ๐Ÿ”—

@@ -127,8 +127,15 @@ public class IqGenerator extends AbstractGenerator {
 
 	public IqPacket publishNick(String nick) {
 		final Element item = new Element("item");
-		item.addChild("nick", "http://jabber.org/protocol/nick").setContent(nick);
-		return publish("http://jabber.org/protocol/nick", item);
+		item.addChild("nick", Namespace.NICK).setContent(nick);
+		return publish(Namespace.NICK, item);
+	}
+
+	public IqPacket deleteNode(String node) {
+		IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+		final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
+		pubsub.addChild("delete").setAttribute("node",node);
+		return packet;
 	}
 
 	public IqPacket publishAvatar(Avatar avatar) {

src/main/java/eu/siacs/conversations/parser/MessageParser.java ๐Ÿ”—

@@ -43,795 +43,819 @@ import rocks.xmpp.addr.Jid;
 
 public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
 
-	private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
-
-	public MessageParser(XmppConnectionService service) {
-		super(service);
-	}
-
-	private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
-		final Jid by;
-		final boolean safeToExtract;
-		if (isTypeGroupChat) {
-			by = conversation.getJid().asBareJid();
-			safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
-		} else {
-			Account account = conversation.getAccount();
-			by = account.getJid().asBareJid();
-			safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
-		}
-		return safeToExtract ? extractStanzaId(packet, by) : null;
-	}
-
-	private static String extractStanzaId(Element packet, Jid by) {
-		for (Element child : packet.getChildren()) {
-			if (child.getName().equals("stanza-id")
-					&& Namespace.STANZA_IDS.equals(child.getNamespace())
-					&& by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) {
-				return child.getAttribute("id");
-			}
-		}
-		return null;
-	}
-
-	private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
-		final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
-		Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
-		return result != null ? result : fallback;
-	}
-
-	private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
-		ChatState state = ChatState.parse(packet);
-		if (state != null && c != null) {
-			final Account account = c.getAccount();
-			Jid from = packet.getFrom();
-			if (from.asBareJid().equals(account.getJid().asBareJid())) {
-				c.setOutgoingChatState(state);
-				if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
-					mXmppConnectionService.markRead(c);
-					activateGracePeriod(account);
-				}
-				return false;
-			} else {
-				if (isTypeGroupChat) {
-					MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
-					if (user != null) {
-						return user.setChatState(state);
-					} else {
-						return false;
-					}
-				} else {
-					return c.setIncomingChatState(state);
-				}
-			}
-		}
-		return false;
-	}
-
-	private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, boolean postpone) {
-		final AxolotlService service = conversation.getAccount().getAxolotlService();
-		final XmppAxolotlMessage xmppAxolotlMessage;
-		try {
-			xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
-		} catch (Exception e) {
-			Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": invalid omemo message received " + e.getMessage());
-			return null;
-		}
-		if (xmppAxolotlMessage.hasPayload()) {
-			final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
-			try {
-				plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
-			} catch (NotEncryptedForThisDeviceException e) {
-				return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
-			}
-			if (plaintextMessage != null) {
-				Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
-				finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
-				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint());
-				return finishedMessage;
-			}
-		} else {
-			Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OMEMO key transport message");
-			service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
-		}
-		return null;
-	}
-
-	private Invite extractInvite(Account account, Element message) {
-		Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
-		if (x != null) {
-			Element invite = x.findChild("invite");
-			if (invite != null) {
-				String password = x.findChildContent("password");
-				Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
-				Contact contact = from == null ? null : account.getRoster().getContact(from);
-				Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
-				if (room == null) {
-					return null;
-				}
-				return new Invite(room, password, contact);
-			}
-		} else {
-			x = message.findChild("x", "jabber:x:conference");
-			if (x != null) {
-				Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
-				Contact contact = from == null ? null : account.getRoster().getContact(from);
-				Jid room = InvalidJid.getNullForInvalid(x.getAttributeAsJid("jid"));
-				if (room == null) {
-					return null;
-				}
-				return new Invite(room, x.getAttribute("password"), contact);
-			}
-		}
-		return null;
-	}
-
-	private void parseEvent(final Element event, final Jid from, final Account account) {
-		Element items = event.findChild("items");
-		String node = items == null ? null : items.getAttribute("node");
-		if ("urn:xmpp:avatar:metadata".equals(node)) {
-			Avatar avatar = Avatar.parseMetadata(items);
-			if (avatar != null) {
-				avatar.owner = from.asBareJid();
-				if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
-					if (account.getJid().asBareJid().equals(from)) {
-						if (account.setAvatar(avatar.getFilename())) {
-							mXmppConnectionService.databaseBackend.updateAccount(account);
-						}
-						mXmppConnectionService.getAvatarService().clear(account);
-						mXmppConnectionService.updateConversationUi();
-						mXmppConnectionService.updateAccountUi();
-					} else {
-						Contact contact = account.getRoster().getContact(from);
-						if (contact.setAvatar(avatar)) {
-							mXmppConnectionService.syncRoster(account);
-							mXmppConnectionService.getAvatarService().clear(contact);
-							mXmppConnectionService.updateConversationUi();
-							mXmppConnectionService.updateRosterUi();
-						}
-					}
-				} else if (mXmppConnectionService.isDataSaverDisabled()) {
-					mXmppConnectionService.fetchAvatar(account, avatar);
-				}
-			}
-		} else if ("http://jabber.org/protocol/nick".equals(node)) {
-			final Element i = items.findChild("item");
-			final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
-			if (nick != null) {
-				Contact contact = account.getRoster().getContact(from);
-				if (contact.setPresenceName(nick)) {
-					mXmppConnectionService.getAvatarService().clear(contact);
-				}
-				mXmppConnectionService.updateConversationUi();
-				mXmppConnectionService.updateAccountUi();
-			}
-		} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
-			Element item = items.findChild("item");
-			Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
-			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
-			AxolotlService axolotlService = account.getAxolotlService();
-			axolotlService.registerDevices(from, deviceIds);
-		} else if (Namespace.BOOKMARKS.equals(node)) {
-			Log.d(Config.LOGTAG,"received bookmarks from "+from);
-			if (account.getJid().asBareJid().equals(from)) {
-				final Element i = items.findChild("item");
-				final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
-				mXmppConnectionService.processBookmarks(account,storage);
-			}
-		}
-	}
-
-	private boolean handleErrorMessage(Account account, MessagePacket packet) {
-		if (packet.getType() == MessagePacket.TYPE_ERROR) {
-			Jid from = packet.getFrom();
-			if (from != null) {
-				mXmppConnectionService.markMessage(account,
-						from.asBareJid(),
-						packet.getId(),
-						Message.STATUS_SEND_FAILED,
-						extractErrorMessage(packet));
-			}
-			return true;
-		}
-		return false;
-	}
-
-	@Override
-	public void onMessagePacketReceived(Account account, MessagePacket original) {
-		if (handleErrorMessage(account, original)) {
-			return;
-		}
-		final MessagePacket packet;
-		Long timestamp = null;
-		boolean isCarbon = false;
-		String serverMsgId = null;
-		final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
-		if (fin != null) {
-			mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
-			return;
-		}
-		final Element result = MessageArchiveService.Version.findResult(original);
-		final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid"));
-		if (query != null && query.validFrom(original.getFrom())) {
-			Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", query.version.namespace);
-			if (f == null) {
-				return;
-			}
-			timestamp = f.second;
-			packet = f.first;
-			serverMsgId = result.getAttribute("id");
-			query.incrementMessageCount();
-		} else if (query != null) {
-			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result from invalid sender");
-			return;
-		} else if (original.fromServer(account)) {
-			Pair<MessagePacket, Long> f;
-			f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
-			f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
-			packet = f != null ? f.first : original;
-			if (handleErrorMessage(account, packet)) {
-				return;
-			}
-			timestamp = f != null ? f.second : null;
-			isCarbon = f != null;
-		} else {
-			packet = original;
-		}
-
-		if (timestamp == null) {
-			timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
-		}
-		final String body = packet.getBody();
-		final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
-		final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
-		final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
-		final Element oob = packet.findChild("x", Namespace.OOB);
-		final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
-		final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
-		final String oobUrl = oob != null ? oob.findChildContent("url") : null;
-		final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
-		final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
-		int status;
-		final Jid counterpart;
-		final Jid to = packet.getTo();
-		final Jid from = packet.getFrom();
-		final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
-		final String remoteMsgId;
-		if (originId != null && originId.getAttribute("id") != null) {
-			remoteMsgId = originId.getAttribute("id");
-		} else {
-			remoteMsgId = packet.getId();
-		}
-		boolean notify = false;
-
-		if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
-			Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
-			return;
-		}
-
-		boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
-		if (query != null && !query.muc() && isTypeGroupChat) {
-			Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
-			return;
-		}
-		boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
-		boolean selfAddressed;
-		if (packet.fromAccount(account)) {
-			status = Message.STATUS_SEND;
-			selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
-			if (selfAddressed) {
-				counterpart = from;
-			} else {
-				counterpart = to != null ? to : account.getJid();
-			}
-		} else {
-			status = Message.STATUS_RECEIVED;
-			counterpart = from;
-			selfAddressed = false;
-		}
-
-		Invite invite = extractInvite(account, packet);
-		if (invite != null && invite.execute(account)) {
-			return;
-		}
-
-		if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
-			final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain());
-			final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
-			final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
-
-			if (serverMsgId == null) {
-				serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
-			}
-
-
-			if (selfAddressed) {
-				if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
-					return;
-				}
-				status = Message.STATUS_RECEIVED;
-				if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
-					return;
-				}
-			}
-
-			if (isTypeGroupChat) {
-				if (conversation.getMucOptions().isSelf(counterpart)) {
-					status = Message.STATUS_SEND_RECEIVED;
-					isCarbon = true; //not really carbon but received from another resource
-					if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
-						return;
-					} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
-						Message message = conversation.findSentMessageWithBody(packet.getBody());
-						if (message != null) {
-							mXmppConnectionService.markMessage(message, status);
-							return;
-						}
-					}
-				} else {
-					status = Message.STATUS_RECEIVED;
-				}
-			}
-			final Message message;
-			if (xP1S3url != null) {
-				message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
-				message.setOob(true);
-				if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
-					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
-				}
-			} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
-				message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
-			} else if (axolotlEncrypted != null && Config.supportOmemo()) {
-				Jid origin;
-				Set<Jid> fallbacksBySourceId = Collections.emptySet();
-				if (conversationMultiMode) {
-					final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
-					origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
-					if (origin == null) {
-						try {
-							fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted));
-						} catch (IllegalArgumentException e) {
-							//ignoring
-						}
-					}
-					if (origin == null && fallbacksBySourceId.size() == 0) {
-						Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks");
-						return;
-					}
-				} else {
-					fallbacksBySourceId = Collections.emptySet();
-					origin = from;
-				}
-				if (origin != null) {
-					message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, query != null);
-				} else {
-					Message trial = null;
-					for (Jid fallback : fallbacksBySourceId) {
-						trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, query != null);
-						if (trial != null) {
-							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback");
-							origin = fallback;
-							break;
-						}
-					}
-					message = trial;
-				}
-				if (message == null) {
-					if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
-						mXmppConnectionService.updateConversationUi();
-					}
-					if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
-						Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
-						if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
-							previouslySent.setServerMsgId(serverMsgId);
-							mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false);
-							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating...");
-						}
-					}
-					return;
-				}
-				if (conversationMultiMode) {
-					message.setTrueCounterpart(origin);
-				}
-			} else if (body == null && oobUrl != null) {
-				message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
-				message.setOob(true);
-				if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
-					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
-				}
-			} else {
-				message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
-			}
-
-			message.setCounterpart(counterpart);
-			message.setRemoteMsgId(remoteMsgId);
-			message.setServerMsgId(serverMsgId);
-			message.setCarbon(isCarbon);
-			message.setTime(timestamp);
-			if (body != null && body.equals(oobUrl)) {
-				message.setOob(true);
-				if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
-					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
-				}
-			}
-			message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
-			if (conversationMultiMode) {
-				message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
-				final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
-				Jid trueCounterpart;
-				if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
-					trueCounterpart = message.getTrueCounterpart();
-				} else if (query != null && query.safeToExtractTrueCounterpart()) {
-					trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
-				} else {
-					trueCounterpart = fallback;
-				}
-				if (trueCounterpart != null && trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
-					status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
-				}
-				message.setStatus(status);
-				message.setTrueCounterpart(trueCounterpart);
-				if (!isTypeGroupChat) {
-					message.setType(Message.TYPE_PRIVATE);
-				}
-			} else {
-				updateLastseen(account, from);
-			}
-
-			if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
-				final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
-						counterpart,
-						message.getStatus() == Message.STATUS_RECEIVED,
-						message.isCarbon());
-				if (replacedMessage != null) {
-					final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
-							|| replacedMessage.getFingerprint().equals(message.getFingerprint());
-					final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
-							&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
-					final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
-					final boolean duplicate = conversation.hasDuplicateMessage(message);
-					if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
-						Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
-						synchronized (replacedMessage) {
-							final String uuid = replacedMessage.getUuid();
-							replacedMessage.setUuid(UUID.randomUUID().toString());
-							replacedMessage.setBody(message.getBody());
-							replacedMessage.setEdited(replacedMessage.getRemoteMsgId());
-							replacedMessage.setRemoteMsgId(remoteMsgId);
-							if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
-								replacedMessage.setServerMsgId(message.getServerMsgId());
-							}
-							replacedMessage.setEncryption(message.getEncryption());
-							if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
-								replacedMessage.markUnread();
-							}
-							extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
-							mXmppConnectionService.updateMessage(replacedMessage, uuid);
-							mXmppConnectionService.getNotificationService().updateNotification(false);
-							if (mXmppConnectionService.confirmMessages()
-									&& replacedMessage.getStatus() == Message.STATUS_RECEIVED
-									&& (replacedMessage.trusted() || replacedMessage.getType() == Message.TYPE_PRIVATE)
-									&& remoteMsgId != null
-									&& !selfAddressed
-									&& !isTypeGroupChat) {
-								processMessageReceipts(account, packet, query);
-							}
-							if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
-								conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
-								conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
-							}
-						}
-						return;
-					} else {
-						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
-					}
-				}
-			}
-
-			long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
-			if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
-				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
-				return;
-			}
-
-			boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
-					|| message.getType() == Message.TYPE_PRIVATE
-					|| message.getServerMsgId() != null
-					|| (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
-			if (checkForDuplicates) {
-				final Message duplicate = conversation.findDuplicateMessage(message);
-				if (duplicate != null) {
-					final boolean serverMsgIdUpdated;
-					if (duplicate.getStatus() != Message.STATUS_RECEIVED
-							&& duplicate.getUuid().equals(message.getRemoteMsgId())
-							&& duplicate.getServerMsgId() == null
-							&& message.getServerMsgId() != null) {
-						duplicate.setServerMsgId(message.getServerMsgId());
-						if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
-							serverMsgIdUpdated = true;
-						} else {
-							serverMsgIdUpdated = false;
-							Log.e(Config.LOGTAG,"failed to update message");
-						}
-					} else {
-						serverMsgIdUpdated = false;
-					}
-					Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + Boolean.toString(serverMsgIdUpdated));
-					return;
-				}
-			}
-
-			if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
-				conversation.prepend(query.getActualInThisQuery(), message);
-			} else {
-				conversation.add(message);
-			}
-			if (query != null) {
-				query.incrementActualMessageCount();
-			}
-
-			if (query == null || query.isCatchup()) { //either no mam or catchup
-				if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
-					mXmppConnectionService.markRead(conversation);
-					if (query == null) {
-						activateGracePeriod(account);
-					}
-				} else {
-					message.markUnread();
-					notify = true;
-				}
-			}
-
-			if (message.getEncryption() == Message.ENCRYPTION_PGP) {
-				notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
-			} else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
-				notify = false;
-			}
-
-			if (query == null) {
-				extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
-				mXmppConnectionService.updateConversationUi();
-			}
-
-			if (mXmppConnectionService.confirmMessages()
-					&& message.getStatus() == Message.STATUS_RECEIVED
-					&& (message.trusted() || message.getType() == Message.TYPE_PRIVATE)
-					&& remoteMsgId != null
-					&& !selfAddressed
-					&& !isTypeGroupChat) {
-				processMessageReceipts(account, packet, query);
-			}
-
-			mXmppConnectionService.databaseBackend.createMessage(message);
-			final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
-			if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
-				manager.createNewDownloadConnection(message);
-			} else if (notify) {
-				if (query != null && query.isCatchup()) {
-					mXmppConnectionService.getNotificationService().pushFromBacklog(message);
-				} else {
-					mXmppConnectionService.getNotificationService().push(message);
-				}
-			}
-		} else if (!packet.hasChild("body")) { //no body
-
-			final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
-			if (axolotlEncrypted != null) {
-				Jid origin;
-				if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
-					final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
-					origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
-					if (origin == null) {
-						Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
-						return;
-					}
-				} else if (isTypeGroupChat) {
-					return;
-				} else {
-					origin = from;
-				}
-				try {
-					final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
-					account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
-					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
-				} catch (Exception e) {
-					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
-					return;
-				}
-			}
-
-			if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
-				mXmppConnectionService.updateConversationUi();
-			}
-
-			if (isTypeGroupChat) {
-				if (packet.hasChild("subject")) {
-					if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
-						conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
-						String subject = packet.findInternationalizedChildContent("subject");
-						if (conversation.getMucOptions().setSubject(subject)) {
-							mXmppConnectionService.updateConversation(conversation);
-						}
-						mXmppConnectionService.updateConversationUi();
-						return;
-					}
-				}
-			}
-			if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
-				for (Element child : mucUserElement.getChildren()) {
-					if ("status".equals(child.getName())) {
-						try {
-							int code = Integer.parseInt(child.getAttribute("code"));
-							if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
-								mXmppConnectionService.fetchConferenceConfiguration(conversation);
-								break;
-							}
-						} catch (Exception e) {
-							//ignored
-						}
-					} else if ("item".equals(child.getName())) {
-						MucOptions.User user = AbstractParser.parseItem(conversation, child);
-						Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
-								+ user.getRealJid() + " to " + user.getAffiliation() + " in "
-								+ conversation.getJid().asBareJid());
-						if (!user.realJidMatchesAccount()) {
-							boolean isNew = conversation.getMucOptions().updateUser(user);
-							mXmppConnectionService.getAvatarService().clear(conversation);
-							mXmppConnectionService.updateMucRosterUi();
-							mXmppConnectionService.updateConversationUi();
-							Contact contact = user.getContact();
-							if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
-								Jid jid = user.getRealJid();
-								List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
-								if (cryptoTargets.remove(user.getRealJid())) {
-									Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
-									conversation.setAcceptedCryptoTargets(cryptoTargets);
-									mXmppConnectionService.updateConversation(conversation);
-								}
-							} else if (isNew
-									&& user.getRealJid() != null
-									&& conversation.getMucOptions().isPrivateAndNonAnonymous()
-									&& (contact == null || !contact.mutualPresenceSubscription())
-									&& account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
-								account.getAxolotlService().fetchDeviceIds(user.getRealJid());
-							}
-						}
-					}
-				}
-			}
-		}
-
-		Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
-		if (received == null) {
-			received = packet.findChild("received", "urn:xmpp:receipts");
-		}
-		if (received != null) {
-			String id = received.getAttribute("id");
-			if (packet.fromAccount(account)) {
-				if (query != null && id != null && packet.getTo() != null) {
-					query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
-				}
-			} else {
-				mXmppConnectionService.markMessage(account, from.asBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED);
-			}
-		}
-		Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
-		if (displayed != null) {
-			final String id = displayed.getAttribute("id");
-			final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
-			if (packet.fromAccount(account) && !selfAddressed) {
-				dismissNotification(account, counterpart, query);
-			} else if (isTypeGroupChat) {
-				Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
-				if (conversation != null && id != null && sender != null) {
-					Message message = conversation.findMessageWithRemoteId(id, sender);
-					if (message != null) {
-						final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
-						final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
-						final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
-						if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
-							if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
-								mXmppConnectionService.markRead(conversation);
-							}
-						} else if (!counterpart.isBareJid() && trueJid != null) {
-							ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
-							if (message.addReadByMarker(readByMarker)) {
-								Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
-								mXmppConnectionService.updateMessage(message, false);
-							}
-						}
-					}
-				}
-			} else {
-				final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
-				Message message = displayedMessage == null ? null : displayedMessage.prev();
-				while (message != null
-						&& message.getStatus() == Message.STATUS_SEND_RECEIVED
-						&& message.getTimeSent() < displayedMessage.getTimeSent()) {
-					mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
-					message = message.prev();
-				}
-				if (displayedMessage != null && selfAddressed) {
-					dismissNotification(account, counterpart, query);
-				}
-			}
-		}
-
-		Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
-		if (event != null && InvalidJid.hasValidFrom(original)) {
-			parseEvent(event, original.getFrom(), account);
-		}
-
-		final String nick = packet.findChildContent("nick", Namespace.NICK);
-		if (nick != null && InvalidJid.hasValidFrom(original)) {
-			Contact contact = account.getRoster().getContact(from);
-			if (contact.setPresenceName(nick)) {
-				mXmppConnectionService.getAvatarService().clear(contact);
-			}
-		}
-	}
-
-	private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query) {
-		Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
-		if (conversation != null && (query == null || query.isCatchup())) {
-			mXmppConnectionService.markRead(conversation); //TODO only mark messages read that are older than timestamp
-		}
-	}
-
-	private void processMessageReceipts(Account account, MessagePacket packet, MessageArchiveService.Query query) {
-		final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
-		final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
-		if (query == null) {
-			final ArrayList<String> receiptsNamespaces = new ArrayList<>();
-			if (markable) {
-				receiptsNamespaces.add("urn:xmpp:chat-markers:0");
-			}
-			if (request) {
-				receiptsNamespaces.add("urn:xmpp:receipts");
-			}
-			if (receiptsNamespaces.size() > 0) {
-				MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
-						packet,
-						receiptsNamespaces,
-						packet.getType());
-				mXmppConnectionService.sendMessagePacket(account, receipt);
-			}
-		} else if (query.isCatchup()) {
-			if (request) {
-				query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), packet.getId()));
-			}
-		}
-	}
-
-	private void activateGracePeriod(Account account) {
-		long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
-		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
-		account.activateGracePeriod(duration);
-	}
-
-	private class Invite {
-		final Jid jid;
-		final String password;
-		final Contact inviter;
-
-		Invite(Jid jid, String password, Contact inviter) {
-			this.jid = jid;
-			this.password = password;
-			this.inviter = inviter;
-		}
-
-		public boolean execute(Account account) {
-			if (jid != null) {
-				Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
-				if (!conversation.getMucOptions().online()) {
-					conversation.getMucOptions().setPassword(password);
-					mXmppConnectionService.databaseBackend.updateConversation(conversation);
-					mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
-					mXmppConnectionService.updateConversationUi();
-				}
-				return true;
-			}
-			return false;
-		}
-	}
+    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
+
+    public MessageParser(XmppConnectionService service) {
+        super(service);
+    }
+
+    private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
+        final Jid by;
+        final boolean safeToExtract;
+        if (isTypeGroupChat) {
+            by = conversation.getJid().asBareJid();
+            safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
+        } else {
+            Account account = conversation.getAccount();
+            by = account.getJid().asBareJid();
+            safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
+        }
+        return safeToExtract ? extractStanzaId(packet, by) : null;
+    }
+
+    private static String extractStanzaId(Element packet, Jid by) {
+        for (Element child : packet.getChildren()) {
+            if (child.getName().equals("stanza-id")
+                    && Namespace.STANZA_IDS.equals(child.getNamespace())
+                    && by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) {
+                return child.getAttribute("id");
+            }
+        }
+        return null;
+    }
+
+    private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
+        final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
+        Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
+        return result != null ? result : fallback;
+    }
+
+    private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
+        ChatState state = ChatState.parse(packet);
+        if (state != null && c != null) {
+            final Account account = c.getAccount();
+            Jid from = packet.getFrom();
+            if (from.asBareJid().equals(account.getJid().asBareJid())) {
+                c.setOutgoingChatState(state);
+                if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
+                    mXmppConnectionService.markRead(c);
+                    activateGracePeriod(account);
+                }
+                return false;
+            } else {
+                if (isTypeGroupChat) {
+                    MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
+                    if (user != null) {
+                        return user.setChatState(state);
+                    } else {
+                        return false;
+                    }
+                } else {
+                    return c.setIncomingChatState(state);
+                }
+            }
+        }
+        return false;
+    }
+
+    private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, boolean postpone) {
+        final AxolotlService service = conversation.getAccount().getAxolotlService();
+        final XmppAxolotlMessage xmppAxolotlMessage;
+        try {
+            xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
+        } catch (Exception e) {
+            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": invalid omemo message received " + e.getMessage());
+            return null;
+        }
+        if (xmppAxolotlMessage.hasPayload()) {
+            final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
+            try {
+                plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
+            } catch (NotEncryptedForThisDeviceException e) {
+                return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
+            }
+            if (plaintextMessage != null) {
+                Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
+                finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
+                Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint());
+                return finishedMessage;
+            }
+        } else {
+            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OMEMO key transport message");
+            service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
+        }
+        return null;
+    }
+
+    private Invite extractInvite(Account account, Element message) {
+        Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
+        if (x != null) {
+            Element invite = x.findChild("invite");
+            if (invite != null) {
+                String password = x.findChildContent("password");
+                Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
+                Contact contact = from == null ? null : account.getRoster().getContact(from);
+                Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
+                if (room == null) {
+                    return null;
+                }
+                return new Invite(room, password, contact);
+            }
+        } else {
+            x = message.findChild("x", "jabber:x:conference");
+            if (x != null) {
+                Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
+                Contact contact = from == null ? null : account.getRoster().getContact(from);
+                Jid room = InvalidJid.getNullForInvalid(x.getAttributeAsJid("jid"));
+                if (room == null) {
+                    return null;
+                }
+                return new Invite(room, x.getAttribute("password"), contact);
+            }
+        }
+        return null;
+    }
+
+    private void parseEvent(final Element event, final Jid from, final Account account) {
+        Element items = event.findChild("items");
+        String node = items == null ? null : items.getAttribute("node");
+        if ("urn:xmpp:avatar:metadata".equals(node)) {
+            Avatar avatar = Avatar.parseMetadata(items);
+            if (avatar != null) {
+                avatar.owner = from.asBareJid();
+                if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
+                    if (account.getJid().asBareJid().equals(from)) {
+                        if (account.setAvatar(avatar.getFilename())) {
+                            mXmppConnectionService.databaseBackend.updateAccount(account);
+                        }
+                        mXmppConnectionService.getAvatarService().clear(account);
+                        mXmppConnectionService.updateConversationUi();
+                        mXmppConnectionService.updateAccountUi();
+                    } else {
+                        Contact contact = account.getRoster().getContact(from);
+                        if (contact.setAvatar(avatar)) {
+                            mXmppConnectionService.syncRoster(account);
+                            mXmppConnectionService.getAvatarService().clear(contact);
+                            mXmppConnectionService.updateConversationUi();
+                            mXmppConnectionService.updateRosterUi();
+                        }
+                    }
+                } else if (mXmppConnectionService.isDataSaverDisabled()) {
+                    mXmppConnectionService.fetchAvatar(account, avatar);
+                }
+            }
+        } else if (Namespace.NICK.equals(node)) {
+            final Element i = items.findChild("item");
+            final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
+            if (nick != null) {
+                setNick(account, from, nick);
+            }
+        } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
+            Element item = items.findChild("item");
+            Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+            Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
+            AxolotlService axolotlService = account.getAxolotlService();
+            axolotlService.registerDevices(from, deviceIds);
+        } else if (Namespace.BOOKMARKS.equals(node)) {
+            Log.d(Config.LOGTAG, "received bookmarks from " + from);
+            if (account.getJid().asBareJid().equals(from)) {
+                final Element i = items.findChild("item");
+                final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
+                mXmppConnectionService.processBookmarks(account, storage);
+            }
+        }
+    }
+
+    private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
+        final Element delete = event.findChild("delete");
+        if (delete == null) {
+            return;
+        }
+        String node = delete.getAttribute("node");
+        if (Namespace.NICK.equals(node)) {
+            Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
+            setNick(account, from, null);
+        }
+    }
+
+    private void setNick(Account account, Jid user, String nick) {
+        if (user.asBareJid().equals(account.getJid().asBareJid())) {
+            account.setDisplayName(nick);
+        } else {
+            Contact contact = account.getRoster().getContact(user);
+            if (contact.setPresenceName(nick)) {
+                mXmppConnectionService.getAvatarService().clear(contact);
+            }
+        }
+        mXmppConnectionService.updateConversationUi();
+        mXmppConnectionService.updateAccountUi();
+    }
+
+    private boolean handleErrorMessage(Account account, MessagePacket packet) {
+        if (packet.getType() == MessagePacket.TYPE_ERROR) {
+            Jid from = packet.getFrom();
+            if (from != null) {
+                mXmppConnectionService.markMessage(account,
+                        from.asBareJid(),
+                        packet.getId(),
+                        Message.STATUS_SEND_FAILED,
+                        extractErrorMessage(packet));
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onMessagePacketReceived(Account account, MessagePacket original) {
+        if (handleErrorMessage(account, original)) {
+            return;
+        }
+        final MessagePacket packet;
+        Long timestamp = null;
+        boolean isCarbon = false;
+        String serverMsgId = null;
+        final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
+        if (fin != null) {
+            mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
+            return;
+        }
+        final Element result = MessageArchiveService.Version.findResult(original);
+        final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid"));
+        if (query != null && query.validFrom(original.getFrom())) {
+            Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", query.version.namespace);
+            if (f == null) {
+                return;
+            }
+            timestamp = f.second;
+            packet = f.first;
+            serverMsgId = result.getAttribute("id");
+            query.incrementMessageCount();
+        } else if (query != null) {
+            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result from invalid sender");
+            return;
+        } else if (original.fromServer(account)) {
+            Pair<MessagePacket, Long> f;
+            f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
+            f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
+            packet = f != null ? f.first : original;
+            if (handleErrorMessage(account, packet)) {
+                return;
+            }
+            timestamp = f != null ? f.second : null;
+            isCarbon = f != null;
+        } else {
+            packet = original;
+        }
+
+        if (timestamp == null) {
+            timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
+        }
+        final String body = packet.getBody();
+        final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
+        final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
+        final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
+        final Element oob = packet.findChild("x", Namespace.OOB);
+        final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
+        final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
+        final String oobUrl = oob != null ? oob.findChildContent("url") : null;
+        final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
+        final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
+        int status;
+        final Jid counterpart;
+        final Jid to = packet.getTo();
+        final Jid from = packet.getFrom();
+        final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
+        final String remoteMsgId;
+        if (originId != null && originId.getAttribute("id") != null) {
+            remoteMsgId = originId.getAttribute("id");
+        } else {
+            remoteMsgId = packet.getId();
+        }
+        boolean notify = false;
+
+        if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
+            Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
+            return;
+        }
+
+        boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
+        if (query != null && !query.muc() && isTypeGroupChat) {
+            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
+            return;
+        }
+        boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
+        boolean selfAddressed;
+        if (packet.fromAccount(account)) {
+            status = Message.STATUS_SEND;
+            selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
+            if (selfAddressed) {
+                counterpart = from;
+            } else {
+                counterpart = to != null ? to : account.getJid();
+            }
+        } else {
+            status = Message.STATUS_RECEIVED;
+            counterpart = from;
+            selfAddressed = false;
+        }
+
+        Invite invite = extractInvite(account, packet);
+        if (invite != null && invite.execute(account)) {
+            return;
+        }
+
+        if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
+            final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain());
+            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
+            final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
+
+            if (serverMsgId == null) {
+                serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
+            }
+
+
+            if (selfAddressed) {
+                if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
+                    return;
+                }
+                status = Message.STATUS_RECEIVED;
+                if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
+                    return;
+                }
+            }
+
+            if (isTypeGroupChat) {
+                if (conversation.getMucOptions().isSelf(counterpart)) {
+                    status = Message.STATUS_SEND_RECEIVED;
+                    isCarbon = true; //not really carbon but received from another resource
+                    if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
+                        return;
+                    } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
+                        Message message = conversation.findSentMessageWithBody(packet.getBody());
+                        if (message != null) {
+                            mXmppConnectionService.markMessage(message, status);
+                            return;
+                        }
+                    }
+                } else {
+                    status = Message.STATUS_RECEIVED;
+                }
+            }
+            final Message message;
+            if (xP1S3url != null) {
+                message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
+                message.setOob(true);
+                if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
+                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+                }
+            } else if (pgpEncrypted != null && Config.supportOpenPgp()) {
+                message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
+            } else if (axolotlEncrypted != null && Config.supportOmemo()) {
+                Jid origin;
+                Set<Jid> fallbacksBySourceId = Collections.emptySet();
+                if (conversationMultiMode) {
+                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
+                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
+                    if (origin == null) {
+                        try {
+                            fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted));
+                        } catch (IllegalArgumentException e) {
+                            //ignoring
+                        }
+                    }
+                    if (origin == null && fallbacksBySourceId.size() == 0) {
+                        Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks");
+                        return;
+                    }
+                } else {
+                    fallbacksBySourceId = Collections.emptySet();
+                    origin = from;
+                }
+                if (origin != null) {
+                    message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, query != null);
+                } else {
+                    Message trial = null;
+                    for (Jid fallback : fallbacksBySourceId) {
+                        trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, query != null);
+                        if (trial != null) {
+                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback");
+                            origin = fallback;
+                            break;
+                        }
+                    }
+                    message = trial;
+                }
+                if (message == null) {
+                    if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
+                        mXmppConnectionService.updateConversationUi();
+                    }
+                    if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
+                        Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
+                        if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
+                            previouslySent.setServerMsgId(serverMsgId);
+                            mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false);
+                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating...");
+                        }
+                    }
+                    return;
+                }
+                if (conversationMultiMode) {
+                    message.setTrueCounterpart(origin);
+                }
+            } else if (body == null && oobUrl != null) {
+                message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
+                message.setOob(true);
+                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
+                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+                }
+            } else {
+                message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
+            }
+
+            message.setCounterpart(counterpart);
+            message.setRemoteMsgId(remoteMsgId);
+            message.setServerMsgId(serverMsgId);
+            message.setCarbon(isCarbon);
+            message.setTime(timestamp);
+            if (body != null && body.equals(oobUrl)) {
+                message.setOob(true);
+                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
+                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+                }
+            }
+            message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
+            if (conversationMultiMode) {
+                message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
+                final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
+                Jid trueCounterpart;
+                if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+                    trueCounterpart = message.getTrueCounterpart();
+                } else if (query != null && query.safeToExtractTrueCounterpart()) {
+                    trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
+                } else {
+                    trueCounterpart = fallback;
+                }
+                if (trueCounterpart != null && trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
+                    status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
+                }
+                message.setStatus(status);
+                message.setTrueCounterpart(trueCounterpart);
+                if (!isTypeGroupChat) {
+                    message.setType(Message.TYPE_PRIVATE);
+                }
+            } else {
+                updateLastseen(account, from);
+            }
+
+            if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
+                final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
+                        counterpart,
+                        message.getStatus() == Message.STATUS_RECEIVED,
+                        message.isCarbon());
+                if (replacedMessage != null) {
+                    final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
+                            || replacedMessage.getFingerprint().equals(message.getFingerprint());
+                    final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
+                            && replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
+                    final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
+                    final boolean duplicate = conversation.hasDuplicateMessage(message);
+                    if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
+                        Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
+                        synchronized (replacedMessage) {
+                            final String uuid = replacedMessage.getUuid();
+                            replacedMessage.setUuid(UUID.randomUUID().toString());
+                            replacedMessage.setBody(message.getBody());
+                            replacedMessage.setEdited(replacedMessage.getRemoteMsgId());
+                            replacedMessage.setRemoteMsgId(remoteMsgId);
+                            if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
+                                replacedMessage.setServerMsgId(message.getServerMsgId());
+                            }
+                            replacedMessage.setEncryption(message.getEncryption());
+                            if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
+                                replacedMessage.markUnread();
+                            }
+                            extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
+                            mXmppConnectionService.updateMessage(replacedMessage, uuid);
+                            mXmppConnectionService.getNotificationService().updateNotification(false);
+                            if (mXmppConnectionService.confirmMessages()
+                                    && replacedMessage.getStatus() == Message.STATUS_RECEIVED
+                                    && (replacedMessage.trusted() || replacedMessage.getType() == Message.TYPE_PRIVATE)
+                                    && remoteMsgId != null
+                                    && !selfAddressed
+                                    && !isTypeGroupChat) {
+                                processMessageReceipts(account, packet, query);
+                            }
+                            if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
+                                conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
+                                conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
+                            }
+                        }
+                        return;
+                    } else {
+                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
+                    }
+                }
+            }
+
+            long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
+            if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
+                return;
+            }
+
+            boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
+                    || message.getType() == Message.TYPE_PRIVATE
+                    || message.getServerMsgId() != null
+                    || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
+            if (checkForDuplicates) {
+                final Message duplicate = conversation.findDuplicateMessage(message);
+                if (duplicate != null) {
+                    final boolean serverMsgIdUpdated;
+                    if (duplicate.getStatus() != Message.STATUS_RECEIVED
+                            && duplicate.getUuid().equals(message.getRemoteMsgId())
+                            && duplicate.getServerMsgId() == null
+                            && message.getServerMsgId() != null) {
+                        duplicate.setServerMsgId(message.getServerMsgId());
+                        if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
+                            serverMsgIdUpdated = true;
+                        } else {
+                            serverMsgIdUpdated = false;
+                            Log.e(Config.LOGTAG, "failed to update message");
+                        }
+                    } else {
+                        serverMsgIdUpdated = false;
+                    }
+                    Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + Boolean.toString(serverMsgIdUpdated));
+                    return;
+                }
+            }
+
+            if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
+                conversation.prepend(query.getActualInThisQuery(), message);
+            } else {
+                conversation.add(message);
+            }
+            if (query != null) {
+                query.incrementActualMessageCount();
+            }
+
+            if (query == null || query.isCatchup()) { //either no mam or catchup
+                if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
+                    mXmppConnectionService.markRead(conversation);
+                    if (query == null) {
+                        activateGracePeriod(account);
+                    }
+                } else {
+                    message.markUnread();
+                    notify = true;
+                }
+            }
+
+            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+                notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
+            } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
+                notify = false;
+            }
+
+            if (query == null) {
+                extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
+                mXmppConnectionService.updateConversationUi();
+            }
+
+            if (mXmppConnectionService.confirmMessages()
+                    && message.getStatus() == Message.STATUS_RECEIVED
+                    && (message.trusted() || message.getType() == Message.TYPE_PRIVATE)
+                    && remoteMsgId != null
+                    && !selfAddressed
+                    && !isTypeGroupChat) {
+                processMessageReceipts(account, packet, query);
+            }
+
+            mXmppConnectionService.databaseBackend.createMessage(message);
+            final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
+            if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
+                manager.createNewDownloadConnection(message);
+            } else if (notify) {
+                if (query != null && query.isCatchup()) {
+                    mXmppConnectionService.getNotificationService().pushFromBacklog(message);
+                } else {
+                    mXmppConnectionService.getNotificationService().push(message);
+                }
+            }
+        } else if (!packet.hasChild("body")) { //no body
+
+            final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
+            if (axolotlEncrypted != null) {
+                Jid origin;
+                if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
+                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
+                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
+                    if (origin == null) {
+                        Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
+                        return;
+                    }
+                } else if (isTypeGroupChat) {
+                    return;
+                } else {
+                    origin = from;
+                }
+                try {
+                    final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
+                    account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
+                } catch (Exception e) {
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
+                    return;
+                }
+            }
+
+            if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
+                mXmppConnectionService.updateConversationUi();
+            }
+
+            if (isTypeGroupChat) {
+                if (packet.hasChild("subject")) {
+                    if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
+                        conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
+                        String subject = packet.findInternationalizedChildContent("subject");
+                        if (conversation.getMucOptions().setSubject(subject)) {
+                            mXmppConnectionService.updateConversation(conversation);
+                        }
+                        mXmppConnectionService.updateConversationUi();
+                        return;
+                    }
+                }
+            }
+            if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
+                for (Element child : mucUserElement.getChildren()) {
+                    if ("status".equals(child.getName())) {
+                        try {
+                            int code = Integer.parseInt(child.getAttribute("code"));
+                            if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
+                                mXmppConnectionService.fetchConferenceConfiguration(conversation);
+                                break;
+                            }
+                        } catch (Exception e) {
+                            //ignored
+                        }
+                    } else if ("item".equals(child.getName())) {
+                        MucOptions.User user = AbstractParser.parseItem(conversation, child);
+                        Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
+                                + user.getRealJid() + " to " + user.getAffiliation() + " in "
+                                + conversation.getJid().asBareJid());
+                        if (!user.realJidMatchesAccount()) {
+                            boolean isNew = conversation.getMucOptions().updateUser(user);
+                            mXmppConnectionService.getAvatarService().clear(conversation);
+                            mXmppConnectionService.updateMucRosterUi();
+                            mXmppConnectionService.updateConversationUi();
+                            Contact contact = user.getContact();
+                            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
+                                Jid jid = user.getRealJid();
+                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
+                                if (cryptoTargets.remove(user.getRealJid())) {
+                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
+                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
+                                    mXmppConnectionService.updateConversation(conversation);
+                                }
+                            } else if (isNew
+                                    && user.getRealJid() != null
+                                    && conversation.getMucOptions().isPrivateAndNonAnonymous()
+                                    && (contact == null || !contact.mutualPresenceSubscription())
+                                    && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
+                                account.getAxolotlService().fetchDeviceIds(user.getRealJid());
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
+        if (received == null) {
+            received = packet.findChild("received", "urn:xmpp:receipts");
+        }
+        if (received != null) {
+            String id = received.getAttribute("id");
+            if (packet.fromAccount(account)) {
+                if (query != null && id != null && packet.getTo() != null) {
+                    query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
+                }
+            } else {
+                mXmppConnectionService.markMessage(account, from.asBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED);
+            }
+        }
+        Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
+        if (displayed != null) {
+            final String id = displayed.getAttribute("id");
+            final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
+            if (packet.fromAccount(account) && !selfAddressed) {
+                dismissNotification(account, counterpart, query);
+            } else if (isTypeGroupChat) {
+                Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
+                if (conversation != null && id != null && sender != null) {
+                    Message message = conversation.findMessageWithRemoteId(id, sender);
+                    if (message != null) {
+                        final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
+                        final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
+                        final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
+                        if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
+                            if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
+                                mXmppConnectionService.markRead(conversation);
+                            }
+                        } else if (!counterpart.isBareJid() && trueJid != null) {
+                            ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
+                            if (message.addReadByMarker(readByMarker)) {
+                                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
+                                mXmppConnectionService.updateMessage(message, false);
+                            }
+                        }
+                    }
+                }
+            } else {
+                final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
+                Message message = displayedMessage == null ? null : displayedMessage.prev();
+                while (message != null
+                        && message.getStatus() == Message.STATUS_SEND_RECEIVED
+                        && message.getTimeSent() < displayedMessage.getTimeSent()) {
+                    mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
+                    message = message.prev();
+                }
+                if (displayedMessage != null && selfAddressed) {
+                    dismissNotification(account, counterpart, query);
+                }
+            }
+        }
+
+        final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
+        if (event != null && InvalidJid.hasValidFrom(original)) {
+            if (event.hasChild("items")) {
+                parseEvent(event, original.getFrom(), account);
+            } else if (event.hasChild("delete")) {
+                parseDeleteEvent(event, original.getFrom(), account);
+            }
+        }
+
+        final String nick = packet.findChildContent("nick", Namespace.NICK);
+        if (nick != null && InvalidJid.hasValidFrom(original)) {
+            Contact contact = account.getRoster().getContact(from);
+            if (contact.setPresenceName(nick)) {
+                mXmppConnectionService.getAvatarService().clear(contact);
+            }
+        }
+    }
+
+    private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query) {
+        Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
+        if (conversation != null && (query == null || query.isCatchup())) {
+            mXmppConnectionService.markRead(conversation); //TODO only mark messages read that are older than timestamp
+        }
+    }
+
+    private void processMessageReceipts(Account account, MessagePacket packet, MessageArchiveService.Query query) {
+        final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
+        final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
+        if (query == null) {
+            final ArrayList<String> receiptsNamespaces = new ArrayList<>();
+            if (markable) {
+                receiptsNamespaces.add("urn:xmpp:chat-markers:0");
+            }
+            if (request) {
+                receiptsNamespaces.add("urn:xmpp:receipts");
+            }
+            if (receiptsNamespaces.size() > 0) {
+                MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
+                        packet,
+                        receiptsNamespaces,
+                        packet.getType());
+                mXmppConnectionService.sendMessagePacket(account, receipt);
+            }
+        } else if (query.isCatchup()) {
+            if (request) {
+                query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), packet.getId()));
+            }
+        }
+    }
+
+    private void activateGracePeriod(Account account) {
+        long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
+        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
+        account.activateGracePeriod(duration);
+    }
+
+    private class Invite {
+        final Jid jid;
+        final String password;
+        final Contact inviter;
+
+        Invite(Jid jid, String password, Contact inviter) {
+            this.jid = jid;
+            this.password = password;
+            this.inviter = inviter;
+        }
+
+        public boolean execute(Account account) {
+            if (jid != null) {
+                Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
+                if (!conversation.getMucOptions().online()) {
+                    conversation.getMucOptions().setPassword(password);
+                    mXmppConnectionService.databaseBackend.updateConversation(conversation);
+                    mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
+                    mXmppConnectionService.updateConversationUi();
+                }
+                return true;
+            }
+            return false;
+        }
+    }
 }

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

@@ -285,11 +285,19 @@ public class XmppConnectionService extends Service {
                     }
                 }
             }
-            boolean needsUpdating = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
-            needsUpdating |= account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.getXmppConnection().getFeatures().httpUpload(0));
-            if (needsUpdating) {
+            boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
+            boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.getXmppConnection().getFeatures().httpUpload(0));
+            if (loggedInSuccessfully || gainedFeature) {
                 databaseBackend.updateAccount(account);
             }
+
+            if (loggedInSuccessfully) {
+                if (!TextUtils.isEmpty(account.getDisplayName())) {
+                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": display name wasn't empty on first log in. publishing");
+                    publishDisplayName(account);
+                }
+            }
+
             account.getRoster().clearPresences();
             mJingleConnectionManager.cancelInTransmission();
             mQuickConversationsService.considerSyncBackground(false);
@@ -3828,14 +3836,17 @@ public class XmppConnectionService extends Service {
 
 	public void publishDisplayName(Account account) {
 		String displayName = account.getDisplayName();
-		if (displayName != null && !displayName.isEmpty()) {
-			IqPacket publish = mIqGenerator.publishNick(displayName);
-			sendIqPacket(account, publish, (account1, packet) -> {
-				if (packet.getType() == IqPacket.TYPE.ERROR) {
-					Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": could not publish nick");
-				}
-			});
-		}
+		final IqPacket request;
+		if (TextUtils.isEmpty(displayName)) {
+            request = mIqGenerator.deleteNode(Namespace.NICK);
+		} else {
+            request = mIqGenerator.publishNick(displayName);
+        }
+        sendIqPacket(account, request, (account1, packet) -> {
+            if (packet.getType() == IqPacket.TYPE.ERROR) {
+                Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name "+packet.toString());
+            }
+        });
 	}
 
 	public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {

src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java ๐Ÿ”—

@@ -74,1157 +74,1172 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
 import rocks.xmpp.addr.Jid;
 
 public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
-		OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
-
-	public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification";
-
-	private static final int REQUEST_DATA_SAVER = 0xf244;
-	private static final int REQUEST_CHANGE_STATUS = 0xee11;
-
-	private AlertDialog mCaptchaDialog = null;
-
-	private Jid jidToEdit;
-	private boolean mInitMode = false;
-	private boolean mUsernameMode = Config.DOMAIN_LOCK != null;
-	private boolean mShowOptions = false;
-	private Account mAccount;
-	private String messageFingerprint;
-
-	private final PendingItem<PresenceTemplate> mPendingPresenceTemplate = new PendingItem<>();
-
-	private boolean mFetchingAvatar = false;
-
-	private final OnClickListener mSaveButtonClickListener = new OnClickListener() {
-
-		@Override
-		public void onClick(final View v) {
-			final String password = binding.accountPassword.getText().toString();
-			final boolean wasDisabled = mAccount != null && mAccount.getStatus() == Account.State.DISABLED;
-			final boolean accountInfoEdited = accountInfoEdited();
-
-			if (!mInitMode && passwordChangedInMagicCreateMode()) {
-				gotoChangePassword(password);
-				return;
-			}
-			if (mInitMode && mAccount != null) {
-				mAccount.setOption(Account.OPTION_DISABLED, false);
-			}
-			if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited) {
-				mAccount.setOption(Account.OPTION_DISABLED, false);
-				if (!xmppConnectionService.updateAccount(mAccount)) {
-					Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
-				}
-				return;
-			}
-			final boolean registerNewAccount = binding.accountRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI;
-			if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) {
-				binding.accountJidLayout.setError(getString(R.string.invalid_username));
-				removeErrorsOnAllBut(binding.accountJidLayout);
-				binding.accountJid.requestFocus();
-				return;
-			}
-
-			XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
-			boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB;
-			boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
-			final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
-			URL url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null;
-			if (url != null && !wasDisabled) {
-				try {
-					startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
-					return;
-				} catch (ActivityNotFoundException e) {
-					Toast.makeText(EditAccountActivity.this, R.string.application_found_to_open_website, Toast.LENGTH_SHORT).show();
-					return;
-				}
-			}
-
-			final Jid jid;
-			try {
-				if (mUsernameMode) {
-					jid = Jid.of(binding.accountJid.getText().toString(), getUserModeDomain(), null);
-				} else {
-					jid = Jid.of(binding.accountJid.getText().toString());
-				}
-			} catch (final NullPointerException | IllegalArgumentException e) {
-				if (mUsernameMode) {
-					binding.accountJidLayout.setError(getString(R.string.invalid_username));
-				} else {
-					binding.accountJidLayout.setError(getString(R.string.invalid_jid));
-				}
-				binding.accountJid.requestFocus();
-				removeErrorsOnAllBut(binding.accountJidLayout);
-				return;
-			}
-			String hostname = null;
-			int numericPort = 5222;
-			if (mShowOptions) {
-				hostname = binding.hostname.getText().toString().replaceAll("\\s", "");
-				final String port = binding.port.getText().toString().replaceAll("\\s", "");
-				if (hostname.contains(" ")) {
-					binding.hostnameLayout.setError(getString(R.string.not_valid_hostname));
-					binding.hostname.requestFocus();
-					removeErrorsOnAllBut(binding.hostnameLayout);
-					return;
-				}
-				try {
-					numericPort = Integer.parseInt(port);
-					if (numericPort < 0 || numericPort > 65535) {
-						binding.portLayout.setError(getString(R.string.not_a_valid_port));
-						removeErrorsOnAllBut(binding.portLayout);
-						binding.port.requestFocus();
-						return;
-					}
-
-				} catch (NumberFormatException e) {
-					binding.portLayout.setError(getString(R.string.not_a_valid_port));
-					removeErrorsOnAllBut(binding.portLayout);
-					binding.port.requestFocus();
-					return;
-				}
-			}
-
-			if (jid.getLocal() == null) {
-				if (mUsernameMode) {
-					binding.accountJidLayout.setError(getString(R.string.invalid_username));
-				} else {
-					binding.accountJidLayout.setError(getString(R.string.invalid_jid));
-				}
-				removeErrorsOnAllBut(binding.accountJidLayout);
-				binding.accountJid.requestFocus();
-				return;
-			}
-			if (mAccount != null) {
-				if (mInitMode && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
-					mAccount.setOption(Account.OPTION_MAGIC_CREATE, mAccount.getPassword().contains(password));
-				}
-				mAccount.setJid(jid);
-				mAccount.setPort(numericPort);
-				mAccount.setHostname(hostname);
-				binding.accountJidLayout.setError(null);
-				mAccount.setPassword(password);
-				mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
-				if (!xmppConnectionService.updateAccount(mAccount)) {
-					Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
-					return;
-				}
-			} else {
-				if (xmppConnectionService.findAccountByJid(jid) != null) {
-					binding.accountJidLayout.setError(getString(R.string.account_already_exists));
-					removeErrorsOnAllBut(binding.accountJidLayout);
-					binding.accountJid.requestFocus();
-					return;
-				}
-				mAccount = new Account(jid.asBareJid(), password);
-				mAccount.setPort(numericPort);
-				mAccount.setHostname(hostname);
-				mAccount.setOption(Account.OPTION_USETLS, true);
-				mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
-				mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
-				xmppConnectionService.createAccount(mAccount);
-			}
-			binding.hostnameLayout.setError(null);
-			binding.portLayout.setError(null);
-			if (mAccount.isEnabled()
-					&& !registerNewAccount
-					&& !mInitMode) {
-				finish();
-			} else {
-				updateSaveButton();
-				updateAccountInformation(true);
-			}
-
-		}
-	};
-	private final OnClickListener mCancelButtonClickListener = v -> {
-		deleteAccountAndReturnIfNecessary();
-		finish();
-	};
-	private Toast mFetchingMamPrefsToast;
-	private String mSavedInstanceAccount;
-	private boolean mSavedInstanceInit = false;
-	private XmppUri pendingUri = null;
-	private boolean mUseTor;
-	private ActivityEditAccountBinding binding;
-
-	public void refreshUiReal() {
-		invalidateOptionsMenu();
-		if (mAccount != null
-				&& mAccount.getStatus() != Account.State.ONLINE
-				&& mFetchingAvatar) {
-			Intent intent = new Intent(this, StartConversationActivity.class);
-			StartConversationActivity.addInviteUri(intent, getIntent());
-			startActivity(intent);
-			finish();
-		} else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
-			if (!mFetchingAvatar) {
-				mFetchingAvatar = true;
-				xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback);
-			}
-		}
-		if (mAccount != null) {
-			updateAccountInformation(false);
-		}
-		updateSaveButton();
-	}
-
-	@Override
-	public boolean onNavigateUp() {
-		deleteAccountAndReturnIfNecessary();
-		return super.onNavigateUp();
-	}
-
-	@Override
-	public void onBackPressed() {
-		deleteAccountAndReturnIfNecessary();
-		super.onBackPressed();
-	}
-
-	private void deleteAccountAndReturnIfNecessary() {
-		if (mInitMode && mAccount != null && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
-			xmppConnectionService.deleteAccount(mAccount);
-		}
-
-		if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
-			Intent intent = SignupUtils.getSignUpIntent(this);
-			startActivity(intent);
-		}
-	}
-
-	@Override
-	public void onAccountUpdate() {
-		refreshUi();
-	}
-
-	private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
-
-		@Override
-		public void userInputRequried(final PendingIntent pi, final Avatar avatar) {
-			finishInitialSetup(avatar);
-		}
-
-		@Override
-		public void success(final Avatar avatar) {
-			finishInitialSetup(avatar);
-		}
-
-		@Override
-		public void error(final int errorCode, final Avatar avatar) {
-			finishInitialSetup(avatar);
-		}
-	};
-	private final TextWatcher mTextWatcher = new TextWatcher() {
-
-		@Override
-		public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
-			updatePortLayout();
-			updateSaveButton();
-		}
-
-		@Override
-		public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
-		}
-
-		@Override
-		public void afterTextChanged(final Editable s) {
-
-		}
-	};
-
-	private View.OnFocusChangeListener mEditTextFocusListener = new View.OnFocusChangeListener() {
-		@Override
-		public void onFocusChange(View view, boolean b) {
-			EditText et = (EditText) view;
-			if (b) {
-				int resId = mUsernameMode ? R.string.username : R.string.account_settings_example_jabber_id;
-				if (view.getId() == R.id.hostname) {
-					resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example;
-				}
-				final int res = resId;
-				new Handler().postDelayed(() -> et.setHint(res), 200);
-			} else {
-				et.setHint(null);
-			}
-		}
-	};
-
-
-	private final OnClickListener mAvatarClickListener = new OnClickListener() {
-		@Override
-		public void onClick(final View view) {
-			if (mAccount != null) {
-				final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
-				intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
-				startActivity(intent);
-			}
-		}
-	};
-
-	protected void finishInitialSetup(final Avatar avatar) {
-		runOnUiThread(() -> {
-			SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this);
-			final Intent intent;
-			final XmppConnection connection = mAccount.getXmppConnection();
-			final boolean wasFirstAccount = xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1;
-			if (avatar != null || (connection != null && !connection.getFeatures().pep())) {
-				intent = new Intent(getApplicationContext(), StartConversationActivity.class);
-				if (wasFirstAccount) {
-					intent.putExtra("init", true);
-				}
-			} else {
-				intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
-				intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
-				intent.putExtra("setup", true);
-			}
-			if (wasFirstAccount) {
-				intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-			}
-			StartConversationActivity.addInviteUri(intent, getIntent());
-			startActivity(intent);
-			finish();
-		});
-	}
-
-	@Override
-	public void onActivityResult(int requestCode, int resultCode, Intent data) {
-		super.onActivityResult(requestCode, resultCode, data);
-		if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
-			updateAccountInformation(mAccount == null);
-		}
-		if (requestCode == REQUEST_CHANGE_STATUS) {
-			PresenceTemplate template = mPendingPresenceTemplate.pop();
-			if (template != null && resultCode == Activity.RESULT_OK) {
-				generateSignature(data, template);
-			} else {
-				Log.d(Config.LOGTAG, "pgp result not ok");
-			}
-		}
-	}
-
-	@Override
-	protected void processFingerprintVerification(XmppUri uri) {
-		processFingerprintVerification(uri, true);
-	}
-
-
-	protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) {
-		if (mAccount != null && mAccount.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
-			if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) {
-				Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
-				updateAccountInformation(false);
-			}
-		} else if (showWarningToast) {
-			Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
-		}
-	}
-
-	private void updatePortLayout() {
-		String hostname = this.binding.hostname.getText().toString();
-		this.binding.portLayout.setEnabled(!TextUtils.isEmpty(hostname));
-	}
-
-	protected void updateSaveButton() {
-		boolean accountInfoEdited = accountInfoEdited();
-
-		if (!mInitMode && passwordChangedInMagicCreateMode()) {
-			this.binding.saveButton.setText(R.string.change_password);
-			this.binding.saveButton.setEnabled(true);
-		} else if (accountInfoEdited && !mInitMode) {
-			this.binding.saveButton.setText(R.string.save);
-			this.binding.saveButton.setEnabled(true);
-		} else if (mAccount != null
-				&& (mAccount.getStatus() == Account.State.CONNECTING || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL || mFetchingAvatar)) {
-			this.binding.saveButton.setEnabled(false);
-			this.binding.saveButton.setText(R.string.account_status_connecting);
-		} else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) {
-			this.binding.saveButton.setEnabled(true);
-			this.binding.saveButton.setText(R.string.enable);
-		} else {
-			this.binding.saveButton.setEnabled(true);
-			if (!mInitMode) {
-				if (mAccount != null && mAccount.isOnlineAndConnected()) {
-					this.binding.saveButton.setText(R.string.save);
-					if (!accountInfoEdited) {
-						this.binding.saveButton.setEnabled(false);
-					}
-				} else {
-					XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
-					URL url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null;
-					if (url != null) {
-						this.binding.saveButton.setText(R.string.open_website);
-					} else {
-						this.binding.saveButton.setText(R.string.connect);
-					}
-				}
-			} else {
-				XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
-				URL url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null;
-				if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) {
-					this.binding.saveButton.setText(R.string.open_website);
-				} else {
-					this.binding.saveButton.setText(R.string.next);
-				}
-			}
-		}
-	}
-
-	protected boolean accountInfoEdited() {
-		if (this.mAccount == null) {
-			return false;
-		}
-		return jidEdited() ||
-				!this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString()) ||
-				!this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) ||
-				!String.valueOf(this.mAccount.getPort()).equals(this.binding.port.getText().toString());
-	}
-
-	protected boolean jidEdited() {
-		final String unmodified;
-		if (mUsernameMode) {
-			unmodified = this.mAccount.getJid().getLocal();
-		} else {
-			unmodified = this.mAccount.getJid().asBareJid().toString();
-		}
-		return !unmodified.equals(this.binding.accountJid.getText().toString());
-	}
-
-	protected boolean passwordChangedInMagicCreateMode() {
-		return mAccount != null
-				&& mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
-				&& !this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString())
-				&& !this.jidEdited()
-				&& mAccount.isOnlineAndConnected();
-	}
-
-	@Override
-	protected String getShareableUri(boolean http) {
-		if (mAccount != null) {
-			return http ? mAccount.getShareableLink() : mAccount.getShareableUri();
-		} else {
-			return null;
-		}
-	}
-
-	@Override
-	protected void onCreate(final Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
-		if (savedInstanceState != null) {
-			this.mSavedInstanceAccount = savedInstanceState.getString("account");
-			this.mSavedInstanceInit = savedInstanceState.getBoolean("initMode", false);
-		}
-		this.binding = DataBindingUtil.setContentView(this, R.layout.activity_edit_account);
-		setSupportActionBar((Toolbar) binding.toolbar);
-		binding.accountJid.addTextChangedListener(this.mTextWatcher);
-		binding.accountJid.setOnFocusChangeListener(this.mEditTextFocusListener);
-		this.binding.accountPassword.addTextChangedListener(this.mTextWatcher);
-
-		this.binding.avater.setOnClickListener(this.mAvatarClickListener);
-		this.binding.hostname.addTextChangedListener(mTextWatcher);
-		this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener);
-		this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog());
-		this.binding.port.setText("5222");
-		this.binding.port.addTextChangedListener(mTextWatcher);
-		this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener);
-		this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener);
-		if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) {
-			changeMoreTableVisibility(true);
-		}
-		final OnCheckedChangeListener OnCheckedShowConfirmPassword = (buttonView, isChecked) -> updateSaveButton();
-		this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
-		if (Config.DISALLOW_REGISTRATION_IN_UI) {
-			this.binding.accountRegisterNew.setVisibility(View.GONE);
-		}
-	}
-
-	@Override
-	public boolean onCreateOptionsMenu(final Menu menu) {
-		super.onCreateOptionsMenu(menu);
-		getMenuInflater().inflate(R.menu.editaccount, menu);
-		final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
-		final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
-		final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
-		final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
-		final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
-		final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
-		final MenuItem share = menu.findItem(R.id.action_share);
-		renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
-
-		share.setVisible(mAccount != null && !mInitMode);
-
-		if (mAccount != null && mAccount.isOnlineAndConnected()) {
-			if (!mAccount.getXmppConnection().getFeatures().blocking()) {
-				showBlocklist.setVisible(false);
-			}
-
-			if (!mAccount.getXmppConnection().getFeatures().register()) {
-				changePassword.setVisible(false);
-			}
-			mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
-			changePresence.setVisible(!mInitMode);
-		} else {
-			showBlocklist.setVisible(false);
-			showMoreInfo.setVisible(false);
-			changePassword.setVisible(false);
-			mamPrefs.setVisible(false);
-			changePresence.setVisible(false);
-		}
-		return super.onCreateOptionsMenu(menu);
-	}
-
-	@Override
-	public boolean onPrepareOptionsMenu(Menu menu) {
-		final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
-		if (showMoreInfo.isVisible()) {
-			showMoreInfo.setChecked(binding.serverInfoMore.getVisibility() == View.VISIBLE);
-		}
-		return super.onPrepareOptionsMenu(menu);
-	}
-
-	@Override
-	protected void onStart() {
-		super.onStart();
-		final Intent intent = getIntent();
-		final int theme = findTheme();
-		if (this.mTheme != theme) {
-			recreate();
-		} else if (intent != null) {
-			try {
-				this.jidToEdit = Jid.of(intent.getStringExtra("jid"));
-			} catch (final IllegalArgumentException | NullPointerException ignored) {
-				this.jidToEdit = null;
-			}
-			if (jidToEdit != null && intent.getData() != null && intent.getBooleanExtra("scanned", false)) {
-				final XmppUri uri = new XmppUri(intent.getData());
-				if (xmppConnectionServiceBound) {
-					processFingerprintVerification(uri, false);
-				} else {
-					this.pendingUri = uri;
-				}
-			}
-			boolean init = intent.getBooleanExtra("init", false);
-			boolean openedFromNotification = intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false);
-			this.mInitMode = init || this.jidToEdit == null;
-			this.messageFingerprint = intent.getStringExtra("fingerprint");
-			if (!mInitMode) {
-				this.binding.accountRegisterNew.setVisibility(View.GONE);
-				setTitle(getString(R.string.account_details));
-				configureActionBar(getSupportActionBar(), !openedFromNotification);
-			} else {
-				this.binding.avater.setVisibility(View.GONE);
-				configureActionBar(getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null));
-				setTitle(R.string.action_add_account);
-			}
-		}
-		SharedPreferences preferences = getPreferences();
-		mUseTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", getResources().getBoolean(R.bool.use_tor));
-		this.mShowOptions = mUseTor || (QuickConversationsService.isConversations() && preferences.getBoolean("show_connection_options", getResources().getBoolean(R.bool.show_connection_options)));
-		this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
-	}
-
-	@Override
-	public void onNewIntent(Intent intent) {
-		if (intent != null && intent.getData() != null) {
-			final XmppUri uri = new XmppUri(intent.getData());
-			if (xmppConnectionServiceBound) {
-				processFingerprintVerification(uri, false);
-			} else {
-				this.pendingUri = uri;
-			}
-		}
-	}
-
-	@Override
-	public void onSaveInstanceState(final Bundle savedInstanceState) {
-		if (mAccount != null) {
-			savedInstanceState.putString("account", mAccount.getJid().asBareJid().toString());
-			savedInstanceState.putBoolean("initMode", mInitMode);
-			savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE);
-		}
-		super.onSaveInstanceState(savedInstanceState);
-	}
-
-	protected void onBackendConnected() {
-		boolean init = true;
-		if (mSavedInstanceAccount != null) {
-			try {
-				this.mAccount = xmppConnectionService.findAccountByJid(Jid.of(mSavedInstanceAccount));
-				this.mInitMode = mSavedInstanceInit;
-				init = false;
-			} catch (IllegalArgumentException e) {
-				this.mAccount = null;
-			}
-
-		} else if (this.jidToEdit != null) {
-			this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
-		}
-
-		if (mAccount != null) {
-			this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
-			this.mUsernameMode |= mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && mAccount.isOptionSet(Account.OPTION_REGISTER);
-			if (this.mAccount.getPrivateKeyAlias() != null) {
-				this.binding.accountPassword.setHint(R.string.authenticate_with_certificate);
-				if (this.mInitMode) {
-					this.binding.accountPassword.requestFocus();
-				}
-			}
-			if (mPendingFingerprintVerificationUri != null) {
-				processFingerprintVerification(mPendingFingerprintVerificationUri, false);
-				mPendingFingerprintVerificationUri = null;
-			}
-			updateAccountInformation(init);
-		}
-
-
-		if (Config.MAGIC_CREATE_DOMAIN == null && this.xmppConnectionService.getAccounts().size() == 0) {
-			this.binding.cancelButton.setEnabled(false);
-		}
-		if (mUsernameMode) {
-			this.binding.accountJidLayout.setHint(getString(R.string.username_hint));
-			this.binding.accountJid.setHint(R.string.username_hint);
-		} else {
-			final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
-					R.layout.simple_list_item,
-					xmppConnectionService.getKnownHosts());
-			this.binding.accountJid.setAdapter(mKnownHostsAdapter);
-		}
-
-		if (pendingUri != null) {
-			processFingerprintVerification(pendingUri, false);
-			pendingUri = null;
-		}
-		updatePortLayout();
-		updateSaveButton();
-		invalidateOptionsMenu();
-	}
-
-	private String getUserModeDomain() {
-		if (mAccount != null && mAccount.getJid().getDomain() != null) {
-			return mAccount.getJid().getDomain();
-		} else {
-			return Config.DOMAIN_LOCK;
-		}
-	}
-
-	@Override
-	public boolean onOptionsItemSelected(final MenuItem item) {
-		if (MenuDoubleTabUtil.shouldIgnoreTap()) {
-			return false;
-		}
-		switch (item.getItemId()) {
-			case R.id.action_show_block_list:
-				final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class);
-				showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
-				startActivity(showBlocklistIntent);
-				break;
-			case R.id.action_server_info_show_more:
-				changeMoreTableVisibility(!item.isChecked());
-				break;
-			case R.id.action_share_barcode:
-				shareBarcode();
-				break;
-			case R.id.action_share_http:
-				shareLink(true);
-				break;
-			case R.id.action_share_uri:
-				shareLink(false);
-				break;
-			case R.id.action_change_password_on_server:
-				gotoChangePassword(null);
-				break;
-			case R.id.action_mam_prefs:
-				editMamPrefs();
-				break;
-			case R.id.action_renew_certificate:
-				renewCertificate();
-				break;
-			case R.id.action_change_presence:
-				changePresence();
-				break;
-		}
-		return super.onOptionsItemSelected(item);
-	}
-
-	private void shareBarcode() {
-		Intent intent = new Intent(Intent.ACTION_SEND);
-		intent.putExtra(Intent.EXTRA_STREAM, BarcodeProvider.getUriForAccount(this, mAccount));
-		intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-		intent.setType("image/png");
-		startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
-	}
-
-	private void changeMoreTableVisibility(boolean visible) {
-		binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE);
-	}
-
-	private void gotoChangePassword(String newPassword) {
-		final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
-		changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
-		if (newPassword != null) {
-			changePasswordIntent.putExtra("password", newPassword);
-		}
-		startActivity(changePasswordIntent);
-	}
-
-	private void renewCertificate() {
-		KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
-	}
-
-	private void changePresence() {
-		SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
-		boolean manualStatus = sharedPreferences.getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence));
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
-		final DialogPresenceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
-		String current = mAccount.getPresenceStatusMessage();
-		if (current != null && !current.trim().isEmpty()) {
-			binding.statusMessage.append(current);
-		}
-		setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding);
-		binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE);
-		List<PresenceTemplate> templates = xmppConnectionService.getPresenceTemplates(mAccount);
-		PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter(this, R.layout.simple_list_item, templates);
-		binding.statusMessage.setAdapter(presenceTemplateAdapter);
-		binding.statusMessage.setOnItemClickListener((parent, view, position, id) -> {
-			PresenceTemplate template = (PresenceTemplate) parent.getItemAtPosition(position);
-			setAvailabilityRadioButton(template.getStatus(), binding);
-		});
-		builder.setTitle(R.string.edit_status_message_title);
-		builder.setView(binding.getRoot());
-		builder.setNegativeButton(R.string.cancel, null);
-		builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
-			PresenceTemplate template = new PresenceTemplate(getAvailabilityRadioButton(binding), binding.statusMessage.getText().toString().trim());
-			if (mAccount.getPgpId() != 0 && hasPgp()) {
-				generateSignature(null, template);
-			} else {
-				xmppConnectionService.changeStatus(mAccount, template, null);
-			}
-		});
-		builder.create().show();
-	}
-
-	private void generateSignature(Intent intent, PresenceTemplate template) {
-		xmppConnectionService.getPgpEngine().generateSignature(intent, mAccount, template.getStatusMessage(), new UiCallback<String>() {
-			@Override
-			public void success(String signature) {
-				xmppConnectionService.changeStatus(mAccount, template, signature);
-			}
-
-			@Override
-			public void error(int errorCode, String object) {
-
-			}
-
-			@Override
-			public void userInputRequried(PendingIntent pi, String object) {
-				mPendingPresenceTemplate.push(template);
-				try {
-					startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0);
-				} catch (final IntentSender.SendIntentException ignored) {
-				}
-			}
-		});
-	}
-
-	private static void setAvailabilityRadioButton(Presence.Status status, DialogPresenceBinding binding) {
-		if (status == null) {
-			binding.online.setChecked(true);
-			return;
-		}
-		switch (status) {
-			case DND:
-				binding.dnd.setChecked(true);
-				break;
-			case XA:
-				binding.xa.setChecked(true);
-				break;
-			case AWAY:
-				binding.xa.setChecked(true);
-				break;
-			default:
-				binding.online.setChecked(true);
-		}
-	}
-
-	private static Presence.Status getAvailabilityRadioButton(DialogPresenceBinding binding) {
-		if (binding.dnd.isChecked()) {
-			return Presence.Status.DND;
-		} else if (binding.xa.isChecked()) {
-			return Presence.Status.XA;
-		} else if (binding.away.isChecked()) {
-			return Presence.Status.AWAY;
-		} else {
-			return Presence.Status.ONLINE;
-		}
-	}
-
-	@Override
-	public void alias(String alias) {
-		if (alias != null) {
-			xmppConnectionService.updateKeyInAccount(mAccount, alias);
-		}
-	}
-
-	private void updateAccountInformation(boolean init) {
-		if (init) {
-			this.binding.accountJid.getEditableText().clear();
-			if (mUsernameMode) {
-				this.binding.accountJid.getEditableText().append(this.mAccount.getJid().getLocal());
-			} else {
-				this.binding.accountJid.getEditableText().append(this.mAccount.getJid().asBareJid().toString());
-			}
-			this.binding.accountPassword.getEditableText().clear();
-			this.binding.accountPassword.getEditableText().append(this.mAccount.getPassword());
-			this.binding.hostname.setText("");
-			this.binding.hostname.getEditableText().append(this.mAccount.getHostname());
-			this.binding.port.setText("");
-			this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort()));
-			this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
-
-		}
-
-		final boolean editable = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && QuickConversationsService.isConversations();
-		this.binding.accountJid.setEnabled(editable);
-		this.binding.accountJid.setFocusable(editable);
-		this.binding.accountJid.setFocusableInTouchMode(editable);
-
-
-		if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
-			this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(true);
-		} else {
-			this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(false);
-		}
-
-		if (!mInitMode) {
-			this.binding.avater.setVisibility(View.VISIBLE);
-			this.binding.avater.setImageBitmap(avatarService().get(this.mAccount, (int) getResources().getDimension(R.dimen.avatar_on_details_screen_size)));
-		} else {
-			this.binding.avater.setVisibility(View.GONE);
-		}
-		this.binding.accountRegisterNew.setChecked(this.mAccount.isOptionSet(Account.OPTION_REGISTER));
-		if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
-			if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
-				ActionBar actionBar = getSupportActionBar();
-				if (actionBar != null) {
-					actionBar.setTitle(R.string.create_account);
-				}
-			}
-			this.binding.accountRegisterNew.setVisibility(View.GONE);
-		} else if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
-			this.binding.accountRegisterNew.setVisibility(View.VISIBLE);
-		} else {
-			this.binding.accountRegisterNew.setVisibility(View.GONE);
-		}
-		if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
-			Features features = this.mAccount.getXmppConnection().getFeatures();
-			this.binding.stats.setVisibility(View.VISIBLE);
-			boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery();
-			boolean showDataSaverWarning = isAffectedByDataSaver();
-			showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning);
-			this.binding.sessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection()
-					.getLastSessionEstablished()));
-			if (features.rosterVersioning()) {
-				this.binding.serverInfoRosterVersion.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable);
-			}
-			if (features.carbons()) {
-				this.binding.serverInfoCarbons.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable);
-			}
-			if (features.mam()) {
-				this.binding.serverInfoMam.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoMam.setText(R.string.server_info_unavailable);
-			}
-			if (features.csi()) {
-				this.binding.serverInfoCsi.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoCsi.setText(R.string.server_info_unavailable);
-			}
-			if (features.blocking()) {
-				this.binding.serverInfoBlocking.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoBlocking.setText(R.string.server_info_unavailable);
-			}
-			if (features.sm()) {
-				this.binding.serverInfoSm.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoSm.setText(R.string.server_info_unavailable);
-			}
-			if (features.pep()) {
-				AxolotlService axolotlService = this.mAccount.getAxolotlService();
-				if (axolotlService != null && axolotlService.isPepBroken()) {
-					this.binding.serverInfoPep.setText(R.string.server_info_broken);
-				} else if (features.pepPublishOptions() || features.pepOmemoWhitelisted()) {
-					this.binding.serverInfoPep.setText(R.string.server_info_available);
-				} else {
-					this.binding.serverInfoPep.setText(R.string.server_info_partial);
-				}
-			} else {
-				this.binding.serverInfoPep.setText(R.string.server_info_unavailable);
-			}
-			if (features.httpUpload(0)) {
-				this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
-			} else if (features.p1S3FileTransfer()) {
-				this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer);
-				this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
-			}
-
-			this.binding.pushRow.setVisibility(xmppConnectionService.getPushManagementService().isStub() ? View.GONE : View.VISIBLE);
-
-			if (xmppConnectionService.getPushManagementService().available(mAccount)) {
-				this.binding.serverInfoPush.setText(R.string.server_info_available);
-			} else {
-				this.binding.serverInfoPush.setText(R.string.server_info_unavailable);
-			}
-			final long pgpKeyId = this.mAccount.getPgpId();
-			if (pgpKeyId != 0 && Config.supportOpenPgp()) {
-				OnClickListener openPgp = view -> launchOpenKeyChain(pgpKeyId);
-				OnClickListener delete = view -> showDeletePgpDialog();
-				this.binding.pgpFingerprintBox.setVisibility(View.VISIBLE);
-				this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId));
-				this.binding.pgpFingerprint.setOnClickListener(openPgp);
-				if ("pgp".equals(messageFingerprint)) {
-					this.binding.pgpFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
-				}
-				this.binding.pgpFingerprintDesc.setOnClickListener(openPgp);
-				this.binding.actionDeletePgp.setOnClickListener(delete);
-			} else {
-				this.binding.pgpFingerprintBox.setVisibility(View.GONE);
-			}
-			final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint();
-			if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
-				this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
-				if (ownAxolotlFingerprint.equals(messageFingerprint)) {
-					this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
-					this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint_selected_message);
-				} else {
-					this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption);
-					this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint);
-				}
-				this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2)));
-				this.binding.actionCopyAxolotlToClipboard.setVisibility(View.VISIBLE);
-				this.binding.actionCopyAxolotlToClipboard.setOnClickListener(v -> copyOmemoFingerprint(ownAxolotlFingerprint));
-			} else {
-				this.binding.axolotlFingerprintBox.setVisibility(View.GONE);
-			}
-			boolean hasKeys = false;
-			binding.otherDeviceKeys.removeAllViews();
-			for (XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
-				if (!session.getTrust().isCompromised()) {
-					boolean highlight = session.getFingerprint().equals(messageFingerprint);
-					addFingerprintRow(binding.otherDeviceKeys, session, highlight);
-					hasKeys = true;
-				}
-			}
-			if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices
-				this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE);
-				Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
-				if (otherDevices == null || otherDevices.isEmpty()) {
-					binding.clearDevices.setVisibility(View.GONE);
-				} else {
-					binding.clearDevices.setVisibility(View.VISIBLE);
-				}
-			} else {
-				this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
-			}
-		} else {
-			final TextInputLayout errorLayout;
-			if (this.mAccount.errorStatus()) {
-				if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED) {
-					errorLayout = this.binding.accountPasswordLayout;
-				} else if (mShowOptions
-						&& this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND
-						&& this.binding.hostname.getText().length() > 0) {
-					errorLayout = this.binding.hostnameLayout;
-				} else {
-					errorLayout = this.binding.accountJidLayout;
-				}
-				errorLayout.setError(getString(this.mAccount.getStatus().getReadableId()));
-				if (init || !accountInfoEdited()) {
-					errorLayout.requestFocus();
-				}
-			} else {
-				errorLayout = null;
-			}
-			removeErrorsOnAllBut(errorLayout);
-			this.binding.stats.setVisibility(View.GONE);
-			this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
-		}
-	}
-
-	private void removeErrorsOnAllBut(TextInputLayout exception) {
-		if (this.binding.accountJidLayout != exception) {
-			this.binding.accountJidLayout.setErrorEnabled(false);
-			this.binding.accountJidLayout.setError(null);
-		}
-		if (this.binding.accountPasswordLayout != exception) {
-			this.binding.accountPasswordLayout.setErrorEnabled(false);
-			this.binding.accountPasswordLayout.setError(null);
-		}
-		if (this.binding.hostnameLayout != exception) {
-			this.binding.hostnameLayout.setErrorEnabled(false);
-			this.binding.hostnameLayout.setError(null);
-		}
-		if (this.binding.portLayout != exception) {
-			this.binding.portLayout.setErrorEnabled(false);
-			this.binding.portLayout.setError(null);
-		}
-	}
-
-	private void showDeletePgpDialog() {
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
-		builder.setTitle(R.string.unpublish_pgp);
-		builder.setMessage(R.string.unpublish_pgp_message);
-		builder.setNegativeButton(R.string.cancel, null);
-		builder.setPositiveButton(R.string.confirm, (dialogInterface, i) -> {
-			mAccount.setPgpSignId(0);
-			mAccount.unsetPgpSignature();
-			xmppConnectionService.databaseBackend.updateAccount(mAccount);
-			xmppConnectionService.sendPresence(mAccount);
-			refreshUiReal();
-		});
-		builder.create().show();
-	}
-
-	private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) {
-		this.binding.osOptimization.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
-		if (showDataSaverWarning) {
-			this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled);
-			this.binding.osOptimizationBody.setText(R.string.data_saver_enabled_explained);
-			this.binding.osOptimizationDisable.setText(R.string.allow);
-			this.binding.osOptimizationDisable.setOnClickListener(v -> {
-				Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
-				Uri uri = Uri.parse("package:" + getPackageName());
-				intent.setData(uri);
-				try {
-					startActivityForResult(intent, REQUEST_DATA_SAVER);
-				} catch (ActivityNotFoundException e) {
-					Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_data_saver, Toast.LENGTH_SHORT).show();
-				}
-			});
-		} else if (showBatteryWarning) {
-			this.binding.osOptimizationDisable.setText(R.string.disable);
-			this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled);
-			this.binding.osOptimizationBody.setText(R.string.battery_optimizations_enabled_explained);
-			this.binding.osOptimizationDisable.setOnClickListener(v -> {
-				Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
-				Uri uri = Uri.parse("package:" + getPackageName());
-				intent.setData(uri);
-				try {
-					startActivityForResult(intent, REQUEST_BATTERY_OP);
-				} catch (ActivityNotFoundException e) {
-					Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
-				}
-			});
-		}
-	}
-
-	public void showWipePepDialog() {
-		Builder builder = new Builder(this);
-		builder.setTitle(getString(R.string.clear_other_devices));
-		builder.setIconAttribute(android.R.attr.alertDialogIcon);
-		builder.setMessage(getString(R.string.clear_other_devices_desc));
-		builder.setNegativeButton(getString(R.string.cancel), null);
-		builder.setPositiveButton(getString(R.string.accept),
-				(dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices());
-		builder.create().show();
-	}
-
-	private void editMamPrefs() {
-		this.mFetchingMamPrefsToast = Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG);
-		this.mFetchingMamPrefsToast.show();
-		xmppConnectionService.fetchMamPreferences(mAccount, this);
-	}
-
-	@Override
-	public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
-		refreshUi();
-	}
-
-	@Override
-	public void onCaptchaRequested(final Account account, final String id, final Data data, final Bitmap captcha) {
-		runOnUiThread(() -> {
-			if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
-				mCaptchaDialog.dismiss();
-			}
-			final Builder builder = new Builder(EditAccountActivity.this);
-			final View view = getLayoutInflater().inflate(R.layout.captcha, null);
-			final ImageView imageView = view.findViewById(R.id.captcha);
-			final EditText input = view.findViewById(R.id.input);
-			imageView.setImageBitmap(captcha);
-
-			builder.setTitle(getString(R.string.captcha_required));
-			builder.setView(view);
-
-			builder.setPositiveButton(getString(R.string.ok),
-					(dialog, which) -> {
-						String rc = input.getText().toString();
-						data.put("username", account.getUsername());
-						data.put("password", account.getPassword());
-						data.put("ocr", rc);
-						data.submit();
-
-						if (xmppConnectionServiceBound) {
-							xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, id, data);
-						}
-					});
-			builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> {
-				if (xmppConnectionService != null) {
-					xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
-				}
-			});
-
-			builder.setOnCancelListener(dialog -> {
-				if (xmppConnectionService != null) {
-					xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
-				}
-			});
-			mCaptchaDialog = builder.create();
-			mCaptchaDialog.show();
-			input.requestFocus();
-		});
-	}
-
-	public void onShowErrorToast(final int resId) {
-		runOnUiThread(() -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show());
-	}
-
-	@Override
-	public void onPreferencesFetched(final Element prefs) {
-		runOnUiThread(() -> {
-			if (mFetchingMamPrefsToast != null) {
-				mFetchingMamPrefsToast.cancel();
-			}
-			Builder builder = new Builder(EditAccountActivity.this);
-			builder.setTitle(R.string.server_side_mam_prefs);
-			String defaultAttr = prefs.getAttribute("default");
-			final List<String> defaults = Arrays.asList("never", "roster", "always");
-			final AtomicInteger choice = new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr)));
-			builder.setSingleChoiceItems(R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which));
-			builder.setNegativeButton(R.string.cancel, null);
-			builder.setPositiveButton(R.string.ok, (dialog, which) -> {
-				prefs.setAttribute("default", defaults.get(choice.get()));
-				xmppConnectionService.pushMamPreferences(mAccount, prefs);
-			});
-			builder.create().show();
-		});
-	}
-
-	@Override
-	public void onPreferencesFetchFailed() {
-		runOnUiThread(() -> {
-			if (mFetchingMamPrefsToast != null) {
-				mFetchingMamPrefsToast.cancel();
-			}
-			Toast.makeText(EditAccountActivity.this, R.string.unable_to_fetch_mam_prefs, Toast.LENGTH_LONG).show();
-		});
-	}
-
-	@Override
-	public void OnUpdateBlocklist(Status status) {
-		refreshUi();
-	}
+        OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
+
+    public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification";
+
+    private static final int REQUEST_DATA_SAVER = 0xf244;
+    private static final int REQUEST_CHANGE_STATUS = 0xee11;
+    private final PendingItem<PresenceTemplate> mPendingPresenceTemplate = new PendingItem<>();
+    private AlertDialog mCaptchaDialog = null;
+    private Jid jidToEdit;
+    private boolean mInitMode = false;
+    private boolean mUsernameMode = Config.DOMAIN_LOCK != null;
+    private boolean mShowOptions = false;
+    private Account mAccount;
+    private final OnClickListener mCancelButtonClickListener = v -> {
+        deleteAccountAndReturnIfNecessary();
+        finish();
+    };
+    private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
+
+        @Override
+        public void userInputRequried(final PendingIntent pi, final Avatar avatar) {
+            finishInitialSetup(avatar);
+        }
+
+        @Override
+        public void success(final Avatar avatar) {
+            finishInitialSetup(avatar);
+        }
+
+        @Override
+        public void error(final int errorCode, final Avatar avatar) {
+            finishInitialSetup(avatar);
+        }
+    };
+    private final OnClickListener mAvatarClickListener = new OnClickListener() {
+        @Override
+        public void onClick(final View view) {
+            if (mAccount != null) {
+                final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
+                intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
+                startActivity(intent);
+            }
+        }
+    };
+    private String messageFingerprint;
+    private boolean mFetchingAvatar = false;
+    private Toast mFetchingMamPrefsToast;
+    private String mSavedInstanceAccount;
+    private boolean mSavedInstanceInit = false;
+    private XmppUri pendingUri = null;
+    private boolean mUseTor;
+    private ActivityEditAccountBinding binding;
+    private final OnClickListener mSaveButtonClickListener = new OnClickListener() {
+
+        @Override
+        public void onClick(final View v) {
+            final String password = binding.accountPassword.getText().toString();
+            final boolean wasDisabled = mAccount != null && mAccount.getStatus() == Account.State.DISABLED;
+            final boolean accountInfoEdited = accountInfoEdited();
+
+            if (!mInitMode && passwordChangedInMagicCreateMode()) {
+                gotoChangePassword(password);
+                return;
+            }
+            if (mInitMode && mAccount != null) {
+                mAccount.setOption(Account.OPTION_DISABLED, false);
+            }
+            if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited) {
+                mAccount.setOption(Account.OPTION_DISABLED, false);
+                if (!xmppConnectionService.updateAccount(mAccount)) {
+                    Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
+                }
+                return;
+            }
+            final boolean registerNewAccount = binding.accountRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI;
+            if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) {
+                binding.accountJidLayout.setError(getString(R.string.invalid_username));
+                removeErrorsOnAllBut(binding.accountJidLayout);
+                binding.accountJid.requestFocus();
+                return;
+            }
+
+            XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
+            boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB;
+            boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
+            final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
+            URL url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null;
+            if (url != null && !wasDisabled) {
+                try {
+                    startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
+                    return;
+                } catch (ActivityNotFoundException e) {
+                    Toast.makeText(EditAccountActivity.this, R.string.application_found_to_open_website, Toast.LENGTH_SHORT).show();
+                    return;
+                }
+            }
+
+            final Jid jid;
+            try {
+                if (mUsernameMode) {
+                    jid = Jid.of(binding.accountJid.getText().toString(), getUserModeDomain(), null);
+                } else {
+                    jid = Jid.of(binding.accountJid.getText().toString());
+                }
+            } catch (final NullPointerException | IllegalArgumentException e) {
+                if (mUsernameMode) {
+                    binding.accountJidLayout.setError(getString(R.string.invalid_username));
+                } else {
+                    binding.accountJidLayout.setError(getString(R.string.invalid_jid));
+                }
+                binding.accountJid.requestFocus();
+                removeErrorsOnAllBut(binding.accountJidLayout);
+                return;
+            }
+            String hostname = null;
+            int numericPort = 5222;
+            if (mShowOptions) {
+                hostname = binding.hostname.getText().toString().replaceAll("\\s", "");
+                final String port = binding.port.getText().toString().replaceAll("\\s", "");
+                if (hostname.contains(" ")) {
+                    binding.hostnameLayout.setError(getString(R.string.not_valid_hostname));
+                    binding.hostname.requestFocus();
+                    removeErrorsOnAllBut(binding.hostnameLayout);
+                    return;
+                }
+                try {
+                    numericPort = Integer.parseInt(port);
+                    if (numericPort < 0 || numericPort > 65535) {
+                        binding.portLayout.setError(getString(R.string.not_a_valid_port));
+                        removeErrorsOnAllBut(binding.portLayout);
+                        binding.port.requestFocus();
+                        return;
+                    }
+
+                } catch (NumberFormatException e) {
+                    binding.portLayout.setError(getString(R.string.not_a_valid_port));
+                    removeErrorsOnAllBut(binding.portLayout);
+                    binding.port.requestFocus();
+                    return;
+                }
+            }
+
+            if (jid.getLocal() == null) {
+                if (mUsernameMode) {
+                    binding.accountJidLayout.setError(getString(R.string.invalid_username));
+                } else {
+                    binding.accountJidLayout.setError(getString(R.string.invalid_jid));
+                }
+                removeErrorsOnAllBut(binding.accountJidLayout);
+                binding.accountJid.requestFocus();
+                return;
+            }
+            if (mAccount != null) {
+                if (mInitMode && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
+                    mAccount.setOption(Account.OPTION_MAGIC_CREATE, mAccount.getPassword().contains(password));
+                }
+                mAccount.setJid(jid);
+                mAccount.setPort(numericPort);
+                mAccount.setHostname(hostname);
+                binding.accountJidLayout.setError(null);
+                mAccount.setPassword(password);
+                mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+                if (!xmppConnectionService.updateAccount(mAccount)) {
+                    Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
+                    return;
+                }
+            } else {
+                if (xmppConnectionService.findAccountByJid(jid) != null) {
+                    binding.accountJidLayout.setError(getString(R.string.account_already_exists));
+                    removeErrorsOnAllBut(binding.accountJidLayout);
+                    binding.accountJid.requestFocus();
+                    return;
+                }
+                mAccount = new Account(jid.asBareJid(), password);
+                mAccount.setPort(numericPort);
+                mAccount.setHostname(hostname);
+                mAccount.setOption(Account.OPTION_USETLS, true);
+                mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
+                mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+                xmppConnectionService.createAccount(mAccount);
+            }
+            binding.hostnameLayout.setError(null);
+            binding.portLayout.setError(null);
+            if (mAccount.isEnabled()
+                    && !registerNewAccount
+                    && !mInitMode) {
+                finish();
+            } else {
+                updateSaveButton();
+                updateAccountInformation(true);
+            }
+
+        }
+    };
+    private final TextWatcher mTextWatcher = new TextWatcher() {
+
+        @Override
+        public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+            updatePortLayout();
+            updateSaveButton();
+        }
+
+        @Override
+        public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
+        }
+
+        @Override
+        public void afterTextChanged(final Editable s) {
+
+        }
+    };
+    private View.OnFocusChangeListener mEditTextFocusListener = new View.OnFocusChangeListener() {
+        @Override
+        public void onFocusChange(View view, boolean b) {
+            EditText et = (EditText) view;
+            if (b) {
+                int resId = mUsernameMode ? R.string.username : R.string.account_settings_example_jabber_id;
+                if (view.getId() == R.id.hostname) {
+                    resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example;
+                }
+                final int res = resId;
+                new Handler().postDelayed(() -> et.setHint(res), 200);
+            } else {
+                et.setHint(null);
+            }
+        }
+    };
+
+    private static void setAvailabilityRadioButton(Presence.Status status, DialogPresenceBinding binding) {
+        if (status == null) {
+            binding.online.setChecked(true);
+            return;
+        }
+        switch (status) {
+            case DND:
+                binding.dnd.setChecked(true);
+                break;
+            case XA:
+                binding.xa.setChecked(true);
+                break;
+            case AWAY:
+                binding.xa.setChecked(true);
+                break;
+            default:
+                binding.online.setChecked(true);
+        }
+    }
+
+    private static Presence.Status getAvailabilityRadioButton(DialogPresenceBinding binding) {
+        if (binding.dnd.isChecked()) {
+            return Presence.Status.DND;
+        } else if (binding.xa.isChecked()) {
+            return Presence.Status.XA;
+        } else if (binding.away.isChecked()) {
+            return Presence.Status.AWAY;
+        } else {
+            return Presence.Status.ONLINE;
+        }
+    }
+
+    public void refreshUiReal() {
+        invalidateOptionsMenu();
+        if (mAccount != null
+                && mAccount.getStatus() != Account.State.ONLINE
+                && mFetchingAvatar) {
+            Intent intent = new Intent(this, StartConversationActivity.class);
+            StartConversationActivity.addInviteUri(intent, getIntent());
+            startActivity(intent);
+            finish();
+        } else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
+            if (!mFetchingAvatar) {
+                mFetchingAvatar = true;
+                xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback);
+            }
+        }
+        if (mAccount != null) {
+            updateAccountInformation(false);
+        }
+        updateSaveButton();
+    }
+
+    @Override
+    public boolean onNavigateUp() {
+        deleteAccountAndReturnIfNecessary();
+        return super.onNavigateUp();
+    }
+
+    @Override
+    public void onBackPressed() {
+        deleteAccountAndReturnIfNecessary();
+        super.onBackPressed();
+    }
+
+    private void deleteAccountAndReturnIfNecessary() {
+        if (mInitMode && mAccount != null && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
+            xmppConnectionService.deleteAccount(mAccount);
+        }
+
+        if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+            Intent intent = SignupUtils.getSignUpIntent(this);
+            startActivity(intent);
+        }
+    }
+
+    @Override
+    public void onAccountUpdate() {
+        refreshUi();
+    }
+
+    protected void finishInitialSetup(final Avatar avatar) {
+        runOnUiThread(() -> {
+            SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this);
+            final Intent intent;
+            final XmppConnection connection = mAccount.getXmppConnection();
+            final boolean wasFirstAccount = xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1;
+            if (avatar != null || (connection != null && !connection.getFeatures().pep())) {
+                intent = new Intent(getApplicationContext(), StartConversationActivity.class);
+                if (wasFirstAccount) {
+                    intent.putExtra("init", true);
+                }
+            } else {
+                intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
+                intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
+                intent.putExtra("setup", true);
+            }
+            if (wasFirstAccount) {
+                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            }
+            StartConversationActivity.addInviteUri(intent, getIntent());
+            startActivity(intent);
+            finish();
+        });
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
+            updateAccountInformation(mAccount == null);
+        }
+        if (requestCode == REQUEST_CHANGE_STATUS) {
+            PresenceTemplate template = mPendingPresenceTemplate.pop();
+            if (template != null && resultCode == Activity.RESULT_OK) {
+                generateSignature(data, template);
+            } else {
+                Log.d(Config.LOGTAG, "pgp result not ok");
+            }
+        }
+    }
+
+    @Override
+    protected void processFingerprintVerification(XmppUri uri) {
+        processFingerprintVerification(uri, true);
+    }
+
+    protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) {
+        if (mAccount != null && mAccount.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+            if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) {
+                Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
+                updateAccountInformation(false);
+            }
+        } else if (showWarningToast) {
+            Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void updatePortLayout() {
+        String hostname = this.binding.hostname.getText().toString();
+        this.binding.portLayout.setEnabled(!TextUtils.isEmpty(hostname));
+    }
+
+    protected void updateSaveButton() {
+        boolean accountInfoEdited = accountInfoEdited();
+
+        if (!mInitMode && passwordChangedInMagicCreateMode()) {
+            this.binding.saveButton.setText(R.string.change_password);
+            this.binding.saveButton.setEnabled(true);
+        } else if (accountInfoEdited && !mInitMode) {
+            this.binding.saveButton.setText(R.string.save);
+            this.binding.saveButton.setEnabled(true);
+        } else if (mAccount != null
+                && (mAccount.getStatus() == Account.State.CONNECTING || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL || mFetchingAvatar)) {
+            this.binding.saveButton.setEnabled(false);
+            this.binding.saveButton.setText(R.string.account_status_connecting);
+        } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) {
+            this.binding.saveButton.setEnabled(true);
+            this.binding.saveButton.setText(R.string.enable);
+        } else {
+            this.binding.saveButton.setEnabled(true);
+            if (!mInitMode) {
+                if (mAccount != null && mAccount.isOnlineAndConnected()) {
+                    this.binding.saveButton.setText(R.string.save);
+                    if (!accountInfoEdited) {
+                        this.binding.saveButton.setEnabled(false);
+                    }
+                } else {
+                    XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
+                    URL url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null;
+                    if (url != null) {
+                        this.binding.saveButton.setText(R.string.open_website);
+                    } else {
+                        this.binding.saveButton.setText(R.string.connect);
+                    }
+                }
+            } else {
+                XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
+                URL url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null;
+                if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) {
+                    this.binding.saveButton.setText(R.string.open_website);
+                } else {
+                    this.binding.saveButton.setText(R.string.next);
+                }
+            }
+        }
+    }
+
+    protected boolean accountInfoEdited() {
+        if (this.mAccount == null) {
+            return false;
+        }
+        return jidEdited() ||
+                !this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString()) ||
+                !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) ||
+                !String.valueOf(this.mAccount.getPort()).equals(this.binding.port.getText().toString());
+    }
+
+    protected boolean jidEdited() {
+        final String unmodified;
+        if (mUsernameMode) {
+            unmodified = this.mAccount.getJid().getLocal();
+        } else {
+            unmodified = this.mAccount.getJid().asBareJid().toString();
+        }
+        return !unmodified.equals(this.binding.accountJid.getText().toString());
+    }
+
+    protected boolean passwordChangedInMagicCreateMode() {
+        return mAccount != null
+                && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
+                && !this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString())
+                && !this.jidEdited()
+                && mAccount.isOnlineAndConnected();
+    }
+
+    @Override
+    protected String getShareableUri(boolean http) {
+        if (mAccount != null) {
+            return http ? mAccount.getShareableLink() : mAccount.getShareableUri();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState != null) {
+            this.mSavedInstanceAccount = savedInstanceState.getString("account");
+            this.mSavedInstanceInit = savedInstanceState.getBoolean("initMode", false);
+        }
+        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_edit_account);
+        setSupportActionBar((Toolbar) binding.toolbar);
+        binding.accountJid.addTextChangedListener(this.mTextWatcher);
+        binding.accountJid.setOnFocusChangeListener(this.mEditTextFocusListener);
+        this.binding.accountPassword.addTextChangedListener(this.mTextWatcher);
+
+        this.binding.avater.setOnClickListener(this.mAvatarClickListener);
+        this.binding.hostname.addTextChangedListener(mTextWatcher);
+        this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener);
+        this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog());
+        this.binding.port.setText("5222");
+        this.binding.port.addTextChangedListener(mTextWatcher);
+        this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener);
+        this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener);
+        if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) {
+            changeMoreTableVisibility(true);
+        }
+        final OnCheckedChangeListener OnCheckedShowConfirmPassword = (buttonView, isChecked) -> updateSaveButton();
+        this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
+        if (Config.DISALLOW_REGISTRATION_IN_UI) {
+            this.binding.accountRegisterNew.setVisibility(View.GONE);
+        }
+        this.binding.actionEditYourName.setOnClickListener(this::onEditYourNameClicked);
+    }
+
+    private void onEditYourNameClicked(View view) {
+        quickEdit(mAccount.getDisplayName(), R.string.your_name, value -> {
+            final String displayName = value.trim();
+            updateDisplayName(displayName);
+            mAccount.setDisplayName(displayName);
+            xmppConnectionService.publishDisplayName(mAccount);
+            return null;
+        }, true);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(final Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.editaccount, menu);
+        final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
+        final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
+        final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
+        final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
+        final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
+        final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
+        final MenuItem share = menu.findItem(R.id.action_share);
+        renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
+
+        share.setVisible(mAccount != null && !mInitMode);
+
+        if (mAccount != null && mAccount.isOnlineAndConnected()) {
+            if (!mAccount.getXmppConnection().getFeatures().blocking()) {
+                showBlocklist.setVisible(false);
+            }
+
+            if (!mAccount.getXmppConnection().getFeatures().register()) {
+                changePassword.setVisible(false);
+            }
+            mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
+            changePresence.setVisible(!mInitMode);
+        } else {
+            showBlocklist.setVisible(false);
+            showMoreInfo.setVisible(false);
+            changePassword.setVisible(false);
+            mamPrefs.setVisible(false);
+            changePresence.setVisible(false);
+        }
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
+        if (showMoreInfo.isVisible()) {
+            showMoreInfo.setChecked(binding.serverInfoMore.getVisibility() == View.VISIBLE);
+        }
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        final Intent intent = getIntent();
+        final int theme = findTheme();
+        if (this.mTheme != theme) {
+            recreate();
+        } else if (intent != null) {
+            try {
+                this.jidToEdit = Jid.of(intent.getStringExtra("jid"));
+            } catch (final IllegalArgumentException | NullPointerException ignored) {
+                this.jidToEdit = null;
+            }
+            if (jidToEdit != null && intent.getData() != null && intent.getBooleanExtra("scanned", false)) {
+                final XmppUri uri = new XmppUri(intent.getData());
+                if (xmppConnectionServiceBound) {
+                    processFingerprintVerification(uri, false);
+                } else {
+                    this.pendingUri = uri;
+                }
+            }
+            boolean init = intent.getBooleanExtra("init", false);
+            boolean openedFromNotification = intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false);
+            this.mInitMode = init || this.jidToEdit == null;
+            this.messageFingerprint = intent.getStringExtra("fingerprint");
+            if (!mInitMode) {
+                this.binding.accountRegisterNew.setVisibility(View.GONE);
+                setTitle(getString(R.string.account_details));
+                configureActionBar(getSupportActionBar(), !openedFromNotification);
+            } else {
+                this.binding.avater.setVisibility(View.GONE);
+                configureActionBar(getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null));
+                setTitle(R.string.action_add_account);
+            }
+        }
+        SharedPreferences preferences = getPreferences();
+        mUseTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", getResources().getBoolean(R.bool.use_tor));
+        this.mShowOptions = mUseTor || (QuickConversationsService.isConversations() && preferences.getBoolean("show_connection_options", getResources().getBoolean(R.bool.show_connection_options)));
+        this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        if (intent != null && intent.getData() != null) {
+            final XmppUri uri = new XmppUri(intent.getData());
+            if (xmppConnectionServiceBound) {
+                processFingerprintVerification(uri, false);
+            } else {
+                this.pendingUri = uri;
+            }
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(final Bundle savedInstanceState) {
+        if (mAccount != null) {
+            savedInstanceState.putString("account", mAccount.getJid().asBareJid().toString());
+            savedInstanceState.putBoolean("initMode", mInitMode);
+            savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE);
+        }
+        super.onSaveInstanceState(savedInstanceState);
+    }
+
+    protected void onBackendConnected() {
+        boolean init = true;
+        if (mSavedInstanceAccount != null) {
+            try {
+                this.mAccount = xmppConnectionService.findAccountByJid(Jid.of(mSavedInstanceAccount));
+                this.mInitMode = mSavedInstanceInit;
+                init = false;
+            } catch (IllegalArgumentException e) {
+                this.mAccount = null;
+            }
+
+        } else if (this.jidToEdit != null) {
+            this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
+        }
+
+        if (mAccount != null) {
+            this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
+            this.mUsernameMode |= mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && mAccount.isOptionSet(Account.OPTION_REGISTER);
+            if (this.mAccount.getPrivateKeyAlias() != null) {
+                this.binding.accountPassword.setHint(R.string.authenticate_with_certificate);
+                if (this.mInitMode) {
+                    this.binding.accountPassword.requestFocus();
+                }
+            }
+            if (mPendingFingerprintVerificationUri != null) {
+                processFingerprintVerification(mPendingFingerprintVerificationUri, false);
+                mPendingFingerprintVerificationUri = null;
+            }
+            updateAccountInformation(init);
+        }
+
+
+        if (Config.MAGIC_CREATE_DOMAIN == null && this.xmppConnectionService.getAccounts().size() == 0) {
+            this.binding.cancelButton.setEnabled(false);
+        }
+        if (mUsernameMode) {
+            this.binding.accountJidLayout.setHint(getString(R.string.username_hint));
+            this.binding.accountJid.setHint(R.string.username_hint);
+        } else {
+            final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
+                    R.layout.simple_list_item,
+                    xmppConnectionService.getKnownHosts());
+            this.binding.accountJid.setAdapter(mKnownHostsAdapter);
+        }
+
+        if (pendingUri != null) {
+            processFingerprintVerification(pendingUri, false);
+            pendingUri = null;
+        }
+        updatePortLayout();
+        updateSaveButton();
+        invalidateOptionsMenu();
+    }
+
+    private String getUserModeDomain() {
+        if (mAccount != null && mAccount.getJid().getDomain() != null) {
+            return mAccount.getJid().getDomain();
+        } else {
+            return Config.DOMAIN_LOCK;
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(final MenuItem item) {
+        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
+            return false;
+        }
+        switch (item.getItemId()) {
+            case R.id.action_show_block_list:
+                final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class);
+                showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
+                startActivity(showBlocklistIntent);
+                break;
+            case R.id.action_server_info_show_more:
+                changeMoreTableVisibility(!item.isChecked());
+                break;
+            case R.id.action_share_barcode:
+                shareBarcode();
+                break;
+            case R.id.action_share_http:
+                shareLink(true);
+                break;
+            case R.id.action_share_uri:
+                shareLink(false);
+                break;
+            case R.id.action_change_password_on_server:
+                gotoChangePassword(null);
+                break;
+            case R.id.action_mam_prefs:
+                editMamPrefs();
+                break;
+            case R.id.action_renew_certificate:
+                renewCertificate();
+                break;
+            case R.id.action_change_presence:
+                changePresence();
+                break;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void shareBarcode() {
+        Intent intent = new Intent(Intent.ACTION_SEND);
+        intent.putExtra(Intent.EXTRA_STREAM, BarcodeProvider.getUriForAccount(this, mAccount));
+        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        intent.setType("image/png");
+        startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
+    }
+
+    private void changeMoreTableVisibility(boolean visible) {
+        binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE);
+    }
+
+    private void gotoChangePassword(String newPassword) {
+        final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
+        changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
+        if (newPassword != null) {
+            changePasswordIntent.putExtra("password", newPassword);
+        }
+        startActivity(changePasswordIntent);
+    }
+
+    private void renewCertificate() {
+        KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+    }
+
+    private void changePresence() {
+        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+        boolean manualStatus = sharedPreferences.getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence));
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        final DialogPresenceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
+        String current = mAccount.getPresenceStatusMessage();
+        if (current != null && !current.trim().isEmpty()) {
+            binding.statusMessage.append(current);
+        }
+        setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding);
+        binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE);
+        List<PresenceTemplate> templates = xmppConnectionService.getPresenceTemplates(mAccount);
+        PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter(this, R.layout.simple_list_item, templates);
+        binding.statusMessage.setAdapter(presenceTemplateAdapter);
+        binding.statusMessage.setOnItemClickListener((parent, view, position, id) -> {
+            PresenceTemplate template = (PresenceTemplate) parent.getItemAtPosition(position);
+            setAvailabilityRadioButton(template.getStatus(), binding);
+        });
+        builder.setTitle(R.string.edit_status_message_title);
+        builder.setView(binding.getRoot());
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
+            PresenceTemplate template = new PresenceTemplate(getAvailabilityRadioButton(binding), binding.statusMessage.getText().toString().trim());
+            if (mAccount.getPgpId() != 0 && hasPgp()) {
+                generateSignature(null, template);
+            } else {
+                xmppConnectionService.changeStatus(mAccount, template, null);
+            }
+        });
+        builder.create().show();
+    }
+
+    private void generateSignature(Intent intent, PresenceTemplate template) {
+        xmppConnectionService.getPgpEngine().generateSignature(intent, mAccount, template.getStatusMessage(), new UiCallback<String>() {
+            @Override
+            public void success(String signature) {
+                xmppConnectionService.changeStatus(mAccount, template, signature);
+            }
+
+            @Override
+            public void error(int errorCode, String object) {
+
+            }
+
+            @Override
+            public void userInputRequried(PendingIntent pi, String object) {
+                mPendingPresenceTemplate.push(template);
+                try {
+                    startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0);
+                } catch (final IntentSender.SendIntentException ignored) {
+                }
+            }
+        });
+    }
+
+    @Override
+    public void alias(String alias) {
+        if (alias != null) {
+            xmppConnectionService.updateKeyInAccount(mAccount, alias);
+        }
+    }
+
+    private void updateAccountInformation(boolean init) {
+        if (init) {
+            this.binding.accountJid.getEditableText().clear();
+            if (mUsernameMode) {
+                this.binding.accountJid.getEditableText().append(this.mAccount.getJid().getLocal());
+            } else {
+                this.binding.accountJid.getEditableText().append(this.mAccount.getJid().asBareJid().toString());
+            }
+            this.binding.accountPassword.getEditableText().clear();
+            this.binding.accountPassword.getEditableText().append(this.mAccount.getPassword());
+            this.binding.hostname.setText("");
+            this.binding.hostname.getEditableText().append(this.mAccount.getHostname());
+            this.binding.port.setText("");
+            this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort()));
+            this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
+
+        }
+
+        final boolean editable = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && QuickConversationsService.isConversations();
+        this.binding.accountJid.setEnabled(editable);
+        this.binding.accountJid.setFocusable(editable);
+        this.binding.accountJid.setFocusableInTouchMode(editable);
+
+
+        final String displayName = mAccount.getDisplayName();
+        updateDisplayName(displayName);
+
+
+        if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
+            this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(true);
+        } else {
+            this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(false);
+        }
+
+        if (!mInitMode) {
+            this.binding.avater.setVisibility(View.VISIBLE);
+            this.binding.avater.setImageBitmap(avatarService().get(this.mAccount, (int) getResources().getDimension(R.dimen.avatar_on_details_screen_size)));
+        } else {
+            this.binding.avater.setVisibility(View.GONE);
+        }
+        this.binding.accountRegisterNew.setChecked(this.mAccount.isOptionSet(Account.OPTION_REGISTER));
+        if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
+            if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
+                ActionBar actionBar = getSupportActionBar();
+                if (actionBar != null) {
+                    actionBar.setTitle(R.string.create_account);
+                }
+            }
+            this.binding.accountRegisterNew.setVisibility(View.GONE);
+        } else if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
+            this.binding.accountRegisterNew.setVisibility(View.VISIBLE);
+        } else {
+            this.binding.accountRegisterNew.setVisibility(View.GONE);
+        }
+        if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
+            Features features = this.mAccount.getXmppConnection().getFeatures();
+            this.binding.stats.setVisibility(View.VISIBLE);
+            boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery();
+            boolean showDataSaverWarning = isAffectedByDataSaver();
+            showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning);
+            this.binding.sessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection()
+                    .getLastSessionEstablished()));
+            if (features.rosterVersioning()) {
+                this.binding.serverInfoRosterVersion.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable);
+            }
+            if (features.carbons()) {
+                this.binding.serverInfoCarbons.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable);
+            }
+            if (features.mam()) {
+                this.binding.serverInfoMam.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoMam.setText(R.string.server_info_unavailable);
+            }
+            if (features.csi()) {
+                this.binding.serverInfoCsi.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoCsi.setText(R.string.server_info_unavailable);
+            }
+            if (features.blocking()) {
+                this.binding.serverInfoBlocking.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoBlocking.setText(R.string.server_info_unavailable);
+            }
+            if (features.sm()) {
+                this.binding.serverInfoSm.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoSm.setText(R.string.server_info_unavailable);
+            }
+            if (features.pep()) {
+                AxolotlService axolotlService = this.mAccount.getAxolotlService();
+                if (axolotlService != null && axolotlService.isPepBroken()) {
+                    this.binding.serverInfoPep.setText(R.string.server_info_broken);
+                } else if (features.pepPublishOptions() || features.pepOmemoWhitelisted()) {
+                    this.binding.serverInfoPep.setText(R.string.server_info_available);
+                } else {
+                    this.binding.serverInfoPep.setText(R.string.server_info_partial);
+                }
+            } else {
+                this.binding.serverInfoPep.setText(R.string.server_info_unavailable);
+            }
+            if (features.httpUpload(0)) {
+                this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
+            } else if (features.p1S3FileTransfer()) {
+                this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer);
+                this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
+            }
+
+            this.binding.pushRow.setVisibility(xmppConnectionService.getPushManagementService().isStub() ? View.GONE : View.VISIBLE);
+
+            if (xmppConnectionService.getPushManagementService().available(mAccount)) {
+                this.binding.serverInfoPush.setText(R.string.server_info_available);
+            } else {
+                this.binding.serverInfoPush.setText(R.string.server_info_unavailable);
+            }
+            final long pgpKeyId = this.mAccount.getPgpId();
+            if (pgpKeyId != 0 && Config.supportOpenPgp()) {
+                OnClickListener openPgp = view -> launchOpenKeyChain(pgpKeyId);
+                OnClickListener delete = view -> showDeletePgpDialog();
+                this.binding.pgpFingerprintBox.setVisibility(View.VISIBLE);
+                this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId));
+                this.binding.pgpFingerprint.setOnClickListener(openPgp);
+                if ("pgp".equals(messageFingerprint)) {
+                    this.binding.pgpFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
+                }
+                this.binding.pgpFingerprintDesc.setOnClickListener(openPgp);
+                this.binding.actionDeletePgp.setOnClickListener(delete);
+            } else {
+                this.binding.pgpFingerprintBox.setVisibility(View.GONE);
+            }
+            final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint();
+            if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
+                this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
+                if (ownAxolotlFingerprint.equals(messageFingerprint)) {
+                    this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
+                    this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint_selected_message);
+                } else {
+                    this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption);
+                    this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint);
+                }
+                this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2)));
+                this.binding.actionCopyAxolotlToClipboard.setVisibility(View.VISIBLE);
+                this.binding.actionCopyAxolotlToClipboard.setOnClickListener(v -> copyOmemoFingerprint(ownAxolotlFingerprint));
+            } else {
+                this.binding.axolotlFingerprintBox.setVisibility(View.GONE);
+            }
+            boolean hasKeys = false;
+            binding.otherDeviceKeys.removeAllViews();
+            for (XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
+                if (!session.getTrust().isCompromised()) {
+                    boolean highlight = session.getFingerprint().equals(messageFingerprint);
+                    addFingerprintRow(binding.otherDeviceKeys, session, highlight);
+                    hasKeys = true;
+                }
+            }
+            if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices
+                this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE);
+                Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
+                if (otherDevices == null || otherDevices.isEmpty()) {
+                    binding.clearDevices.setVisibility(View.GONE);
+                } else {
+                    binding.clearDevices.setVisibility(View.VISIBLE);
+                }
+            } else {
+                this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
+            }
+        } else {
+            final TextInputLayout errorLayout;
+            if (this.mAccount.errorStatus()) {
+                if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED) {
+                    errorLayout = this.binding.accountPasswordLayout;
+                } else if (mShowOptions
+                        && this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND
+                        && this.binding.hostname.getText().length() > 0) {
+                    errorLayout = this.binding.hostnameLayout;
+                } else {
+                    errorLayout = this.binding.accountJidLayout;
+                }
+                errorLayout.setError(getString(this.mAccount.getStatus().getReadableId()));
+                if (init || !accountInfoEdited()) {
+                    errorLayout.requestFocus();
+                }
+            } else {
+                errorLayout = null;
+            }
+            removeErrorsOnAllBut(errorLayout);
+            this.binding.stats.setVisibility(View.GONE);
+            this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
+        }
+    }
+
+    private void updateDisplayName(String displayName) {
+        if (TextUtils.isEmpty(displayName)) {
+            this.binding.yourName.setText(R.string.no_name_set_instructions);
+            this.binding.yourName.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1_Tertiary);
+        } else {
+            this.binding.yourName.setText(displayName);
+            this.binding.yourName.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1);
+        }
+    }
+
+    private void removeErrorsOnAllBut(TextInputLayout exception) {
+        if (this.binding.accountJidLayout != exception) {
+            this.binding.accountJidLayout.setErrorEnabled(false);
+            this.binding.accountJidLayout.setError(null);
+        }
+        if (this.binding.accountPasswordLayout != exception) {
+            this.binding.accountPasswordLayout.setErrorEnabled(false);
+            this.binding.accountPasswordLayout.setError(null);
+        }
+        if (this.binding.hostnameLayout != exception) {
+            this.binding.hostnameLayout.setErrorEnabled(false);
+            this.binding.hostnameLayout.setError(null);
+        }
+        if (this.binding.portLayout != exception) {
+            this.binding.portLayout.setErrorEnabled(false);
+            this.binding.portLayout.setError(null);
+        }
+    }
+
+    private void showDeletePgpDialog() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle(R.string.unpublish_pgp);
+        builder.setMessage(R.string.unpublish_pgp_message);
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(R.string.confirm, (dialogInterface, i) -> {
+            mAccount.setPgpSignId(0);
+            mAccount.unsetPgpSignature();
+            xmppConnectionService.databaseBackend.updateAccount(mAccount);
+            xmppConnectionService.sendPresence(mAccount);
+            refreshUiReal();
+        });
+        builder.create().show();
+    }
+
+    private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) {
+        this.binding.osOptimization.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
+        if (showDataSaverWarning) {
+            this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled);
+            this.binding.osOptimizationBody.setText(R.string.data_saver_enabled_explained);
+            this.binding.osOptimizationDisable.setText(R.string.allow);
+            this.binding.osOptimizationDisable.setOnClickListener(v -> {
+                Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
+                Uri uri = Uri.parse("package:" + getPackageName());
+                intent.setData(uri);
+                try {
+                    startActivityForResult(intent, REQUEST_DATA_SAVER);
+                } catch (ActivityNotFoundException e) {
+                    Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_data_saver, Toast.LENGTH_SHORT).show();
+                }
+            });
+        } else if (showBatteryWarning) {
+            this.binding.osOptimizationDisable.setText(R.string.disable);
+            this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled);
+            this.binding.osOptimizationBody.setText(R.string.battery_optimizations_enabled_explained);
+            this.binding.osOptimizationDisable.setOnClickListener(v -> {
+                Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+                Uri uri = Uri.parse("package:" + getPackageName());
+                intent.setData(uri);
+                try {
+                    startActivityForResult(intent, REQUEST_BATTERY_OP);
+                } catch (ActivityNotFoundException e) {
+                    Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
+                }
+            });
+        }
+    }
+
+    public void showWipePepDialog() {
+        Builder builder = new Builder(this);
+        builder.setTitle(getString(R.string.clear_other_devices));
+        builder.setIconAttribute(android.R.attr.alertDialogIcon);
+        builder.setMessage(getString(R.string.clear_other_devices_desc));
+        builder.setNegativeButton(getString(R.string.cancel), null);
+        builder.setPositiveButton(getString(R.string.accept),
+                (dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices());
+        builder.create().show();
+    }
+
+    private void editMamPrefs() {
+        this.mFetchingMamPrefsToast = Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG);
+        this.mFetchingMamPrefsToast.show();
+        xmppConnectionService.fetchMamPreferences(mAccount, this);
+    }
+
+    @Override
+    public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
+        refreshUi();
+    }
+
+    @Override
+    public void onCaptchaRequested(final Account account, final String id, final Data data, final Bitmap captcha) {
+        runOnUiThread(() -> {
+            if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
+                mCaptchaDialog.dismiss();
+            }
+            final Builder builder = new Builder(EditAccountActivity.this);
+            final View view = getLayoutInflater().inflate(R.layout.captcha, null);
+            final ImageView imageView = view.findViewById(R.id.captcha);
+            final EditText input = view.findViewById(R.id.input);
+            imageView.setImageBitmap(captcha);
+
+            builder.setTitle(getString(R.string.captcha_required));
+            builder.setView(view);
+
+            builder.setPositiveButton(getString(R.string.ok),
+                    (dialog, which) -> {
+                        String rc = input.getText().toString();
+                        data.put("username", account.getUsername());
+                        data.put("password", account.getPassword());
+                        data.put("ocr", rc);
+                        data.submit();
+
+                        if (xmppConnectionServiceBound) {
+                            xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, id, data);
+                        }
+                    });
+            builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> {
+                if (xmppConnectionService != null) {
+                    xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
+                }
+            });
+
+            builder.setOnCancelListener(dialog -> {
+                if (xmppConnectionService != null) {
+                    xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null);
+                }
+            });
+            mCaptchaDialog = builder.create();
+            mCaptchaDialog.show();
+            input.requestFocus();
+        });
+    }
+
+    public void onShowErrorToast(final int resId) {
+        runOnUiThread(() -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show());
+    }
+
+    @Override
+    public void onPreferencesFetched(final Element prefs) {
+        runOnUiThread(() -> {
+            if (mFetchingMamPrefsToast != null) {
+                mFetchingMamPrefsToast.cancel();
+            }
+            Builder builder = new Builder(EditAccountActivity.this);
+            builder.setTitle(R.string.server_side_mam_prefs);
+            String defaultAttr = prefs.getAttribute("default");
+            final List<String> defaults = Arrays.asList("never", "roster", "always");
+            final AtomicInteger choice = new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr)));
+            builder.setSingleChoiceItems(R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which));
+            builder.setNegativeButton(R.string.cancel, null);
+            builder.setPositiveButton(R.string.ok, (dialog, which) -> {
+                prefs.setAttribute("default", defaults.get(choice.get()));
+                xmppConnectionService.pushMamPreferences(mAccount, prefs);
+            });
+            builder.create().show();
+        });
+    }
+
+    @Override
+    public void onPreferencesFetchFailed() {
+        runOnUiThread(() -> {
+            if (mFetchingMamPrefsToast != null) {
+                mFetchingMamPrefsToast.cancel();
+            }
+            Toast.makeText(EditAccountActivity.this, R.string.unable_to_fetch_mam_prefs, Toast.LENGTH_LONG).show();
+        });
+    }
+
+    @Override
+    public void OnUpdateBlocklist(Status status) {
+        refreshUi();
+    }
 }

src/main/java/eu/siacs/conversations/xml/Namespace.java ๐Ÿ”—

@@ -16,6 +16,7 @@ public final class Namespace {
 	public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
 	public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB+"#publish-options";
 	public static final String PUBSUB_ERROR = PUBSUB+"#errors";
+	public static final String PUBSUB_OWNER = PUBSUB+"#owner";
 	public static final String NICK = "http://jabber.org/protocol/nick";
 	public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
 	public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";

src/main/res/layout/activity_edit_account.xml ๐Ÿ”—

@@ -470,11 +470,53 @@
                         </TableLayout>
 
                         <RelativeLayout
-                            android:id="@+id/pgp_fingerprint_box"
+                            android:id="@+id/your_name_box"
                             android:layout_width="wrap_content"
                             android:layout_height="match_parent"
                             android:layout_marginTop="32dp">
 
+                            <LinearLayout
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_alignParentLeft="true"
+                                android:layout_centerVertical="true"
+                                android:layout_toLeftOf="@+id/action_edit_your_name"
+                                android:orientation="vertical">
+
+                                <TextView
+                                    android:id="@+id/your_name"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:text="@string/no_name_set_instructions"
+                                    android:textAppearance="@style/TextAppearance.Conversations.Body1.Tertiary"/>
+
+                                <TextView
+                                    android:id="@+id/your_name_desc"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:text="@string/your_name"
+                                    android:textAppearance="@style/TextAppearance.Conversations.Caption"/>
+                            </LinearLayout>
+
+                            <ImageButton
+                                android:id="@+id/action_edit_your_name"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_alignParentRight="true"
+                                android:layout_centerVertical="true"
+                                android:alpha="?attr/icon_alpha"
+                                android:background="?attr/selectableItemBackgroundBorderless"
+                                android:padding="@dimen/image_button_padding"
+                                android:src="?attr/icon_edit_body"
+                                android:visibility="visible"/>
+                        </RelativeLayout>
+
+                        <RelativeLayout
+                            android:id="@+id/pgp_fingerprint_box"
+                            android:layout_width="wrap_content"
+                            android:layout_height="match_parent"
+                            android:layout_marginTop="16dp">
+
                             <LinearLayout
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
@@ -514,7 +556,7 @@
                             android:id="@+id/axolotl_fingerprint_box"
                             android:layout_width="wrap_content"
                             android:layout_height="match_parent"
-                            android:layout_marginTop="24dp">
+                            android:layout_marginTop="16dp">
 
                             <LinearLayout
                                 android:layout_width="wrap_content"

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

@@ -515,6 +515,9 @@
     <string name="share_uri_with">Share URI withโ€ฆ</string>
     <string name="welcome_header" translatable="false">Join the Conversation</string>
     <string name="welcome_text">Jabber is a provider independent instant messaging network. You can use this client with what ever Jabber server you choose.\nHowever for your convenience we made it easy to create an account on conversations.imยน; a provider specially suited for the use with Conversations.</string>
+    <string name="welcome_header_quicksy" translatable="false">Have some Quick Conversations</string>
+    <string name="welcome_text_quicksy"><![CDATA[Quicksy is a spin off of the popular Jabber/XMPP client Conversations with automatic contact discovery.<br><br>You sign up with your phone number and Quicksy will automaticallyโ€”based on the phone numbers in your address bookโ€”suggest possible contacts to you.<br><br>By signing up you agree to our <a href="https://quicksy.im/#privacy">privacy policy</a>.]]></string>
+    <string name="agree_and_continue">Agree &amp; continue</string>
     <string name="magic_create_text">We will guide you through the process of creating an account on conversations.im.ยน\nWhen picking conversations.im as a provider you will be able to communicate with users of other providers by giving them your full Jabber ID.</string>
     <string name="your_full_jid_will_be">Your full Jabber ID will be: %s</string>
     <string name="create_account">Create Account</string>
@@ -789,4 +792,9 @@
     <string name="too_many_attempts">Too many attempts</string>
     <string name="the_app_is_out_of_date">You are using an out of date version of this app.</string>
     <string name="update">Update</string>
+    <string name="logged_in_with_another_device">This phone number is currently logged in with another device.</string>
+    <string name="enter_your_name_instructions">Please enter your name to let people, who donโ€™t have you in their address books, know who you are.</string>
+    <string name="your_name">Your name</string>
+    <string name="enter_your_name">Enter your name</string>
+    <string name="no_name_set_instructions">Use the edit button to set your name.</string>
 </resources>

src/main/res/values/styles.xml ๐Ÿ”—

@@ -80,6 +80,10 @@
         <item name="android:textColor">?android:textColorSecondary</item>
     </style>
 
+    <style name="TextAppearance.Conversations.Body1.Tertiary" parent="TextAppearance.Conversations.Body1">
+        <item name="android:textColor">?android:textColorTertiary</item>
+    </style>
+
     <style name="TextAppearance.Conversations.Fingerprint" parent="TextAppearance.Conversations.Body1">
         <item name="android:fontFamily" tools:targetApi="jelly_bean">monospace</item>
         <item name="android:typeface">monospace</item>

src/quicksy/AndroidManifest.xml ๐Ÿ”—

@@ -19,5 +19,15 @@
             android:launchMode="singleTask"
             android:label="@string/verify_your_phone_number"/>
 
+        <activity
+            android:name=".ui.TosActivity"
+            android:launchMode="singleTask"
+            android:label="@string/app_name"/>
+
+        <activity
+            android:name=".ui.EnterNameActivity"
+            android:launchMode="singleTask"
+            android:label="@string/enter_your_name"/>
+
     </application>
 </manifest>

src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java ๐Ÿ”—

@@ -0,0 +1,54 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.databinding.DataBindingUtil;
+import android.os.Bundle;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityEnterNameBinding;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.AccountUtils;
+
+public class EnterNameActivity extends XmppActivity {
+
+    private ActivityEnterNameBinding binding;
+
+    private Account account;
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_name);
+        setSupportActionBar((Toolbar) this.binding.toolbar);
+        this.binding.next.setOnClickListener(this::next);
+    }
+
+    private void next(View view) {
+        if (account != null) {
+
+            String name = this.binding.name.getText().toString().trim();
+
+            account.setDisplayName(name);
+
+            xmppConnectionService.publishDisplayName(account);
+
+            Intent intent = new Intent(this, PublishProfilePictureActivity.class);
+            intent.putExtra(PublishProfilePictureActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+            intent.putExtra("setup", true);
+            startActivity(intent);
+        }
+        finish();
+    }
+
+    @Override
+    protected void refreshUiReal() {
+
+    }
+
+    @Override
+    void onBackendConnected() {
+        this.account = AccountUtils.getFirst(xmppConnectionService);
+    }
+}

src/quicksy/java/eu/siacs/conversations/ui/TosActivity.java ๐Ÿ”—

@@ -0,0 +1,80 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.widget.Button;
+import android.widget.TextView;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.XmppUri;
+
+public class TosActivity extends XmppActivity {
+
+    @Override
+    protected void refreshUiReal() {
+
+    }
+
+    @Override
+    void onBackendConnected() {
+
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        final int theme = findTheme();
+        if (this.mTheme != theme) {
+            recreate();
+        }
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        if (intent != null) {
+            setIntent(intent);
+        }
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        if (getResources().getBoolean(R.bool.portrait_only)) {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        }
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_tos);
+        setSupportActionBar(findViewById(R.id.toolbar));
+        final ActionBar ab = getSupportActionBar();
+        if (ab != null) {
+            ab.setDisplayShowHomeEnabled(false);
+            ab.setDisplayHomeAsUpEnabled(false);
+        }
+        final Button agreeButton = findViewById(R.id.agree);
+        final TextView welcomeText = findViewById(R.id.welcome_text);
+        agreeButton.setOnClickListener(v -> {
+            final Intent intent = new Intent(this, EnterPhoneNumberActivity.class);
+            SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+            preferences.edit().putBoolean("tos", true).apply();
+            addInviteUri(intent);
+            startActivity(intent);
+            finish();
+        });
+        welcomeText.setText(Html.fromHtml(getString(R.string.welcome_text_quicksy)));
+        welcomeText.setMovementMethod(LinkMovementMethod.getInstance());
+
+    }
+
+    public void addInviteUri(Intent intent) {
+        StartConversationActivity.addInviteUri(intent, getIntent());
+    }
+}

src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java ๐Ÿ”—

@@ -269,9 +269,7 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
 
     private void performPostVerificationRedirect() {
         if (redirectInProgress.compareAndSet(false, true)) {
-            Intent intent = new Intent(this, PublishProfilePictureActivity.class);
-            intent.putExtra(PublishProfilePictureActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
-            intent.putExtra("setup", true);
+            Intent intent = new Intent(this, EnterNameActivity.class);
             startActivity(intent);
             finish();
         }

src/quicksy/java/eu/siacs/conversations/utils/SignupUtils.java ๐Ÿ”—

@@ -2,6 +2,8 @@ package eu.siacs.conversations.utils;
 
 import android.app.Activity;
 import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
 import android.util.Log;
 
 import eu.siacs.conversations.Config;
@@ -9,6 +11,7 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.EnterPhoneNumberActivity;
 import eu.siacs.conversations.ui.StartConversationActivity;
+import eu.siacs.conversations.ui.TosActivity;
 import eu.siacs.conversations.ui.VerifyActivity;
 
 public class SignupUtils {
@@ -28,7 +31,12 @@ public class SignupUtils {
                 intent = new Intent(activity, StartConversationActivity.class);
             }
         } else {
-            intent = getSignUpIntent(activity);
+            SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+            if (preferences.getBoolean("tos",false)) {
+                intent = getSignUpIntent(activity);
+            } else {
+                intent = new Intent(activity, TosActivity.class);
+            }
 
         }
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);

src/quicksy/res/layout/activity_enter_name.xml ๐Ÿ”—

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <include android:id="@+id/toolbar" layout="@layout/toolbar" />
+
+        <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:fillViewport="true">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <TextView
+                    android:id="@+id/instructions"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:padding="16dp"
+                    android:gravity="center_horizontal"
+                    android:textAppearance="@style/TextAppearance.Conversations.Body1"
+                    android:text="@string/enter_your_name_instructions"/>
+
+                <LinearLayout
+                    android:id="@+id/name_box"
+                    android:layout_width="256dp"
+                    android:layout_height="wrap_content"
+                    android:layout_above="@+id/next"
+                    android:layout_below="@+id/instructions"
+                    android:layout_centerHorizontal="true"
+                    android:orientation="vertical">
+
+                    <EditText
+                        android:imeOptions="flagNoExtractUi"
+                        android:id="@+id/name"
+                        style="@style/Widget.Conversations.EditText"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:hint="@string/your_name"
+                        android:longClickable="false" />
+
+                </LinearLayout>
+                <Button
+                    android:id="@+id/next"
+                    android:layout_alignParentBottom="true"
+                    android:layout_alignParentEnd="true"
+                    style="@style/Widget.Conversations.Button.Borderless"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/next"
+                    android:textColor="?colorAccent"/>
+            </RelativeLayout>
+        </ScrollView>
+    </LinearLayout>
+</layout>

src/quicksy/res/layout/activity_tos.xml ๐Ÿ”—

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <include layout="@layout/toolbar" />
+
+    <ScrollView android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fillViewport="true">
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="?attr/color_background_primary">
+
+            <LinearLayout
+                android:id="@+id/linearLayout"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:minHeight="256dp"
+                android:orientation="vertical">
+                <Space
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"/>
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:paddingLeft="16dp"
+                    android:paddingRight="16dp"
+                    android:paddingBottom="16dp"
+                    android:orientation="vertical">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/welcome_header_quicksy"
+                    android:textAppearance="@style/TextAppearance.Conversations.Title"/>
+                <TextView
+                    android:id="@+id/welcome_text"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="8dp"
+                    android:text="@string/welcome_text_quicksy"
+                    android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                </LinearLayout>
+                <Button
+                    android:id="@+id/agree"
+                    style="@style/Widget.Conversations.Button.Borderless"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="end"
+                    android:text="@string/agree_and_continue"
+                    android:textColor="?colorAccent"/>
+            </LinearLayout>
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_above="@+id/linearLayout"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true">
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerHorizontal="true"
+                    android:layout_centerVertical="true"
+                    android:padding="8dp"
+                    android:src="@drawable/main_logo"/>
+            </RelativeLayout>
+        </RelativeLayout>
+    </ScrollView>
+</LinearLayout>