1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.os.SystemClock;
6import android.util.Log;
7
8import com.google.common.base.Strings;
9
10import org.json.JSONException;
11import org.json.JSONObject;
12
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.HashMap;
16import java.util.HashSet;
17import java.util.List;
18import java.util.Map;
19import java.util.Set;
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.Jid;
32import eu.siacs.conversations.xmpp.XmppConnection;
33import eu.siacs.conversations.xmpp.jingle.RtpCapability;
34
35public class Account extends AbstractEntity implements AvatarService.Avatarable {
36
37 public static final String TABLENAME = "accounts";
38
39 public static final String USERNAME = "username";
40 public static final String SERVER = "server";
41 public static final String PASSWORD = "password";
42 public static final String OPTIONS = "options";
43 public static final String ROSTERVERSION = "rosterversion";
44 public static final String KEYS = "keys";
45 public static final String AVATAR = "avatar";
46 public static final String DISPLAY_NAME = "display_name";
47 public static final String HOSTNAME = "hostname";
48 public static final String PORT = "port";
49 public static final String STATUS = "status";
50 public static final String STATUS_MESSAGE = "status_message";
51 public static final String RESOURCE = "resource";
52
53 public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
54 public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
55
56 public static final int OPTION_USETLS = 0;
57 public static final int OPTION_DISABLED = 1;
58 public static final int OPTION_REGISTER = 2;
59 public static final int OPTION_USECOMPRESSION = 3;
60 public static final int OPTION_MAGIC_CREATE = 4;
61 public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
62 public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
63 public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
64 public static final int OPTION_UNVERIFIED = 8;
65 public static final int OPTION_FIXED_USERNAME = 9;
66 private static final String KEY_PGP_SIGNATURE = "pgp_signature";
67 private static final String KEY_PGP_ID = "pgp_id";
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);
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 Jid getDomain() {
236 return jid.getDomain();
237 }
238
239 public String getServer() {
240 return jid.getDomain().toEscapedString();
241 }
242
243 public String getPassword() {
244 return password;
245 }
246
247 public void setPassword(final String password) {
248 this.password = password;
249 }
250
251 public String getHostname() {
252 return Strings.nullToEmpty(this.hostname);
253 }
254
255 public void setHostname(String hostname) {
256 this.hostname = hostname;
257 }
258
259 public boolean isOnion() {
260 final String server = getServer();
261 return server != null && server.endsWith(".onion");
262 }
263
264 public int getPort() {
265 return this.port;
266 }
267
268 public void setPort(int port) {
269 this.port = port;
270 }
271
272 public State getStatus() {
273 if (isOptionSet(OPTION_DISABLED)) {
274 return State.DISABLED;
275 } else {
276 return this.status;
277 }
278 }
279
280 public State getLastErrorStatus() {
281 return this.lastErrorStatus;
282 }
283
284 public void setStatus(final State status) {
285 this.status = status;
286 if (status.isError || status == State.ONLINE) {
287 this.lastErrorStatus = status;
288 }
289 }
290
291 public State getTrueStatus() {
292 return this.status;
293 }
294
295 public boolean errorStatus() {
296 return getStatus().isError();
297 }
298
299 public boolean hasErrorStatus() {
300 return getXmppConnection() != null
301 && (getStatus().isError() || getStatus() == State.CONNECTING)
302 && getXmppConnection().getAttempt() >= 3;
303 }
304
305 public Presence.Status getPresenceStatus() {
306 return this.presenceStatus;
307 }
308
309 public void setPresenceStatus(Presence.Status status) {
310 this.presenceStatus = status;
311 }
312
313 public String getPresenceStatusMessage() {
314 return this.presenceStatusMessage;
315 }
316
317 public void setPresenceStatusMessage(String message) {
318 this.presenceStatusMessage = message;
319 }
320
321 public String getResource() {
322 return jid.getResource();
323 }
324
325 public void setResource(final String resource) {
326 this.jid = this.jid.withResource(resource);
327 }
328
329 public Jid getJid() {
330 return jid;
331 }
332
333 public JSONObject getKeys() {
334 return keys;
335 }
336
337 public String getKey(final String name) {
338 synchronized (this.keys) {
339 return this.keys.optString(name, null);
340 }
341 }
342
343 public int getKeyAsInt(final String name, int defaultValue) {
344 String key = getKey(name);
345 try {
346 return key == null ? defaultValue : Integer.parseInt(key);
347 } catch (NumberFormatException e) {
348 return defaultValue;
349 }
350 }
351
352 public boolean setKey(final String keyName, final String keyValue) {
353 synchronized (this.keys) {
354 try {
355 this.keys.put(keyName, keyValue);
356 return true;
357 } catch (final JSONException e) {
358 return false;
359 }
360 }
361 }
362
363 public boolean setPrivateKeyAlias(String alias) {
364 return setKey("private_key_alias", alias);
365 }
366
367 public String getPrivateKeyAlias() {
368 return getKey("private_key_alias");
369 }
370
371 @Override
372 public ContentValues getContentValues() {
373 final ContentValues values = new ContentValues();
374 values.put(UUID, uuid);
375 values.put(USERNAME, jid.getLocal());
376 values.put(SERVER, jid.getDomain().toEscapedString());
377 values.put(PASSWORD, password);
378 values.put(OPTIONS, options);
379 synchronized (this.keys) {
380 values.put(KEYS, this.keys.toString());
381 }
382 values.put(ROSTERVERSION, rosterVersion);
383 values.put(AVATAR, avatar);
384 values.put(DISPLAY_NAME, displayName);
385 values.put(HOSTNAME, hostname);
386 values.put(PORT, port);
387 values.put(STATUS, presenceStatus.toShowString());
388 values.put(STATUS_MESSAGE, presenceStatusMessage);
389 values.put(RESOURCE, jid.getResource());
390 return values;
391 }
392
393 public AxolotlService getAxolotlService() {
394 return axolotlService;
395 }
396
397 public void initAccountServices(final XmppConnectionService context) {
398 this.axolotlService = new AxolotlService(this, context);
399 this.pgpDecryptionService = new PgpDecryptionService(context);
400 if (xmppConnection != null) {
401 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
402 }
403 }
404
405 public PgpDecryptionService getPgpDecryptionService() {
406 return this.pgpDecryptionService;
407 }
408
409 public XmppConnection getXmppConnection() {
410 return this.xmppConnection;
411 }
412
413 public void setXmppConnection(final XmppConnection connection) {
414 this.xmppConnection = connection;
415 }
416
417 public String getRosterVersion() {
418 if (this.rosterVersion == null) {
419 return "";
420 } else {
421 return this.rosterVersion;
422 }
423 }
424
425 public void setRosterVersion(final String version) {
426 this.rosterVersion = version;
427 }
428
429 public int countPresences() {
430 return this.getSelfContact().getPresences().size();
431 }
432
433 public int activeDevicesWithRtpCapability() {
434 int i = 0;
435 for(Presence presence : getSelfContact().getPresences().getPresences()) {
436 if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
437 i++;
438 }
439 }
440 return i;
441 }
442
443 public String getPgpSignature() {
444 return getKey(KEY_PGP_SIGNATURE);
445 }
446
447 public boolean setPgpSignature(String signature) {
448 return setKey(KEY_PGP_SIGNATURE, signature);
449 }
450
451 public boolean unsetPgpSignature() {
452 synchronized (this.keys) {
453 return keys.remove(KEY_PGP_SIGNATURE) != null;
454 }
455 }
456
457 public long getPgpId() {
458 synchronized (this.keys) {
459 if (keys.has(KEY_PGP_ID)) {
460 try {
461 return keys.getLong(KEY_PGP_ID);
462 } catch (JSONException e) {
463 return 0;
464 }
465 } else {
466 return 0;
467 }
468 }
469 }
470
471 public boolean setPgpSignId(long pgpID) {
472 synchronized (this.keys) {
473 try {
474 if (pgpID == 0) {
475 keys.remove(KEY_PGP_ID);
476 } else {
477 keys.put(KEY_PGP_ID, pgpID);
478 }
479 } catch (JSONException e) {
480 return false;
481 }
482 return true;
483 }
484 }
485
486 public Roster getRoster() {
487 return this.roster;
488 }
489
490 public Collection<Bookmark> getBookmarks() {
491 return this.bookmarks.values();
492 }
493
494 public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
495 synchronized (this.bookmarks) {
496 this.bookmarks.clear();
497 this.bookmarks.putAll(bookmarks);
498 }
499 }
500
501 public void putBookmark(Bookmark bookmark) {
502 synchronized (this.bookmarks) {
503 this.bookmarks.put(bookmark.getJid(), bookmark);
504 }
505 }
506
507 public void removeBookmark(Bookmark bookmark) {
508 synchronized (this.bookmarks) {
509 this.bookmarks.remove(bookmark.getJid());
510 }
511 }
512
513 public void removeBookmark(Jid jid) {
514 synchronized (this.bookmarks) {
515 this.bookmarks.remove(jid);
516 }
517 }
518
519 public Set<Jid> getBookmarkedJids() {
520 synchronized (this.bookmarks) {
521 return new HashSet<>(this.bookmarks.keySet());
522 }
523 }
524
525 public Bookmark getBookmark(final Jid jid) {
526 synchronized (this.bookmarks) {
527 return this.bookmarks.get(jid.asBareJid());
528 }
529 }
530
531 public boolean setAvatar(final String filename) {
532 if (this.avatar != null && this.avatar.equals(filename)) {
533 return false;
534 } else {
535 this.avatar = filename;
536 return true;
537 }
538 }
539
540 public String getAvatar() {
541 return this.avatar;
542 }
543
544 public void activateGracePeriod(final long duration) {
545 if (duration > 0) {
546 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
547 }
548 }
549
550 public void deactivateGracePeriod() {
551 this.mEndGracePeriod = 0L;
552 }
553
554 public boolean inGracePeriod() {
555 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
556 }
557
558 public String getShareableUri() {
559 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
560 String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString();
561 if (fingerprints.size() > 0) {
562 return XmppUri.getFingerprintUri(uri, fingerprints, ';');
563 } else {
564 return uri;
565 }
566 }
567
568 public String getShareableLink() {
569 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
570 String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
571 if (fingerprints.size() > 0) {
572 return XmppUri.getFingerprintUri(uri, fingerprints, '&');
573 } else {
574 return uri;
575 }
576 }
577
578 private List<XmppUri.Fingerprint> getFingerprints() {
579 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
580 if (axolotlService == null) {
581 return fingerprints;
582 }
583 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
584 for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
585 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
586 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
587 }
588 }
589 return fingerprints;
590 }
591
592 public boolean isBlocked(final ListItem contact) {
593 final Jid jid = contact.getJid();
594 return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
595 }
596
597 public boolean isBlocked(final Jid jid) {
598 return jid != null && blocklist.contains(jid.asBareJid());
599 }
600
601 public Collection<Jid> getBlocklist() {
602 return this.blocklist;
603 }
604
605 public void clearBlocklist() {
606 getBlocklist().clear();
607 }
608
609 public boolean isOnlineAndConnected() {
610 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
611 }
612
613 @Override
614 public int getAvatarBackgroundColor() {
615 return UIHelper.getColorForName(jid.asBareJid().toString());
616 }
617
618 @Override
619 public String getAvatarName() {
620 throw new IllegalStateException("This method should not be called");
621 }
622
623 public enum State {
624 DISABLED(false, false),
625 OFFLINE(false),
626 CONNECTING(false),
627 ONLINE(false),
628 NO_INTERNET(false),
629 UNAUTHORIZED,
630 TEMPORARY_AUTH_FAILURE,
631 SERVER_NOT_FOUND,
632 REGISTRATION_SUCCESSFUL(false),
633 REGISTRATION_FAILED(true, false),
634 REGISTRATION_WEB(true, false),
635 REGISTRATION_CONFLICT(true, false),
636 REGISTRATION_NOT_SUPPORTED(true, false),
637 REGISTRATION_PLEASE_WAIT(true, false),
638 REGISTRATION_INVALID_TOKEN(true,false),
639 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
640 TLS_ERROR,
641 TLS_ERROR_DOMAIN,
642 INCOMPATIBLE_SERVER,
643 TOR_NOT_AVAILABLE,
644 DOWNGRADE_ATTACK,
645 SESSION_FAILURE,
646 BIND_FAILURE,
647 HOST_UNKNOWN,
648 STREAM_ERROR,
649 STREAM_OPENING_ERROR,
650 POLICY_VIOLATION,
651 PAYMENT_REQUIRED,
652 MISSING_INTERNET_PERMISSION(false);
653
654 private final boolean isError;
655 private final boolean attemptReconnect;
656
657 State(final boolean isError) {
658 this(isError, true);
659 }
660
661 State(final boolean isError, final boolean reconnect) {
662 this.isError = isError;
663 this.attemptReconnect = reconnect;
664 }
665
666 State() {
667 this(true, true);
668 }
669
670 public boolean isError() {
671 return this.isError;
672 }
673
674 public boolean isAttemptReconnect() {
675 return this.attemptReconnect;
676 }
677
678 public int getReadableId() {
679 switch (this) {
680 case DISABLED:
681 return R.string.account_status_disabled;
682 case ONLINE:
683 return R.string.account_status_online;
684 case CONNECTING:
685 return R.string.account_status_connecting;
686 case OFFLINE:
687 return R.string.account_status_offline;
688 case UNAUTHORIZED:
689 return R.string.account_status_unauthorized;
690 case SERVER_NOT_FOUND:
691 return R.string.account_status_not_found;
692 case NO_INTERNET:
693 return R.string.account_status_no_internet;
694 case REGISTRATION_FAILED:
695 return R.string.account_status_regis_fail;
696 case REGISTRATION_WEB:
697 return R.string.account_status_regis_web;
698 case REGISTRATION_CONFLICT:
699 return R.string.account_status_regis_conflict;
700 case REGISTRATION_SUCCESSFUL:
701 return R.string.account_status_regis_success;
702 case REGISTRATION_NOT_SUPPORTED:
703 return R.string.account_status_regis_not_sup;
704 case REGISTRATION_INVALID_TOKEN:
705 return R.string.account_status_regis_invalid_token;
706 case TLS_ERROR:
707 return R.string.account_status_tls_error;
708 case TLS_ERROR_DOMAIN:
709 return R.string.account_status_tls_error_domain;
710 case INCOMPATIBLE_SERVER:
711 return R.string.account_status_incompatible_server;
712 case TOR_NOT_AVAILABLE:
713 return R.string.account_status_tor_unavailable;
714 case BIND_FAILURE:
715 return R.string.account_status_bind_failure;
716 case SESSION_FAILURE:
717 return R.string.session_failure;
718 case DOWNGRADE_ATTACK:
719 return R.string.sasl_downgrade;
720 case HOST_UNKNOWN:
721 return R.string.account_status_host_unknown;
722 case POLICY_VIOLATION:
723 return R.string.account_status_policy_violation;
724 case REGISTRATION_PLEASE_WAIT:
725 return R.string.registration_please_wait;
726 case REGISTRATION_PASSWORD_TOO_WEAK:
727 return R.string.registration_password_too_weak;
728 case STREAM_ERROR:
729 return R.string.account_status_stream_error;
730 case STREAM_OPENING_ERROR:
731 return R.string.account_status_stream_opening_error;
732 case PAYMENT_REQUIRED:
733 return R.string.payment_required;
734 case MISSING_INTERNET_PERMISSION:
735 return R.string.missing_internet_permission;
736 case TEMPORARY_AUTH_FAILURE:
737 return R.string.account_status_temporary_auth_failure;
738 default:
739 return R.string.account_status_unknown;
740 }
741 }
742 }
743}