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.CopyOnWriteArraySet;
20
21import eu.siacs.conversations.Config;
22import eu.siacs.conversations.R;
23import eu.siacs.conversations.crypto.PgpDecryptionService;
24import eu.siacs.conversations.crypto.axolotl.AxolotlService;
25import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
26import eu.siacs.conversations.services.AvatarService;
27import eu.siacs.conversations.services.XmppConnectionService;
28import eu.siacs.conversations.utils.UIHelper;
29import eu.siacs.conversations.utils.XmppUri;
30import eu.siacs.conversations.xmpp.XmppConnection;
31import eu.siacs.conversations.xmpp.jingle.RtpCapability;
32import eu.siacs.conversations.xmpp.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 int activeDevicesWithRtpCapability() {
430 int i = 0;
431 for(Presence presence : getSelfContact().getPresences().getPresences().values()) {
432 if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
433 i++;
434 }
435 }
436 return i;
437 }
438
439 public String getPgpSignature() {
440 return getKey(KEY_PGP_SIGNATURE);
441 }
442
443 public boolean setPgpSignature(String signature) {
444 return setKey(KEY_PGP_SIGNATURE, signature);
445 }
446
447 public boolean unsetPgpSignature() {
448 synchronized (this.keys) {
449 return keys.remove(KEY_PGP_SIGNATURE) != null;
450 }
451 }
452
453 public long getPgpId() {
454 synchronized (this.keys) {
455 if (keys.has(KEY_PGP_ID)) {
456 try {
457 return keys.getLong(KEY_PGP_ID);
458 } catch (JSONException e) {
459 return 0;
460 }
461 } else {
462 return 0;
463 }
464 }
465 }
466
467 public boolean setPgpSignId(long pgpID) {
468 synchronized (this.keys) {
469 try {
470 if (pgpID == 0) {
471 keys.remove(KEY_PGP_ID);
472 } else {
473 keys.put(KEY_PGP_ID, pgpID);
474 }
475 } catch (JSONException e) {
476 return false;
477 }
478 return true;
479 }
480 }
481
482 public Roster getRoster() {
483 return this.roster;
484 }
485
486 public Collection<Bookmark> getBookmarks() {
487 return this.bookmarks.values();
488 }
489
490 public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
491 synchronized (this.bookmarks) {
492 this.bookmarks.clear();
493 this.bookmarks.putAll(bookmarks);
494 }
495 }
496
497 public void putBookmark(Bookmark bookmark) {
498 synchronized (this.bookmarks) {
499 this.bookmarks.put(bookmark.getJid(), bookmark);
500 }
501 }
502
503 public void removeBookmark(Bookmark bookmark) {
504 synchronized (this.bookmarks) {
505 this.bookmarks.remove(bookmark.getJid());
506 }
507 }
508
509 public void removeBookmark(Jid jid) {
510 synchronized (this.bookmarks) {
511 this.bookmarks.remove(jid);
512 }
513 }
514
515 public Set<Jid> getBookmarkedJids() {
516 synchronized (this.bookmarks) {
517 return new HashSet<>(this.bookmarks.keySet());
518 }
519 }
520
521 public Bookmark getBookmark(final Jid jid) {
522 synchronized (this.bookmarks) {
523 return this.bookmarks.get(jid.asBareJid());
524 }
525 }
526
527 public boolean setAvatar(final String filename) {
528 if (this.avatar != null && this.avatar.equals(filename)) {
529 return false;
530 } else {
531 this.avatar = filename;
532 return true;
533 }
534 }
535
536 public String getAvatar() {
537 return this.avatar;
538 }
539
540 public void activateGracePeriod(final long duration) {
541 if (duration > 0) {
542 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
543 }
544 }
545
546 public void deactivateGracePeriod() {
547 this.mEndGracePeriod = 0L;
548 }
549
550 public boolean inGracePeriod() {
551 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
552 }
553
554 public String getShareableUri() {
555 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
556 String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString();
557 if (fingerprints.size() > 0) {
558 return XmppUri.getFingerprintUri(uri, fingerprints, ';');
559 } else {
560 return uri;
561 }
562 }
563
564 public String getShareableLink() {
565 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
566 String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
567 if (fingerprints.size() > 0) {
568 return XmppUri.getFingerprintUri(uri, fingerprints, '&');
569 } else {
570 return uri;
571 }
572 }
573
574 private List<XmppUri.Fingerprint> getFingerprints() {
575 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
576 if (axolotlService == null) {
577 return fingerprints;
578 }
579 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
580 for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
581 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
582 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
583 }
584 }
585 return fingerprints;
586 }
587
588 public boolean isBlocked(final ListItem contact) {
589 final Jid jid = contact.getJid();
590 return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(Jid.ofDomain(jid.getDomain())));
591 }
592
593 public boolean isBlocked(final Jid jid) {
594 return jid != null && blocklist.contains(jid.asBareJid());
595 }
596
597 public Collection<Jid> getBlocklist() {
598 return this.blocklist;
599 }
600
601 public void clearBlocklist() {
602 getBlocklist().clear();
603 }
604
605 public boolean isOnlineAndConnected() {
606 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
607 }
608
609 @Override
610 public int getAvatarBackgroundColor() {
611 return UIHelper.getColorForName(jid.asBareJid().toString());
612 }
613
614 public enum State {
615 DISABLED(false, false),
616 OFFLINE(false),
617 CONNECTING(false),
618 ONLINE(false),
619 NO_INTERNET(false),
620 UNAUTHORIZED,
621 SERVER_NOT_FOUND,
622 REGISTRATION_SUCCESSFUL(false),
623 REGISTRATION_FAILED(true, false),
624 REGISTRATION_WEB(true, false),
625 REGISTRATION_CONFLICT(true, false),
626 REGISTRATION_NOT_SUPPORTED(true, false),
627 REGISTRATION_PLEASE_WAIT(true, false),
628 REGISTRATION_INVALID_TOKEN(true,false),
629 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
630 TLS_ERROR,
631 INCOMPATIBLE_SERVER,
632 TOR_NOT_AVAILABLE,
633 DOWNGRADE_ATTACK,
634 SESSION_FAILURE,
635 BIND_FAILURE,
636 HOST_UNKNOWN,
637 STREAM_ERROR,
638 STREAM_OPENING_ERROR,
639 POLICY_VIOLATION,
640 PAYMENT_REQUIRED,
641 MISSING_INTERNET_PERMISSION(false);
642
643 private final boolean isError;
644 private final boolean attemptReconnect;
645
646 State(final boolean isError) {
647 this(isError, true);
648 }
649
650 State(final boolean isError, final boolean reconnect) {
651 this.isError = isError;
652 this.attemptReconnect = reconnect;
653 }
654
655 State() {
656 this(true, true);
657 }
658
659 public boolean isError() {
660 return this.isError;
661 }
662
663 public boolean isAttemptReconnect() {
664 return this.attemptReconnect;
665 }
666
667 public int getReadableId() {
668 switch (this) {
669 case DISABLED:
670 return R.string.account_status_disabled;
671 case ONLINE:
672 return R.string.account_status_online;
673 case CONNECTING:
674 return R.string.account_status_connecting;
675 case OFFLINE:
676 return R.string.account_status_offline;
677 case UNAUTHORIZED:
678 return R.string.account_status_unauthorized;
679 case SERVER_NOT_FOUND:
680 return R.string.account_status_not_found;
681 case NO_INTERNET:
682 return R.string.account_status_no_internet;
683 case REGISTRATION_FAILED:
684 return R.string.account_status_regis_fail;
685 case REGISTRATION_WEB:
686 return R.string.account_status_regis_web;
687 case REGISTRATION_CONFLICT:
688 return R.string.account_status_regis_conflict;
689 case REGISTRATION_SUCCESSFUL:
690 return R.string.account_status_regis_success;
691 case REGISTRATION_NOT_SUPPORTED:
692 return R.string.account_status_regis_not_sup;
693 case REGISTRATION_INVALID_TOKEN:
694 return R.string.account_status_regis_invalid_token;
695 case TLS_ERROR:
696 return R.string.account_status_tls_error;
697 case INCOMPATIBLE_SERVER:
698 return R.string.account_status_incompatible_server;
699 case TOR_NOT_AVAILABLE:
700 return R.string.account_status_tor_unavailable;
701 case BIND_FAILURE:
702 return R.string.account_status_bind_failure;
703 case SESSION_FAILURE:
704 return R.string.session_failure;
705 case DOWNGRADE_ATTACK:
706 return R.string.sasl_downgrade;
707 case HOST_UNKNOWN:
708 return R.string.account_status_host_unknown;
709 case POLICY_VIOLATION:
710 return R.string.account_status_policy_violation;
711 case REGISTRATION_PLEASE_WAIT:
712 return R.string.registration_please_wait;
713 case REGISTRATION_PASSWORD_TOO_WEAK:
714 return R.string.registration_password_too_weak;
715 case STREAM_ERROR:
716 return R.string.account_status_stream_error;
717 case STREAM_OPENING_ERROR:
718 return R.string.account_status_stream_opening_error;
719 case PAYMENT_REQUIRED:
720 return R.string.payment_required;
721 case MISSING_INTERNET_PERMISSION:
722 return R.string.missing_internet_permission;
723 default:
724 return R.string.account_status_unknown;
725 }
726 }
727 }
728}