1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5
6import net.java.otr4j.OtrException;
7import net.java.otr4j.crypto.OtrCryptoException;
8import net.java.otr4j.session.SessionID;
9import net.java.otr4j.session.SessionImpl;
10import net.java.otr4j.session.SessionStatus;
11
12import org.json.JSONException;
13import org.json.JSONObject;
14
15import java.security.interfaces.DSAPublicKey;
16import java.util.ArrayList;
17import java.util.Collections;
18import java.util.Comparator;
19import java.util.Iterator;
20import java.util.List;
21
22import eu.siacs.conversations.Config;
23import eu.siacs.conversations.crypto.axolotl.AxolotlService;
24import eu.siacs.conversations.xmpp.chatstate.ChatState;
25import eu.siacs.conversations.xmpp.jid.InvalidJidException;
26import eu.siacs.conversations.xmpp.jid.Jid;
27
28public class Conversation extends AbstractEntity implements Blockable {
29 public static final String TABLENAME = "conversations";
30
31 public static final int STATUS_AVAILABLE = 0;
32 public static final int STATUS_ARCHIVED = 1;
33 public static final int STATUS_DELETED = 2;
34
35 public static final int MODE_MULTI = 1;
36 public static final int MODE_SINGLE = 0;
37
38 public static final String NAME = "name";
39 public static final String ACCOUNT = "accountUuid";
40 public static final String CONTACT = "contactUuid";
41 public static final String CONTACTJID = "contactJid";
42 public static final String STATUS = "status";
43 public static final String CREATED = "created";
44 public static final String MODE = "mode";
45 public static final String ATTRIBUTES = "attributes";
46
47 public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
48 public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
49 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
50 public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
51
52 private String name;
53 private String contactUuid;
54 private String accountUuid;
55 private Jid contactJid;
56 private int status;
57 private long created;
58 private int mode;
59
60 private JSONObject attributes = new JSONObject();
61
62 private Jid nextCounterpart;
63
64 protected final ArrayList<Message> messages = new ArrayList<>();
65 protected Account account = null;
66
67 private transient SessionImpl otrSession;
68
69 private transient String otrFingerprint = null;
70 private Smp mSmp = new Smp();
71
72 private String nextMessage;
73
74 private transient MucOptions mucOptions = null;
75
76 private byte[] symmetricKey;
77
78 private Bookmark bookmark;
79
80 private boolean messagesLeftOnServer = true;
81 private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
82 private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
83 private String mLastReceivedOtrMessageId = null;
84
85 public boolean hasMessagesLeftOnServer() {
86 return messagesLeftOnServer;
87 }
88
89 public void setHasMessagesLeftOnServer(boolean value) {
90 this.messagesLeftOnServer = value;
91 }
92
93 public Message findUnsentMessageWithUuid(String uuid) {
94 synchronized(this.messages) {
95 for (final Message message : this.messages) {
96 final int s = message.getStatus();
97 if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
98 return message;
99 }
100 }
101 }
102 return null;
103 }
104
105 public void findWaitingMessages(OnMessageFound onMessageFound) {
106 synchronized (this.messages) {
107 for(Message message : this.messages) {
108 if (message.getStatus() == Message.STATUS_WAITING) {
109 onMessageFound.onMessageFound(message);
110 }
111 }
112 }
113 }
114
115 public void findUnreadMessages(OnMessageFound onMessageFound) {
116 synchronized (this.messages) {
117 for(Message message : this.messages) {
118 if (!message.isRead()) {
119 onMessageFound.onMessageFound(message);
120 }
121 }
122 }
123 }
124
125 public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
126 synchronized (this.messages) {
127 for (final Message message : this.messages) {
128 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
129 && message.getEncryption() != Message.ENCRYPTION_PGP) {
130 onMessageFound.onMessageFound(message);
131 }
132 }
133 }
134 }
135
136 public Message findMessageWithFileAndUuid(final String uuid) {
137 synchronized (this.messages) {
138 for (final Message message : this.messages) {
139 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
140 && message.getEncryption() != Message.ENCRYPTION_PGP
141 && message.getUuid().equals(uuid)) {
142 return message;
143 }
144 }
145 }
146 return null;
147 }
148
149 public void clearMessages() {
150 synchronized (this.messages) {
151 this.messages.clear();
152 }
153 }
154
155 public boolean setIncomingChatState(ChatState state) {
156 if (this.mIncomingChatState == state) {
157 return false;
158 }
159 this.mIncomingChatState = state;
160 return true;
161 }
162
163 public ChatState getIncomingChatState() {
164 return this.mIncomingChatState;
165 }
166
167 public boolean setOutgoingChatState(ChatState state) {
168 if (mode == MODE_MULTI) {
169 return false;
170 }
171 if (this.mOutgoingChatState != state) {
172 this.mOutgoingChatState = state;
173 return true;
174 } else {
175 return false;
176 }
177 }
178
179 public ChatState getOutgoingChatState() {
180 return this.mOutgoingChatState;
181 }
182
183 public void trim() {
184 synchronized (this.messages) {
185 final int size = messages.size();
186 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
187 if (size > maxsize) {
188 this.messages.subList(0, size - maxsize).clear();
189 }
190 }
191 }
192
193 public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
194 synchronized (this.messages) {
195 for (Message message : this.messages) {
196 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
197 && (message.getEncryption() == encryptionType)) {
198 onMessageFound.onMessageFound(message);
199 }
200 }
201 }
202 }
203
204 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
205 synchronized (this.messages) {
206 for (Message message : this.messages) {
207 if (message.getType() != Message.TYPE_IMAGE
208 && message.getStatus() == Message.STATUS_UNSEND) {
209 onMessageFound.onMessageFound(message);
210 }
211 }
212 }
213 }
214
215 public Message findSentMessageWithUuidOrRemoteId(String id) {
216 synchronized (this.messages) {
217 for (Message message : this.messages) {
218 if (id.equals(message.getUuid())
219 || (message.getStatus() >= Message.STATUS_SEND
220 && id.equals(message.getRemoteMsgId()))) {
221 return message;
222 }
223 }
224 }
225 return null;
226 }
227
228 public Message findSentMessageWithUuid(String id) {
229 synchronized (this.messages) {
230 for (Message message : this.messages) {
231 if (id.equals(message.getUuid())) {
232 return message;
233 }
234 }
235 }
236 return null;
237 }
238
239 public void populateWithMessages(final List<Message> messages) {
240 synchronized (this.messages) {
241 messages.clear();
242 messages.addAll(this.messages);
243 }
244 for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
245 if (iterator.next().wasMergedIntoPrevious()) {
246 iterator.remove();
247 }
248 }
249 }
250
251 @Override
252 public boolean isBlocked() {
253 return getContact().isBlocked();
254 }
255
256 @Override
257 public boolean isDomainBlocked() {
258 return getContact().isDomainBlocked();
259 }
260
261 @Override
262 public Jid getBlockedJid() {
263 return getContact().getBlockedJid();
264 }
265
266 public String getLastReceivedOtrMessageId() {
267 return this.mLastReceivedOtrMessageId;
268 }
269
270 public void setLastReceivedOtrMessageId(String id) {
271 this.mLastReceivedOtrMessageId = id;
272 }
273
274 public int countMessages() {
275 synchronized (this.messages) {
276 return this.messages.size();
277 }
278 }
279
280 public interface OnMessageFound {
281 void onMessageFound(final Message message);
282 }
283
284 public Conversation(final String name, final Account account, final Jid contactJid,
285 final int mode) {
286 this(java.util.UUID.randomUUID().toString(), name, null, account
287 .getUuid(), contactJid, System.currentTimeMillis(),
288 STATUS_AVAILABLE, mode, "");
289 this.account = account;
290 }
291
292 public Conversation(final String uuid, final String name, final String contactUuid,
293 final String accountUuid, final Jid contactJid, final long created, final int status,
294 final int mode, final String attributes) {
295 this.uuid = uuid;
296 this.name = name;
297 this.contactUuid = contactUuid;
298 this.accountUuid = accountUuid;
299 this.contactJid = contactJid;
300 this.created = created;
301 this.status = status;
302 this.mode = mode;
303 try {
304 this.attributes = new JSONObject(attributes == null ? "" : attributes);
305 } catch (JSONException e) {
306 this.attributes = new JSONObject();
307 }
308 }
309
310 public boolean isRead() {
311 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
312 }
313
314 public List<Message> markRead() {
315 final List<Message> unread = new ArrayList<>();
316 synchronized (this.messages) {
317 for(Message message : this.messages) {
318 if (!message.isRead()) {
319 message.markRead();
320 unread.add(message);
321 }
322 }
323 }
324 return unread;
325 }
326
327 public Message getLatestMarkableMessage() {
328 for (int i = this.messages.size() - 1; i >= 0; --i) {
329 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
330 && this.messages.get(i).markable) {
331 if (this.messages.get(i).isRead()) {
332 return null;
333 } else {
334 return this.messages.get(i);
335 }
336 }
337 }
338 return null;
339 }
340
341 public Message getLatestMessage() {
342 if (this.messages.size() == 0) {
343 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
344 message.setTime(getCreated());
345 return message;
346 } else {
347 Message message = this.messages.get(this.messages.size() - 1);
348 message.setConversation(this);
349 return message;
350 }
351 }
352
353 public String getName() {
354 if (getMode() == MODE_MULTI) {
355 if (getMucOptions().getSubject() != null) {
356 return getMucOptions().getSubject();
357 } else if (bookmark != null && bookmark.getBookmarkName() != null) {
358 return bookmark.getBookmarkName();
359 } else {
360 String generatedName = getMucOptions().createNameFromParticipants();
361 if (generatedName != null) {
362 return generatedName;
363 } else {
364 return getJid().getLocalpart();
365 }
366 }
367 } else {
368 return this.getContact().getDisplayName();
369 }
370 }
371
372 public String getAccountUuid() {
373 return this.accountUuid;
374 }
375
376 public Account getAccount() {
377 return this.account;
378 }
379
380 public Contact getContact() {
381 return this.account.getRoster().getContact(this.contactJid);
382 }
383
384 public void setAccount(final Account account) {
385 this.account = account;
386 }
387
388 @Override
389 public Jid getJid() {
390 return this.contactJid;
391 }
392
393 public int getStatus() {
394 return this.status;
395 }
396
397 public long getCreated() {
398 return this.created;
399 }
400
401 public ContentValues getContentValues() {
402 ContentValues values = new ContentValues();
403 values.put(UUID, uuid);
404 values.put(NAME, name);
405 values.put(CONTACT, contactUuid);
406 values.put(ACCOUNT, accountUuid);
407 values.put(CONTACTJID, contactJid.toString());
408 values.put(CREATED, created);
409 values.put(STATUS, status);
410 values.put(MODE, mode);
411 values.put(ATTRIBUTES, attributes.toString());
412 return values;
413 }
414
415 public static Conversation fromCursor(Cursor cursor) {
416 Jid jid;
417 try {
418 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
419 } catch (final InvalidJidException e) {
420 // Borked DB..
421 jid = null;
422 }
423 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
424 cursor.getString(cursor.getColumnIndex(NAME)),
425 cursor.getString(cursor.getColumnIndex(CONTACT)),
426 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
427 jid,
428 cursor.getLong(cursor.getColumnIndex(CREATED)),
429 cursor.getInt(cursor.getColumnIndex(STATUS)),
430 cursor.getInt(cursor.getColumnIndex(MODE)),
431 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
432 }
433
434 public void setStatus(int status) {
435 this.status = status;
436 }
437
438 public int getMode() {
439 return this.mode;
440 }
441
442 public void setMode(int mode) {
443 this.mode = mode;
444 }
445
446 public SessionImpl startOtrSession(String presence, boolean sendStart) {
447 if (this.otrSession != null) {
448 return this.otrSession;
449 } else {
450 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
451 presence,
452 "xmpp");
453 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
454 try {
455 if (sendStart) {
456 this.otrSession.startSession();
457 return this.otrSession;
458 }
459 return this.otrSession;
460 } catch (OtrException e) {
461 return null;
462 }
463 }
464
465 }
466
467 public SessionImpl getOtrSession() {
468 return this.otrSession;
469 }
470
471 public void resetOtrSession() {
472 this.otrFingerprint = null;
473 this.otrSession = null;
474 this.mSmp.hint = null;
475 this.mSmp.secret = null;
476 this.mSmp.status = Smp.STATUS_NONE;
477 }
478
479 public Smp smp() {
480 return mSmp;
481 }
482
483 public void startOtrIfNeeded() {
484 if (this.otrSession != null
485 && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
486 try {
487 this.otrSession.startSession();
488 } catch (OtrException e) {
489 this.resetOtrSession();
490 }
491 }
492 }
493
494 public boolean endOtrIfNeeded() {
495 if (this.otrSession != null) {
496 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
497 try {
498 this.otrSession.endSession();
499 this.resetOtrSession();
500 return true;
501 } catch (OtrException e) {
502 this.resetOtrSession();
503 return false;
504 }
505 } else {
506 this.resetOtrSession();
507 return false;
508 }
509 } else {
510 return false;
511 }
512 }
513
514 public boolean hasValidOtrSession() {
515 return this.otrSession != null;
516 }
517
518 public synchronized String getOtrFingerprint() {
519 if (this.otrFingerprint == null) {
520 try {
521 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
522 return null;
523 }
524 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
525 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
526 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
527 return null;
528 }
529 }
530 return this.otrFingerprint;
531 }
532
533 public boolean verifyOtrFingerprint() {
534 final String fingerprint = getOtrFingerprint();
535 if (fingerprint != null) {
536 getContact().addOtrFingerprint(fingerprint);
537 return true;
538 } else {
539 return false;
540 }
541 }
542
543 public boolean isOtrFingerprintVerified() {
544 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
545 }
546
547 /**
548 * short for is Private and Non-anonymous
549 */
550 private boolean isPnNA() {
551 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
552 }
553
554 public synchronized MucOptions getMucOptions() {
555 if (this.mucOptions == null) {
556 this.mucOptions = new MucOptions(this);
557 }
558 return this.mucOptions;
559 }
560
561 public void resetMucOptions() {
562 this.mucOptions = null;
563 }
564
565 public void setContactJid(final Jid jid) {
566 this.contactJid = jid;
567 }
568
569 public void setNextCounterpart(Jid jid) {
570 this.nextCounterpart = jid;
571 }
572
573 public Jid getNextCounterpart() {
574 return this.nextCounterpart;
575 }
576
577 private int getMostRecentlyUsedOutgoingEncryption() {
578 synchronized (this.messages) {
579 for(int i = this.messages.size() -1; i >= 0; --i) {
580 final Message m = this.messages.get(i);
581 if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
582 final int e = m.getEncryption();
583 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
584 return Message.ENCRYPTION_PGP;
585 } else {
586 return e;
587 }
588 }
589 }
590 }
591 return Message.ENCRYPTION_NONE;
592 }
593
594 private int getMostRecentlyUsedIncomingEncryption() {
595 synchronized (this.messages) {
596 for(int i = this.messages.size() -1; i >= 0; --i) {
597 final Message m = this.messages.get(i);
598 if (m.getStatus() == Message.STATUS_RECEIVED) {
599 final int e = m.getEncryption();
600 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
601 return Message.ENCRYPTION_PGP;
602 } else {
603 return e;
604 }
605 }
606 }
607 }
608 return Message.ENCRYPTION_NONE;
609 }
610
611 public int getNextEncryption() {
612 final AxolotlService axolotlService = getAccount().getAxolotlService();
613 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
614 if (next == -1) {
615 if (Config.X509_VERIFICATION && mode == MODE_SINGLE) {
616 if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
617 return Message.ENCRYPTION_AXOLOTL;
618 } else {
619 return Message.ENCRYPTION_NONE;
620 }
621 }
622 int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
623 if (outgoing == Message.ENCRYPTION_NONE) {
624 next = this.getMostRecentlyUsedIncomingEncryption();
625 } else {
626 next = outgoing;
627 }
628 }
629 if (Config.FORCE_ENCRYPTION && mode == MODE_SINGLE && next <= 0) {
630 if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
631 return Message.ENCRYPTION_AXOLOTL;
632 } else {
633 return Message.ENCRYPTION_OTR;
634 }
635 }
636 return next;
637 }
638
639 public void setNextEncryption(int encryption) {
640 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
641 }
642
643 public String getNextMessage() {
644 if (this.nextMessage == null) {
645 return "";
646 } else {
647 return this.nextMessage;
648 }
649 }
650
651 public boolean smpRequested() {
652 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
653 }
654
655 public void setNextMessage(String message) {
656 this.nextMessage = message;
657 }
658
659 public void setSymmetricKey(byte[] key) {
660 this.symmetricKey = key;
661 }
662
663 public byte[] getSymmetricKey() {
664 return this.symmetricKey;
665 }
666
667 public void setBookmark(Bookmark bookmark) {
668 this.bookmark = bookmark;
669 this.bookmark.setConversation(this);
670 }
671
672 public void deregisterWithBookmark() {
673 if (this.bookmark != null) {
674 this.bookmark.setConversation(null);
675 }
676 }
677
678 public Bookmark getBookmark() {
679 return this.bookmark;
680 }
681
682 public boolean hasDuplicateMessage(Message message) {
683 synchronized (this.messages) {
684 for (int i = this.messages.size() - 1; i >= 0; --i) {
685 if (this.messages.get(i).equals(message)) {
686 return true;
687 }
688 }
689 }
690 return false;
691 }
692
693 public Message findSentMessageWithBody(String body) {
694 synchronized (this.messages) {
695 for (int i = this.messages.size() - 1; i >= 0; --i) {
696 Message message = this.messages.get(i);
697 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
698 String otherBody;
699 if (message.hasFileOnRemoteHost()) {
700 otherBody = message.getFileParams().url.toString();
701 } else {
702 otherBody = message.body;
703 }
704 if (otherBody != null && otherBody.equals(body)) {
705 return message;
706 }
707 }
708 }
709 return null;
710 }
711 }
712
713 public long getLastMessageTransmitted() {
714 synchronized (this.messages) {
715 for(int i = this.messages.size() - 1; i >= 0; --i) {
716 Message message = this.messages.get(i);
717 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
718 return message.getTimeSent();
719 }
720 }
721 }
722 return 0;
723 }
724
725 public void setMutedTill(long value) {
726 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
727 }
728
729 public boolean isMuted() {
730 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
731 }
732
733 public boolean alwaysNotify() {
734 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY,Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
735 }
736
737 public boolean setAttribute(String key, String value) {
738 try {
739 this.attributes.put(key, value);
740 return true;
741 } catch (JSONException e) {
742 return false;
743 }
744 }
745
746 public String getAttribute(String key) {
747 try {
748 return this.attributes.getString(key);
749 } catch (JSONException e) {
750 return null;
751 }
752 }
753
754 public int getIntAttribute(String key, int defaultValue) {
755 String value = this.getAttribute(key);
756 if (value == null) {
757 return defaultValue;
758 } else {
759 try {
760 return Integer.parseInt(value);
761 } catch (NumberFormatException e) {
762 return defaultValue;
763 }
764 }
765 }
766
767 public long getLongAttribute(String key, long defaultValue) {
768 String value = this.getAttribute(key);
769 if (value == null) {
770 return defaultValue;
771 } else {
772 try {
773 return Long.parseLong(value);
774 } catch (NumberFormatException e) {
775 return defaultValue;
776 }
777 }
778 }
779
780 public boolean getBooleanAttribute(String key, boolean defaultValue) {
781 String value = this.getAttribute(key);
782 if (value == null) {
783 return defaultValue;
784 } else {
785 return Boolean.parseBoolean(value);
786 }
787 }
788
789 public void add(Message message) {
790 message.setConversation(this);
791 synchronized (this.messages) {
792 this.messages.add(message);
793 }
794 }
795
796 public void addAll(int index, List<Message> messages) {
797 synchronized (this.messages) {
798 this.messages.addAll(index, messages);
799 }
800 account.getPgpDecryptionService().addAll(messages);
801 }
802
803 public void sort() {
804 synchronized (this.messages) {
805 Collections.sort(this.messages, new Comparator<Message>() {
806 @Override
807 public int compare(Message left, Message right) {
808 if (left.getTimeSent() < right.getTimeSent()) {
809 return -1;
810 } else if (left.getTimeSent() > right.getTimeSent()) {
811 return 1;
812 } else {
813 return 0;
814 }
815 }
816 });
817 for(Message message : this.messages) {
818 message.untie();
819 }
820 }
821 }
822
823 public int unreadCount() {
824 synchronized (this.messages) {
825 int count = 0;
826 for(int i = this.messages.size() - 1; i >= 0; --i) {
827 if (this.messages.get(i).isRead()) {
828 return count;
829 }
830 ++count;
831 }
832 return count;
833 }
834 }
835
836 public class Smp {
837 public static final int STATUS_NONE = 0;
838 public static final int STATUS_CONTACT_REQUESTED = 1;
839 public static final int STATUS_WE_REQUESTED = 2;
840 public static final int STATUS_FAILED = 3;
841 public static final int STATUS_VERIFIED = 4;
842
843 public String secret = null;
844 public String hint = null;
845 public int status = 0;
846 }
847}