Message.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.database.Cursor;
   5import android.graphics.Color;
   6import android.text.SpannableStringBuilder;
   7import android.util.Log;
   8
   9import com.google.common.base.Strings;
  10import com.google.common.collect.Collections2;
  11import com.google.common.collect.ImmutableSet;
  12import com.google.common.primitives.Longs;
  13
  14import org.json.JSONException;
  15
  16import java.lang.ref.WeakReference;
  17import java.util.ArrayList;
  18import java.util.Arrays;
  19import java.util.Collection;
  20import java.util.Collections;
  21import java.util.Iterator;
  22import java.util.List;
  23import java.util.Set;
  24import java.util.concurrent.CopyOnWriteArraySet;
  25
  26import eu.siacs.conversations.Config;
  27import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  28import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  29import eu.siacs.conversations.http.URL;
  30import eu.siacs.conversations.services.AvatarService;
  31import eu.siacs.conversations.ui.util.PresenceSelector;
  32import eu.siacs.conversations.utils.CryptoHelper;
  33import eu.siacs.conversations.utils.Emoticons;
  34import eu.siacs.conversations.utils.GeoHelper;
  35import eu.siacs.conversations.utils.MessageUtils;
  36import eu.siacs.conversations.utils.MimeUtils;
  37import eu.siacs.conversations.utils.UIHelper;
  38import eu.siacs.conversations.xmpp.Jid;
  39
  40public class Message extends AbstractEntity implements AvatarService.Avatarable {
  41
  42    public static final String TABLENAME = "messages";
  43
  44    public static final int STATUS_RECEIVED = 0;
  45    public static final int STATUS_UNSEND = 1;
  46    public static final int STATUS_SEND = 2;
  47    public static final int STATUS_SEND_FAILED = 3;
  48    public static final int STATUS_WAITING = 5;
  49    public static final int STATUS_OFFERED = 6;
  50    public static final int STATUS_SEND_RECEIVED = 7;
  51    public static final int STATUS_SEND_DISPLAYED = 8;
  52
  53    public static final int ENCRYPTION_NONE = 0;
  54    public static final int ENCRYPTION_PGP = 1;
  55    public static final int ENCRYPTION_OTR = 2;
  56    public static final int ENCRYPTION_DECRYPTED = 3;
  57    public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
  58    public static final int ENCRYPTION_AXOLOTL = 5;
  59    public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
  60    public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
  61
  62    public static final int TYPE_TEXT = 0;
  63    public static final int TYPE_IMAGE = 1;
  64    public static final int TYPE_FILE = 2;
  65    public static final int TYPE_STATUS = 3;
  66    public static final int TYPE_PRIVATE = 4;
  67    public static final int TYPE_PRIVATE_FILE = 5;
  68    public static final int TYPE_RTP_SESSION = 6;
  69
  70    public static final String CONVERSATION = "conversationUuid";
  71    public static final String COUNTERPART = "counterpart";
  72    public static final String TRUE_COUNTERPART = "trueCounterpart";
  73    public static final String BODY = "body";
  74    public static final String BODY_LANGUAGE = "bodyLanguage";
  75    public static final String TIME_SENT = "timeSent";
  76    public static final String ENCRYPTION = "encryption";
  77    public static final String STATUS = "status";
  78    public static final String TYPE = "type";
  79    public static final String CARBON = "carbon";
  80    public static final String OOB = "oob";
  81    public static final String EDITED = "edited";
  82    public static final String REMOTE_MSG_ID = "remoteMsgId";
  83    public static final String SERVER_MSG_ID = "serverMsgId";
  84    public static final String RELATIVE_FILE_PATH = "relativeFilePath";
  85    public static final String FINGERPRINT = "axolotl_fingerprint";
  86    public static final String READ = "read";
  87    public static final String ERROR_MESSAGE = "errorMsg";
  88    public static final String READ_BY_MARKERS = "readByMarkers";
  89    public static final String MARKABLE = "markable";
  90    public static final String DELETED = "deleted";
  91    public static final String OCCUPANT_ID = "occupantId";
  92    public static final String REACTIONS = "reactions";
  93    public static final String ME_COMMAND = "/me ";
  94
  95    public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
  96
  97
  98    public boolean markable = false;
  99    protected String conversationUuid;
 100    protected Jid counterpart;
 101    protected Jid trueCounterpart;
 102    protected String body;
 103    protected String encryptedBody;
 104    protected long timeSent;
 105    protected int encryption;
 106    protected int status;
 107    protected int type;
 108    protected boolean deleted = false;
 109    protected boolean carbon = false;
 110    protected boolean oob = false;
 111    protected List<Edit> edits = new ArrayList<>();
 112    protected String relativeFilePath;
 113    protected boolean read = true;
 114    protected String remoteMsgId = null;
 115    private String bodyLanguage = null;
 116    protected String serverMsgId = null;
 117    private final Conversational conversation;
 118    protected Transferable transferable = null;
 119    private Message mNextMessage = null;
 120    private Message mPreviousMessage = null;
 121    private String axolotlFingerprint = null;
 122    private String errorMessage = null;
 123    private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
 124    private String occupantId;
 125    private Collection<Reaction> reactions = Collections.emptyList();
 126
 127    private Boolean isGeoUri = null;
 128    private Boolean isEmojisOnly = null;
 129    private Boolean treatAsDownloadable = null;
 130    private FileParams fileParams = null;
 131    private List<MucOptions.User> counterparts;
 132    private WeakReference<MucOptions.User> user;
 133
 134    protected Message(Conversational conversation) {
 135        this.conversation = conversation;
 136    }
 137
 138    public Message(Conversational conversation, String body, int encryption) {
 139        this(conversation, body, encryption, STATUS_UNSEND);
 140    }
 141
 142    public Message(Conversational conversation, String body, int encryption, int status) {
 143        this(conversation, java.util.UUID.randomUUID().toString(),
 144                conversation.getUuid(),
 145                conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 146                null,
 147                body,
 148                System.currentTimeMillis(),
 149                encryption,
 150                status,
 151                TYPE_TEXT,
 152                false,
 153                null,
 154                null,
 155                null,
 156                null,
 157                true,
 158                null,
 159                false,
 160                null,
 161                null,
 162                false,
 163                false,
 164                null,
 165                null,
 166                Collections.emptyList());
 167    }
 168
 169    public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
 170        this(conversation, java.util.UUID.randomUUID().toString(),
 171                conversation.getUuid(),
 172                conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 173                null,
 174                null,
 175                System.currentTimeMillis(),
 176                Message.ENCRYPTION_NONE,
 177                status,
 178                type,
 179                false,
 180                remoteMsgId,
 181                null,
 182                null,
 183                null,
 184                true,
 185                null,
 186                false,
 187                null,
 188                null,
 189                false,
 190                false,
 191                null,
 192                null,
 193                Collections.emptyList());
 194    }
 195
 196    protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
 197                      final Jid trueCounterpart, final String body, final long timeSent,
 198                      final int encryption, final int status, final int type, final boolean carbon,
 199                      final String remoteMsgId, final String relativeFilePath,
 200                      final String serverMsgId, final String fingerprint, final boolean read,
 201                      final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
 202                      final boolean markable, final boolean deleted, final String bodyLanguage, final String occupantId, final Collection<Reaction> reactions) {
 203        this.conversation = conversation;
 204        this.uuid = uuid;
 205        this.conversationUuid = conversationUUid;
 206        this.counterpart = counterpart;
 207        this.trueCounterpart = trueCounterpart;
 208        this.body = body == null ? "" : body;
 209        this.timeSent = timeSent;
 210        this.encryption = encryption;
 211        this.status = status;
 212        this.type = type;
 213        this.carbon = carbon;
 214        this.remoteMsgId = remoteMsgId;
 215        this.relativeFilePath = relativeFilePath;
 216        this.serverMsgId = serverMsgId;
 217        this.axolotlFingerprint = fingerprint;
 218        this.read = read;
 219        this.edits = Edit.fromJson(edited);
 220        this.oob = oob;
 221        this.errorMessage = errorMessage;
 222        this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
 223        this.markable = markable;
 224        this.deleted = deleted;
 225        this.bodyLanguage = bodyLanguage;
 226        this.occupantId = occupantId;
 227        this.reactions = reactions;
 228    }
 229
 230    public static Message fromCursor(Cursor cursor, Conversation conversation) {
 231        return new Message(conversation,
 232                cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
 233                cursor.getString(cursor.getColumnIndexOrThrow(CONVERSATION)),
 234                fromString(cursor.getString(cursor.getColumnIndexOrThrow(COUNTERPART))),
 235                fromString(cursor.getString(cursor.getColumnIndexOrThrow(TRUE_COUNTERPART))),
 236                cursor.getString(cursor.getColumnIndexOrThrow(BODY)),
 237                cursor.getLong(cursor.getColumnIndexOrThrow(TIME_SENT)),
 238                cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTION)),
 239                cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
 240                cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)),
 241                cursor.getInt(cursor.getColumnIndexOrThrow(CARBON)) > 0,
 242                cursor.getString(cursor.getColumnIndexOrThrow(REMOTE_MSG_ID)),
 243                cursor.getString(cursor.getColumnIndexOrThrow(RELATIVE_FILE_PATH)),
 244                cursor.getString(cursor.getColumnIndexOrThrow(SERVER_MSG_ID)),
 245                cursor.getString(cursor.getColumnIndexOrThrow(FINGERPRINT)),
 246                cursor.getInt(cursor.getColumnIndexOrThrow(READ)) > 0,
 247                cursor.getString(cursor.getColumnIndexOrThrow(EDITED)),
 248                cursor.getInt(cursor.getColumnIndexOrThrow(OOB)) > 0,
 249                cursor.getString(cursor.getColumnIndexOrThrow(ERROR_MESSAGE)),
 250                ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))),
 251                cursor.getInt(cursor.getColumnIndexOrThrow(MARKABLE)) > 0,
 252                cursor.getInt(cursor.getColumnIndexOrThrow(DELETED)) > 0,
 253                cursor.getString(cursor.getColumnIndexOrThrow(BODY_LANGUAGE)),
 254                cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)),
 255                Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS)))
 256
 257        );
 258    }
 259
 260    private static Jid fromString(String value) {
 261        try {
 262            if (value != null) {
 263                return Jid.of(value);
 264            }
 265        } catch (IllegalArgumentException e) {
 266            return null;
 267        }
 268        return null;
 269    }
 270
 271    public static Message createStatusMessage(Conversation conversation, String body) {
 272        final Message message = new Message(conversation);
 273        message.setType(Message.TYPE_STATUS);
 274        message.setStatus(Message.STATUS_RECEIVED);
 275        message.body = body;
 276        return message;
 277    }
 278
 279    public static Message createLoadMoreMessage(Conversation conversation) {
 280        final Message message = new Message(conversation);
 281        message.setType(Message.TYPE_STATUS);
 282        message.body = "LOAD_MORE";
 283        return message;
 284    }
 285
 286    @Override
 287    public ContentValues getContentValues() {
 288        final var values = new ContentValues();
 289        values.put(UUID, uuid);
 290        values.put(CONVERSATION, conversationUuid);
 291        if (counterpart == null) {
 292            values.putNull(COUNTERPART);
 293        } else {
 294            values.put(COUNTERPART, counterpart.toString());
 295        }
 296        if (trueCounterpart == null) {
 297            values.putNull(TRUE_COUNTERPART);
 298        } else {
 299            values.put(TRUE_COUNTERPART, trueCounterpart.toString());
 300        }
 301        values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
 302        values.put(TIME_SENT, timeSent);
 303        values.put(ENCRYPTION, encryption);
 304        values.put(STATUS, status);
 305        values.put(TYPE, type);
 306        values.put(CARBON, carbon ? 1 : 0);
 307        values.put(REMOTE_MSG_ID, remoteMsgId);
 308        values.put(RELATIVE_FILE_PATH, relativeFilePath);
 309        values.put(SERVER_MSG_ID, serverMsgId);
 310        values.put(FINGERPRINT, axolotlFingerprint);
 311        values.put(READ, read ? 1 : 0);
 312        try {
 313            values.put(EDITED, Edit.toJson(edits));
 314        } catch (JSONException e) {
 315            Log.e(Config.LOGTAG, "error persisting json for edits", e);
 316        }
 317        values.put(OOB, oob ? 1 : 0);
 318        values.put(ERROR_MESSAGE, errorMessage);
 319        values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
 320        values.put(MARKABLE, markable ? 1 : 0);
 321        values.put(DELETED, deleted ? 1 : 0);
 322        values.put(BODY_LANGUAGE, bodyLanguage);
 323        values.put(OCCUPANT_ID, occupantId);
 324        values.put(REACTIONS, Reaction.toString(this.reactions));
 325        return values;
 326    }
 327
 328    public String getConversationUuid() {
 329        return conversationUuid;
 330    }
 331
 332    public Conversational getConversation() {
 333        return this.conversation;
 334    }
 335
 336    public Jid getCounterpart() {
 337        return counterpart;
 338    }
 339
 340    public void setCounterpart(final Jid counterpart) {
 341        this.counterpart = counterpart;
 342    }
 343
 344    public Contact getContact() {
 345        if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
 346            return this.conversation.getContact();
 347        } else {
 348            if (this.trueCounterpart == null) {
 349                return null;
 350            } else {
 351                return this.conversation.getAccount().getRoster()
 352                        .getContactFromContactList(this.trueCounterpart);
 353            }
 354        }
 355    }
 356
 357    public String getBody() {
 358        return body;
 359    }
 360
 361    public synchronized void setBody(String body) {
 362        if (body == null) {
 363            throw new Error("You should not set the message body to null");
 364        }
 365        this.body = body;
 366        this.isGeoUri = null;
 367        this.isEmojisOnly = null;
 368        this.treatAsDownloadable = null;
 369        this.fileParams = null;
 370    }
 371
 372    public void setMucUser(MucOptions.User user) {
 373        this.user = new WeakReference<>(user);
 374    }
 375
 376    public boolean sameMucUser(Message otherMessage) {
 377        final MucOptions.User thisUser = this.user == null ? null : this.user.get();
 378        final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
 379        return thisUser != null && thisUser == otherUser;
 380    }
 381
 382    public String getErrorMessage() {
 383        return errorMessage;
 384    }
 385
 386    public boolean setErrorMessage(String message) {
 387        boolean changed = (message != null && !message.equals(errorMessage))
 388                || (message == null && errorMessage != null);
 389        this.errorMessage = message;
 390        return changed;
 391    }
 392
 393    public long getTimeSent() {
 394        return timeSent;
 395    }
 396
 397    public int getEncryption() {
 398        return encryption;
 399    }
 400
 401    public void setEncryption(int encryption) {
 402        this.encryption = encryption;
 403    }
 404
 405    public int getStatus() {
 406        return status;
 407    }
 408
 409    public void setStatus(int status) {
 410        this.status = status;
 411    }
 412
 413    public String getRelativeFilePath() {
 414        return this.relativeFilePath;
 415    }
 416
 417    public void setRelativeFilePath(String path) {
 418        this.relativeFilePath = path;
 419    }
 420
 421    public String getRemoteMsgId() {
 422        return this.remoteMsgId;
 423    }
 424
 425    public void setRemoteMsgId(String id) {
 426        this.remoteMsgId = id;
 427    }
 428
 429    public String getServerMsgId() {
 430        return this.serverMsgId;
 431    }
 432
 433    public void setServerMsgId(String id) {
 434        this.serverMsgId = id;
 435    }
 436
 437    public boolean isRead() {
 438        return this.read;
 439    }
 440
 441    public boolean isDeleted() {
 442        return this.deleted;
 443    }
 444
 445    public void setDeleted(boolean deleted) {
 446        this.deleted = deleted;
 447    }
 448
 449    public void markRead() {
 450        this.read = true;
 451    }
 452
 453    public void markUnread() {
 454        this.read = false;
 455    }
 456
 457    public void setTime(long time) {
 458        this.timeSent = time;
 459    }
 460
 461    public String getEncryptedBody() {
 462        return this.encryptedBody;
 463    }
 464
 465    public void setEncryptedBody(String body) {
 466        this.encryptedBody = body;
 467    }
 468
 469    public int getType() {
 470        return this.type;
 471    }
 472
 473    public void setType(int type) {
 474        this.type = type;
 475    }
 476
 477    public boolean isCarbon() {
 478        return carbon;
 479    }
 480
 481    public void setCarbon(boolean carbon) {
 482        this.carbon = carbon;
 483    }
 484
 485    public void putEdited(String edited, String serverMsgId) {
 486        final Edit edit = new Edit(edited, serverMsgId);
 487        if (this.edits.size() < 128 && !this.edits.contains(edit)) {
 488            this.edits.add(edit);
 489        }
 490    }
 491
 492    boolean remoteMsgIdMatchInEdit(String id) {
 493        for (Edit edit : this.edits) {
 494            if (id.equals(edit.getEditedId())) {
 495                return true;
 496            }
 497        }
 498        return false;
 499    }
 500
 501    public String getBodyLanguage() {
 502        return this.bodyLanguage;
 503    }
 504
 505    public void setBodyLanguage(String language) {
 506        this.bodyLanguage = language;
 507    }
 508
 509    public boolean edited() {
 510        return this.edits.size() > 0;
 511    }
 512
 513    public void setTrueCounterpart(Jid trueCounterpart) {
 514        this.trueCounterpart = trueCounterpart;
 515    }
 516
 517    public Jid getTrueCounterpart() {
 518        return this.trueCounterpart;
 519    }
 520
 521    public Transferable getTransferable() {
 522        return this.transferable;
 523    }
 524
 525    public synchronized void setTransferable(Transferable transferable) {
 526        this.fileParams = null;
 527        this.transferable = transferable;
 528    }
 529
 530    public boolean addReadByMarker(final ReadByMarker readByMarker) {
 531        if (readByMarker.getRealJid() != null) {
 532            if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
 533                return false;
 534            }
 535        } else if (readByMarker.getFullJid() != null) {
 536            if (readByMarker.getFullJid().equals(counterpart)) {
 537                return false;
 538            }
 539        }
 540        if (this.readByMarkers.add(readByMarker)) {
 541            if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
 542                Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
 543                while (iterator.hasNext()) {
 544                    ReadByMarker marker = iterator.next();
 545                    if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
 546                        iterator.remove();
 547                    }
 548                }
 549            }
 550            return true;
 551        } else {
 552            return false;
 553        }
 554    }
 555
 556    public Set<ReadByMarker> getReadByMarkers() {
 557        return ImmutableSet.copyOf(this.readByMarkers);
 558    }
 559
 560    public Set<Jid> getReadyByTrue() {
 561        return ImmutableSet.copyOf(
 562                Collections2.transform(
 563                        Collections2.filter(this.readByMarkers, m -> m.getRealJid() != null),
 564                        ReadByMarker::getRealJid));
 565    }
 566
 567    boolean similar(Message message) {
 568        if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
 569            return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
 570        } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
 571            return true;
 572        } else if (this.body == null || this.counterpart == null) {
 573            return false;
 574        } else {
 575            String body, otherBody;
 576            if (this.hasFileOnRemoteHost()) {
 577                body = getFileParams().url;
 578                otherBody = message.body == null ? null : message.body.trim();
 579            } else {
 580                body = this.body;
 581                otherBody = message.body;
 582            }
 583            final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
 584            if (message.getRemoteMsgId() != null) {
 585                final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
 586                if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
 587                    return true;
 588                }
 589                return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
 590                        && matchingCounterpart
 591                        && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
 592            } else {
 593                return this.remoteMsgId == null
 594                        && matchingCounterpart
 595                        && body.equals(otherBody)
 596                        && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
 597            }
 598        }
 599    }
 600
 601    public Message next() {
 602        if (this.conversation instanceof Conversation) {
 603            final Conversation conversation = (Conversation) this.conversation;
 604            synchronized (conversation.messages) {
 605                if (this.mNextMessage == null) {
 606                    int index = conversation.messages.indexOf(this);
 607                    if (index < 0 || index >= conversation.messages.size() - 1) {
 608                        this.mNextMessage = null;
 609                    } else {
 610                        this.mNextMessage = conversation.messages.get(index + 1);
 611                    }
 612                }
 613                return this.mNextMessage;
 614            }
 615        } else {
 616            throw new AssertionError("Calling next should be disabled for stubs");
 617        }
 618    }
 619
 620    public Message prev() {
 621        if (this.conversation instanceof Conversation) {
 622            final Conversation conversation = (Conversation) this.conversation;
 623            synchronized (conversation.messages) {
 624                if (this.mPreviousMessage == null) {
 625                    int index = conversation.messages.indexOf(this);
 626                    if (index <= 0 || index > conversation.messages.size()) {
 627                        this.mPreviousMessage = null;
 628                    } else {
 629                        this.mPreviousMessage = conversation.messages.get(index - 1);
 630                    }
 631                }
 632            }
 633            return this.mPreviousMessage;
 634        } else {
 635            throw new AssertionError("Calling prev should be disabled for stubs");
 636        }
 637    }
 638
 639    public boolean isLastCorrectableMessage() {
 640        Message next = next();
 641        while (next != null) {
 642            if (next.isEditable()) {
 643                return false;
 644            }
 645            next = next.next();
 646        }
 647        return isEditable();
 648    }
 649
 650    public boolean isEditable() {
 651        return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
 652    }
 653
 654    public boolean mergeable(final Message message) {
 655        return message != null &&
 656                (message.getType() == Message.TYPE_TEXT &&
 657                        this.getTransferable() == null &&
 658                        message.getTransferable() == null &&
 659                        message.getEncryption() != Message.ENCRYPTION_PGP &&
 660                        message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
 661                        this.getType() == message.getType() &&
 662                        this.isReactionsEmpty() &&
 663                        message.isReactionsEmpty() &&
 664                        isStatusMergeable(this.getStatus(), message.getStatus()) &&
 665                        isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
 666                        this.getCounterpart() != null &&
 667                        this.getCounterpart().equals(message.getCounterpart()) &&
 668                        this.edited() == message.edited() &&
 669                        (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
 670                        this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
 671                        !message.isGeoUri() &&
 672                        !this.isGeoUri() &&
 673                        !message.isOOb() &&
 674                        !this.isOOb() &&
 675                        !message.treatAsDownloadable() &&
 676                        !this.treatAsDownloadable() &&
 677                        !message.hasMeCommand() &&
 678                        !this.hasMeCommand() &&
 679                        !this.bodyIsOnlyEmojis() &&
 680                        !message.bodyIsOnlyEmojis() &&
 681                        ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
 682                        UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
 683                        this.getReadByMarkers().equals(message.getReadByMarkers()) &&
 684                        !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
 685                );
 686    }
 687
 688    private static boolean isStatusMergeable(int a, int b) {
 689        return a == b || (
 690                (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
 691                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
 692                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
 693                        || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
 694                        || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
 695        );
 696    }
 697
 698    private static boolean isEncryptionMergeable(final int a, final int b) {
 699        return a == b
 700                && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
 701                        .contains(a);
 702    }
 703
 704    public void setCounterparts(List<MucOptions.User> counterparts) {
 705        this.counterparts = counterparts;
 706    }
 707
 708    public List<MucOptions.User> getCounterparts() {
 709        return this.counterparts;
 710    }
 711
 712    @Override
 713    public int getAvatarBackgroundColor() {
 714        if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
 715            return Color.TRANSPARENT;
 716        } else {
 717            return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
 718        }
 719    }
 720
 721    @Override
 722    public String getAvatarName() {
 723        return UIHelper.getMessageDisplayName(this);
 724    }
 725
 726    public boolean isOOb() {
 727        return oob;
 728    }
 729
 730    public void setOccupantId(final String id) {
 731        this.occupantId = id;
 732    }
 733
 734    public String getOccupantId() {
 735        return this.occupantId;
 736    }
 737
 738    public Collection<Reaction> getReactions() {
 739        return this.reactions;
 740    }
 741
 742    public boolean isReactionsEmpty() {
 743        return this.reactions.isEmpty();
 744    }
 745
 746    public Reaction.Aggregated getAggregatedReactions() {
 747        return Reaction.aggregated(this.reactions);
 748    }
 749
 750    public void setReactions(final Collection<Reaction> reactions) {
 751        this.reactions = reactions;
 752    }
 753
 754    public static class MergeSeparator {
 755    }
 756
 757    public SpannableStringBuilder getMergedBody() {
 758        SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
 759        Message current = this;
 760        while (current.mergeable(current.next())) {
 761            current = current.next();
 762            if (current == null) {
 763                break;
 764            }
 765            body.append("\n\n");
 766            body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
 767                    SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
 768            body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
 769        }
 770        return body;
 771    }
 772
 773    public boolean hasMeCommand() {
 774        return this.body.trim().startsWith(ME_COMMAND);
 775    }
 776
 777    public int getMergedStatus() {
 778        int status = this.status;
 779        Message current = this;
 780        while (current.mergeable(current.next())) {
 781            current = current.next();
 782            if (current == null) {
 783                break;
 784            }
 785            status = current.status;
 786        }
 787        return status;
 788    }
 789
 790    public long getMergedTimeSent() {
 791        long time = this.timeSent;
 792        Message current = this;
 793        while (current.mergeable(current.next())) {
 794            current = current.next();
 795            if (current == null) {
 796                break;
 797            }
 798            time = current.timeSent;
 799        }
 800        return time;
 801    }
 802
 803    public boolean wasMergedIntoPrevious() {
 804        Message prev = this.prev();
 805        return prev != null && prev.mergeable(this);
 806    }
 807
 808    public boolean trusted() {
 809        Contact contact = this.getContact();
 810        return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
 811    }
 812
 813    public boolean fixCounterpart() {
 814        final Presences presences = conversation.getContact().getPresences();
 815        if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
 816            return true;
 817        } else if (presences.size() >= 1) {
 818            counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
 819            return true;
 820        } else {
 821            counterpart = null;
 822            return false;
 823        }
 824    }
 825
 826    public void setUuid(String uuid) {
 827        this.uuid = uuid;
 828    }
 829
 830    public String getEditedId() {
 831        if (edits.size() > 0) {
 832            return edits.get(edits.size() - 1).getEditedId();
 833        } else {
 834            throw new IllegalStateException("Attempting to store unedited message");
 835        }
 836    }
 837
 838    public String getEditedIdWireFormat() {
 839        if (edits.size() > 0) {
 840            return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
 841        } else {
 842            throw new IllegalStateException("Attempting to store unedited message");
 843        }
 844    }
 845
 846    public void setOob(boolean isOob) {
 847        this.oob = isOob;
 848    }
 849
 850    public String getMimeType() {
 851        String extension;
 852        if (relativeFilePath != null) {
 853            extension = MimeUtils.extractRelevantExtension(relativeFilePath);
 854        } else {
 855            final String url = URL.tryParse(body.split("\n")[0]);
 856            if (url == null) {
 857                return null;
 858            }
 859            extension = MimeUtils.extractRelevantExtension(url);
 860        }
 861        return MimeUtils.guessMimeTypeFromExtension(extension);
 862    }
 863
 864    public synchronized boolean treatAsDownloadable() {
 865        if (treatAsDownloadable == null) {
 866            treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
 867        }
 868        return treatAsDownloadable;
 869    }
 870
 871    public synchronized boolean bodyIsOnlyEmojis() {
 872        if (isEmojisOnly == null) {
 873            isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
 874        }
 875        return isEmojisOnly;
 876    }
 877
 878    public synchronized boolean isGeoUri() {
 879        if (isGeoUri == null) {
 880            isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
 881        }
 882        return isGeoUri;
 883    }
 884
 885    public synchronized void resetFileParams() {
 886        this.fileParams = null;
 887    }
 888
 889    public synchronized FileParams getFileParams() {
 890        if (fileParams == null) {
 891            fileParams = new FileParams();
 892            if (this.transferable != null) {
 893                fileParams.size = this.transferable.getFileSize();
 894            }
 895            final String[] parts = body == null ? new String[0] : body.split("\\|");
 896            switch (parts.length) {
 897                case 1:
 898                    try {
 899                        fileParams.size = Long.parseLong(parts[0]);
 900                    } catch (final NumberFormatException e) {
 901                        fileParams.url = URL.tryParse(parts[0]);
 902                    }
 903                    break;
 904                case 5:
 905                    fileParams.runtime = parseInt(parts[4]);
 906                case 4:
 907                    fileParams.width = parseInt(parts[2]);
 908                    fileParams.height = parseInt(parts[3]);
 909                case 2:
 910                    fileParams.url = URL.tryParse(parts[0]);
 911                    fileParams.size = Longs.tryParse(parts[1]);
 912                    break;
 913                case 3:
 914                    fileParams.size = Longs.tryParse(parts[0]);
 915                    fileParams.width = parseInt(parts[1]);
 916                    fileParams.height = parseInt(parts[2]);
 917                    break;
 918            }
 919        }
 920        return fileParams;
 921    }
 922
 923    private static int parseInt(String value) {
 924        try {
 925            return Integer.parseInt(value);
 926        } catch (NumberFormatException e) {
 927            return 0;
 928        }
 929    }
 930
 931    public void untie() {
 932        this.mNextMessage = null;
 933        this.mPreviousMessage = null;
 934    }
 935
 936    public boolean isPrivateMessage() {
 937        return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
 938    }
 939
 940    public boolean isFileOrImage() {
 941        return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
 942    }
 943
 944
 945    public boolean isTypeText() {
 946        return type == TYPE_TEXT || type == TYPE_PRIVATE;
 947    }
 948
 949    public boolean hasFileOnRemoteHost() {
 950        return isFileOrImage() && getFileParams().url != null;
 951    }
 952
 953    public boolean needsUploading() {
 954        return isFileOrImage() && getFileParams().url == null;
 955    }
 956
 957    public static class FileParams {
 958        public String url;
 959        public Long size = null;
 960        public int width = 0;
 961        public int height = 0;
 962        public int runtime = 0;
 963
 964        public long getSize() {
 965            return size == null ? 0 : size;
 966        }
 967    }
 968
 969    public void setFingerprint(String fingerprint) {
 970        this.axolotlFingerprint = fingerprint;
 971    }
 972
 973    public String getFingerprint() {
 974        return axolotlFingerprint;
 975    }
 976
 977    public boolean isTrusted() {
 978        final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
 979        final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
 980        return s != null && s.isTrusted();
 981    }
 982
 983    private int getPreviousEncryption() {
 984        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
 985            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 986                continue;
 987            }
 988            return iterator.getEncryption();
 989        }
 990        return ENCRYPTION_NONE;
 991    }
 992
 993    private int getNextEncryption() {
 994        if (this.conversation instanceof Conversation) {
 995            Conversation conversation = (Conversation) this.conversation;
 996            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
 997                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 998                    continue;
 999                }
1000                return iterator.getEncryption();
1001            }
1002            return conversation.getNextEncryption();
1003        } else {
1004            throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1005        }
1006    }
1007
1008    public boolean isValidInSession() {
1009        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1010        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1011
1012        boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1013                || futureEncryption == ENCRYPTION_NONE
1014                || pastEncryption != futureEncryption;
1015
1016        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1017    }
1018
1019    private static int getCleanedEncryption(int encryption) {
1020        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1021            return ENCRYPTION_PGP;
1022        }
1023        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1024            return ENCRYPTION_AXOLOTL;
1025        }
1026        return encryption;
1027    }
1028
1029    public static boolean configurePrivateMessage(final Message message) {
1030        return configurePrivateMessage(message, false);
1031    }
1032
1033    public static boolean configurePrivateFileMessage(final Message message) {
1034        return configurePrivateMessage(message, true);
1035    }
1036
1037    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1038        final Conversation conversation;
1039        if (message.conversation instanceof Conversation) {
1040            conversation = (Conversation) message.conversation;
1041        } else {
1042            return false;
1043        }
1044        if (conversation.getMode() == Conversation.MODE_MULTI) {
1045            final Jid nextCounterpart = conversation.getNextCounterpart();
1046            return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1047        }
1048        return false;
1049    }
1050
1051    public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1052        final Conversation conversation;
1053        if (message.conversation instanceof Conversation) {
1054            conversation = (Conversation) message.conversation;
1055        } else {
1056            return false;
1057        }
1058        return configurePrivateMessage(conversation, message, counterpart, false);
1059    }
1060
1061    private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1062        if (counterpart == null) {
1063            return false;
1064        }
1065        message.setCounterpart(counterpart);
1066        message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1067        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1068        return true;
1069    }
1070}