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