Message.java

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