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 SERVER_NOT_FOUND,
631 REGISTRATION_SUCCESSFUL(false),
632 REGISTRATION_FAILED(true, false),
633 REGISTRATION_WEB(true, false),
634 REGISTRATION_CONFLICT(true, false),
635 REGISTRATION_NOT_SUPPORTED(true, false),
636 REGISTRATION_PLEASE_WAIT(true, false),
637 REGISTRATION_INVALID_TOKEN(true,false),
638 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
639 TLS_ERROR,
640 TLS_ERROR_DOMAIN,
641 INCOMPATIBLE_SERVER,
642 TOR_NOT_AVAILABLE,
643 DOWNGRADE_ATTACK,
644 SESSION_FAILURE,
645 BIND_FAILURE,
646 HOST_UNKNOWN,
647 STREAM_ERROR,
648 STREAM_OPENING_ERROR,
649 POLICY_VIOLATION,
650 PAYMENT_REQUIRED,
651 MISSING_INTERNET_PERMISSION(false);
652
653 private final boolean isError;
654 private final boolean attemptReconnect;
655
656 State(final boolean isError) {
657 this(isError, true);
658 }
659
660 State(final boolean isError, final boolean reconnect) {
661 this.isError = isError;
662 this.attemptReconnect = reconnect;
663 }
664
665 State() {
666 this(true, true);
667 }
668
669 public boolean isError() {
670 return this.isError;
671 }
672
673 public boolean isAttemptReconnect() {
674 return this.attemptReconnect;
675 }
676
677 public int getReadableId() {
678 switch (this) {
679 case DISABLED:
680 return R.string.account_status_disabled;
681 case ONLINE:
682 return R.string.account_status_online;
683 case CONNECTING:
684 return R.string.account_status_connecting;
685 case OFFLINE:
686 return R.string.account_status_offline;
687 case UNAUTHORIZED:
688 return R.string.account_status_unauthorized;
689 case SERVER_NOT_FOUND:
690 return R.string.account_status_not_found;
691 case NO_INTERNET:
692 return R.string.account_status_no_internet;
693 case REGISTRATION_FAILED:
694 return R.string.account_status_regis_fail;
695 case REGISTRATION_WEB:
696 return R.string.account_status_regis_web;
697 case REGISTRATION_CONFLICT:
698 return R.string.account_status_regis_conflict;
699 case REGISTRATION_SUCCESSFUL:
700 return R.string.account_status_regis_success;
701 case REGISTRATION_NOT_SUPPORTED:
702 return R.string.account_status_regis_not_sup;
703 case REGISTRATION_INVALID_TOKEN:
704 return R.string.account_status_regis_invalid_token;
705 case TLS_ERROR:
706 return R.string.account_status_tls_error;
707 case TLS_ERROR_DOMAIN:
708 return R.string.account_status_tls_error_domain;
709 case INCOMPATIBLE_SERVER:
710 return R.string.account_status_incompatible_server;
711 case TOR_NOT_AVAILABLE:
712 return R.string.account_status_tor_unavailable;
713 case BIND_FAILURE:
714 return R.string.account_status_bind_failure;
715 case SESSION_FAILURE:
716 return R.string.session_failure;
717 case DOWNGRADE_ATTACK:
718 return R.string.sasl_downgrade;
719 case HOST_UNKNOWN:
720 return R.string.account_status_host_unknown;
721 case POLICY_VIOLATION:
722 return R.string.account_status_policy_violation;
723 case REGISTRATION_PLEASE_WAIT:
724 return R.string.registration_please_wait;
725 case REGISTRATION_PASSWORD_TOO_WEAK:
726 return R.string.registration_password_too_weak;
727 case STREAM_ERROR:
728 return R.string.account_status_stream_error;
729 case STREAM_OPENING_ERROR:
730 return R.string.account_status_stream_opening_error;
731 case PAYMENT_REQUIRED:
732 return R.string.payment_required;
733 case MISSING_INTERNET_PERMISSION:
734 return R.string.missing_internet_permission;
735 default:
736 return R.string.account_status_unknown;
737 }
738 }
739 }
740}