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