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