1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.os.SystemClock;
6import android.util.Log;
7import android.util.Pair;
8
9import org.json.JSONException;
10import org.json.JSONObject;
11
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Map;
18import java.util.Set;
19import java.util.concurrent.CopyOnWriteArraySet;
20
21import eu.siacs.conversations.Config;
22import eu.siacs.conversations.R;
23import eu.siacs.conversations.crypto.PgpDecryptionService;
24import eu.siacs.conversations.crypto.axolotl.AxolotlService;
25import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
26import eu.siacs.conversations.services.AvatarService;
27import eu.siacs.conversations.services.XmppConnectionService;
28import eu.siacs.conversations.utils.UIHelper;
29import eu.siacs.conversations.utils.XmppUri;
30import eu.siacs.conversations.xmpp.Jid;
31import eu.siacs.conversations.xmpp.XmppConnection;
32import eu.siacs.conversations.xmpp.jingle.RtpCapability;
33
34public class Account extends AbstractEntity implements AvatarService.Avatarable {
35
36 public static final String TABLENAME = "accounts";
37
38 public static final String USERNAME = "username";
39 public static final String SERVER = "server";
40 public static final String PASSWORD = "password";
41 public static final String OPTIONS = "options";
42 public static final String ROSTERVERSION = "rosterversion";
43 public static final String KEYS = "keys";
44 public static final String AVATAR = "avatar";
45 public static final String DISPLAY_NAME = "display_name";
46 public static final String HOSTNAME = "hostname";
47 public static final String PORT = "port";
48 public static final String STATUS = "status";
49 public static final String STATUS_MESSAGE = "status_message";
50 public static final String RESOURCE = "resource";
51
52 public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
53 public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
54
55 public static final int OPTION_USETLS = 0;
56 public static final int OPTION_DISABLED = 1;
57 public static final int OPTION_REGISTER = 2;
58 public static final int OPTION_USECOMPRESSION = 3;
59 public static final int OPTION_MAGIC_CREATE = 4;
60 public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
61 public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
62 public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
63 public static final int OPTION_UNVERIFIED = 8;
64 public static final int OPTION_FIXED_USERNAME = 9;
65 private static final String KEY_PGP_SIGNATURE = "pgp_signature";
66 private static final String KEY_PGP_ID = "pgp_id";
67 protected final JSONObject keys;
68 private final Roster roster = new Roster(this);
69 private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
70 public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
71 public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
72 public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
73 public final Set<Conversation> inProgressConferencePings = new HashSet<>();
74 protected Jid jid;
75 protected String password;
76 protected int options = 0;
77 protected State status = State.OFFLINE;
78 private State lastErrorStatus = State.OFFLINE;
79 protected String resource;
80 protected String avatar;
81 protected String hostname = null;
82 protected int port = 5222;
83 protected boolean online = false;
84 private String rosterVersion;
85 private String displayName = null;
86 private AxolotlService axolotlService = null;
87 private PgpDecryptionService pgpDecryptionService = null;
88 private XmppConnection xmppConnection = null;
89 private long mEndGracePeriod = 0L;
90 private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
91 private Presence.Status presenceStatus = Presence.Status.ONLINE;
92 private String presenceStatusMessage = null;
93
94 public Account(final Jid jid, final String password) {
95 this(java.util.UUID.randomUUID().toString(), jid,
96 password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
97 }
98
99 private Account(final String uuid, final Jid jid,
100 final String password, final int options, final String rosterVersion, final String keys,
101 final String avatar, String displayName, String hostname, int port,
102 final Presence.Status status, String statusMessage) {
103 this.uuid = uuid;
104 this.jid = jid;
105 this.password = password;
106 this.options = options;
107 this.rosterVersion = rosterVersion;
108 JSONObject tmp;
109 try {
110 tmp = new JSONObject(keys);
111 } catch (JSONException e) {
112 tmp = new JSONObject();
113 }
114 this.keys = tmp;
115 this.avatar = avatar;
116 this.displayName = displayName;
117 this.hostname = hostname;
118 this.port = port;
119 this.presenceStatus = status;
120 this.presenceStatusMessage = statusMessage;
121 }
122
123 public static Account fromCursor(final Cursor cursor) {
124 final Jid jid;
125 try {
126 String resource = cursor.getString(cursor.getColumnIndex(RESOURCE));
127 jid = Jid.of(
128 cursor.getString(cursor.getColumnIndex(USERNAME)),
129 cursor.getString(cursor.getColumnIndex(SERVER)),
130 resource == null || resource.trim().isEmpty() ? null : resource);
131 } catch (final IllegalArgumentException ignored) {
132 Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndex(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndex(SERVER)));
133 throw new AssertionError(ignored);
134 }
135 return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
136 jid,
137 cursor.getString(cursor.getColumnIndex(PASSWORD)),
138 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
139 cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
140 cursor.getString(cursor.getColumnIndex(KEYS)),
141 cursor.getString(cursor.getColumnIndex(AVATAR)),
142 cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
143 cursor.getString(cursor.getColumnIndex(HOSTNAME)),
144 cursor.getInt(cursor.getColumnIndex(PORT)),
145 Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
146 cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
147 }
148
149 public boolean httpUploadAvailable(long filesize) {
150 return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
151 }
152
153 public boolean httpUploadAvailable() {
154 return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE) || httpUploadAvailable(0);
155 }
156
157 public String getDisplayName() {
158 return displayName;
159 }
160
161 public void setDisplayName(String displayName) {
162 this.displayName = displayName;
163 }
164
165 public XmppConnection.Identity getServerIdentity() {
166 if (xmppConnection == null) {
167 return XmppConnection.Identity.UNKNOWN;
168 } else {
169 return xmppConnection.getServerIdentity();
170 }
171 }
172
173 public Contact getSelfContact() {
174 return getRoster().getContact(jid);
175 }
176
177 public boolean hasPendingPgpIntent(Conversation conversation) {
178 return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
179 }
180
181 public boolean isPgpDecryptionServiceConnected() {
182 return pgpDecryptionService != null && pgpDecryptionService.isConnected();
183 }
184
185 public boolean setShowErrorNotification(boolean newValue) {
186 boolean oldValue = showErrorNotification();
187 setKey("show_error", Boolean.toString(newValue));
188 return newValue != oldValue;
189 }
190
191 public boolean showErrorNotification() {
192 String key = getKey("show_error");
193 return key == null || Boolean.parseBoolean(key);
194 }
195
196 public boolean isEnabled() {
197 return !isOptionSet(Account.OPTION_DISABLED);
198 }
199
200 public boolean isOptionSet(final int option) {
201 return ((options & (1 << option)) != 0);
202 }
203
204 public boolean setOption(final int option, final boolean value) {
205 final int before = this.options;
206 if (value) {
207 this.options |= 1 << option;
208 } else {
209 this.options &= ~(1 << option);
210 }
211 return before != this.options;
212 }
213
214 public String getUsername() {
215 return jid.getEscapedLocal();
216 }
217
218 public boolean setJid(final Jid next) {
219 final Jid previousFull = this.jid;
220 final Jid prev = this.jid != null ? this.jid.asBareJid() : null;
221 final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid()));
222 if (changed) {
223 final AxolotlService oldAxolotlService = this.axolotlService;
224 if (oldAxolotlService != null) {
225 oldAxolotlService.destroy();
226 this.jid = next;
227 this.axolotlService = oldAxolotlService.makeNew();
228 }
229 }
230 this.jid = next;
231 return next != null && !next.equals(previousFull);
232 }
233
234 public Jid getDomain() {
235 return jid.getDomain();
236 }
237
238 public String getServer() {
239 return jid.getDomain().toEscapedString();
240 }
241
242 public String getPassword() {
243 return password;
244 }
245
246 public void setPassword(final String password) {
247 this.password = password;
248 }
249
250 public String getHostname() {
251 return this.hostname == null ? "" : this.hostname;
252 }
253
254 public void setHostname(String hostname) {
255 this.hostname = hostname;
256 }
257
258 public boolean isOnion() {
259 final String server = getServer();
260 return server != null && server.endsWith(".onion");
261 }
262
263 public int getPort() {
264 return this.port;
265 }
266
267 public void setPort(int port) {
268 this.port = port;
269 }
270
271 public State getStatus() {
272 if (isOptionSet(OPTION_DISABLED)) {
273 return State.DISABLED;
274 } else {
275 return this.status;
276 }
277 }
278
279 public State getLastErrorStatus() {
280 return this.lastErrorStatus;
281 }
282
283 public void setStatus(final State status) {
284 this.status = status;
285 if (status.isError || status == State.ONLINE) {
286 this.lastErrorStatus = status;
287 }
288 }
289
290 public State getTrueStatus() {
291 return this.status;
292 }
293
294 public boolean errorStatus() {
295 return getStatus().isError();
296 }
297
298 public boolean hasErrorStatus() {
299 return getXmppConnection() != null
300 && (getStatus().isError() || getStatus() == State.CONNECTING)
301 && getXmppConnection().getAttempt() >= 3;
302 }
303
304 public Presence.Status getPresenceStatus() {
305 return this.presenceStatus;
306 }
307
308 public void setPresenceStatus(Presence.Status status) {
309 this.presenceStatus = status;
310 }
311
312 public String getPresenceStatusMessage() {
313 return this.presenceStatusMessage;
314 }
315
316 public void setPresenceStatusMessage(String message) {
317 this.presenceStatusMessage = message;
318 }
319
320 public String getResource() {
321 return jid.getResource();
322 }
323
324 public void setResource(final String resource) {
325 this.jid = this.jid.withResource(resource);
326 }
327
328 public Jid getJid() {
329 return jid;
330 }
331
332 public JSONObject getKeys() {
333 return keys;
334 }
335
336 public String getKey(final String name) {
337 synchronized (this.keys) {
338 return this.keys.optString(name, null);
339 }
340 }
341
342 public int getKeyAsInt(final String name, int defaultValue) {
343 String key = getKey(name);
344 try {
345 return key == null ? defaultValue : Integer.parseInt(key);
346 } catch (NumberFormatException e) {
347 return defaultValue;
348 }
349 }
350
351 public boolean setKey(final String keyName, final String keyValue) {
352 synchronized (this.keys) {
353 try {
354 this.keys.put(keyName, keyValue);
355 return true;
356 } catch (final JSONException e) {
357 return false;
358 }
359 }
360 }
361
362 public boolean setPrivateKeyAlias(String alias) {
363 return setKey("private_key_alias", alias);
364 }
365
366 public String getPrivateKeyAlias() {
367 return getKey("private_key_alias");
368 }
369
370 @Override
371 public ContentValues getContentValues() {
372 final ContentValues values = new ContentValues();
373 values.put(UUID, uuid);
374 values.put(USERNAME, jid.getLocal());
375 values.put(SERVER, jid.getDomain().toEscapedString());
376 values.put(PASSWORD, password);
377 values.put(OPTIONS, options);
378 synchronized (this.keys) {
379 values.put(KEYS, this.keys.toString());
380 }
381 values.put(ROSTERVERSION, rosterVersion);
382 values.put(AVATAR, avatar);
383 values.put(DISPLAY_NAME, displayName);
384 values.put(HOSTNAME, hostname);
385 values.put(PORT, port);
386 values.put(STATUS, presenceStatus.toShowString());
387 values.put(STATUS_MESSAGE, presenceStatusMessage);
388 values.put(RESOURCE, jid.getResource());
389 return values;
390 }
391
392 public AxolotlService getAxolotlService() {
393 return axolotlService;
394 }
395
396 public void initAccountServices(final XmppConnectionService context) {
397 this.axolotlService = new AxolotlService(this, context);
398 this.pgpDecryptionService = new PgpDecryptionService(context);
399 if (xmppConnection != null) {
400 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
401 }
402 }
403
404 public PgpDecryptionService getPgpDecryptionService() {
405 return this.pgpDecryptionService;
406 }
407
408 public XmppConnection getXmppConnection() {
409 return this.xmppConnection;
410 }
411
412 public void setXmppConnection(final XmppConnection connection) {
413 this.xmppConnection = connection;
414 }
415
416 public String getRosterVersion() {
417 if (this.rosterVersion == null) {
418 return "";
419 } else {
420 return this.rosterVersion;
421 }
422 }
423
424 public void setRosterVersion(final String version) {
425 this.rosterVersion = version;
426 }
427
428 public int countPresences() {
429 return this.getSelfContact().getPresences().size();
430 }
431
432 public int activeDevicesWithRtpCapability() {
433 int i = 0;
434 for(Presence presence : getSelfContact().getPresences().getPresences()) {
435 if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
436 i++;
437 }
438 }
439 return i;
440 }
441
442 public String getPgpSignature() {
443 return getKey(KEY_PGP_SIGNATURE);
444 }
445
446 public boolean setPgpSignature(String signature) {
447 return setKey(KEY_PGP_SIGNATURE, signature);
448 }
449
450 public boolean unsetPgpSignature() {
451 synchronized (this.keys) {
452 return keys.remove(KEY_PGP_SIGNATURE) != null;
453 }
454 }
455
456 public long getPgpId() {
457 synchronized (this.keys) {
458 if (keys.has(KEY_PGP_ID)) {
459 try {
460 return keys.getLong(KEY_PGP_ID);
461 } catch (JSONException e) {
462 return 0;
463 }
464 } else {
465 return 0;
466 }
467 }
468 }
469
470 public boolean setPgpSignId(long pgpID) {
471 synchronized (this.keys) {
472 try {
473 if (pgpID == 0) {
474 keys.remove(KEY_PGP_ID);
475 } else {
476 keys.put(KEY_PGP_ID, pgpID);
477 }
478 } catch (JSONException e) {
479 return false;
480 }
481 return true;
482 }
483 }
484
485 public Roster getRoster() {
486 return this.roster;
487 }
488
489 public Collection<Bookmark> getBookmarks() {
490 return this.bookmarks.values();
491 }
492
493 public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
494 synchronized (this.bookmarks) {
495 this.bookmarks.clear();
496 this.bookmarks.putAll(bookmarks);
497 }
498 }
499
500 public void putBookmark(Bookmark bookmark) {
501 synchronized (this.bookmarks) {
502 this.bookmarks.put(bookmark.getJid(), bookmark);
503 }
504 }
505
506 public void removeBookmark(Bookmark bookmark) {
507 synchronized (this.bookmarks) {
508 this.bookmarks.remove(bookmark.getJid());
509 }
510 }
511
512 public void removeBookmark(Jid jid) {
513 synchronized (this.bookmarks) {
514 this.bookmarks.remove(jid);
515 }
516 }
517
518 public Set<Jid> getBookmarkedJids() {
519 synchronized (this.bookmarks) {
520 return new HashSet<>(this.bookmarks.keySet());
521 }
522 }
523
524 public Bookmark getBookmark(final Jid jid) {
525 synchronized (this.bookmarks) {
526 return this.bookmarks.get(jid.asBareJid());
527 }
528 }
529
530 public boolean setAvatar(final String filename) {
531 if (this.avatar != null && this.avatar.equals(filename)) {
532 return false;
533 } else {
534 this.avatar = filename;
535 return true;
536 }
537 }
538
539 public String getAvatar() {
540 return this.avatar;
541 }
542
543 public void activateGracePeriod(final long duration) {
544 if (duration > 0) {
545 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
546 }
547 }
548
549 public void deactivateGracePeriod() {
550 this.mEndGracePeriod = 0L;
551 }
552
553 public boolean inGracePeriod() {
554 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
555 }
556
557 public String getShareableUri() {
558 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
559 String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString();
560 if (fingerprints.size() > 0) {
561 return XmppUri.getFingerprintUri(uri, fingerprints, ';');
562 } else {
563 return uri;
564 }
565 }
566
567 public String getShareableLink() {
568 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
569 String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
570 if (fingerprints.size() > 0) {
571 return XmppUri.getFingerprintUri(uri, fingerprints, '&');
572 } else {
573 return uri;
574 }
575 }
576
577 private List<XmppUri.Fingerprint> getFingerprints() {
578 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
579 if (axolotlService == null) {
580 return fingerprints;
581 }
582 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
583 for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
584 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
585 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
586 }
587 }
588 return fingerprints;
589 }
590
591 public boolean isBlocked(final ListItem contact) {
592 final Jid jid = contact.getJid();
593 return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
594 }
595
596 public boolean isBlocked(final Jid jid) {
597 return jid != null && blocklist.contains(jid.asBareJid());
598 }
599
600 public Collection<Jid> getBlocklist() {
601 return this.blocklist;
602 }
603
604 public void clearBlocklist() {
605 getBlocklist().clear();
606 }
607
608 public boolean isOnlineAndConnected() {
609 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
610 }
611
612 @Override
613 public int getAvatarBackgroundColor() {
614 return UIHelper.getColorForName(jid.asBareJid().toString());
615 }
616
617 @Override
618 public String getAvatarName() {
619 throw new IllegalStateException("This method should not be called");
620 }
621
622 public enum State {
623 DISABLED(false, false),
624 OFFLINE(false),
625 CONNECTING(false),
626 ONLINE(false),
627 NO_INTERNET(false),
628 UNAUTHORIZED,
629 SERVER_NOT_FOUND,
630 REGISTRATION_SUCCESSFUL(false),
631 REGISTRATION_FAILED(true, false),
632 REGISTRATION_WEB(true, false),
633 REGISTRATION_CONFLICT(true, false),
634 REGISTRATION_NOT_SUPPORTED(true, false),
635 REGISTRATION_PLEASE_WAIT(true, false),
636 REGISTRATION_INVALID_TOKEN(true,false),
637 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
638 TLS_ERROR,
639 INCOMPATIBLE_SERVER,
640 TOR_NOT_AVAILABLE,
641 DOWNGRADE_ATTACK,
642 SESSION_FAILURE,
643 BIND_FAILURE,
644 HOST_UNKNOWN,
645 STREAM_ERROR,
646 STREAM_OPENING_ERROR,
647 POLICY_VIOLATION,
648 PAYMENT_REQUIRED,
649 MISSING_INTERNET_PERMISSION(false);
650
651 private final boolean isError;
652 private final boolean attemptReconnect;
653
654 State(final boolean isError) {
655 this(isError, true);
656 }
657
658 State(final boolean isError, final boolean reconnect) {
659 this.isError = isError;
660 this.attemptReconnect = reconnect;
661 }
662
663 State() {
664 this(true, true);
665 }
666
667 public boolean isError() {
668 return this.isError;
669 }
670
671 public boolean isAttemptReconnect() {
672 return this.attemptReconnect;
673 }
674
675 public int getReadableId() {
676 switch (this) {
677 case DISABLED:
678 return R.string.account_status_disabled;
679 case ONLINE:
680 return R.string.account_status_online;
681 case CONNECTING:
682 return R.string.account_status_connecting;
683 case OFFLINE:
684 return R.string.account_status_offline;
685 case UNAUTHORIZED:
686 return R.string.account_status_unauthorized;
687 case SERVER_NOT_FOUND:
688 return R.string.account_status_not_found;
689 case NO_INTERNET:
690 return R.string.account_status_no_internet;
691 case REGISTRATION_FAILED:
692 return R.string.account_status_regis_fail;
693 case REGISTRATION_WEB:
694 return R.string.account_status_regis_web;
695 case REGISTRATION_CONFLICT:
696 return R.string.account_status_regis_conflict;
697 case REGISTRATION_SUCCESSFUL:
698 return R.string.account_status_regis_success;
699 case REGISTRATION_NOT_SUPPORTED:
700 return R.string.account_status_regis_not_sup;
701 case REGISTRATION_INVALID_TOKEN:
702 return R.string.account_status_regis_invalid_token;
703 case TLS_ERROR:
704 return R.string.account_status_tls_error;
705 case INCOMPATIBLE_SERVER:
706 return R.string.account_status_incompatible_server;
707 case TOR_NOT_AVAILABLE:
708 return R.string.account_status_tor_unavailable;
709 case BIND_FAILURE:
710 return R.string.account_status_bind_failure;
711 case SESSION_FAILURE:
712 return R.string.session_failure;
713 case DOWNGRADE_ATTACK:
714 return R.string.sasl_downgrade;
715 case HOST_UNKNOWN:
716 return R.string.account_status_host_unknown;
717 case POLICY_VIOLATION:
718 return R.string.account_status_policy_violation;
719 case REGISTRATION_PLEASE_WAIT:
720 return R.string.registration_please_wait;
721 case REGISTRATION_PASSWORD_TOO_WEAK:
722 return R.string.registration_password_too_weak;
723 case STREAM_ERROR:
724 return R.string.account_status_stream_error;
725 case STREAM_OPENING_ERROR:
726 return R.string.account_status_stream_opening_error;
727 case PAYMENT_REQUIRED:
728 return R.string.payment_required;
729 case MISSING_INTERNET_PERMISSION:
730 return R.string.missing_internet_permission;
731 default:
732 return R.string.account_status_unknown;
733 }
734 }
735 }
736}