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