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 synchronized (this.bookmarks) {
492 return new HashSet<>(this.bookmarks.values());
493 }
494 }
495
496 public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
497 synchronized (this.bookmarks) {
498 this.bookmarks.clear();
499 this.bookmarks.putAll(bookmarks);
500 }
501 }
502
503 public void putBookmark(Bookmark bookmark) {
504 synchronized (this.bookmarks) {
505 this.bookmarks.put(bookmark.getJid(), bookmark);
506 }
507 }
508
509 public void removeBookmark(Bookmark bookmark) {
510 synchronized (this.bookmarks) {
511 this.bookmarks.remove(bookmark.getJid());
512 }
513 }
514
515 public void removeBookmark(Jid jid) {
516 synchronized (this.bookmarks) {
517 this.bookmarks.remove(jid);
518 }
519 }
520
521 public Set<Jid> getBookmarkedJids() {
522 synchronized (this.bookmarks) {
523 return new HashSet<>(this.bookmarks.keySet());
524 }
525 }
526
527 public Bookmark getBookmark(final Jid jid) {
528 synchronized (this.bookmarks) {
529 return this.bookmarks.get(jid.asBareJid());
530 }
531 }
532
533 public boolean setAvatar(final String filename) {
534 if (this.avatar != null && this.avatar.equals(filename)) {
535 return false;
536 } else {
537 this.avatar = filename;
538 return true;
539 }
540 }
541
542 public String getAvatar() {
543 return this.avatar;
544 }
545
546 public void activateGracePeriod(final long duration) {
547 if (duration > 0) {
548 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
549 }
550 }
551
552 public void deactivateGracePeriod() {
553 this.mEndGracePeriod = 0L;
554 }
555
556 public boolean inGracePeriod() {
557 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
558 }
559
560 public String getShareableUri() {
561 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
562 String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString();
563 if (fingerprints.size() > 0) {
564 return XmppUri.getFingerprintUri(uri, fingerprints, ';');
565 } else {
566 return uri;
567 }
568 }
569
570 public String getShareableLink() {
571 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
572 String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
573 if (fingerprints.size() > 0) {
574 return XmppUri.getFingerprintUri(uri, fingerprints, '&');
575 } else {
576 return uri;
577 }
578 }
579
580 private List<XmppUri.Fingerprint> getFingerprints() {
581 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
582 if (axolotlService == null) {
583 return fingerprints;
584 }
585 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
586 for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
587 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
588 fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
589 }
590 }
591 return fingerprints;
592 }
593
594 public boolean isBlocked(final ListItem contact) {
595 final Jid jid = contact.getJid();
596 return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
597 }
598
599 public boolean isBlocked(final Jid jid) {
600 return jid != null && blocklist.contains(jid.asBareJid());
601 }
602
603 public Collection<Jid> getBlocklist() {
604 return this.blocklist;
605 }
606
607 public void clearBlocklist() {
608 getBlocklist().clear();
609 }
610
611 public boolean isOnlineAndConnected() {
612 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
613 }
614
615 @Override
616 public int getAvatarBackgroundColor() {
617 return UIHelper.getColorForName(jid.asBareJid().toString());
618 }
619
620 @Override
621 public String getAvatarName() {
622 throw new IllegalStateException("This method should not be called");
623 }
624
625 public enum State {
626 DISABLED(false, false),
627 OFFLINE(false),
628 CONNECTING(false),
629 ONLINE(false),
630 NO_INTERNET(false),
631 UNAUTHORIZED,
632 TEMPORARY_AUTH_FAILURE,
633 SERVER_NOT_FOUND,
634 REGISTRATION_SUCCESSFUL(false),
635 REGISTRATION_FAILED(true, false),
636 REGISTRATION_WEB(true, false),
637 REGISTRATION_CONFLICT(true, false),
638 REGISTRATION_NOT_SUPPORTED(true, false),
639 REGISTRATION_PLEASE_WAIT(true, false),
640 REGISTRATION_INVALID_TOKEN(true,false),
641 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
642 TLS_ERROR,
643 TLS_ERROR_DOMAIN,
644 INCOMPATIBLE_SERVER,
645 TOR_NOT_AVAILABLE,
646 DOWNGRADE_ATTACK,
647 SESSION_FAILURE,
648 BIND_FAILURE,
649 HOST_UNKNOWN,
650 STREAM_ERROR,
651 STREAM_OPENING_ERROR,
652 POLICY_VIOLATION,
653 PAYMENT_REQUIRED,
654 MISSING_INTERNET_PERMISSION(false);
655
656 private final boolean isError;
657 private final boolean attemptReconnect;
658
659 State(final boolean isError) {
660 this(isError, true);
661 }
662
663 State(final boolean isError, final boolean reconnect) {
664 this.isError = isError;
665 this.attemptReconnect = reconnect;
666 }
667
668 State() {
669 this(true, true);
670 }
671
672 public boolean isError() {
673 return this.isError;
674 }
675
676 public boolean isAttemptReconnect() {
677 return this.attemptReconnect;
678 }
679
680 public int getReadableId() {
681 switch (this) {
682 case DISABLED:
683 return R.string.account_status_disabled;
684 case ONLINE:
685 return R.string.account_status_online;
686 case CONNECTING:
687 return R.string.account_status_connecting;
688 case OFFLINE:
689 return R.string.account_status_offline;
690 case UNAUTHORIZED:
691 return R.string.account_status_unauthorized;
692 case SERVER_NOT_FOUND:
693 return R.string.account_status_not_found;
694 case NO_INTERNET:
695 return R.string.account_status_no_internet;
696 case REGISTRATION_FAILED:
697 return R.string.account_status_regis_fail;
698 case REGISTRATION_WEB:
699 return R.string.account_status_regis_web;
700 case REGISTRATION_CONFLICT:
701 return R.string.account_status_regis_conflict;
702 case REGISTRATION_SUCCESSFUL:
703 return R.string.account_status_regis_success;
704 case REGISTRATION_NOT_SUPPORTED:
705 return R.string.account_status_regis_not_sup;
706 case REGISTRATION_INVALID_TOKEN:
707 return R.string.account_status_regis_invalid_token;
708 case TLS_ERROR:
709 return R.string.account_status_tls_error;
710 case TLS_ERROR_DOMAIN:
711 return R.string.account_status_tls_error_domain;
712 case INCOMPATIBLE_SERVER:
713 return R.string.account_status_incompatible_server;
714 case TOR_NOT_AVAILABLE:
715 return R.string.account_status_tor_unavailable;
716 case BIND_FAILURE:
717 return R.string.account_status_bind_failure;
718 case SESSION_FAILURE:
719 return R.string.session_failure;
720 case DOWNGRADE_ATTACK:
721 return R.string.sasl_downgrade;
722 case HOST_UNKNOWN:
723 return R.string.account_status_host_unknown;
724 case POLICY_VIOLATION:
725 return R.string.account_status_policy_violation;
726 case REGISTRATION_PLEASE_WAIT:
727 return R.string.registration_please_wait;
728 case REGISTRATION_PASSWORD_TOO_WEAK:
729 return R.string.registration_password_too_weak;
730 case STREAM_ERROR:
731 return R.string.account_status_stream_error;
732 case STREAM_OPENING_ERROR:
733 return R.string.account_status_stream_opening_error;
734 case PAYMENT_REQUIRED:
735 return R.string.payment_required;
736 case MISSING_INTERNET_PERMISSION:
737 return R.string.missing_internet_permission;
738 case TEMPORARY_AUTH_FAILURE:
739 return R.string.account_status_temporary_auth_failure;
740 default:
741 return R.string.account_status_unknown;
742 }
743 }
744 }
745}