1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.graphics.drawable.Drawable;
6import android.graphics.Color;
7import android.os.Build;
8import android.text.Html;
9import android.text.SpannableStringBuilder;
10import android.util.Base64;
11import android.util.Log;
12import android.util.Pair;
13
14import com.cheogram.android.BobTransfer;
15import com.cheogram.android.GetThumbnailForCid;
16
17import com.google.common.io.ByteSource;
18import com.google.common.base.Strings;
19import com.google.common.collect.ImmutableSet;
20import com.google.common.primitives.Longs;
21
22import org.json.JSONException;
23
24import java.lang.ref.WeakReference;
25import java.io.IOException;
26import java.net.URI;
27import java.net.URISyntaxException;
28import java.time.Duration;
29import java.security.NoSuchAlgorithmException;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Iterator;
33import java.util.List;
34import java.util.Set;
35import java.util.stream.Collectors;
36import java.util.concurrent.CopyOnWriteArraySet;
37
38import io.ipfs.cid.Cid;
39
40import eu.siacs.conversations.Config;
41import eu.siacs.conversations.crypto.axolotl.AxolotlService;
42import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
43import eu.siacs.conversations.http.URL;
44import eu.siacs.conversations.services.AvatarService;
45import eu.siacs.conversations.ui.util.PresenceSelector;
46import eu.siacs.conversations.utils.CryptoHelper;
47import eu.siacs.conversations.utils.Emoticons;
48import eu.siacs.conversations.utils.GeoHelper;
49import eu.siacs.conversations.utils.MessageUtils;
50import eu.siacs.conversations.utils.MimeUtils;
51import eu.siacs.conversations.utils.UIHelper;
52import eu.siacs.conversations.xmpp.Jid;
53import eu.siacs.conversations.xml.Element;
54import eu.siacs.conversations.xml.Namespace;
55import eu.siacs.conversations.xml.Tag;
56import eu.siacs.conversations.xml.XmlReader;
57
58public class Message extends AbstractEntity implements AvatarService.Avatarable {
59
60 public static final String TABLENAME = "messages";
61
62 public static final int STATUS_RECEIVED = 0;
63 public static final int STATUS_UNSEND = 1;
64 public static final int STATUS_SEND = 2;
65 public static final int STATUS_SEND_FAILED = 3;
66 public static final int STATUS_WAITING = 5;
67 public static final int STATUS_OFFERED = 6;
68 public static final int STATUS_SEND_RECEIVED = 7;
69 public static final int STATUS_SEND_DISPLAYED = 8;
70
71 public static final int ENCRYPTION_NONE = 0;
72 public static final int ENCRYPTION_PGP = 1;
73 public static final int ENCRYPTION_OTR = 2;
74 public static final int ENCRYPTION_DECRYPTED = 3;
75 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
76 public static final int ENCRYPTION_AXOLOTL = 5;
77 public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
78 public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
79
80 public static final int TYPE_TEXT = 0;
81 public static final int TYPE_IMAGE = 1;
82 public static final int TYPE_FILE = 2;
83 public static final int TYPE_STATUS = 3;
84 public static final int TYPE_PRIVATE = 4;
85 public static final int TYPE_PRIVATE_FILE = 5;
86 public static final int TYPE_RTP_SESSION = 6;
87
88 public static final String CONVERSATION = "conversationUuid";
89 public static final String COUNTERPART = "counterpart";
90 public static final String TRUE_COUNTERPART = "trueCounterpart";
91 public static final String BODY = "body";
92 public static final String BODY_LANGUAGE = "bodyLanguage";
93 public static final String TIME_SENT = "timeSent";
94 public static final String ENCRYPTION = "encryption";
95 public static final String STATUS = "status";
96 public static final String TYPE = "type";
97 public static final String CARBON = "carbon";
98 public static final String OOB = "oob";
99 public static final String EDITED = "edited";
100 public static final String REMOTE_MSG_ID = "remoteMsgId";
101 public static final String SERVER_MSG_ID = "serverMsgId";
102 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
103 public static final String FINGERPRINT = "axolotl_fingerprint";
104 public static final String READ = "read";
105 public static final String ERROR_MESSAGE = "errorMsg";
106 public static final String READ_BY_MARKERS = "readByMarkers";
107 public static final String MARKABLE = "markable";
108 public static final String DELETED = "deleted";
109 public static final String ME_COMMAND = "/me ";
110
111 public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
112
113
114 public boolean markable = false;
115 protected String conversationUuid;
116 protected Jid counterpart;
117 protected Jid trueCounterpart;
118 protected String body;
119 protected String subject;
120 protected String encryptedBody;
121 protected long timeSent;
122 protected long timeReceived;
123 protected int encryption;
124 protected int status;
125 protected int type;
126 protected boolean deleted = false;
127 protected boolean carbon = false;
128 private boolean oob = false;
129 protected List<Element> payloads = new ArrayList<>();
130 protected List<Edit> edits = new ArrayList<>();
131 protected String relativeFilePath;
132 protected boolean read = true;
133 protected String remoteMsgId = null;
134 private String bodyLanguage = null;
135 protected String serverMsgId = null;
136 private final Conversational conversation;
137 protected Transferable transferable = null;
138 private Message mNextMessage = null;
139 private Message mPreviousMessage = null;
140 private String axolotlFingerprint = null;
141 private String errorMessage = null;
142 private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
143
144 private Boolean isGeoUri = null;
145 private Boolean isEmojisOnly = null;
146 private Boolean treatAsDownloadable = null;
147 private FileParams fileParams = null;
148 private List<MucOptions.User> counterparts;
149 private WeakReference<MucOptions.User> user;
150
151 protected Message(Conversational conversation) {
152 this.conversation = conversation;
153 }
154
155 public Message(Conversational conversation, String body, int encryption) {
156 this(conversation, body, encryption, STATUS_UNSEND);
157 }
158
159 public Message(Conversational conversation, String body, int encryption, int status) {
160 this(conversation, java.util.UUID.randomUUID().toString(),
161 conversation.getUuid(),
162 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
163 null,
164 body,
165 System.currentTimeMillis(),
166 encryption,
167 status,
168 TYPE_TEXT,
169 false,
170 null,
171 null,
172 null,
173 null,
174 true,
175 null,
176 false,
177 null,
178 null,
179 false,
180 false,
181 null,
182 System.currentTimeMillis(),
183 null,
184 null,
185 null);
186 }
187
188 public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
189 this(conversation, java.util.UUID.randomUUID().toString(),
190 conversation.getUuid(),
191 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
192 null,
193 null,
194 System.currentTimeMillis(),
195 Message.ENCRYPTION_NONE,
196 status,
197 type,
198 false,
199 remoteMsgId,
200 null,
201 null,
202 null,
203 true,
204 null,
205 false,
206 null,
207 null,
208 false,
209 false,
210 null,
211 System.currentTimeMillis(),
212 null,
213 null,
214 null);
215 }
216
217 protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
218 final Jid trueCounterpart, final String body, final long timeSent,
219 final int encryption, final int status, final int type, final boolean carbon,
220 final String remoteMsgId, final String relativeFilePath,
221 final String serverMsgId, final String fingerprint, final boolean read,
222 final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
223 final boolean markable, final boolean deleted, final String bodyLanguage, final long timeReceived, final String subject, final String fileParams, final List<Element> payloads) {
224 this.conversation = conversation;
225 this.uuid = uuid;
226 this.conversationUuid = conversationUUid;
227 this.counterpart = counterpart;
228 this.trueCounterpart = trueCounterpart;
229 this.body = body == null ? "" : body;
230 this.timeSent = timeSent;
231 this.encryption = encryption;
232 this.status = status;
233 this.type = type;
234 this.carbon = carbon;
235 this.remoteMsgId = remoteMsgId;
236 this.relativeFilePath = relativeFilePath;
237 this.serverMsgId = serverMsgId;
238 this.axolotlFingerprint = fingerprint;
239 this.read = read;
240 this.edits = Edit.fromJson(edited);
241 this.oob = oob;
242 this.errorMessage = errorMessage;
243 this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
244 this.markable = markable;
245 this.deleted = deleted;
246 this.bodyLanguage = bodyLanguage;
247 this.timeReceived = timeReceived;
248 this.subject = subject;
249 if (payloads != null) this.payloads = payloads;
250 if (fileParams != null && getSims().isEmpty()) this.fileParams = new FileParams(fileParams);
251 }
252
253 public static Message fromCursor(Cursor cursor, Conversation conversation) throws IOException {
254 String payloadsStr = cursor.getString(cursor.getColumnIndex("payloads"));
255 List<Element> payloads = new ArrayList<>();
256 if (payloadsStr != null) {
257 final XmlReader xmlReader = new XmlReader();
258 xmlReader.setInputStream(ByteSource.wrap(payloadsStr.getBytes()).openStream());
259 Tag tag;
260 while ((tag = xmlReader.readTag()) != null) {
261 payloads.add(xmlReader.readElement(tag));
262 }
263 }
264
265 return new Message(conversation,
266 cursor.getString(cursor.getColumnIndex(UUID)),
267 cursor.getString(cursor.getColumnIndex(CONVERSATION)),
268 fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
269 fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
270 cursor.getString(cursor.getColumnIndex(BODY)),
271 cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
272 cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
273 cursor.getInt(cursor.getColumnIndex(STATUS)),
274 cursor.getInt(cursor.getColumnIndex(TYPE)),
275 cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
276 cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
277 cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
278 cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
279 cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
280 cursor.getInt(cursor.getColumnIndex(READ)) > 0,
281 cursor.getString(cursor.getColumnIndex(EDITED)),
282 cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
283 cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
284 ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
285 cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
286 cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
287 cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
288 cursor.getLong(cursor.getColumnIndex(cursor.isNull(cursor.getColumnIndex("timeReceived")) ? TIME_SENT : "timeReceived")),
289 cursor.getString(cursor.getColumnIndex("subject")),
290 cursor.getString(cursor.getColumnIndex("fileParams")),
291 payloads
292 );
293 }
294
295 private static Jid fromString(String value) {
296 try {
297 if (value != null) {
298 return Jid.of(value);
299 }
300 } catch (IllegalArgumentException e) {
301 return null;
302 }
303 return null;
304 }
305
306 public static Message createStatusMessage(Conversation conversation, String body) {
307 final Message message = new Message(conversation);
308 message.setType(Message.TYPE_STATUS);
309 message.setStatus(Message.STATUS_RECEIVED);
310 message.body = body;
311 return message;
312 }
313
314 public static Message createLoadMoreMessage(Conversation conversation) {
315 final Message message = new Message(conversation);
316 message.setType(Message.TYPE_STATUS);
317 message.body = "LOAD_MORE";
318 return message;
319 }
320
321 public ContentValues getCheogramContentValues() {
322 ContentValues values = new ContentValues();
323 values.put(UUID, uuid);
324 values.put("subject", subject);
325 values.put("fileParams", fileParams == null ? null : fileParams.toString());
326 if (fileParams != null) {
327 List<Element> sims = getSims();
328 if (sims.isEmpty()) {
329 addPayload(fileParams.toSims());
330 } else {
331 sims.get(0).replaceChildren(fileParams.toSims().getChildren());
332 }
333 }
334 values.put("payloads", payloads.size() < 1 ? null : payloads.stream().map(Object::toString).collect(Collectors.joining()));
335 return values;
336 }
337
338 @Override
339 public ContentValues getContentValues() {
340 ContentValues values = new ContentValues();
341 values.put(UUID, uuid);
342 values.put(CONVERSATION, conversationUuid);
343 if (counterpart == null) {
344 values.putNull(COUNTERPART);
345 } else {
346 values.put(COUNTERPART, counterpart.toString());
347 }
348 if (trueCounterpart == null) {
349 values.putNull(TRUE_COUNTERPART);
350 } else {
351 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
352 }
353 values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
354 values.put(TIME_SENT, timeSent);
355 values.put(ENCRYPTION, encryption);
356 values.put(STATUS, status);
357 values.put(TYPE, type);
358 values.put(CARBON, carbon ? 1 : 0);
359 values.put(REMOTE_MSG_ID, remoteMsgId);
360 values.put(RELATIVE_FILE_PATH, relativeFilePath);
361 values.put(SERVER_MSG_ID, serverMsgId);
362 values.put(FINGERPRINT, axolotlFingerprint);
363 values.put(READ, read ? 1 : 0);
364 try {
365 values.put(EDITED, Edit.toJson(edits));
366 } catch (JSONException e) {
367 Log.e(Config.LOGTAG, "error persisting json for edits", e);
368 }
369 values.put(OOB, oob ? 1 : 0);
370 values.put(ERROR_MESSAGE, errorMessage);
371 values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
372 values.put(MARKABLE, markable ? 1 : 0);
373 values.put(DELETED, deleted ? 1 : 0);
374 values.put(BODY_LANGUAGE, bodyLanguage);
375 return values;
376 }
377
378 public String getConversationUuid() {
379 return conversationUuid;
380 }
381
382 public Conversational getConversation() {
383 return this.conversation;
384 }
385
386 public Jid getCounterpart() {
387 return counterpart;
388 }
389
390 public void setCounterpart(final Jid counterpart) {
391 this.counterpart = counterpart;
392 }
393
394 public Contact getContact() {
395 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
396 if (this.trueCounterpart != null) {
397 return this.conversation.getAccount().getRoster()
398 .getContact(this.trueCounterpart);
399 }
400
401 return this.conversation.getContact();
402 } else {
403 if (this.trueCounterpart == null) {
404 return null;
405 } else {
406 return this.conversation.getAccount().getRoster()
407 .getContactFromContactList(this.trueCounterpart);
408 }
409 }
410 }
411
412 public String getBody() {
413 StringBuilder body = new StringBuilder(this.body);
414
415 List<Element> fallbacks = getFallbacks();
416 List<Pair<Integer, Integer>> spans = new ArrayList<>();
417 for (Element fallback : fallbacks) {
418 for (Element span : fallback.getChildren()) {
419 if (!span.getName().equals("body") && !span.getNamespace().equals("urn:xmpp:fallback:0")) continue;
420 if (span.getAttribute("start") == null || span.getAttribute("end") == null) return "";
421 spans.add(new Pair(parseInt(span.getAttribute("start")), parseInt(span.getAttribute("end"))));
422 }
423 }
424 // Do them in reverse order so that span deletions don't affect the indexes of other spans
425 spans.sort((x, y) -> y.first.compareTo(x.first));
426 try {
427 for (Pair<Integer, Integer> span : spans) {
428 body.delete(span.first, span.second);
429 }
430 } catch (final StringIndexOutOfBoundsException e) { spans.clear(); }
431
432 if (spans.isEmpty() && getOob() != null) {
433 return body.toString().replace(getOob().toString(), "");
434 } else {
435 return body.toString();
436 }
437 }
438
439 public synchronized void setBody(String body) {
440 if (body == null) {
441 throw new Error("You should not set the message body to null");
442 }
443 this.body = body;
444 this.isGeoUri = null;
445 this.isEmojisOnly = null;
446 this.treatAsDownloadable = null;
447 }
448
449 public String getSubject() {
450 return subject;
451 }
452
453 public synchronized void setSubject(String subject) {
454 this.subject = subject;
455 }
456
457 public Element getThread() {
458 if (this.payloads == null) return null;
459
460 for (Element el : this.payloads) {
461 if (el.getName().equals("thread") && el.getNamespace().equals("jabber:client")) {
462 return el;
463 }
464 }
465
466 return null;
467 }
468
469 public void setThread(Element thread) {
470 payloads.removeIf(el -> el.getName().equals("thread") && el.getNamespace().equals("jabber:client"));
471 addPayload(thread);
472 }
473
474 public void setMucUser(MucOptions.User user) {
475 this.user = new WeakReference<>(user);
476 }
477
478 public boolean sameMucUser(Message otherMessage) {
479 final MucOptions.User thisUser = this.user == null ? null : this.user.get();
480 final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
481 return thisUser != null && thisUser == otherUser;
482 }
483
484 public String getErrorMessage() {
485 return errorMessage;
486 }
487
488 public boolean setErrorMessage(String message) {
489 boolean changed = (message != null && !message.equals(errorMessage))
490 || (message == null && errorMessage != null);
491 this.errorMessage = message;
492 return changed;
493 }
494
495 public long getTimeReceived() {
496 return timeReceived;
497 }
498
499 public long getTimeSent() {
500 return timeSent;
501 }
502
503 public int getEncryption() {
504 return encryption;
505 }
506
507 public void setEncryption(int encryption) {
508 this.encryption = encryption;
509 }
510
511 public int getStatus() {
512 return status;
513 }
514
515 public void setStatus(int status) {
516 this.status = status;
517 }
518
519 public String getRelativeFilePath() {
520 return this.relativeFilePath;
521 }
522
523 public void setRelativeFilePath(String path) {
524 this.relativeFilePath = path;
525 }
526
527 public String getRemoteMsgId() {
528 return this.remoteMsgId;
529 }
530
531 public void setRemoteMsgId(String id) {
532 this.remoteMsgId = id;
533 }
534
535 public String getServerMsgId() {
536 return this.serverMsgId;
537 }
538
539 public void setServerMsgId(String id) {
540 this.serverMsgId = id;
541 }
542
543 public boolean isRead() {
544 return this.read;
545 }
546
547 public boolean isDeleted() {
548 return this.deleted;
549 }
550
551 public void setDeleted(boolean deleted) {
552 this.deleted = deleted;
553 }
554
555 public void markRead() {
556 this.read = true;
557 }
558
559 public void markUnread() {
560 this.read = false;
561 }
562
563 public void setTime(long time) {
564 this.timeSent = time;
565 }
566
567 public void setTimeReceived(long time) {
568 this.timeReceived = time;
569 }
570
571 public String getEncryptedBody() {
572 return this.encryptedBody;
573 }
574
575 public void setEncryptedBody(String body) {
576 this.encryptedBody = body;
577 }
578
579 public int getType() {
580 return this.type;
581 }
582
583 public void setType(int type) {
584 this.type = type;
585 }
586
587 public boolean isCarbon() {
588 return carbon;
589 }
590
591 public void setCarbon(boolean carbon) {
592 this.carbon = carbon;
593 }
594
595 public void putEdited(String edited, String serverMsgId) {
596 final Edit edit = new Edit(edited, serverMsgId);
597 if (this.edits.size() < 128 && !this.edits.contains(edit)) {
598 this.edits.add(edit);
599 }
600 }
601
602 boolean remoteMsgIdMatchInEdit(String id) {
603 for (Edit edit : this.edits) {
604 if (id.equals(edit.getEditedId())) {
605 return true;
606 }
607 }
608 return false;
609 }
610
611 public String getBodyLanguage() {
612 return this.bodyLanguage;
613 }
614
615 public void setBodyLanguage(String language) {
616 this.bodyLanguage = language;
617 }
618
619 public boolean edited() {
620 return this.edits.size() > 0;
621 }
622
623 public void setTrueCounterpart(Jid trueCounterpart) {
624 this.trueCounterpart = trueCounterpart;
625 }
626
627 public Jid getTrueCounterpart() {
628 return this.trueCounterpart;
629 }
630
631 public Transferable getTransferable() {
632 return this.transferable;
633 }
634
635 public synchronized void setTransferable(Transferable transferable) {
636 this.transferable = transferable;
637 }
638
639 public boolean addReadByMarker(ReadByMarker readByMarker) {
640 if (readByMarker.getRealJid() != null) {
641 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
642 return false;
643 }
644 } else if (readByMarker.getFullJid() != null) {
645 if (readByMarker.getFullJid().equals(counterpart)) {
646 return false;
647 }
648 }
649 if (this.readByMarkers.add(readByMarker)) {
650 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
651 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
652 while (iterator.hasNext()) {
653 ReadByMarker marker = iterator.next();
654 if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
655 iterator.remove();
656 }
657 }
658 }
659 return true;
660 } else {
661 return false;
662 }
663 }
664
665 public Set<ReadByMarker> getReadByMarkers() {
666 return ImmutableSet.copyOf(this.readByMarkers);
667 }
668
669 boolean similar(Message message) {
670 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
671 return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
672 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
673 return true;
674 } else if (this.body == null || this.counterpart == null) {
675 return false;
676 } else {
677 String body, otherBody;
678 if (this.hasFileOnRemoteHost()) {
679 body = getFileParams().url;
680 otherBody = message.body == null ? null : message.body.trim();
681 } else {
682 body = this.body;
683 otherBody = message.body;
684 }
685 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
686 if (message.getRemoteMsgId() != null) {
687 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
688 if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
689 return true;
690 }
691 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
692 && matchingCounterpart
693 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
694 } else {
695 return this.remoteMsgId == null
696 && matchingCounterpart
697 && body.equals(otherBody)
698 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
699 }
700 }
701 }
702
703 public Message next() {
704 if (this.conversation instanceof Conversation) {
705 final Conversation conversation = (Conversation) this.conversation;
706 synchronized (conversation.messages) {
707 if (this.mNextMessage == null) {
708 int index = conversation.messages.indexOf(this);
709 if (index < 0 || index >= conversation.messages.size() - 1) {
710 this.mNextMessage = null;
711 } else {
712 this.mNextMessage = conversation.messages.get(index + 1);
713 }
714 }
715 return this.mNextMessage;
716 }
717 } else {
718 throw new AssertionError("Calling next should be disabled for stubs");
719 }
720 }
721
722 public Message prev() {
723 if (this.conversation instanceof Conversation) {
724 final Conversation conversation = (Conversation) this.conversation;
725 synchronized (conversation.messages) {
726 if (this.mPreviousMessage == null) {
727 int index = conversation.messages.indexOf(this);
728 if (index <= 0 || index > conversation.messages.size()) {
729 this.mPreviousMessage = null;
730 } else {
731 this.mPreviousMessage = conversation.messages.get(index - 1);
732 }
733 }
734 }
735 return this.mPreviousMessage;
736 } else {
737 throw new AssertionError("Calling prev should be disabled for stubs");
738 }
739 }
740
741 public boolean isLastCorrectableMessage() {
742 Message next = next();
743 while (next != null) {
744 if (next.isEditable()) {
745 return false;
746 }
747 next = next.next();
748 }
749 return isEditable();
750 }
751
752 public boolean isEditable() {
753 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
754 }
755
756 public boolean mergeable(final Message message) {
757 return message != null &&
758 (message.getType() == Message.TYPE_TEXT &&
759 this.getTransferable() == null &&
760 message.getTransferable() == null &&
761 message.getEncryption() != Message.ENCRYPTION_PGP &&
762 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
763 this.getType() == message.getType() &&
764 this.getSubject() != null &&
765 isStatusMergeable(this.getStatus(), message.getStatus()) &&
766 isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
767 this.getCounterpart() != null &&
768 this.getCounterpart().equals(message.getCounterpart()) &&
769 this.edited() == message.edited() &&
770 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
771 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
772 !message.isGeoUri() &&
773 !this.isGeoUri() &&
774 !message.isOOb() &&
775 !this.isOOb() &&
776 !message.treatAsDownloadable() &&
777 !this.treatAsDownloadable() &&
778 !message.hasMeCommand() &&
779 !this.hasMeCommand() &&
780 !this.bodyIsOnlyEmojis() &&
781 !message.bodyIsOnlyEmojis() &&
782 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
783 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
784 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
785 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
786 );
787 }
788
789 private static boolean isStatusMergeable(int a, int b) {
790 return a == b || (
791 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
792 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
793 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
794 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
795 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
796 );
797 }
798
799 private static boolean isEncryptionMergeable(final int a, final int b) {
800 return a == b
801 && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
802 .contains(a);
803 }
804
805 public void setCounterparts(List<MucOptions.User> counterparts) {
806 this.counterparts = counterparts;
807 }
808
809 public List<MucOptions.User> getCounterparts() {
810 return this.counterparts;
811 }
812
813 @Override
814 public int getAvatarBackgroundColor() {
815 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
816 return Color.TRANSPARENT;
817 } else {
818 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
819 }
820 }
821
822 @Override
823 public String getAvatarName() {
824 return UIHelper.getMessageDisplayName(this);
825 }
826
827 public boolean isOOb() {
828 return oob || getFileParams().url != null;
829 }
830
831 public static class MergeSeparator {
832 }
833
834 public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
835 final Element html = getHtml();
836 if (html == null || Build.VERSION.SDK_INT < 24) {
837 return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
838 } else {
839 SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml(
840 MessageUtils.filterLtrRtl(html.toString()).trim(),
841 Html.FROM_HTML_MODE_COMPACT,
842 (source) -> {
843 try {
844 if (thumbnailer == null) return fallbackImg;
845 Cid cid = BobTransfer.cid(new URI(source));
846 if (cid == null) return fallbackImg;
847 Drawable thumbnail = thumbnailer.getThumbnail(cid);
848 if (thumbnail == null) return fallbackImg;
849 return thumbnail;
850 } catch (final URISyntaxException e) {
851 return fallbackImg;
852 }
853 },
854 (opening, tag, output, xmlReader) -> {}
855 ));
856
857 // https://stackoverflow.com/a/10187511/8611
858 int i = spannable.length();
859 while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { }
860 return (SpannableStringBuilder) spannable.subSequence(0, i+1);
861 }
862 }
863
864 public SpannableStringBuilder getMergedBody() {
865 return getMergedBody(null, null);
866 }
867
868 public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
869 SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
870 Message current = this;
871 while (current.mergeable(current.next())) {
872 current = current.next();
873 if (current == null) {
874 break;
875 }
876 body.append("\n\n");
877 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
878 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
879 body.append(current.getSpannableBody(thumbnailer, fallbackImg));
880 }
881 return body;
882 }
883
884 public boolean hasMeCommand() {
885 return this.body.trim().startsWith(ME_COMMAND);
886 }
887
888 public int getMergedStatus() {
889 int status = this.status;
890 Message current = this;
891 while (current.mergeable(current.next())) {
892 current = current.next();
893 if (current == null) {
894 break;
895 }
896 status = current.status;
897 }
898 return status;
899 }
900
901 public long getMergedTimeSent() {
902 long time = this.timeSent;
903 Message current = this;
904 while (current.mergeable(current.next())) {
905 current = current.next();
906 if (current == null) {
907 break;
908 }
909 time = current.timeSent;
910 }
911 return time;
912 }
913
914 public boolean wasMergedIntoPrevious() {
915 Message prev = this.prev();
916 return prev != null && prev.mergeable(this);
917 }
918
919 public boolean trusted() {
920 Contact contact = this.getContact();
921 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
922 }
923
924 public boolean fixCounterpart() {
925 final Presences presences = conversation.getContact().getPresences();
926 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
927 return true;
928 } else if (presences.size() >= 1) {
929 counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
930 return true;
931 } else {
932 counterpart = null;
933 return false;
934 }
935 }
936
937 public void setUuid(String uuid) {
938 this.uuid = uuid;
939 }
940
941 public String getEditedId() {
942 if (edits.size() > 0) {
943 return edits.get(edits.size() - 1).getEditedId();
944 } else {
945 throw new IllegalStateException("Attempting to store unedited message");
946 }
947 }
948
949 public String getEditedIdWireFormat() {
950 if (edits.size() > 0) {
951 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
952 } else {
953 throw new IllegalStateException("Attempting to store unedited message");
954 }
955 }
956
957 public URI getOob() {
958 final String url = getFileParams().url;
959 try {
960 return url == null ? null : new URI(url);
961 } catch (final URISyntaxException e) {
962 return null;
963 }
964 }
965
966 public void addPayload(Element el) {
967 if (el == null) return;
968
969 this.payloads.add(el);
970 }
971
972 public List<Element> getPayloads() {
973 return new ArrayList<>(this.payloads);
974 }
975
976 public List<Element> getFallbacks() {
977 List<Element> fallbacks = new ArrayList<>();
978
979 if (this.payloads == null) return fallbacks;
980
981 for (Element el : this.payloads) {
982 if (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0")) {
983 final String fallbackFor = el.getAttribute("for");
984 if (fallbackFor == null) continue;
985 if (fallbackFor.equals("http://jabber.org/protocol/address") || fallbackFor.equals(Namespace.OOB)) {
986 fallbacks.add(el);
987 }
988 }
989 }
990
991 return fallbacks;
992 }
993
994 public Element getHtml() {
995 if (this.payloads == null) return null;
996
997 for (Element el : this.payloads) {
998 if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
999 return el.getChildren().get(0);
1000 }
1001 }
1002
1003 return null;
1004 }
1005
1006 public List<Element> getCommands() {
1007 if (this.payloads == null) return null;
1008
1009 for (Element el : this.payloads) {
1010 if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
1011 return el.getChildren();
1012 }
1013 }
1014
1015 return null;
1016 }
1017
1018 public String getMimeType() {
1019 String extension;
1020 if (relativeFilePath != null) {
1021 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
1022 } else {
1023 final String url = URL.tryParse(getOob() == null ? body.split("\n")[0] : getOob().toString());
1024 if (url == null) {
1025 return null;
1026 }
1027 extension = MimeUtils.extractRelevantExtension(url);
1028 }
1029 return MimeUtils.guessMimeTypeFromExtension(extension);
1030 }
1031
1032 public synchronized boolean treatAsDownloadable() {
1033 if (treatAsDownloadable == null) {
1034 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, isOOb());
1035 }
1036 return treatAsDownloadable;
1037 }
1038
1039 public synchronized boolean bodyIsOnlyEmojis() {
1040 if (isEmojisOnly == null) {
1041 isEmojisOnly = Emoticons.isOnlyEmoji(getBody().replaceAll("\\s", ""));
1042 }
1043 return isEmojisOnly;
1044 }
1045
1046 public synchronized boolean isGeoUri() {
1047 if (isGeoUri == null) {
1048 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
1049 }
1050 return isGeoUri;
1051 }
1052
1053 protected List<Element> getSims() {
1054 return payloads.stream().filter(el ->
1055 el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0") &&
1056 el.findChild("media-sharing", "urn:xmpp:sims:1") != null
1057 ).collect(Collectors.toList());
1058 }
1059
1060 public synchronized void resetFileParams() {
1061 this.fileParams = null;
1062 }
1063
1064 public synchronized void setFileParams(FileParams fileParams) {
1065 if (this.fileParams != null && this.fileParams.sims != null && fileParams.sims == null) {
1066 fileParams.sims = this.fileParams.sims;
1067 }
1068 this.fileParams = fileParams;
1069 }
1070
1071 public synchronized FileParams getFileParams() {
1072 if (fileParams == null) {
1073 List<Element> sims = getSims();
1074 fileParams = sims.isEmpty() ? new FileParams(oob ? this.body : "") : new FileParams(sims.get(0));
1075 if (this.transferable != null) {
1076 fileParams.size = this.transferable.getFileSize();
1077 }
1078 }
1079
1080 return fileParams;
1081 }
1082
1083 private static int parseInt(String value) {
1084 try {
1085 return Integer.parseInt(value);
1086 } catch (NumberFormatException e) {
1087 return 0;
1088 }
1089 }
1090
1091 public void untie() {
1092 this.mNextMessage = null;
1093 this.mPreviousMessage = null;
1094 }
1095
1096 public boolean isPrivateMessage() {
1097 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
1098 }
1099
1100 public boolean isFileOrImage() {
1101 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
1102 }
1103
1104
1105 public boolean isTypeText() {
1106 return type == TYPE_TEXT || type == TYPE_PRIVATE;
1107 }
1108
1109 public boolean hasFileOnRemoteHost() {
1110 return isFileOrImage() && getFileParams().url != null;
1111 }
1112
1113 public boolean needsUploading() {
1114 return isFileOrImage() && getFileParams().url == null;
1115 }
1116
1117 public static class FileParams {
1118 public String url;
1119 public Long size = null;
1120 public int width = 0;
1121 public int height = 0;
1122 public int runtime = 0;
1123 public Element sims = null;
1124
1125 public FileParams() { }
1126
1127 public FileParams(Element el) {
1128 if (el.getName().equals("x") && el.getNamespace().equals(Namespace.OOB)) {
1129 this.url = el.findChildContent("url", Namespace.OOB);
1130 }
1131 if (el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0")) {
1132 sims = el;
1133 final String refUri = el.getAttribute("uri");
1134 if (refUri != null) url = refUri;
1135 final Element mediaSharing = el.findChild("media-sharing", "urn:xmpp:sims:1");
1136 if (mediaSharing != null) {
1137 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1138 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1139 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1140 if (file != null) {
1141 String sizeS = file.findChildContent("size", file.getNamespace());
1142 if (sizeS != null) size = new Long(sizeS);
1143 String widthS = file.findChildContent("width", "https://schema.org/");
1144 if (widthS != null) width = parseInt(widthS);
1145 String heightS = file.findChildContent("height", "https://schema.org/");
1146 if (heightS != null) height = parseInt(heightS);
1147 String durationS = file.findChildContent("duration", "https://schema.org/");
1148 if (durationS != null) runtime = (int)(Duration.parse(durationS).toMillis() / 1000L);
1149 }
1150
1151 final Element sources = mediaSharing.findChild("sources", "urn:xmpp:sims:1");
1152 if (sources != null) {
1153 final Element ref = sources.findChild("reference", "urn:xmpp:reference:0");
1154 if (ref != null) url = ref.getAttribute("uri");
1155 }
1156 }
1157 }
1158 }
1159
1160 public FileParams(String ser) {
1161 final String[] parts = ser == null ? new String[0] : ser.split("\\|");
1162 switch (parts.length) {
1163 case 1:
1164 try {
1165 this.size = Long.parseLong(parts[0]);
1166 } catch (final NumberFormatException e) {
1167 this.url = URL.tryParse(parts[0]);
1168 }
1169 break;
1170 case 5:
1171 this.runtime = parseInt(parts[4]);
1172 case 4:
1173 this.width = parseInt(parts[2]);
1174 this.height = parseInt(parts[3]);
1175 case 2:
1176 this.url = URL.tryParse(parts[0]);
1177 this.size = Longs.tryParse(parts[1]);
1178 break;
1179 case 3:
1180 this.size = Longs.tryParse(parts[0]);
1181 this.width = parseInt(parts[1]);
1182 this.height = parseInt(parts[2]);
1183 break;
1184 }
1185 }
1186
1187 public long getSize() {
1188 return size == null ? 0 : size;
1189 }
1190
1191 public Element toSims() {
1192 if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
1193 sims.setAttribute("type", "data");
1194 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1195 if (mediaSharing == null) mediaSharing = sims.addChild("media-sharing", "urn:xmpp:sims:1");
1196
1197 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1198 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1199 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1200 if (file == null) file = mediaSharing.addChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1201
1202 file.removeChild(file.findChild("size", file.getNamespace()));
1203 if (size != null) file.addChild("size", file.getNamespace()).setContent(size.toString());
1204
1205 file.removeChild(file.findChild("width", "https://schema.org/"));
1206 if (width > 0) file.addChild("width", "https://schema.org/").setContent(String.valueOf(width));
1207
1208 file.removeChild(file.findChild("height", "https://schema.org/"));
1209 if (height > 0) file.addChild("height", "https://schema.org/").setContent(String.valueOf(height));
1210
1211 file.removeChild(file.findChild("duration", "https://schema.org/"));
1212 if (runtime > 0) file.addChild("duration", "https://schema.org/").setContent("PT" + runtime + "S");
1213
1214 if (url != null) {
1215 Element sources = mediaSharing.findChild("sources", mediaSharing.getNamespace());
1216 if (sources == null) sources = mediaSharing.addChild("sources", mediaSharing.getNamespace());
1217
1218 Element source = sources.findChild("reference", "urn:xmpp:reference:0");
1219 if (source == null) source = sources.addChild("reference", "urn:xmpp:reference:0");
1220 source.setAttribute("type", "data");
1221 source.setAttribute("uri", url);
1222 }
1223
1224 return sims;
1225 }
1226
1227 protected Element getFileElement() {
1228 Element file = null;
1229 if (sims == null) return file;
1230
1231 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1232 if (mediaSharing == null) return file;
1233 file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1234 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1235 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1236 return file;
1237 }
1238
1239 public List<Cid> getCids() {
1240 List<Cid> cids = new ArrayList<>();
1241 Element file = getFileElement();
1242 if (file == null) return cids;
1243
1244 for (Element child : file.getChildren()) {
1245 if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1246 try {
1247 cids.add(CryptoHelper.cid(Base64.decode(child.getContent(), Base64.DEFAULT), child.getAttribute("algo")));
1248 } catch (final NoSuchAlgorithmException | IllegalStateException e) { }
1249 }
1250 }
1251
1252 cids.sort((x, y) -> y.getType().compareTo(x.getType()));
1253
1254 return cids;
1255 }
1256
1257 public List<Element> getThumbnails() {
1258 List<Element> thumbs = new ArrayList<>();
1259 Element file = getFileElement();
1260 if (file == null) return thumbs;
1261
1262 for (Element child : file.getChildren()) {
1263 if (child.getName().equals("thumbnail") && child.getNamespace().equals("urn:xmpp:thumbs:1")) {
1264 thumbs.add(child);
1265 }
1266 }
1267
1268 return thumbs;
1269 }
1270
1271 public String toString() {
1272 final StringBuilder builder = new StringBuilder();
1273 if (url != null) builder.append(url);
1274 if (size != null) builder.append('|').append(size.toString());
1275 if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width);
1276 if (height > 0 || runtime > 0) builder.append('|').append(height);
1277 if (runtime > 0) builder.append('|').append(runtime);
1278 return builder.toString();
1279 }
1280
1281 public boolean equals(Object o) {
1282 if (!(o instanceof FileParams)) return false;
1283
1284 return url.equals(((FileParams) o).url);
1285 }
1286
1287 public int hashCode() {
1288 return url.hashCode();
1289 }
1290 }
1291
1292 public void setFingerprint(String fingerprint) {
1293 this.axolotlFingerprint = fingerprint;
1294 }
1295
1296 public String getFingerprint() {
1297 return axolotlFingerprint;
1298 }
1299
1300 public boolean isTrusted() {
1301 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
1302 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
1303 return s != null && s.isTrusted();
1304 }
1305
1306 private int getPreviousEncryption() {
1307 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
1308 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1309 continue;
1310 }
1311 return iterator.getEncryption();
1312 }
1313 return ENCRYPTION_NONE;
1314 }
1315
1316 private int getNextEncryption() {
1317 if (this.conversation instanceof Conversation) {
1318 Conversation conversation = (Conversation) this.conversation;
1319 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
1320 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1321 continue;
1322 }
1323 return iterator.getEncryption();
1324 }
1325 return conversation.getNextEncryption();
1326 } else {
1327 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1328 }
1329 }
1330
1331 public boolean isValidInSession() {
1332 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1333 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1334
1335 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1336 || futureEncryption == ENCRYPTION_NONE
1337 || pastEncryption != futureEncryption;
1338
1339 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1340 }
1341
1342 private static int getCleanedEncryption(int encryption) {
1343 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1344 return ENCRYPTION_PGP;
1345 }
1346 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1347 return ENCRYPTION_AXOLOTL;
1348 }
1349 return encryption;
1350 }
1351
1352 public static boolean configurePrivateMessage(final Message message) {
1353 return configurePrivateMessage(message, false);
1354 }
1355
1356 public static boolean configurePrivateFileMessage(final Message message) {
1357 return configurePrivateMessage(message, true);
1358 }
1359
1360 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1361 final Conversation conversation;
1362 if (message.conversation instanceof Conversation) {
1363 conversation = (Conversation) message.conversation;
1364 } else {
1365 return false;
1366 }
1367 if (conversation.getMode() == Conversation.MODE_MULTI) {
1368 final Jid nextCounterpart = conversation.getNextCounterpart();
1369 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1370 }
1371 return false;
1372 }
1373
1374 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1375 final Conversation conversation;
1376 if (message.conversation instanceof Conversation) {
1377 conversation = (Conversation) message.conversation;
1378 } else {
1379 return false;
1380 }
1381 return configurePrivateMessage(conversation, message, counterpart, false);
1382 }
1383
1384 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1385 if (counterpart == null) {
1386 return false;
1387 }
1388 message.setCounterpart(counterpart);
1389 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1390 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1391 return true;
1392 }
1393}