1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5
6import java.net.MalformedURLException;
7import java.net.URL;
8import java.util.Arrays;
9
10import eu.siacs.conversations.Config;
11import eu.siacs.conversations.xmpp.jid.InvalidJidException;
12import eu.siacs.conversations.xmpp.jid.Jid;
13
14public class Message extends AbstractEntity {
15
16 public static final String TABLENAME = "messages";
17
18 public static final int STATUS_RECEIVED = 0;
19 public static final int STATUS_UNSEND = 1;
20 public static final int STATUS_SEND = 2;
21 public static final int STATUS_SEND_FAILED = 3;
22 public static final int STATUS_WAITING = 5;
23 public static final int STATUS_OFFERED = 6;
24 public static final int STATUS_SEND_RECEIVED = 7;
25 public static final int STATUS_SEND_DISPLAYED = 8;
26
27 public static final int ENCRYPTION_NONE = 0;
28 public static final int ENCRYPTION_PGP = 1;
29 public static final int ENCRYPTION_OTR = 2;
30 public static final int ENCRYPTION_DECRYPTED = 3;
31 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
32
33 public static final int TYPE_TEXT = 0;
34 public static final int TYPE_IMAGE = 1;
35 public static final int TYPE_FILE = 2;
36 public static final int TYPE_STATUS = 3;
37 public static final int TYPE_PRIVATE = 4;
38
39 public static final String CONVERSATION = "conversationUuid";
40 public static final String COUNTERPART = "counterpart";
41 public static final String TRUE_COUNTERPART = "trueCounterpart";
42 public static final String BODY = "body";
43 public static final String TIME_SENT = "timeSent";
44 public static final String ENCRYPTION = "encryption";
45 public static final String STATUS = "status";
46 public static final String TYPE = "type";
47 public static final String REMOTE_MSG_ID = "remoteMsgId";
48 public static final String SERVER_MSG_ID = "serverMsgId";
49 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
50 public static final String ME_COMMAND = "/me ";
51
52 public boolean markable = false;
53 protected String conversationUuid;
54 protected Jid counterpart;
55 protected Jid trueCounterpart;
56 protected String body;
57 protected String encryptedBody;
58 protected long timeSent;
59 protected int encryption;
60 protected int status;
61 protected int type;
62 protected String relativeFilePath;
63 protected boolean read = true;
64 protected String remoteMsgId = null;
65 protected String serverMsgId = null;
66 protected Conversation conversation = null;
67 protected Downloadable downloadable = null;
68 private Message mNextMessage = null;
69 private Message mPreviousMessage = null;
70
71 private Message() {
72
73 }
74
75 public Message(Conversation conversation, String body, int encryption) {
76 this(conversation, body, encryption, STATUS_UNSEND);
77 }
78
79 public Message(Conversation conversation, String body, int encryption, int status) {
80 this(java.util.UUID.randomUUID().toString(),
81 conversation.getUuid(),
82 conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
83 null,
84 body,
85 System.currentTimeMillis(),
86 encryption,
87 status,
88 TYPE_TEXT,
89 null,
90 null,
91 null);
92 this.conversation = conversation;
93 }
94
95 private Message(final String uuid, final String conversationUUid, final Jid counterpart,
96 final Jid trueCounterpart, final String body, final long timeSent,
97 final int encryption, final int status, final int type, final String remoteMsgId,
98 final String relativeFilePath, final String serverMsgId) {
99 this.uuid = uuid;
100 this.conversationUuid = conversationUUid;
101 this.counterpart = counterpart;
102 this.trueCounterpart = trueCounterpart;
103 this.body = body;
104 this.timeSent = timeSent;
105 this.encryption = encryption;
106 this.status = status;
107 this.type = type;
108 this.remoteMsgId = remoteMsgId;
109 this.relativeFilePath = relativeFilePath;
110 this.serverMsgId = serverMsgId;
111 }
112
113 public static Message fromCursor(Cursor cursor) {
114 Jid jid;
115 try {
116 String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
117 if (value != null) {
118 jid = Jid.fromString(value);
119 } else {
120 jid = null;
121 }
122 } catch (InvalidJidException e) {
123 jid = null;
124 }
125 Jid trueCounterpart;
126 try {
127 String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
128 if (value != null) {
129 trueCounterpart = Jid.fromString(value);
130 } else {
131 trueCounterpart = null;
132 }
133 } catch (InvalidJidException e) {
134 trueCounterpart = null;
135 }
136 return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
137 cursor.getString(cursor.getColumnIndex(CONVERSATION)),
138 jid,
139 trueCounterpart,
140 cursor.getString(cursor.getColumnIndex(BODY)),
141 cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
142 cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
143 cursor.getInt(cursor.getColumnIndex(STATUS)),
144 cursor.getInt(cursor.getColumnIndex(TYPE)),
145 cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
146 cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
147 cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
148 }
149
150 public static Message createStatusMessage(Conversation conversation) {
151 Message message = new Message();
152 message.setType(Message.TYPE_STATUS);
153 message.setConversation(conversation);
154 return message;
155 }
156
157 @Override
158 public ContentValues getContentValues() {
159 ContentValues values = new ContentValues();
160 values.put(UUID, uuid);
161 values.put(CONVERSATION, conversationUuid);
162 if (counterpart == null) {
163 values.putNull(COUNTERPART);
164 } else {
165 values.put(COUNTERPART, counterpart.toString());
166 }
167 if (trueCounterpart == null) {
168 values.putNull(TRUE_COUNTERPART);
169 } else {
170 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
171 }
172 values.put(BODY, body);
173 values.put(TIME_SENT, timeSent);
174 values.put(ENCRYPTION, encryption);
175 values.put(STATUS, status);
176 values.put(TYPE, type);
177 values.put(REMOTE_MSG_ID, remoteMsgId);
178 values.put(RELATIVE_FILE_PATH, relativeFilePath);
179 values.put(SERVER_MSG_ID,serverMsgId);
180 return values;
181 }
182
183 public String getConversationUuid() {
184 return conversationUuid;
185 }
186
187 public Conversation getConversation() {
188 return this.conversation;
189 }
190
191 public void setConversation(Conversation conv) {
192 this.conversation = conv;
193 }
194
195 public Jid getCounterpart() {
196 return counterpart;
197 }
198
199 public void setCounterpart(final Jid counterpart) {
200 this.counterpart = counterpart;
201 }
202
203 public Contact getContact() {
204 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
205 return this.conversation.getContact();
206 } else {
207 if (this.trueCounterpart == null) {
208 return null;
209 } else {
210 return this.conversation.getAccount().getRoster()
211 .getContactFromRoster(this.trueCounterpart);
212 }
213 }
214 }
215
216 public String getBody() {
217 return body;
218 }
219
220 public void setBody(String body) {
221 this.body = body;
222 }
223
224 public long getTimeSent() {
225 return timeSent;
226 }
227
228 public int getEncryption() {
229 return encryption;
230 }
231
232 public void setEncryption(int encryption) {
233 this.encryption = encryption;
234 }
235
236 public int getStatus() {
237 return status;
238 }
239
240 public void setStatus(int status) {
241 this.status = status;
242 }
243
244 public String getRelativeFilePath() {
245 return this.relativeFilePath;
246 }
247
248 public void setRelativeFilePath(String path) {
249 this.relativeFilePath = path;
250 }
251
252 public String getRemoteMsgId() {
253 return this.remoteMsgId;
254 }
255
256 public void setRemoteMsgId(String id) {
257 this.remoteMsgId = id;
258 }
259
260 public String getServerMsgId() {
261 return this.serverMsgId;
262 }
263
264 public void setServerMsgId(String id) {
265 this.serverMsgId = id;
266 }
267
268 public boolean isRead() {
269 return this.read;
270 }
271
272 public void markRead() {
273 this.read = true;
274 }
275
276 public void markUnread() {
277 this.read = false;
278 }
279
280 public void setTime(long time) {
281 this.timeSent = time;
282 }
283
284 public String getEncryptedBody() {
285 return this.encryptedBody;
286 }
287
288 public void setEncryptedBody(String body) {
289 this.encryptedBody = body;
290 }
291
292 public int getType() {
293 return this.type;
294 }
295
296 public void setType(int type) {
297 this.type = type;
298 }
299
300 public void setTrueCounterpart(Jid trueCounterpart) {
301 this.trueCounterpart = trueCounterpart;
302 }
303
304 public Downloadable getDownloadable() {
305 return this.downloadable;
306 }
307
308 public void setDownloadable(Downloadable downloadable) {
309 this.downloadable = downloadable;
310 }
311
312 public boolean equals(Message message) {
313 if (this.serverMsgId != null && message.getServerMsgId() != null) {
314 return this.serverMsgId.equals(message.getServerMsgId());
315 } else if (this.body == null || this.counterpart == null) {
316 return false;
317 } else if (message.getRemoteMsgId() != null) {
318 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
319 && this.counterpart.equals(message.getCounterpart())
320 && this.body.equals(message.getBody());
321 } else {
322 return this.remoteMsgId == null
323 && this.counterpart.equals(message.getCounterpart())
324 && this.body.equals(message.getBody())
325 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.PING_TIMEOUT * 500;
326 }
327 }
328
329 public Message next() {
330 synchronized (this.conversation.messages) {
331 if (this.mNextMessage == null) {
332 int index = this.conversation.messages.indexOf(this);
333 if (index < 0 || index >= this.conversation.messages.size() - 1) {
334 this.mNextMessage = null;
335 } else {
336 this.mNextMessage = this.conversation.messages.get(index + 1);
337 }
338 }
339 return this.mNextMessage;
340 }
341 }
342
343 public Message prev() {
344 synchronized (this.conversation.messages) {
345 if (this.mPreviousMessage == null) {
346 int index = this.conversation.messages.indexOf(this);
347 if (index <= 0 || index > this.conversation.messages.size()) {
348 this.mPreviousMessage = null;
349 } else {
350 this.mPreviousMessage = this.conversation.messages.get(index - 1);
351 }
352 }
353 return this.mPreviousMessage;
354 }
355 }
356
357 public boolean mergeable(final Message message) {
358 return message != null &&
359 (message.getType() == Message.TYPE_TEXT &&
360 this.getDownloadable() == null &&
361 message.getDownloadable() == null &&
362 message.getEncryption() != Message.ENCRYPTION_PGP &&
363 this.getType() == message.getType() &&
364 this.getStatus() == message.getStatus() &&
365 this.getEncryption() == message.getEncryption() &&
366 this.getCounterpart() != null &&
367 this.getCounterpart().equals(message.getCounterpart()) &&
368 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
369 !message.bodyContainsDownloadable() &&
370 !this.bodyContainsDownloadable() &&
371 !message.getBody().startsWith(ME_COMMAND) &&
372 !this.getBody().startsWith(ME_COMMAND)
373 );
374 }
375
376 public String getMergedBody() {
377 final Message next = this.next();
378 if (this.mergeable(next)) {
379 return getBody().trim() + '\n' + next.getMergedBody();
380 }
381 return getBody().trim();
382 }
383
384 public boolean hasMeCommand() {
385 return getMergedBody().startsWith(ME_COMMAND);
386 }
387
388 public int getMergedStatus() {
389 return getStatus();
390 }
391
392 public long getMergedTimeSent() {
393 Message next = this.next();
394 if (this.mergeable(next)) {
395 return next.getMergedTimeSent();
396 } else {
397 return getTimeSent();
398 }
399 }
400
401 public boolean wasMergedIntoPrevious() {
402 Message prev = this.prev();
403 return prev != null && prev.mergeable(this);
404 }
405
406 public boolean trusted() {
407 Contact contact = this.getContact();
408 return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
409 }
410
411 public boolean bodyContainsDownloadable() {
412 try {
413 URL url = new URL(this.getBody());
414 if (!url.getProtocol().equalsIgnoreCase("http")
415 && !url.getProtocol().equalsIgnoreCase("https")) {
416 return false;
417 }
418 if (url.getPath() == null) {
419 return false;
420 }
421 String[] pathParts = url.getPath().split("/");
422 String filename;
423 if (pathParts.length > 0) {
424 filename = pathParts[pathParts.length - 1].toLowerCase();
425 } else {
426 return false;
427 }
428 String[] extensionParts = filename.split("\\.");
429 if (extensionParts.length == 2
430 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
431 extensionParts[extensionParts.length - 1])) {
432 return true;
433 } else if (extensionParts.length == 3
434 && Arrays
435 .asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
436 .contains(extensionParts[extensionParts.length - 1])
437 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
438 extensionParts[extensionParts.length - 2])) {
439 return true;
440 } else {
441 return false;
442 }
443 } catch (MalformedURLException e) {
444 return false;
445 }
446 }
447
448 public ImageParams getImageParams() {
449 ImageParams params = getLegacyImageParams();
450 if (params != null) {
451 return params;
452 }
453 params = new ImageParams();
454 if (this.downloadable != null) {
455 params.size = this.downloadable.getFileSize();
456 }
457 if (body == null) {
458 return params;
459 }
460 String parts[] = body.split("\\|");
461 if (parts.length == 1) {
462 try {
463 params.size = Long.parseLong(parts[0]);
464 } catch (NumberFormatException e) {
465 params.origin = parts[0];
466 try {
467 params.url = new URL(parts[0]);
468 } catch (MalformedURLException e1) {
469 params.url = null;
470 }
471 }
472 } else if (parts.length == 3) {
473 try {
474 params.size = Long.parseLong(parts[0]);
475 } catch (NumberFormatException e) {
476 params.size = 0;
477 }
478 try {
479 params.width = Integer.parseInt(parts[1]);
480 } catch (NumberFormatException e) {
481 params.width = 0;
482 }
483 try {
484 params.height = Integer.parseInt(parts[2]);
485 } catch (NumberFormatException e) {
486 params.height = 0;
487 }
488 } else if (parts.length == 4) {
489 params.origin = parts[0];
490 try {
491 params.url = new URL(parts[0]);
492 } catch (MalformedURLException e1) {
493 params.url = null;
494 }
495 try {
496 params.size = Long.parseLong(parts[1]);
497 } catch (NumberFormatException e) {
498 params.size = 0;
499 }
500 try {
501 params.width = Integer.parseInt(parts[2]);
502 } catch (NumberFormatException e) {
503 params.width = 0;
504 }
505 try {
506 params.height = Integer.parseInt(parts[3]);
507 } catch (NumberFormatException e) {
508 params.height = 0;
509 }
510 }
511 return params;
512 }
513
514 public ImageParams getLegacyImageParams() {
515 ImageParams params = new ImageParams();
516 if (body == null) {
517 return params;
518 }
519 String parts[] = body.split(",");
520 if (parts.length == 3) {
521 try {
522 params.size = Long.parseLong(parts[0]);
523 } catch (NumberFormatException e) {
524 return null;
525 }
526 try {
527 params.width = Integer.parseInt(parts[1]);
528 } catch (NumberFormatException e) {
529 return null;
530 }
531 try {
532 params.height = Integer.parseInt(parts[2]);
533 } catch (NumberFormatException e) {
534 return null;
535 }
536 return params;
537 } else {
538 return null;
539 }
540 }
541
542 public void untie() {
543 this.mNextMessage = null;
544 this.mPreviousMessage = null;
545 }
546
547 public boolean isFileOrImage() {
548 return type == TYPE_FILE || type == TYPE_IMAGE;
549 }
550
551 public class ImageParams {
552 public URL url;
553 public long size = 0;
554 public int width = 0;
555 public int height = 0;
556 public String origin;
557 }
558}