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