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.ImmutableSet;
  11import com.google.common.primitives.Longs;
  12
  13import org.json.JSONException;
  14
  15import java.lang.ref.WeakReference;
  16import java.util.ArrayList;
  17import java.util.Iterator;
  18import java.util.List;
  19import java.util.Set;
  20import java.util.concurrent.CopyOnWriteArraySet;
  21
  22import eu.siacs.conversations.Config;
  23import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  24import eu.siacs.conversations.http.URL;
  25import eu.siacs.conversations.services.AvatarService;
  26import eu.siacs.conversations.ui.util.PresenceSelector;
  27import eu.siacs.conversations.utils.CryptoHelper;
  28import eu.siacs.conversations.utils.Emoticons;
  29import eu.siacs.conversations.utils.GeoHelper;
  30import eu.siacs.conversations.utils.MessageUtils;
  31import eu.siacs.conversations.utils.MimeUtils;
  32import eu.siacs.conversations.utils.UIHelper;
  33import eu.siacs.conversations.xmpp.Jid;
  34
  35public class Message extends AbstractEntity implements AvatarService.Avatarable {
  36
  37    public static final String TABLENAME = "messages";
  38
  39    public static final int STATUS_RECEIVED = 0;
  40    public static final int STATUS_UNSEND = 1;
  41    public static final int STATUS_SEND = 2;
  42    public static final int STATUS_SEND_FAILED = 3;
  43    public static final int STATUS_WAITING = 5;
  44    public static final int STATUS_OFFERED = 6;
  45    public static final int STATUS_SEND_RECEIVED = 7;
  46    public static final int STATUS_SEND_DISPLAYED = 8;
  47
  48    public static final int ENCRYPTION_NONE = 0;
  49    public static final int ENCRYPTION_PGP = 1;
  50    public static final int ENCRYPTION_OTR = 2;
  51    public static final int ENCRYPTION_DECRYPTED = 3;
  52    public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
  53    public static final int ENCRYPTION_AXOLOTL = 5;
  54    public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
  55    public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
  56
  57    public static final int TYPE_TEXT = 0;
  58    public static final int TYPE_IMAGE = 1;
  59    public static final int TYPE_FILE = 2;
  60    public static final int TYPE_STATUS = 3;
  61    public static final int TYPE_PRIVATE = 4;
  62    public static final int TYPE_PRIVATE_FILE = 5;
  63    public static final int TYPE_RTP_SESSION = 6;
  64
  65    public static final String CONVERSATION = "conversationUuid";
  66    public static final String COUNTERPART = "counterpart";
  67    public static final String TRUE_COUNTERPART = "trueCounterpart";
  68    public static final String BODY = "body";
  69    public static final String BODY_LANGUAGE = "bodyLanguage";
  70    public static final String TIME_SENT = "timeSent";
  71    public static final String ENCRYPTION = "encryption";
  72    public static final String STATUS = "status";
  73    public static final String TYPE = "type";
  74    public static final String CARBON = "carbon";
  75    public static final String OOB = "oob";
  76    public static final String EDITED = "edited";
  77    public static final String REMOTE_MSG_ID = "remoteMsgId";
  78    public static final String SERVER_MSG_ID = "serverMsgId";
  79    public static final String RELATIVE_FILE_PATH = "relativeFilePath";
  80    public static final String FINGERPRINT = "axolotl_fingerprint";
  81    public static final String READ = "read";
  82    public static final String ERROR_MESSAGE = "errorMsg";
  83    public static final String READ_BY_MARKERS = "readByMarkers";
  84    public static final String MARKABLE = "markable";
  85    public static final String DELETED = "deleted";
  86    public static final String ME_COMMAND = "/me ";
  87
  88    public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
  89
  90
  91    public boolean markable = false;
  92    protected String conversationUuid;
  93    protected Jid counterpart;
  94    protected Jid trueCounterpart;
  95    protected String body;
  96    protected String subject;
  97    protected String encryptedBody;
  98    protected long timeSent;
  99    protected int encryption;
 100    protected int status;
 101    protected int type;
 102    protected boolean deleted = false;
 103    protected boolean carbon = false;
 104    protected boolean oob = false;
 105    protected List<Edit> edits = new ArrayList<>();
 106    protected String relativeFilePath;
 107    protected boolean read = true;
 108    protected String remoteMsgId = null;
 109    private String bodyLanguage = null;
 110    protected String serverMsgId = null;
 111    private final Conversational conversation;
 112    protected Transferable transferable = null;
 113    private Message mNextMessage = null;
 114    private Message mPreviousMessage = null;
 115    private String axolotlFingerprint = null;
 116    private String errorMessage = null;
 117    private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
 118
 119    private Boolean isGeoUri = null;
 120    private Boolean isEmojisOnly = null;
 121    private Boolean treatAsDownloadable = null;
 122    private FileParams fileParams = null;
 123    private List<MucOptions.User> counterparts;
 124    private WeakReference<MucOptions.User> user;
 125
 126    protected Message(Conversational conversation) {
 127        this.conversation = conversation;
 128    }
 129
 130    public Message(Conversational conversation, String body, int encryption) {
 131        this(conversation, body, encryption, STATUS_UNSEND);
 132    }
 133
 134    public Message(Conversational conversation, String body, int encryption, int status) {
 135        this(conversation, java.util.UUID.randomUUID().toString(),
 136                conversation.getUuid(),
 137                conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 138                null,
 139                body,
 140                System.currentTimeMillis(),
 141                encryption,
 142                status,
 143                TYPE_TEXT,
 144                false,
 145                null,
 146                null,
 147                null,
 148                null,
 149                true,
 150                null,
 151                false,
 152                null,
 153                null,
 154                false,
 155                false,
 156                null,
 157                null);
 158    }
 159
 160    public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
 161        this(conversation, java.util.UUID.randomUUID().toString(),
 162                conversation.getUuid(),
 163                conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 164                null,
 165                null,
 166                System.currentTimeMillis(),
 167                Message.ENCRYPTION_NONE,
 168                status,
 169                type,
 170                false,
 171                remoteMsgId,
 172                null,
 173                null,
 174                null,
 175                true,
 176                null,
 177                false,
 178                null,
 179                null,
 180                false,
 181                false,
 182                null,
 183                null);
 184    }
 185
 186    protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
 187                      final Jid trueCounterpart, final String body, final long timeSent,
 188                      final int encryption, final int status, final int type, final boolean carbon,
 189                      final String remoteMsgId, final String relativeFilePath,
 190                      final String serverMsgId, final String fingerprint, final boolean read,
 191                      final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
 192                      final boolean markable, final boolean deleted, final String bodyLanguage, final String subject) {
 193        this.conversation = conversation;
 194        this.uuid = uuid;
 195        this.conversationUuid = conversationUUid;
 196        this.counterpart = counterpart;
 197        this.trueCounterpart = trueCounterpart;
 198        this.body = body == null ? "" : body;
 199        this.timeSent = timeSent;
 200        this.encryption = encryption;
 201        this.status = status;
 202        this.type = type;
 203        this.carbon = carbon;
 204        this.remoteMsgId = remoteMsgId;
 205        this.relativeFilePath = relativeFilePath;
 206        this.serverMsgId = serverMsgId;
 207        this.axolotlFingerprint = fingerprint;
 208        this.read = read;
 209        this.edits = Edit.fromJson(edited);
 210        this.oob = oob;
 211        this.errorMessage = errorMessage;
 212        this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
 213        this.markable = markable;
 214        this.deleted = deleted;
 215        this.bodyLanguage = bodyLanguage;
 216        this.subject = subject;
 217    }
 218
 219    public static Message fromCursor(Cursor cursor, Conversation conversation) {
 220        return new Message(conversation,
 221                cursor.getString(cursor.getColumnIndex(UUID)),
 222                cursor.getString(cursor.getColumnIndex(CONVERSATION)),
 223                fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
 224                fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
 225                cursor.getString(cursor.getColumnIndex(BODY)),
 226                cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
 227                cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
 228                cursor.getInt(cursor.getColumnIndex(STATUS)),
 229                cursor.getInt(cursor.getColumnIndex(TYPE)),
 230                cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
 231                cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
 232                cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
 233                cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
 234                cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
 235                cursor.getInt(cursor.getColumnIndex(READ)) > 0,
 236                cursor.getString(cursor.getColumnIndex(EDITED)),
 237                cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
 238                cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
 239                ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
 240                cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
 241                cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
 242                cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
 243                cursor.getString(cursor.getColumnIndex("subject"))
 244        );
 245    }
 246
 247    private static Jid fromString(String value) {
 248        try {
 249            if (value != null) {
 250                return Jid.of(value);
 251            }
 252        } catch (IllegalArgumentException e) {
 253            return null;
 254        }
 255        return null;
 256    }
 257
 258    public static Message createStatusMessage(Conversation conversation, String body) {
 259        final Message message = new Message(conversation);
 260        message.setType(Message.TYPE_STATUS);
 261        message.setStatus(Message.STATUS_RECEIVED);
 262        message.body = body;
 263        return message;
 264    }
 265
 266    public static Message createLoadMoreMessage(Conversation conversation) {
 267        final Message message = new Message(conversation);
 268        message.setType(Message.TYPE_STATUS);
 269        message.body = "LOAD_MORE";
 270        return message;
 271    }
 272
 273    public ContentValues getCheogramContentValues() {
 274        ContentValues values = new ContentValues();
 275        values.put(UUID, uuid);
 276        values.put("subject", subject);
 277        return values;
 278    }
 279
 280    @Override
 281    public ContentValues getContentValues() {
 282        ContentValues values = new ContentValues();
 283        values.put(UUID, uuid);
 284        values.put(CONVERSATION, conversationUuid);
 285        if (counterpart == null) {
 286            values.putNull(COUNTERPART);
 287        } else {
 288            values.put(COUNTERPART, counterpart.toString());
 289        }
 290        if (trueCounterpart == null) {
 291            values.putNull(TRUE_COUNTERPART);
 292        } else {
 293            values.put(TRUE_COUNTERPART, trueCounterpart.toString());
 294        }
 295        values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
 296        values.put(TIME_SENT, timeSent);
 297        values.put(ENCRYPTION, encryption);
 298        values.put(STATUS, status);
 299        values.put(TYPE, type);
 300        values.put(CARBON, carbon ? 1 : 0);
 301        values.put(REMOTE_MSG_ID, remoteMsgId);
 302        values.put(RELATIVE_FILE_PATH, relativeFilePath);
 303        values.put(SERVER_MSG_ID, serverMsgId);
 304        values.put(FINGERPRINT, axolotlFingerprint);
 305        values.put(READ, read ? 1 : 0);
 306        try {
 307            values.put(EDITED, Edit.toJson(edits));
 308        } catch (JSONException e) {
 309            Log.e(Config.LOGTAG, "error persisting json for edits", e);
 310        }
 311        values.put(OOB, oob ? 1 : 0);
 312        values.put(ERROR_MESSAGE, errorMessage);
 313        values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
 314        values.put(MARKABLE, markable ? 1 : 0);
 315        values.put(DELETED, deleted ? 1 : 0);
 316        values.put(BODY_LANGUAGE, bodyLanguage);
 317        return values;
 318    }
 319
 320    public String getConversationUuid() {
 321        return conversationUuid;
 322    }
 323
 324    public Conversational getConversation() {
 325        return this.conversation;
 326    }
 327
 328    public Jid getCounterpart() {
 329        return counterpart;
 330    }
 331
 332    public void setCounterpart(final Jid counterpart) {
 333        this.counterpart = counterpart;
 334    }
 335
 336    public Contact getContact() {
 337        if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
 338            return this.conversation.getContact();
 339        } else {
 340            if (this.trueCounterpart == null) {
 341                return null;
 342            } else {
 343                return this.conversation.getAccount().getRoster()
 344                        .getContactFromContactList(this.trueCounterpart);
 345            }
 346        }
 347    }
 348
 349    public String getBody() {
 350        return body;
 351    }
 352
 353    public synchronized void setBody(String body) {
 354        if (body == null) {
 355            throw new Error("You should not set the message body to null");
 356        }
 357        this.body = body;
 358        this.isGeoUri = null;
 359        this.isEmojisOnly = null;
 360        this.treatAsDownloadable = null;
 361        this.fileParams = null;
 362    }
 363
 364    public String getSubject() {
 365        return subject;
 366    }
 367
 368    public synchronized void setSubject(String subject) {
 369        this.subject = subject;
 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(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    boolean similar(Message message) {
 561        if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
 562            return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
 563        } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
 564            return true;
 565        } else if (this.body == null || this.counterpart == null) {
 566            return false;
 567        } else {
 568            String body, otherBody;
 569            if (this.hasFileOnRemoteHost()) {
 570                body = getFileParams().url;
 571                otherBody = message.body == null ? null : message.body.trim();
 572            } else {
 573                body = this.body;
 574                otherBody = message.body;
 575            }
 576            final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
 577            if (message.getRemoteMsgId() != null) {
 578                final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
 579                if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
 580                    return true;
 581                }
 582                return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
 583                        && matchingCounterpart
 584                        && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
 585            } else {
 586                return this.remoteMsgId == null
 587                        && matchingCounterpart
 588                        && body.equals(otherBody)
 589                        && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
 590            }
 591        }
 592    }
 593
 594    public Message next() {
 595        if (this.conversation instanceof Conversation) {
 596            final Conversation conversation = (Conversation) this.conversation;
 597            synchronized (conversation.messages) {
 598                if (this.mNextMessage == null) {
 599                    int index = conversation.messages.indexOf(this);
 600                    if (index < 0 || index >= conversation.messages.size() - 1) {
 601                        this.mNextMessage = null;
 602                    } else {
 603                        this.mNextMessage = conversation.messages.get(index + 1);
 604                    }
 605                }
 606                return this.mNextMessage;
 607            }
 608        } else {
 609            throw new AssertionError("Calling next should be disabled for stubs");
 610        }
 611    }
 612
 613    public Message prev() {
 614        if (this.conversation instanceof Conversation) {
 615            final Conversation conversation = (Conversation) this.conversation;
 616            synchronized (conversation.messages) {
 617                if (this.mPreviousMessage == null) {
 618                    int index = conversation.messages.indexOf(this);
 619                    if (index <= 0 || index > conversation.messages.size()) {
 620                        this.mPreviousMessage = null;
 621                    } else {
 622                        this.mPreviousMessage = conversation.messages.get(index - 1);
 623                    }
 624                }
 625            }
 626            return this.mPreviousMessage;
 627        } else {
 628            throw new AssertionError("Calling prev should be disabled for stubs");
 629        }
 630    }
 631
 632    public boolean isLastCorrectableMessage() {
 633        Message next = next();
 634        while (next != null) {
 635            if (next.isEditable()) {
 636                return false;
 637            }
 638            next = next.next();
 639        }
 640        return isEditable();
 641    }
 642
 643    public boolean isEditable() {
 644        return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
 645    }
 646
 647    public boolean mergeable(final Message message) {
 648        return message != null &&
 649                (message.getType() == Message.TYPE_TEXT &&
 650                        this.getTransferable() == null &&
 651                        message.getTransferable() == null &&
 652                        message.getEncryption() != Message.ENCRYPTION_PGP &&
 653                        message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
 654                        this.getType() == message.getType() &&
 655                        this.getSubject() != null &&
 656                        //this.getStatus() == message.getStatus() &&
 657                        isStatusMergeable(this.getStatus(), message.getStatus()) &&
 658                        this.getEncryption() == message.getEncryption() &&
 659                        this.getCounterpart() != null &&
 660                        this.getCounterpart().equals(message.getCounterpart()) &&
 661                        this.edited() == message.edited() &&
 662                        (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
 663                        this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
 664                        !message.isGeoUri() &&
 665                        !this.isGeoUri() &&
 666                        !message.isOOb() &&
 667                        !this.isOOb() &&
 668                        !message.treatAsDownloadable() &&
 669                        !this.treatAsDownloadable() &&
 670                        !message.hasMeCommand() &&
 671                        !this.hasMeCommand() &&
 672                        !this.bodyIsOnlyEmojis() &&
 673                        !message.bodyIsOnlyEmojis() &&
 674                        ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
 675                        UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
 676                        this.getReadByMarkers().equals(message.getReadByMarkers()) &&
 677                        !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
 678                );
 679    }
 680
 681    private static boolean isStatusMergeable(int a, int b) {
 682        return a == b || (
 683                (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
 684                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
 685                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
 686                        || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
 687                        || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
 688        );
 689    }
 690
 691    public void setCounterparts(List<MucOptions.User> counterparts) {
 692        this.counterparts = counterparts;
 693    }
 694
 695    public List<MucOptions.User> getCounterparts() {
 696        return this.counterparts;
 697    }
 698
 699    @Override
 700    public int getAvatarBackgroundColor() {
 701        if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
 702            return Color.TRANSPARENT;
 703        } else {
 704            return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
 705        }
 706    }
 707
 708    @Override
 709    public String getAvatarName() {
 710        return UIHelper.getMessageDisplayName(this);
 711    }
 712
 713    public boolean isOOb() {
 714        return oob;
 715    }
 716
 717    public static class MergeSeparator {
 718    }
 719
 720    public SpannableStringBuilder getMergedBody() {
 721        SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
 722        Message current = this;
 723        while (current.mergeable(current.next())) {
 724            current = current.next();
 725            if (current == null) {
 726                break;
 727            }
 728            body.append("\n\n");
 729            body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
 730                    SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
 731            body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
 732        }
 733        return body;
 734    }
 735
 736    public boolean hasMeCommand() {
 737        return this.body.trim().startsWith(ME_COMMAND);
 738    }
 739
 740    public int getMergedStatus() {
 741        int status = this.status;
 742        Message current = this;
 743        while (current.mergeable(current.next())) {
 744            current = current.next();
 745            if (current == null) {
 746                break;
 747            }
 748            status = current.status;
 749        }
 750        return status;
 751    }
 752
 753    public long getMergedTimeSent() {
 754        long time = this.timeSent;
 755        Message current = this;
 756        while (current.mergeable(current.next())) {
 757            current = current.next();
 758            if (current == null) {
 759                break;
 760            }
 761            time = current.timeSent;
 762        }
 763        return time;
 764    }
 765
 766    public boolean wasMergedIntoPrevious() {
 767        Message prev = this.prev();
 768        return prev != null && prev.mergeable(this);
 769    }
 770
 771    public boolean trusted() {
 772        Contact contact = this.getContact();
 773        return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
 774    }
 775
 776    public boolean fixCounterpart() {
 777        final Presences presences = conversation.getContact().getPresences();
 778        if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
 779            return true;
 780        } else if (presences.size() >= 1) {
 781            counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
 782            return true;
 783        } else {
 784            counterpart = null;
 785            return false;
 786        }
 787    }
 788
 789    public void setUuid(String uuid) {
 790        this.uuid = uuid;
 791    }
 792
 793    public String getEditedId() {
 794        if (edits.size() > 0) {
 795            return edits.get(edits.size() - 1).getEditedId();
 796        } else {
 797            throw new IllegalStateException("Attempting to store unedited message");
 798        }
 799    }
 800
 801    public String getEditedIdWireFormat() {
 802        if (edits.size() > 0) {
 803            return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
 804        } else {
 805            throw new IllegalStateException("Attempting to store unedited message");
 806        }
 807    }
 808
 809    public void setOob(boolean isOob) {
 810        this.oob = isOob;
 811    }
 812
 813    public String getMimeType() {
 814        String extension;
 815        if (relativeFilePath != null) {
 816            extension = MimeUtils.extractRelevantExtension(relativeFilePath);
 817        } else {
 818            final String url = URL.tryParse(body.split("\n")[0]);
 819            if (url == null) {
 820                return null;
 821            }
 822            extension = MimeUtils.extractRelevantExtension(url);
 823        }
 824        return MimeUtils.guessMimeTypeFromExtension(extension);
 825    }
 826
 827    public synchronized boolean treatAsDownloadable() {
 828        if (treatAsDownloadable == null) {
 829            treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
 830        }
 831        return treatAsDownloadable;
 832    }
 833
 834    public synchronized boolean bodyIsOnlyEmojis() {
 835        if (isEmojisOnly == null) {
 836            isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
 837        }
 838        return isEmojisOnly;
 839    }
 840
 841    public synchronized boolean isGeoUri() {
 842        if (isGeoUri == null) {
 843            isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
 844        }
 845        return isGeoUri;
 846    }
 847
 848    public synchronized void resetFileParams() {
 849        this.fileParams = null;
 850    }
 851
 852    public synchronized FileParams getFileParams() {
 853        if (fileParams == null) {
 854            fileParams = new FileParams();
 855            if (this.transferable != null) {
 856                fileParams.size = this.transferable.getFileSize();
 857            }
 858            final String[] parts = body == null ? new String[0] : body.split("\\|");
 859            switch (parts.length) {
 860                case 1:
 861                    try {
 862                        fileParams.size = Long.parseLong(parts[0]);
 863                    } catch (final NumberFormatException e) {
 864                        fileParams.url = URL.tryParse(parts[0]);
 865                    }
 866                    break;
 867                case 5:
 868                    fileParams.runtime = parseInt(parts[4]);
 869                case 4:
 870                    fileParams.width = parseInt(parts[2]);
 871                    fileParams.height = parseInt(parts[3]);
 872                case 2:
 873                    fileParams.url = URL.tryParse(parts[0]);
 874                    fileParams.size = Longs.tryParse(parts[1]);
 875                    break;
 876                case 3:
 877                    fileParams.size = Longs.tryParse(parts[0]);
 878                    fileParams.width = parseInt(parts[1]);
 879                    fileParams.height = parseInt(parts[2]);
 880                    break;
 881            }
 882        }
 883        return fileParams;
 884    }
 885
 886    private static int parseInt(String value) {
 887        try {
 888            return Integer.parseInt(value);
 889        } catch (NumberFormatException e) {
 890            return 0;
 891        }
 892    }
 893
 894    public void untie() {
 895        this.mNextMessage = null;
 896        this.mPreviousMessage = null;
 897    }
 898
 899    public boolean isPrivateMessage() {
 900        return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
 901    }
 902
 903    public boolean isFileOrImage() {
 904        return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
 905    }
 906
 907
 908    public boolean isTypeText() {
 909        return type == TYPE_TEXT || type == TYPE_PRIVATE;
 910    }
 911
 912    public boolean hasFileOnRemoteHost() {
 913        return isFileOrImage() && getFileParams().url != null;
 914    }
 915
 916    public boolean needsUploading() {
 917        return isFileOrImage() && getFileParams().url == null;
 918    }
 919
 920    public static class FileParams {
 921        public String url;
 922        public Long size = null;
 923        public int width = 0;
 924        public int height = 0;
 925        public int runtime = 0;
 926
 927        public long getSize() {
 928            return size == null ? 0 : size;
 929        }
 930    }
 931
 932    public void setFingerprint(String fingerprint) {
 933        this.axolotlFingerprint = fingerprint;
 934    }
 935
 936    public String getFingerprint() {
 937        return axolotlFingerprint;
 938    }
 939
 940    public boolean isTrusted() {
 941        FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
 942        return s != null && s.isTrusted();
 943    }
 944
 945    private int getPreviousEncryption() {
 946        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
 947            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 948                continue;
 949            }
 950            return iterator.getEncryption();
 951        }
 952        return ENCRYPTION_NONE;
 953    }
 954
 955    private int getNextEncryption() {
 956        if (this.conversation instanceof Conversation) {
 957            Conversation conversation = (Conversation) this.conversation;
 958            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
 959                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 960                    continue;
 961                }
 962                return iterator.getEncryption();
 963            }
 964            return conversation.getNextEncryption();
 965        } else {
 966            throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
 967        }
 968    }
 969
 970    public boolean isValidInSession() {
 971        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
 972        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
 973
 974        boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
 975                || futureEncryption == ENCRYPTION_NONE
 976                || pastEncryption != futureEncryption;
 977
 978        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
 979    }
 980
 981    private static int getCleanedEncryption(int encryption) {
 982        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
 983            return ENCRYPTION_PGP;
 984        }
 985        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
 986            return ENCRYPTION_AXOLOTL;
 987        }
 988        return encryption;
 989    }
 990
 991    public static boolean configurePrivateMessage(final Message message) {
 992        return configurePrivateMessage(message, false);
 993    }
 994
 995    public static boolean configurePrivateFileMessage(final Message message) {
 996        return configurePrivateMessage(message, true);
 997    }
 998
 999    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1000        final Conversation conversation;
1001        if (message.conversation instanceof Conversation) {
1002            conversation = (Conversation) message.conversation;
1003        } else {
1004            return false;
1005        }
1006        if (conversation.getMode() == Conversation.MODE_MULTI) {
1007            final Jid nextCounterpart = conversation.getNextCounterpart();
1008            return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1009        }
1010        return false;
1011    }
1012
1013    public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1014        final Conversation conversation;
1015        if (message.conversation instanceof Conversation) {
1016            conversation = (Conversation) message.conversation;
1017        } else {
1018            return false;
1019        }
1020        return configurePrivateMessage(conversation, message, counterpart, false);
1021    }
1022
1023    private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1024        if (counterpart == null) {
1025            return false;
1026        }
1027        message.setCounterpart(counterpart);
1028        message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1029        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1030        return true;
1031    }
1032}