MessageParser.java

   1package eu.siacs.conversations.parser;
   2
   3import android.util.Log;
   4import android.util.Pair;
   5
   6import com.google.common.collect.ImmutableSet;
   7
   8import java.text.SimpleDateFormat;
   9import java.util.ArrayList;
  10import java.util.Arrays;
  11import java.util.Collections;
  12import java.util.Date;
  13import java.util.List;
  14import java.util.Locale;
  15import java.util.Map;
  16import java.util.Set;
  17import java.util.UUID;
  18import java.util.function.Consumer;
  19
  20import eu.siacs.conversations.AppSettings;
  21import eu.siacs.conversations.Config;
  22import eu.siacs.conversations.R;
  23import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  24import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
  25import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
  26import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
  27import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  28import eu.siacs.conversations.entities.Account;
  29import eu.siacs.conversations.entities.Bookmark;
  30import eu.siacs.conversations.entities.Contact;
  31import eu.siacs.conversations.entities.Conversation;
  32import eu.siacs.conversations.entities.Conversational;
  33import eu.siacs.conversations.entities.Message;
  34import eu.siacs.conversations.entities.MucOptions;
  35import eu.siacs.conversations.entities.Reaction;
  36import eu.siacs.conversations.entities.ReadByMarker;
  37import eu.siacs.conversations.entities.ReceiptRequest;
  38import eu.siacs.conversations.entities.RtpSessionStatus;
  39import eu.siacs.conversations.http.HttpConnectionManager;
  40import eu.siacs.conversations.services.MessageArchiveService;
  41import eu.siacs.conversations.services.QuickConversationsService;
  42import eu.siacs.conversations.services.XmppConnectionService;
  43import eu.siacs.conversations.utils.CryptoHelper;
  44import eu.siacs.conversations.xml.Element;
  45import eu.siacs.conversations.xml.LocalizedContent;
  46import eu.siacs.conversations.xml.Namespace;
  47import eu.siacs.conversations.xmpp.InvalidJid;
  48import eu.siacs.conversations.xmpp.Jid;
  49import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
  50import eu.siacs.conversations.xmpp.chatstate.ChatState;
  51import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  52import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
  53import eu.siacs.conversations.xmpp.pep.Avatar;
  54import im.conversations.android.xmpp.model.Extension;
  55import im.conversations.android.xmpp.model.carbons.Received;
  56import im.conversations.android.xmpp.model.carbons.Sent;
  57import im.conversations.android.xmpp.model.forward.Forwarded;
  58import im.conversations.android.xmpp.model.occupant.OccupantId;
  59import im.conversations.android.xmpp.model.reactions.Reactions;
  60
  61public class MessageParser extends AbstractParser implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
  62
  63    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
  64
  65    private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
  66            Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
  67
  68    public MessageParser(final XmppConnectionService service, final Account account) {
  69        super(service, account);
  70    }
  71
  72    private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
  73        final Jid by;
  74        final boolean safeToExtract;
  75        if (isTypeGroupChat) {
  76            by = conversation.getJid().asBareJid();
  77            safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
  78        } else {
  79            Account account = conversation.getAccount();
  80            by = account.getJid().asBareJid();
  81            safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
  82        }
  83        return safeToExtract ? extractStanzaId(packet, by) : null;
  84    }
  85
  86    private static String extractStanzaId(Account account, Element packet) {
  87        final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
  88        return safeToExtract ? extractStanzaId(packet, account.getJid().asBareJid()) : null;
  89    }
  90
  91    private static String extractStanzaId(Element packet, Jid by) {
  92        for (Element child : packet.getChildren()) {
  93            if (child.getName().equals("stanza-id")
  94                    && Namespace.STANZA_IDS.equals(child.getNamespace())
  95                    && by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) {
  96                return child.getAttribute("id");
  97            }
  98        }
  99        return null;
 100    }
 101
 102    private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
 103        final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
 104        Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
 105        return result != null ? result : fallback;
 106    }
 107
 108    private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final im.conversations.android.xmpp.model.stanza.Message packet) {
 109        ChatState state = ChatState.parse(packet);
 110        if (state != null && c != null) {
 111            final Account account = c.getAccount();
 112            final Jid from = packet.getFrom();
 113            if (from.asBareJid().equals(account.getJid().asBareJid())) {
 114                c.setOutgoingChatState(state);
 115                if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
 116                    if (c.getContact().isSelf()) {
 117                        return false;
 118                    }
 119                    mXmppConnectionService.markRead(c);
 120                    activateGracePeriod(account);
 121                }
 122                return false;
 123            } else {
 124                if (isTypeGroupChat) {
 125                    MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
 126                    if (user != null) {
 127                        return user.setChatState(state);
 128                    } else {
 129                        return false;
 130                    }
 131                } else {
 132                    return c.setIncomingChatState(state);
 133                }
 134            }
 135        }
 136        return false;
 137    }
 138
 139    private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
 140        final AxolotlService service = conversation.getAccount().getAxolotlService();
 141        final XmppAxolotlMessage xmppAxolotlMessage;
 142        try {
 143            xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid());
 144        } catch (Exception e) {
 145            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": invalid omemo message received " + e.getMessage());
 146            return null;
 147        }
 148        if (xmppAxolotlMessage.hasPayload()) {
 149            final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage;
 150            try {
 151                plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
 152            } catch (BrokenSessionException e) {
 153                if (checkedForDuplicates) {
 154                    if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
 155                        service.reportBrokenSessionException(e, postpone);
 156                        return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 157                    } else {
 158                        Log.d(Config.LOGTAG, "ignoring broken session exception because contact was not trusted");
 159                        return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 160                    }
 161                } else {
 162                    Log.d(Config.LOGTAG, "ignoring broken session exception because checkForDuplicates failed");
 163                    return null;
 164                }
 165            } catch (NotEncryptedForThisDeviceException e) {
 166                return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
 167            } catch (OutdatedSenderException e) {
 168                return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
 169            }
 170            if (plaintextMessage != null) {
 171                Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
 172                finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
 173                Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint());
 174                return finishedMessage;
 175            }
 176        } else {
 177            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OMEMO key transport message");
 178            service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
 179        }
 180        return null;
 181    }
 182
 183    private Invite extractInvite(final Element message) {
 184        final Element mucUser = message.findChild("x", Namespace.MUC_USER);
 185        if (mucUser != null) {
 186            final Element invite = mucUser.findChild("invite");
 187            if (invite != null) {
 188                final String password = mucUser.findChildContent("password");
 189                final Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
 190                final Jid to = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("to"));
 191                if (to != null && from == null) {
 192                    Log.d(Config.LOGTAG,"do not parse outgoing mediated invite "+message);
 193                    return null;
 194                }
 195                final Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
 196                if (room == null) {
 197                    return null;
 198                }
 199                return new Invite(room, password, false, from);
 200            }
 201        }
 202        final Element conference = message.findChild("x", "jabber:x:conference");
 203        if (conference != null) {
 204            Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
 205            Jid room = InvalidJid.getNullForInvalid(conference.getAttributeAsJid("jid"));
 206            if (room == null) {
 207                return null;
 208            }
 209            return new Invite(room, conference.getAttribute("password"), true, from);
 210        }
 211        return null;
 212    }
 213
 214    private void parseEvent(final Element event, final Jid from, final Account account) {
 215        final Element items = event.findChild("items");
 216        final String node = items == null ? null : items.getAttribute("node");
 217        if ("urn:xmpp:avatar:metadata".equals(node)) {
 218            Avatar avatar = Avatar.parseMetadata(items);
 219            if (avatar != null) {
 220                avatar.owner = from.asBareJid();
 221                if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
 222                    if (account.getJid().asBareJid().equals(from)) {
 223                        if (account.setAvatar(avatar.getFilename())) {
 224                            mXmppConnectionService.databaseBackend.updateAccount(account);
 225                            mXmppConnectionService.notifyAccountAvatarHasChanged(account);
 226                        }
 227                        mXmppConnectionService.getAvatarService().clear(account);
 228                        mXmppConnectionService.updateConversationUi();
 229                        mXmppConnectionService.updateAccountUi();
 230                    } else {
 231                        final Contact contact = account.getRoster().getContact(from);
 232                        contact.setAvatar(avatar);
 233                        mXmppConnectionService.syncRoster(account);
 234                        mXmppConnectionService.getAvatarService().clear(contact);
 235                        mXmppConnectionService.updateConversationUi();
 236                        mXmppConnectionService.updateRosterUi();
 237                    }
 238                } else if (mXmppConnectionService.isDataSaverDisabled()) {
 239                    mXmppConnectionService.fetchAvatar(account, avatar);
 240                }
 241            }
 242        } else if (Namespace.NICK.equals(node)) {
 243            final Element i = items.findChild("item");
 244            final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
 245            if (nick != null) {
 246                setNick(account, from, nick);
 247            }
 248        } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
 249            Element item = items.findChild("item");
 250            final Set<Integer> deviceIds = IqParser.deviceIds(item);
 251            Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
 252            final AxolotlService axolotlService = account.getAxolotlService();
 253            axolotlService.registerDevices(from, deviceIds);
 254        } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
 255            final var connection = account.getXmppConnection();
 256            if (connection.getFeatures().bookmarksConversion()) {
 257                if (connection.getFeatures().bookmarks2()) {
 258                    Log.w(
 259                            Config.LOGTAG,
 260                            account.getJid().asBareJid()
 261                                    + ": received storage:bookmark notification even though we opted into bookmarks:1");
 262                }
 263                final Element i = items.findChild("item");
 264                final Element storage =
 265                        i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
 266                final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
 267                mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
 268                Log.d(
 269                        Config.LOGTAG,
 270                        account.getJid().asBareJid() + ": processing bookmark PEP event");
 271            } else {
 272                Log.d(
 273                        Config.LOGTAG,
 274                        account.getJid().asBareJid()
 275                                + ": ignoring bookmark PEP event because bookmark conversion was not detected");
 276            }
 277        } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
 278            final Element item = items.findChild("item");
 279            final Element retract = items.findChild("retract");
 280            if (item != null) {
 281                final Bookmark bookmark = Bookmark.parseFromItem(item, account);
 282                if (bookmark != null) {
 283                    account.putBookmark(bookmark);
 284                    mXmppConnectionService.processModifiedBookmark(bookmark);
 285                    mXmppConnectionService.updateConversationUi();
 286                }
 287            }
 288            if (retract != null) {
 289                final Jid id = InvalidJid.getNullForInvalid(retract.getAttributeAsJid("id"));
 290                if (id != null) {
 291                    account.removeBookmark(id);
 292                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id);
 293                    mXmppConnectionService.processDeletedBookmark(account, id);
 294                    mXmppConnectionService.updateConversationUi();
 295                }
 296            }
 297        } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
 298                && Namespace.MDS_DISPLAYED.equals(node)
 299                && account.getJid().asBareJid().equals(from)) {
 300            final Element item = items.findChild("item");
 301            mXmppConnectionService.processMdsItem(account, item);
 302        } else {
 303            Log.d(
 304                    Config.LOGTAG,
 305                    account.getJid().asBareJid()
 306                            + " received pubsub notification for node="
 307                            + node);
 308        }
 309    }
 310
 311    private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
 312        final Element delete = event.findChild("delete");
 313        final String node = delete == null ? null : delete.getAttribute("node");
 314        if (Namespace.NICK.equals(node)) {
 315            Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
 316            setNick(account, from, null);
 317        } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
 318            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
 319            deleteAllBookmarks(account);
 320        } else if (Namespace.AVATAR_METADATA.equals(node) && account.getJid().asBareJid().equals(from)) {
 321            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted avatar metadata node");
 322        }
 323    }
 324
 325    private void parsePurgeEvent(final Element event, final Jid from, final Account account) {
 326        final Element purge = event.findChild("purge");
 327        final String node = purge == null ? null : purge.getAttribute("node");
 328        if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
 329            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
 330            deleteAllBookmarks(account);
 331        }
 332    }
 333
 334    private void deleteAllBookmarks(final Account account) {
 335        final var previous = account.getBookmarkedJids();
 336        account.setBookmarks(Collections.emptyMap());
 337        mXmppConnectionService.processDeletedBookmarks(account, previous);
 338    }
 339
 340    private void setNick(Account account, Jid user, String nick) {
 341        if (user.asBareJid().equals(account.getJid().asBareJid())) {
 342            account.setDisplayName(nick);
 343            if (QuickConversationsService.isQuicksy()) {
 344                mXmppConnectionService.getAvatarService().clear(account);
 345            }
 346        } else {
 347            Contact contact = account.getRoster().getContact(user);
 348            if (contact.setPresenceName(nick)) {
 349                mXmppConnectionService.syncRoster(account);
 350                mXmppConnectionService.getAvatarService().clear(contact);
 351            }
 352        }
 353        mXmppConnectionService.updateConversationUi();
 354        mXmppConnectionService.updateAccountUi();
 355    }
 356
 357    private boolean handleErrorMessage(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) {
 358        if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
 359            if (packet.fromServer(account)) {
 360                final var forwarded = getForwardedMessagePacket(packet,"received", Namespace.CARBONS);
 361                if (forwarded != null) {
 362                    return handleErrorMessage(account, forwarded.first);
 363                }
 364            }
 365            final Jid from = packet.getFrom();
 366            final String id = packet.getId();
 367            if (from != null && id != null) {
 368                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
 369                    final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
 370                    mXmppConnectionService.getJingleConnectionManager()
 371                            .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.FAILED);
 372                    return true;
 373                }
 374                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
 375                    final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
 376                    final String message = extractErrorMessage(packet);
 377                    mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message);
 378                    return true;
 379                }
 380                mXmppConnectionService.markMessage(account,
 381                        from.asBareJid(),
 382                        id,
 383                        Message.STATUS_SEND_FAILED,
 384                        extractErrorMessage(packet));
 385                final Element error = packet.findChild("error");
 386                final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found"));
 387                if (pingWorthyError) {
 388                    Conversation conversation = mXmppConnectionService.find(account, from);
 389                    if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
 390                        if (conversation.getMucOptions().online()) {
 391                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ping worthy error for seemingly online muc at " + from);
 392                            mXmppConnectionService.mucSelfPingAndRejoin(conversation);
 393                        }
 394                    }
 395                }
 396            }
 397            return true;
 398        }
 399        return false;
 400    }
 401
 402    @Override
 403    public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
 404        if (handleErrorMessage(account, original)) {
 405            return;
 406        }
 407        final im.conversations.android.xmpp.model.stanza.Message packet;
 408        Long timestamp = null;
 409        boolean isCarbon = false;
 410        String serverMsgId = null;
 411        final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
 412        if (fin != null) {
 413            mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
 414            return;
 415        }
 416        final Element result = MessageArchiveService.Version.findResult(original);
 417        final String queryId = result == null ? null : result.getAttribute("queryid");
 418        final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
 419        final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved();
 420        if (query != null && query.validFrom(original.getFrom())) {
 421            final var f = getForwardedMessagePacket(original,"result", query.version.namespace);
 422            if (f == null) {
 423                return;
 424            }
 425            timestamp = f.second;
 426            packet = f.first;
 427            serverMsgId = result.getAttribute("id");
 428            query.incrementMessageCount();
 429            if (handleErrorMessage(account, packet)) {
 430                return;
 431            }
 432        } else if (query != null) {
 433            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")");
 434            return;
 435        } else if (original.fromServer(account)
 436                && original.getType() != im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT) {
 437            Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
 438            f = getForwardedMessagePacket(original, Received.class);
 439            f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
 440            packet = f != null ? f.first : original;
 441            if (handleErrorMessage(account, packet)) {
 442                return;
 443            }
 444            timestamp = f != null ? f.second : null;
 445            isCarbon = f != null;
 446        } else {
 447            packet = original;
 448        }
 449
 450        if (timestamp == null) {
 451            timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
 452        }
 453        final Reactions reactions = packet.getExtension(Reactions.class);
 454        final LocalizedContent body = packet.getBody();
 455        final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
 456        final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
 457        final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
 458        final Element oob = packet.findChild("x", Namespace.OOB);
 459        final String oobUrl = oob != null ? oob.findChildContent("url") : null;
 460        final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
 461        final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
 462        int status;
 463        final Jid counterpart;
 464        final Jid to = packet.getTo();
 465        final Jid from = packet.getFrom();
 466        final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
 467        final String remoteMsgId;
 468        if (originId != null && originId.getAttribute("id") != null) {
 469            remoteMsgId = originId.getAttribute("id");
 470        } else {
 471            remoteMsgId = packet.getId();
 472        }
 473        boolean notify = false;
 474
 475        if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) {
 476            Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
 477            return;
 478        }
 479
 480        boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
 481        if (query != null && !query.muc() && isTypeGroupChat) {
 482            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
 483            return;
 484        }
 485        boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
 486        boolean selfAddressed;
 487        if (packet.fromAccount(account)) {
 488            status = Message.STATUS_SEND;
 489            selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid());
 490            if (selfAddressed) {
 491                counterpart = from;
 492            } else {
 493                counterpart = to;
 494            }
 495        } else {
 496            status = Message.STATUS_RECEIVED;
 497            counterpart = from;
 498            selfAddressed = false;
 499        }
 500
 501        final Invite invite = extractInvite(packet);
 502        if (invite != null) {
 503            if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) {
 504                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite to "+invite.jid+" because it matches account");
 505            } else if (isTypeGroupChat) {
 506                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because it was received as group chat");
 507            } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) {
 508                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC");
 509            } else {
 510                invite.execute(account);
 511                return;
 512            }
 513        }
 514
 515        if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
 516            final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
 517            final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
 518            final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
 519
 520            if (serverMsgId == null) {
 521                serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
 522            }
 523
 524
 525            if (selfAddressed) {
 526                if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
 527                    return;
 528                }
 529                status = Message.STATUS_RECEIVED;
 530                if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) {
 531                    return;
 532                }
 533            }
 534
 535            if (isTypeGroupChat) {
 536                if (conversation.getMucOptions().isSelf(counterpart)) {
 537                    status = Message.STATUS_SEND_RECEIVED;
 538                    isCarbon = true; //not really carbon but received from another resource
 539                    if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId, body)) {
 540                        return;
 541                    } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
 542                        if (body != null) {
 543                            Message message = conversation.findSentMessageWithBody(body.content);
 544                            if (message != null) {
 545                                mXmppConnectionService.markMessage(message, status);
 546                                return;
 547                            }
 548                        }
 549                    }
 550                } else {
 551                    status = Message.STATUS_RECEIVED;
 552                }
 553            }
 554            final Message message;
 555            if (pgpEncrypted != null && Config.supportOpenPgp()) {
 556                message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
 557            } else if (axolotlEncrypted != null && Config.supportOmemo()) {
 558                Jid origin;
 559                Set<Jid> fallbacksBySourceId = Collections.emptySet();
 560                if (conversationMultiMode) {
 561                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
 562                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
 563                    if (origin == null) {
 564                        try {
 565                            fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted));
 566                        } catch (IllegalArgumentException e) {
 567                            //ignoring
 568                        }
 569                    }
 570                    if (origin == null && fallbacksBySourceId.size() == 0) {
 571                        Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks");
 572                        return;
 573                    }
 574                } else {
 575                    fallbacksBySourceId = Collections.emptySet();
 576                    origin = from;
 577                }
 578
 579                final boolean liveMessage = query == null && !isTypeGroupChat && mucUserElement == null;
 580                final boolean checkedForDuplicates = liveMessage || (serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId));
 581
 582                if (origin != null) {
 583                    message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates, query != null);
 584                } else {
 585                    Message trial = null;
 586                    for (Jid fallback : fallbacksBySourceId) {
 587                        trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, checkedForDuplicates && fallbacksBySourceId.size() == 1, query != null);
 588                        if (trial != null) {
 589                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback");
 590                            origin = fallback;
 591                            break;
 592                        }
 593                    }
 594                    message = trial;
 595                }
 596                if (message == null) {
 597                    if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
 598                        mXmppConnectionService.updateConversationUi();
 599                    }
 600                    if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
 601                        Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
 602                        if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
 603                            previouslySent.setServerMsgId(serverMsgId);
 604                            mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false);
 605                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating...");
 606                        }
 607                    }
 608                    return;
 609                }
 610                if (conversationMultiMode) {
 611                    message.setTrueCounterpart(origin);
 612                }
 613            } else if (body == null && oobUrl != null) {
 614                message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
 615                message.setOob(true);
 616                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
 617                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 618                }
 619            } else {
 620                message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
 621                if (body.count > 1) {
 622                    message.setBodyLanguage(body.language);
 623                }
 624            }
 625
 626            message.setCounterpart(counterpart);
 627            message.setRemoteMsgId(remoteMsgId);
 628            message.setServerMsgId(serverMsgId);
 629            message.setCarbon(isCarbon);
 630            message.setTime(timestamp);
 631            if (body != null && body.content != null && body.content.equals(oobUrl)) {
 632                message.setOob(true);
 633                if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
 634                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 635                }
 636            }
 637            message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
 638            if (conversationMultiMode) {
 639                final var mucOptions = conversation.getMucOptions();
 640                final var occupantId =
 641                        mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
 642                if (occupantId != null) {
 643                    message.setOccupantId(occupantId.getId());
 644                }
 645                message.setMucUser(mucOptions.findUserByFullJid(counterpart));
 646                final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
 647                Jid trueCounterpart;
 648                if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 649                    trueCounterpart = message.getTrueCounterpart();
 650                } else if (query != null && query.safeToExtractTrueCounterpart()) {
 651                    trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
 652                } else {
 653                    trueCounterpart = fallback;
 654                }
 655                if (trueCounterpart != null && isTypeGroupChat) {
 656                    if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) {
 657                        status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
 658                    } else {
 659                        status = Message.STATUS_RECEIVED;
 660                        message.setCarbon(false);
 661                    }
 662                }
 663                message.setStatus(status);
 664                message.setTrueCounterpart(trueCounterpart);
 665                if (!isTypeGroupChat) {
 666                    message.setType(Message.TYPE_PRIVATE);
 667                }
 668            } else {
 669                updateLastseen(account, from);
 670            }
 671
 672            if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
 673                final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
 674                        counterpart,
 675                        message.getStatus() == Message.STATUS_RECEIVED,
 676                        message.isCarbon());
 677                if (replacedMessage != null) {
 678                    final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
 679                            || replacedMessage.getFingerprint().equals(message.getFingerprint());
 680                    final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
 681                            && message.getTrueCounterpart() != null
 682                            && replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
 683                    final boolean occupantIdMatch =
 684                            replacedMessage.getOccupantId() != null
 685                                    && replacedMessage
 686                                            .getOccupantId()
 687                                            .equals(message.getOccupantId());
 688                    final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
 689                    final boolean duplicate = conversation.hasDuplicateMessage(message);
 690                    if (fingerprintsMatch && (trueCountersMatch || occupantIdMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
 691                        synchronized (replacedMessage) {
 692                            final String uuid = replacedMessage.getUuid();
 693                            replacedMessage.setUuid(UUID.randomUUID().toString());
 694                            replacedMessage.setBody(message.getBody());
 695                            replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
 696                            replacedMessage.setRemoteMsgId(remoteMsgId);
 697                            if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
 698                                replacedMessage.setServerMsgId(message.getServerMsgId());
 699                            }
 700                            replacedMessage.setEncryption(message.getEncryption());
 701                            if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
 702                                replacedMessage.markUnread();
 703                            }
 704                            extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
 705                            mXmppConnectionService.updateMessage(replacedMessage, uuid);
 706                            if (mXmppConnectionService.confirmMessages()
 707                                    && replacedMessage.getStatus() == Message.STATUS_RECEIVED
 708                                    && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
 709                                    && remoteMsgId != null
 710                                    && !selfAddressed
 711                                    && !isTypeGroupChat) {
 712                                processMessageReceipts(account, packet, remoteMsgId, query);
 713                            }
 714                            if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
 715                                conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
 716                                conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
 717                            }
 718                        }
 719                        mXmppConnectionService.getNotificationService().updateNotification();
 720                        return;
 721                    } else {
 722                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
 723                    }
 724                }
 725            }
 726
 727            long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
 728            if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
 729                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
 730                return;
 731            }
 732
 733            boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
 734                    || message.isPrivateMessage()
 735                    || message.getServerMsgId() != null
 736                    || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
 737            if (checkForDuplicates) {
 738                final Message duplicate = conversation.findDuplicateMessage(message);
 739                if (duplicate != null) {
 740                    final boolean serverMsgIdUpdated;
 741                    if (duplicate.getStatus() != Message.STATUS_RECEIVED
 742                            && duplicate.getUuid().equals(message.getRemoteMsgId())
 743                            && duplicate.getServerMsgId() == null
 744                            && message.getServerMsgId() != null) {
 745                        duplicate.setServerMsgId(message.getServerMsgId());
 746                        if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) {
 747                            serverMsgIdUpdated = true;
 748                        } else {
 749                            serverMsgIdUpdated = false;
 750                            Log.e(Config.LOGTAG, "failed to update message");
 751                        }
 752                    } else {
 753                        serverMsgIdUpdated = false;
 754                    }
 755                    Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated);
 756                    return;
 757                }
 758            }
 759
 760            if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
 761                conversation.prepend(query.getActualInThisQuery(), message);
 762            } else {
 763                conversation.add(message);
 764            }
 765            if (query != null) {
 766                query.incrementActualMessageCount();
 767            }
 768
 769            if (query == null || query.isCatchup()) { //either no mam or catchup
 770                if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
 771                    mXmppConnectionService.markRead(conversation);
 772                    if (query == null) {
 773                        activateGracePeriod(account);
 774                    }
 775                } else {
 776                    message.markUnread();
 777                    notify = true;
 778                }
 779            }
 780
 781            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 782                notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
 783            } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
 784                notify = false;
 785            }
 786
 787            if (query == null) {
 788                extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
 789                mXmppConnectionService.updateConversationUi();
 790            }
 791
 792            if (mXmppConnectionService.confirmMessages()
 793                    && message.getStatus() == Message.STATUS_RECEIVED
 794                    && (message.trusted() || message.isPrivateMessage())
 795                    && remoteMsgId != null
 796                    && !selfAddressed
 797                    && !isTypeGroupChat) {
 798                processMessageReceipts(account, packet, remoteMsgId, query);
 799            }
 800
 801            mXmppConnectionService.databaseBackend.createMessage(message);
 802            final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
 803            if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
 804                manager.createNewDownloadConnection(message);
 805            } else if (notify) {
 806                if (query != null && query.isCatchup()) {
 807                    mXmppConnectionService.getNotificationService().pushFromBacklog(message);
 808                } else {
 809                    mXmppConnectionService.getNotificationService().push(message);
 810                }
 811            }
 812        } else if (!packet.hasChild("body")) { //no body
 813
 814            final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid());
 815            if (axolotlEncrypted != null) {
 816                Jid origin;
 817                if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
 818                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
 819                    origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
 820                    if (origin == null) {
 821                        Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received");
 822                        return;
 823                    }
 824                } else if (isTypeGroupChat) {
 825                    return;
 826                } else {
 827                    origin = from;
 828                }
 829                try {
 830                    final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid());
 831                    account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null);
 832                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin);
 833                } catch (Exception e) {
 834                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage());
 835                    return;
 836                }
 837            }
 838
 839            if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) {
 840                mXmppConnectionService.updateConversationUi();
 841            }
 842
 843            if (isTypeGroupChat) {
 844                if (packet.hasChild("subject") && !packet.hasChild("thread")) { // We already know it has no body per above
 845                    if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
 846                        conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
 847                        final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject");
 848                        if (subject != null && conversation.getMucOptions().setSubject(subject.content)) {
 849                            mXmppConnectionService.updateConversation(conversation);
 850                        }
 851                        mXmppConnectionService.updateConversationUi();
 852                        return;
 853                    }
 854                }
 855            }
 856            if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) {
 857                for (Element child : mucUserElement.getChildren()) {
 858                    if ("status".equals(child.getName())) {
 859                        try {
 860                            int code = Integer.parseInt(child.getAttribute("code"));
 861                            if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
 862                                mXmppConnectionService.fetchConferenceConfiguration(conversation);
 863                                break;
 864                            }
 865                        } catch (Exception e) {
 866                            //ignored
 867                        }
 868                    } else if ("item".equals(child.getName())) {
 869                        MucOptions.User user = AbstractParser.parseItem(conversation, child);
 870                        Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
 871                                + user.getRealJid() + " to " + user.getAffiliation() + " in "
 872                                + conversation.getJid().asBareJid());
 873                        if (!user.realJidMatchesAccount()) {
 874                            boolean isNew = conversation.getMucOptions().updateUser(user);
 875                            mXmppConnectionService.getAvatarService().clear(conversation);
 876                            mXmppConnectionService.updateMucRosterUi();
 877                            mXmppConnectionService.updateConversationUi();
 878                            Contact contact = user.getContact();
 879                            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
 880                                Jid jid = user.getRealJid();
 881                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
 882                                if (cryptoTargets.remove(user.getRealJid())) {
 883                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
 884                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
 885                                    mXmppConnectionService.updateConversation(conversation);
 886                                }
 887                            } else if (isNew
 888                                    && user.getRealJid() != null
 889                                    && conversation.getMucOptions().isPrivateAndNonAnonymous()
 890                                    && (contact == null || !contact.mutualPresenceSubscription())
 891                                    && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
 892                                account.getAxolotlService().fetchDeviceIds(user.getRealJid());
 893                            }
 894                        }
 895                    }
 896                }
 897            }
 898            if (!isTypeGroupChat) {
 899                for (Element child : packet.getChildren()) {
 900                    if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
 901                        final String action = child.getName();
 902                        final String sessionId = child.getAttribute("id");
 903                        if (sessionId == null) {
 904                            break;
 905                        }
 906                        if (query == null && offlineMessagesRetrieved) {
 907                            if (serverMsgId == null) {
 908                                serverMsgId = extractStanzaId(account, packet);
 909                            }
 910                            mXmppConnectionService
 911                                    .getJingleConnectionManager()
 912                                    .deliverMessage(
 913                                            account,
 914                                            packet.getTo(),
 915                                            packet.getFrom(),
 916                                            child,
 917                                            remoteMsgId,
 918                                            serverMsgId,
 919                                            timestamp);
 920                            final Contact contact = account.getRoster().getContact(from);
 921                            // this is the same condition that is found in JingleRtpConnection for
 922                            // the 'ringing' response. Responding with delivery receipts predates
 923                            // the 'ringing' spec'd
 924                            final boolean sendReceipts =
 925                                    (mXmppConnectionService.confirmMessages()
 926                                                    && contact.showInContactList())
 927                                            || Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
 928                            if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
 929                                processMessageReceipts(account, packet, remoteMsgId, null);
 930                            }
 931                        } else if ((query != null && query.isCatchup()) || !offlineMessagesRetrieved) {
 932                            if ("propose".equals(action)) {
 933                                final Element description = child.findChild("description");
 934                                final String namespace =
 935                                        description == null ? null : description.getNamespace();
 936                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
 937                                    final Conversation c =
 938                                            mXmppConnectionService.findOrCreateConversation(
 939                                                    account, counterpart.asBareJid(), false, false);
 940                                    final Message preExistingMessage =
 941                                            c.findRtpSession(sessionId, status);
 942                                    if (preExistingMessage != null) {
 943                                        preExistingMessage.setServerMsgId(serverMsgId);
 944                                        mXmppConnectionService.updateMessage(preExistingMessage);
 945                                        break;
 946                                    }
 947                                    final Message message =
 948                                            new Message(
 949                                                    c, status, Message.TYPE_RTP_SESSION, sessionId);
 950                                    message.setServerMsgId(serverMsgId);
 951                                    message.setTime(timestamp);
 952                                    message.setBody(new RtpSessionStatus(false, 0).toString());
 953                                    c.add(message);
 954                                    mXmppConnectionService.databaseBackend.createMessage(message);
 955                                }
 956                            } else if ("proceed".equals(action)) {
 957                                // status needs to be flipped to find the original propose
 958                                final Conversation c =
 959                                        mXmppConnectionService.findOrCreateConversation(
 960                                                account, counterpart.asBareJid(), false, false);
 961                                final int s =
 962                                        packet.fromAccount(account)
 963                                                ? Message.STATUS_RECEIVED
 964                                                : Message.STATUS_SEND;
 965                                final Message message = c.findRtpSession(sessionId, s);
 966                                if (message != null) {
 967                                    message.setBody(new RtpSessionStatus(true, 0).toString());
 968                                    if (serverMsgId != null) {
 969                                        message.setServerMsgId(serverMsgId);
 970                                    }
 971                                    message.setTime(timestamp);
 972                                    mXmppConnectionService.updateMessage(message, true);
 973                                } else {
 974                                    Log.d(
 975                                            Config.LOGTAG,
 976                                            "unable to find original rtp session message for received propose");
 977                                }
 978
 979                            } else if ("finish".equals(action)) {
 980                                Log.d(
 981                                        Config.LOGTAG,
 982                                        "received JMI 'finish' during MAM catch-up. Can be used to update success/failure and duration");
 983                            }
 984                        } else {
 985                            //MAM reloads (non catchups
 986                            if ("propose".equals(action)) {
 987                                final Element description = child.findChild("description");
 988                                final String namespace = description == null ? null : description.getNamespace();
 989                                if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
 990                                    final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false);
 991                                    final Message preExistingMessage = c.findRtpSession(sessionId, status);
 992                                    if (preExistingMessage != null) {
 993                                        preExistingMessage.setServerMsgId(serverMsgId);
 994                                        mXmppConnectionService.updateMessage(preExistingMessage);
 995                                        break;
 996                                    }
 997                                    final Message message = new Message(
 998                                            c,
 999                                            status,
1000                                            Message.TYPE_RTP_SESSION,
1001                                            sessionId
1002                                    );
1003                                    message.setServerMsgId(serverMsgId);
1004                                    message.setTime(timestamp);
1005                                    message.setBody(new RtpSessionStatus(true, 0).toString());
1006                                    if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
1007                                        c.prepend(query.getActualInThisQuery(), message);
1008                                    } else {
1009                                        c.add(message);
1010                                    }
1011                                    query.incrementActualMessageCount();
1012                                    mXmppConnectionService.databaseBackend.createMessage(message);
1013                                }
1014                            }
1015                        }
1016                        break;
1017                    }
1018                }
1019            }
1020        }
1021
1022        Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
1023        if (received == null) {
1024            received = packet.findChild("received", "urn:xmpp:receipts");
1025        }
1026        if (received != null) {
1027            String id = received.getAttribute("id");
1028            if (packet.fromAccount(account)) {
1029                if (query != null && id != null && packet.getTo() != null) {
1030                    query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id));
1031                }
1032            } else if (id != null) {
1033                if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
1034                    final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
1035                    mXmppConnectionService.getJingleConnectionManager()
1036                            .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.DISCOVERED);
1037                } else {
1038                    mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED);
1039                }
1040            }
1041        }
1042        final Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
1043        if (displayed != null) {
1044            final String id = displayed.getAttribute("id");
1045            // TODO we don’t even use 'sender' any more. Remove this!
1046            final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
1047            if (packet.fromAccount(account) && !selfAddressed) {
1048                final Conversation c =
1049                        mXmppConnectionService.find(account, counterpart.asBareJid());
1050                final Message message =
1051                        (c == null || id == null) ? null : c.findReceivedWithRemoteId(id);
1052                if (message != null && (query == null || query.isCatchup())) {
1053                    mXmppConnectionService.markReadUpTo(c, message);
1054                }
1055                if (query == null) {
1056                    activateGracePeriod(account);
1057                }
1058            } else if (isTypeGroupChat) {
1059                final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
1060                final Message message;
1061                if (conversation != null && id != null) {
1062                    if (sender != null) {
1063                        message = conversation.findMessageWithRemoteId(id, sender);
1064                    } else {
1065                        message = conversation.findMessageWithServerMsgId(id);
1066                    }
1067                } else {
1068                    message = null;
1069                }
1070                if (message != null) {
1071                    // TODO use occupantId to extract true counterpart from presence
1072                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
1073                    // TODO try to externalize mucTrueCounterpart
1074                    final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
1075                    final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
1076                    if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
1077                        if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
1078                            mXmppConnectionService.markReadUpTo(conversation, message);
1079                        }
1080                    } else if (!counterpart.isBareJid() && trueJid != null) {
1081                        final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
1082                        if (message.addReadByMarker(readByMarker)) {
1083                            final var mucOptions = conversation.getMucOptions();
1084                            final var everyone = ImmutableSet.copyOf(mucOptions.getMembers(false));
1085                            final var readyBy = message.getReadyByTrue();
1086                            final var mStatus = message.getStatus();
1087                            if (mucOptions.isPrivateAndNonAnonymous()
1088                                    && (mStatus == Message.STATUS_SEND_RECEIVED
1089                                            || mStatus == Message.STATUS_SEND)
1090                                    && readyBy.containsAll(everyone)) {
1091                                message.setStatus(Message.STATUS_SEND_DISPLAYED);
1092                            }
1093                            mXmppConnectionService.updateMessage(message, false);
1094                        }
1095                    }
1096                }
1097            } else {
1098                final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
1099                Message message = displayedMessage == null ? null : displayedMessage.prev();
1100                while (message != null
1101                        && message.getStatus() == Message.STATUS_SEND_RECEIVED
1102                        && message.getTimeSent() < displayedMessage.getTimeSent()) {
1103                    mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
1104                    message = message.prev();
1105                }
1106                if (displayedMessage != null && selfAddressed) {
1107                    dismissNotification(account, counterpart, query, id);
1108                }
1109            }
1110        }
1111
1112        if (reactions != null) {
1113            final String reactingTo = reactions.getId();
1114            final Conversation conversation =
1115                    mXmppConnectionService.find(account, counterpart.asBareJid());
1116
1117            if (conversation != null) {
1118                if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
1119                    final var mucOptions = conversation.getMucOptions();
1120                    final var occupant =
1121                            mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
1122                    final var occupantId = occupant == null ? null : occupant.getId();
1123                    final var message = conversation.findMessageWithServerMsgId(reactingTo);
1124                    // TODO use occupant id for isSelf assessment
1125                    final boolean isReceived = !mucOptions.isSelf(counterpart);
1126                    if (occupantId != null && message != null) {
1127                        final var combinedReactions =
1128                                Reaction.withOccupantId(
1129                                        message.getReactions(),
1130                                        reactions.getReactions(),
1131                                        isReceived,
1132                                        counterpart,
1133                                        null,
1134                                        occupantId);
1135                        message.setReactions(combinedReactions);
1136                        mXmppConnectionService.updateMessage(message, false);
1137                    } else {
1138                        Log.d(Config.LOGTAG,"not found occupant or message");
1139                    }
1140                } else if (conversation.getMode() == Conversational.MODE_SINGLE) {
1141                    final var message = conversation.findMessageWithUuidOrRemoteId(reactingTo);
1142                    final boolean isReceived;
1143                    final Jid reactionFrom;
1144                    if (packet.fromAccount(account)) {
1145                        isReceived = false;
1146                        reactionFrom = account.getJid().asBareJid();
1147                    } else {
1148                        isReceived = true;
1149                        reactionFrom = counterpart;
1150                    }
1151                    packet.fromAccount(account);
1152                    if (message != null) {
1153                        final var combinedReactions =
1154                                Reaction.withFrom(
1155                                        message.getReactions(),
1156                                        reactions.getReactions(),
1157                                        isReceived,
1158                                        reactionFrom);
1159                        message.setReactions(combinedReactions);
1160                        mXmppConnectionService.updateMessage(message, false);
1161                    }
1162                }
1163            }
1164        }
1165
1166        final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
1167        if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) {
1168            if (event.hasChild("items")) {
1169                parseEvent(event, original.getFrom(), account);
1170            } else if (event.hasChild("delete")) {
1171                parseDeleteEvent(event, original.getFrom(), account);
1172            } else if (event.hasChild("purge")) {
1173                parsePurgeEvent(event, original.getFrom(), account);
1174            }
1175        }
1176
1177        final String nick = packet.findChildContent("nick", Namespace.NICK);
1178        if (nick != null && InvalidJid.hasValidFrom(original)) {
1179            if (mXmppConnectionService.isMuc(account, from)) {
1180                return;
1181            }
1182            final Contact contact = account.getRoster().getContact(from);
1183            if (contact.setPresenceName(nick)) {
1184                mXmppConnectionService.syncRoster(account);
1185                mXmppConnectionService.getAvatarService().clear(contact);
1186            }
1187        }
1188    }
1189
1190    private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, Class<? extends Extension> clazz) {
1191        final var extension = original.getExtension(clazz);
1192        final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
1193        if (forwarded == null) {
1194            return null;
1195        }
1196        final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1197        final var forwardedMessage = forwarded.getMessage();
1198        if (forwardedMessage == null) {
1199            return null;
1200        }
1201        return new Pair<>(forwardedMessage,timestamp);
1202    }
1203
1204    private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, final String name, final String namespace) {
1205        final Element wrapper = original.findChild(name, namespace);
1206        final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD);
1207        if (forwardedElement instanceof Forwarded forwarded) {
1208            final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
1209            final var forwardedMessage = forwarded.getMessage();
1210            if (forwardedMessage == null) {
1211                return null;
1212            }
1213            return new Pair<>(forwardedMessage,timestamp);
1214        }
1215        return null;
1216    }
1217
1218    private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
1219        final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
1220        if (conversation != null && (query == null || query.isCatchup())) {
1221            final String displayableId = conversation.findMostRecentRemoteDisplayableId();
1222            if (displayableId != null && displayableId.equals(id)) {
1223                mXmppConnectionService.markRead(conversation);
1224            } else {
1225                Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": received dismissing display marker that did not match our last id in that conversation");
1226            }
1227        }
1228    }
1229
1230    private void processMessageReceipts(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet, final String remoteMsgId, MessageArchiveService.Query query) {
1231        final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
1232        final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
1233        if (query == null) {
1234            final ArrayList<String> receiptsNamespaces = new ArrayList<>();
1235            if (markable) {
1236                receiptsNamespaces.add("urn:xmpp:chat-markers:0");
1237            }
1238            if (request) {
1239                receiptsNamespaces.add("urn:xmpp:receipts");
1240            }
1241            if (receiptsNamespaces.size() > 0) {
1242                final var receipt = mXmppConnectionService.getMessageGenerator().received(account,
1243                        packet.getFrom(),
1244                        remoteMsgId,
1245                        receiptsNamespaces,
1246                        packet.getType());
1247                mXmppConnectionService.sendMessagePacket(account, receipt);
1248            }
1249        } else if (query.isCatchup()) {
1250            if (request) {
1251                query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId));
1252            }
1253        }
1254    }
1255
1256    private void activateGracePeriod(Account account) {
1257        long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
1258        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
1259        account.activateGracePeriod(duration);
1260    }
1261
1262    private class Invite {
1263        final Jid jid;
1264        final String password;
1265        final boolean direct;
1266        final Jid inviter;
1267
1268        Invite(Jid jid, String password, boolean direct, Jid inviter) {
1269            this.jid = jid;
1270            this.password = password;
1271            this.direct = direct;
1272            this.inviter = inviter;
1273        }
1274
1275        public boolean execute(final Account account) {
1276            if (this.jid == null) {
1277                return false;
1278            }
1279            final Contact contact =
1280                    this.inviter != null ? account.getRoster().getContact(this.inviter) : null;
1281            if (contact != null && contact.isBlocked()) {
1282                Log.d(
1283                        Config.LOGTAG,
1284                        account.getJid().asBareJid()
1285                                + ": ignore invite from "
1286                                + contact.getJid()
1287                                + " because contact is blocked");
1288                return false;
1289            }
1290            final AppSettings appSettings = new AppSettings(mXmppConnectionService);
1291            if ((contact != null && contact.showInContactList())
1292                    || appSettings.isAcceptInvitesFromStrangers()) {
1293                final Conversation conversation =
1294                        mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
1295                if (conversation.getMucOptions().online()) {
1296                    Log.d(
1297                            Config.LOGTAG,
1298                            account.getJid().asBareJid()
1299                                    + ": received invite to "
1300                                    + jid
1301                                    + " but muc is considered to be online");
1302                    mXmppConnectionService.mucSelfPingAndRejoin(conversation);
1303                } else {
1304                    conversation.getMucOptions().setPassword(password);
1305                    mXmppConnectionService.databaseBackend.updateConversation(conversation);
1306                    mXmppConnectionService.joinMuc(
1307                            conversation, contact != null && contact.showInContactList());
1308                    mXmppConnectionService.updateConversationUi();
1309                }
1310                return true;
1311            } else {
1312                Log.d(
1313                        Config.LOGTAG,
1314                        account.getJid().asBareJid()
1315                                + ": ignoring invite from "
1316                                + this.inviter
1317                                + " because we are not accepting invites from strangers. direct="
1318                                + direct);
1319                return false;
1320            }
1321        }
1322    }
1323}