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