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