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                        isStatusMergeable(this.getStatus(), message.getStatus()) &&
 663                        isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
 664                        this.getCounterpart() != null &&
 665                        this.getCounterpart().equals(message.getCounterpart()) &&
 666                        this.edited() == message.edited() &&
 667                        (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
 668                        this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
 669                        !message.isGeoUri() &&
 670                        !this.isGeoUri() &&
 671                        !message.isOOb() &&
 672                        !this.isOOb() &&
 673                        !message.treatAsDownloadable() &&
 674                        !this.treatAsDownloadable() &&
 675                        !message.hasMeCommand() &&
 676                        !this.hasMeCommand() &&
 677                        !this.bodyIsOnlyEmojis() &&
 678                        !message.bodyIsOnlyEmojis() &&
 679                        ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
 680                        UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
 681                        this.getReadByMarkers().equals(message.getReadByMarkers()) &&
 682                        !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
 683                );
 684    }
 685
 686    private static boolean isStatusMergeable(int a, int b) {
 687        return a == b || (
 688                (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
 689                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
 690                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
 691                        || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
 692                        || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
 693        );
 694    }
 695
 696    private static boolean isEncryptionMergeable(final int a, final int b) {
 697        return a == b
 698                && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
 699                        .contains(a);
 700    }
 701
 702    public void setCounterparts(List<MucOptions.User> counterparts) {
 703        this.counterparts = counterparts;
 704    }
 705
 706    public List<MucOptions.User> getCounterparts() {
 707        return this.counterparts;
 708    }
 709
 710    @Override
 711    public int getAvatarBackgroundColor() {
 712        if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
 713            return Color.TRANSPARENT;
 714        } else {
 715            return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
 716        }
 717    }
 718
 719    @Override
 720    public String getAvatarName() {
 721        return UIHelper.getMessageDisplayName(this);
 722    }
 723
 724    public boolean isOOb() {
 725        return oob;
 726    }
 727
 728    public void setOccupantId(final String id) {
 729        this.occupantId = id;
 730    }
 731
 732    public String getOccupantId() {
 733        return this.occupantId;
 734    }
 735
 736    public static class MergeSeparator {
 737    }
 738
 739    public SpannableStringBuilder getMergedBody() {
 740        SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
 741        Message current = this;
 742        while (current.mergeable(current.next())) {
 743            current = current.next();
 744            if (current == null) {
 745                break;
 746            }
 747            body.append("\n\n");
 748            body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
 749                    SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
 750            body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
 751        }
 752        return body;
 753    }
 754
 755    public boolean hasMeCommand() {
 756        return this.body.trim().startsWith(ME_COMMAND);
 757    }
 758
 759    public int getMergedStatus() {
 760        int status = this.status;
 761        Message current = this;
 762        while (current.mergeable(current.next())) {
 763            current = current.next();
 764            if (current == null) {
 765                break;
 766            }
 767            status = current.status;
 768        }
 769        return status;
 770    }
 771
 772    public long getMergedTimeSent() {
 773        long time = this.timeSent;
 774        Message current = this;
 775        while (current.mergeable(current.next())) {
 776            current = current.next();
 777            if (current == null) {
 778                break;
 779            }
 780            time = current.timeSent;
 781        }
 782        return time;
 783    }
 784
 785    public boolean wasMergedIntoPrevious() {
 786        Message prev = this.prev();
 787        return prev != null && prev.mergeable(this);
 788    }
 789
 790    public boolean trusted() {
 791        Contact contact = this.getContact();
 792        return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
 793    }
 794
 795    public boolean fixCounterpart() {
 796        final Presences presences = conversation.getContact().getPresences();
 797        if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
 798            return true;
 799        } else if (presences.size() >= 1) {
 800            counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
 801            return true;
 802        } else {
 803            counterpart = null;
 804            return false;
 805        }
 806    }
 807
 808    public void setUuid(String uuid) {
 809        this.uuid = uuid;
 810    }
 811
 812    public String getEditedId() {
 813        if (edits.size() > 0) {
 814            return edits.get(edits.size() - 1).getEditedId();
 815        } else {
 816            throw new IllegalStateException("Attempting to store unedited message");
 817        }
 818    }
 819
 820    public String getEditedIdWireFormat() {
 821        if (edits.size() > 0) {
 822            return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
 823        } else {
 824            throw new IllegalStateException("Attempting to store unedited message");
 825        }
 826    }
 827
 828    public void setOob(boolean isOob) {
 829        this.oob = isOob;
 830    }
 831
 832    public String getMimeType() {
 833        String extension;
 834        if (relativeFilePath != null) {
 835            extension = MimeUtils.extractRelevantExtension(relativeFilePath);
 836        } else {
 837            final String url = URL.tryParse(body.split("\n")[0]);
 838            if (url == null) {
 839                return null;
 840            }
 841            extension = MimeUtils.extractRelevantExtension(url);
 842        }
 843        return MimeUtils.guessMimeTypeFromExtension(extension);
 844    }
 845
 846    public synchronized boolean treatAsDownloadable() {
 847        if (treatAsDownloadable == null) {
 848            treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
 849        }
 850        return treatAsDownloadable;
 851    }
 852
 853    public synchronized boolean bodyIsOnlyEmojis() {
 854        if (isEmojisOnly == null) {
 855            isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
 856        }
 857        return isEmojisOnly;
 858    }
 859
 860    public synchronized boolean isGeoUri() {
 861        if (isGeoUri == null) {
 862            isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
 863        }
 864        return isGeoUri;
 865    }
 866
 867    public synchronized void resetFileParams() {
 868        this.fileParams = null;
 869    }
 870
 871    public synchronized FileParams getFileParams() {
 872        if (fileParams == null) {
 873            fileParams = new FileParams();
 874            if (this.transferable != null) {
 875                fileParams.size = this.transferable.getFileSize();
 876            }
 877            final String[] parts = body == null ? new String[0] : body.split("\\|");
 878            switch (parts.length) {
 879                case 1:
 880                    try {
 881                        fileParams.size = Long.parseLong(parts[0]);
 882                    } catch (final NumberFormatException e) {
 883                        fileParams.url = URL.tryParse(parts[0]);
 884                    }
 885                    break;
 886                case 5:
 887                    fileParams.runtime = parseInt(parts[4]);
 888                case 4:
 889                    fileParams.width = parseInt(parts[2]);
 890                    fileParams.height = parseInt(parts[3]);
 891                case 2:
 892                    fileParams.url = URL.tryParse(parts[0]);
 893                    fileParams.size = Longs.tryParse(parts[1]);
 894                    break;
 895                case 3:
 896                    fileParams.size = Longs.tryParse(parts[0]);
 897                    fileParams.width = parseInt(parts[1]);
 898                    fileParams.height = parseInt(parts[2]);
 899                    break;
 900            }
 901        }
 902        return fileParams;
 903    }
 904
 905    private static int parseInt(String value) {
 906        try {
 907            return Integer.parseInt(value);
 908        } catch (NumberFormatException e) {
 909            return 0;
 910        }
 911    }
 912
 913    public void untie() {
 914        this.mNextMessage = null;
 915        this.mPreviousMessage = null;
 916    }
 917
 918    public boolean isPrivateMessage() {
 919        return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
 920    }
 921
 922    public boolean isFileOrImage() {
 923        return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
 924    }
 925
 926
 927    public boolean isTypeText() {
 928        return type == TYPE_TEXT || type == TYPE_PRIVATE;
 929    }
 930
 931    public boolean hasFileOnRemoteHost() {
 932        return isFileOrImage() && getFileParams().url != null;
 933    }
 934
 935    public boolean needsUploading() {
 936        return isFileOrImage() && getFileParams().url == null;
 937    }
 938
 939    public static class FileParams {
 940        public String url;
 941        public Long size = null;
 942        public int width = 0;
 943        public int height = 0;
 944        public int runtime = 0;
 945
 946        public long getSize() {
 947            return size == null ? 0 : size;
 948        }
 949    }
 950
 951    public void setFingerprint(String fingerprint) {
 952        this.axolotlFingerprint = fingerprint;
 953    }
 954
 955    public String getFingerprint() {
 956        return axolotlFingerprint;
 957    }
 958
 959    public boolean isTrusted() {
 960        final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
 961        final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
 962        return s != null && s.isTrusted();
 963    }
 964
 965    private int getPreviousEncryption() {
 966        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
 967            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 968                continue;
 969            }
 970            return iterator.getEncryption();
 971        }
 972        return ENCRYPTION_NONE;
 973    }
 974
 975    private int getNextEncryption() {
 976        if (this.conversation instanceof Conversation) {
 977            Conversation conversation = (Conversation) this.conversation;
 978            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
 979                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 980                    continue;
 981                }
 982                return iterator.getEncryption();
 983            }
 984            return conversation.getNextEncryption();
 985        } else {
 986            throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
 987        }
 988    }
 989
 990    public boolean isValidInSession() {
 991        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
 992        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
 993
 994        boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
 995                || futureEncryption == ENCRYPTION_NONE
 996                || pastEncryption != futureEncryption;
 997
 998        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
 999    }
1000
1001    private static int getCleanedEncryption(int encryption) {
1002        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1003            return ENCRYPTION_PGP;
1004        }
1005        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1006            return ENCRYPTION_AXOLOTL;
1007        }
1008        return encryption;
1009    }
1010
1011    public static boolean configurePrivateMessage(final Message message) {
1012        return configurePrivateMessage(message, false);
1013    }
1014
1015    public static boolean configurePrivateFileMessage(final Message message) {
1016        return configurePrivateMessage(message, true);
1017    }
1018
1019    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1020        final Conversation conversation;
1021        if (message.conversation instanceof Conversation) {
1022            conversation = (Conversation) message.conversation;
1023        } else {
1024            return false;
1025        }
1026        if (conversation.getMode() == Conversation.MODE_MULTI) {
1027            final Jid nextCounterpart = conversation.getNextCounterpart();
1028            return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1029        }
1030        return false;
1031    }
1032
1033    public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1034        final Conversation conversation;
1035        if (message.conversation instanceof Conversation) {
1036            conversation = (Conversation) message.conversation;
1037        } else {
1038            return false;
1039        }
1040        return configurePrivateMessage(conversation, message, counterpart, false);
1041    }
1042
1043    private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1044        if (counterpart == null) {
1045            return false;
1046        }
1047        message.setCounterpart(counterpart);
1048        message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1049        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1050        return true;
1051    }
1052}