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