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