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