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    public boolean hasFileOnRemoteHost() {
894        return isFileOrImage() && getFileParams().url != null;
895    }
896
897    public boolean needsUploading() {
898        final boolean needsUploading = isFileOrImage() && getFileParams().url == null;
899        Log.d(Config.LOGTAG, "needs uploading " + needsUploading + " url=" + getFileParams().url);
900        return needsUploading;
901    }
902
903    public static class FileParams {
904        public String url;
905        public long size = 0;
906        public int width = 0;
907        public int height = 0;
908        public int runtime = 0;
909    }
910
911    public void setFingerprint(String fingerprint) {
912        this.axolotlFingerprint = fingerprint;
913    }
914
915    public String getFingerprint() {
916        return axolotlFingerprint;
917    }
918
919    public boolean isTrusted() {
920        FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
921        return s != null && s.isTrusted();
922    }
923
924    private int getPreviousEncryption() {
925        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
926            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
927                continue;
928            }
929            return iterator.getEncryption();
930        }
931        return ENCRYPTION_NONE;
932    }
933
934    private int getNextEncryption() {
935        if (this.conversation instanceof Conversation) {
936            Conversation conversation = (Conversation) this.conversation;
937            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
938                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
939                    continue;
940                }
941                return iterator.getEncryption();
942            }
943            return conversation.getNextEncryption();
944        } else {
945            throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
946        }
947    }
948
949    public boolean isValidInSession() {
950        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
951        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
952
953        boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
954                || futureEncryption == ENCRYPTION_NONE
955                || pastEncryption != futureEncryption;
956
957        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
958    }
959
960    private static int getCleanedEncryption(int encryption) {
961        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
962            return ENCRYPTION_PGP;
963        }
964        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
965            return ENCRYPTION_AXOLOTL;
966        }
967        return encryption;
968    }
969
970    public static boolean configurePrivateMessage(final Message message) {
971        return configurePrivateMessage(message, false);
972    }
973
974    public static boolean configurePrivateFileMessage(final Message message) {
975        return configurePrivateMessage(message, true);
976    }
977
978    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
979        final Conversation conversation;
980        if (message.conversation instanceof Conversation) {
981            conversation = (Conversation) message.conversation;
982        } else {
983            return false;
984        }
985        if (conversation.getMode() == Conversation.MODE_MULTI) {
986            final Jid nextCounterpart = conversation.getNextCounterpart();
987            if (nextCounterpart != null) {
988                message.setCounterpart(nextCounterpart);
989                message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
990                message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
991                return true;
992            }
993        }
994        return false;
995    }
996}