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