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