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