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