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, String body) {
151 Message message = new Message();
152 message.setType(Message.TYPE_STATUS);
153 message.setConversation(conversation);
154 message.setBody(body);
155 return message;
156 }
157
158 @Override
159 public ContentValues getContentValues() {
160 ContentValues values = new ContentValues();
161 values.put(UUID, uuid);
162 values.put(CONVERSATION, conversationUuid);
163 if (counterpart == null) {
164 values.putNull(COUNTERPART);
165 } else {
166 values.put(COUNTERPART, counterpart.toString());
167 }
168 if (trueCounterpart == null) {
169 values.putNull(TRUE_COUNTERPART);
170 } else {
171 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
172 }
173 values.put(BODY, body);
174 values.put(TIME_SENT, timeSent);
175 values.put(ENCRYPTION, encryption);
176 values.put(STATUS, status);
177 values.put(TYPE, type);
178 values.put(REMOTE_MSG_ID, remoteMsgId);
179 values.put(RELATIVE_FILE_PATH, relativeFilePath);
180 values.put(SERVER_MSG_ID,serverMsgId);
181 return values;
182 }
183
184 public String getConversationUuid() {
185 return conversationUuid;
186 }
187
188 public Conversation getConversation() {
189 return this.conversation;
190 }
191
192 public void setConversation(Conversation conv) {
193 this.conversation = conv;
194 }
195
196 public Jid getCounterpart() {
197 return counterpart;
198 }
199
200 public void setCounterpart(final Jid counterpart) {
201 this.counterpart = counterpart;
202 }
203
204 public Contact getContact() {
205 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
206 return this.conversation.getContact();
207 } else {
208 if (this.trueCounterpart == null) {
209 return null;
210 } else {
211 return this.conversation.getAccount().getRoster()
212 .getContactFromRoster(this.trueCounterpart);
213 }
214 }
215 }
216
217 public String getBody() {
218 return body;
219 }
220
221 public void setBody(String body) {
222 this.body = body;
223 }
224
225 public long getTimeSent() {
226 return timeSent;
227 }
228
229 public int getEncryption() {
230 return encryption;
231 }
232
233 public void setEncryption(int encryption) {
234 this.encryption = encryption;
235 }
236
237 public int getStatus() {
238 return status;
239 }
240
241 public void setStatus(int status) {
242 this.status = status;
243 }
244
245 public String getRelativeFilePath() {
246 return this.relativeFilePath;
247 }
248
249 public void setRelativeFilePath(String path) {
250 this.relativeFilePath = path;
251 }
252
253 public String getRemoteMsgId() {
254 return this.remoteMsgId;
255 }
256
257 public void setRemoteMsgId(String id) {
258 this.remoteMsgId = id;
259 }
260
261 public String getServerMsgId() {
262 return this.serverMsgId;
263 }
264
265 public void setServerMsgId(String id) {
266 this.serverMsgId = id;
267 }
268
269 public boolean isRead() {
270 return this.read;
271 }
272
273 public void markRead() {
274 this.read = true;
275 }
276
277 public void markUnread() {
278 this.read = false;
279 }
280
281 public void setTime(long time) {
282 this.timeSent = time;
283 }
284
285 public String getEncryptedBody() {
286 return this.encryptedBody;
287 }
288
289 public void setEncryptedBody(String body) {
290 this.encryptedBody = body;
291 }
292
293 public int getType() {
294 return this.type;
295 }
296
297 public void setType(int type) {
298 this.type = type;
299 }
300
301 public void setTrueCounterpart(Jid trueCounterpart) {
302 this.trueCounterpart = trueCounterpart;
303 }
304
305 public Downloadable getDownloadable() {
306 return this.downloadable;
307 }
308
309 public void setDownloadable(Downloadable downloadable) {
310 this.downloadable = downloadable;
311 }
312
313 public boolean equals(Message message) {
314 if (this.serverMsgId != null && message.getServerMsgId() != null) {
315 return this.serverMsgId.equals(message.getServerMsgId());
316 } else if (this.body == null || this.counterpart == null) {
317 return false;
318 } else if (message.getRemoteMsgId() != null) {
319 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
320 && this.counterpart.equals(message.getCounterpart())
321 && this.body.equals(message.getBody());
322 } else {
323 return this.remoteMsgId == null
324 && this.counterpart.equals(message.getCounterpart())
325 && this.body.equals(message.getBody())
326 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.PING_TIMEOUT * 500;
327 }
328 }
329
330 public Message next() {
331 synchronized (this.conversation.messages) {
332 if (this.mNextMessage == null) {
333 int index = this.conversation.messages.indexOf(this);
334 if (index < 0 || index >= this.conversation.messages.size() - 1) {
335 this.mNextMessage = null;
336 } else {
337 this.mNextMessage = this.conversation.messages.get(index + 1);
338 }
339 }
340 return this.mNextMessage;
341 }
342 }
343
344 public Message prev() {
345 synchronized (this.conversation.messages) {
346 if (this.mPreviousMessage == null) {
347 int index = this.conversation.messages.indexOf(this);
348 if (index <= 0 || index > this.conversation.messages.size()) {
349 this.mPreviousMessage = null;
350 } else {
351 this.mPreviousMessage = this.conversation.messages.get(index - 1);
352 }
353 }
354 return this.mPreviousMessage;
355 }
356 }
357
358 public boolean mergeable(final Message message) {
359 return message != null &&
360 (message.getType() == Message.TYPE_TEXT &&
361 this.getDownloadable() == null &&
362 message.getDownloadable() == null &&
363 message.getEncryption() != Message.ENCRYPTION_PGP &&
364 this.getType() == message.getType() &&
365 this.getStatus() == message.getStatus() &&
366 this.getEncryption() == message.getEncryption() &&
367 this.getCounterpart() != null &&
368 this.getCounterpart().equals(message.getCounterpart()) &&
369 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
370 !message.bodyContainsDownloadable() &&
371 !this.bodyContainsDownloadable() &&
372 !message.getBody().startsWith(ME_COMMAND) &&
373 !this.getBody().startsWith(ME_COMMAND)
374 );
375 }
376
377 public String getMergedBody() {
378 final Message next = this.next();
379 if (this.mergeable(next)) {
380 return getBody().trim() + '\n' + next.getMergedBody();
381 }
382 return getBody().trim();
383 }
384
385 public boolean hasMeCommand() {
386 return getMergedBody().startsWith(ME_COMMAND);
387 }
388
389 public int getMergedStatus() {
390 return getStatus();
391 }
392
393 public long getMergedTimeSent() {
394 Message next = this.next();
395 if (this.mergeable(next)) {
396 return next.getMergedTimeSent();
397 } else {
398 return getTimeSent();
399 }
400 }
401
402 public boolean wasMergedIntoPrevious() {
403 Message prev = this.prev();
404 return prev != null && prev.mergeable(this);
405 }
406
407 public boolean trusted() {
408 Contact contact = this.getContact();
409 return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
410 }
411
412 public boolean bodyContainsDownloadable() {
413 try {
414 URL url = new URL(this.getBody());
415 if (!url.getProtocol().equalsIgnoreCase("http")
416 && !url.getProtocol().equalsIgnoreCase("https")) {
417 return false;
418 }
419 if (url.getPath() == null) {
420 return false;
421 }
422 String[] pathParts = url.getPath().split("/");
423 String filename;
424 if (pathParts.length > 0) {
425 filename = pathParts[pathParts.length - 1].toLowerCase();
426 } else {
427 return false;
428 }
429 String[] extensionParts = filename.split("\\.");
430 if (extensionParts.length == 2
431 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
432 extensionParts[extensionParts.length - 1])) {
433 return true;
434 } else if (extensionParts.length == 3
435 && Arrays
436 .asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
437 .contains(extensionParts[extensionParts.length - 1])
438 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
439 extensionParts[extensionParts.length - 2])) {
440 return true;
441 } else {
442 return false;
443 }
444 } catch (MalformedURLException e) {
445 return false;
446 }
447 }
448
449 public ImageParams getImageParams() {
450 ImageParams params = getLegacyImageParams();
451 if (params != null) {
452 return params;
453 }
454 params = new ImageParams();
455 if (this.downloadable != null) {
456 params.size = this.downloadable.getFileSize();
457 }
458 if (body == null) {
459 return params;
460 }
461 String parts[] = body.split("\\|");
462 if (parts.length == 1) {
463 try {
464 params.size = Long.parseLong(parts[0]);
465 } catch (NumberFormatException e) {
466 params.origin = parts[0];
467 try {
468 params.url = new URL(parts[0]);
469 } catch (MalformedURLException e1) {
470 params.url = null;
471 }
472 }
473 } else if (parts.length == 3) {
474 try {
475 params.size = Long.parseLong(parts[0]);
476 } catch (NumberFormatException e) {
477 params.size = 0;
478 }
479 try {
480 params.width = Integer.parseInt(parts[1]);
481 } catch (NumberFormatException e) {
482 params.width = 0;
483 }
484 try {
485 params.height = Integer.parseInt(parts[2]);
486 } catch (NumberFormatException e) {
487 params.height = 0;
488 }
489 } else if (parts.length == 4) {
490 params.origin = parts[0];
491 try {
492 params.url = new URL(parts[0]);
493 } catch (MalformedURLException e1) {
494 params.url = null;
495 }
496 try {
497 params.size = Long.parseLong(parts[1]);
498 } catch (NumberFormatException e) {
499 params.size = 0;
500 }
501 try {
502 params.width = Integer.parseInt(parts[2]);
503 } catch (NumberFormatException e) {
504 params.width = 0;
505 }
506 try {
507 params.height = Integer.parseInt(parts[3]);
508 } catch (NumberFormatException e) {
509 params.height = 0;
510 }
511 }
512 return params;
513 }
514
515 public ImageParams getLegacyImageParams() {
516 ImageParams params = new ImageParams();
517 if (body == null) {
518 return params;
519 }
520 String parts[] = body.split(",");
521 if (parts.length == 3) {
522 try {
523 params.size = Long.parseLong(parts[0]);
524 } catch (NumberFormatException e) {
525 return null;
526 }
527 try {
528 params.width = Integer.parseInt(parts[1]);
529 } catch (NumberFormatException e) {
530 return null;
531 }
532 try {
533 params.height = Integer.parseInt(parts[2]);
534 } catch (NumberFormatException e) {
535 return null;
536 }
537 return params;
538 } else {
539 return null;
540 }
541 }
542
543 public void untie() {
544 this.mNextMessage = null;
545 this.mPreviousMessage = null;
546 }
547
548 public boolean isFileOrImage() {
549 return type == TYPE_FILE || type == TYPE_IMAGE;
550 }
551
552 public class ImageParams {
553 public URL url;
554 public long size = 0;
555 public int width = 0;
556 public int height = 0;
557 public String origin;
558 }
559}