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