1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.net.Uri;
6import android.os.SystemClock;
7import android.util.Log;
8
9import androidx.core.graphics.ColorUtils;
10import androidx.annotation.NonNull;
11
12import com.google.common.base.Strings;
13import com.google.common.collect.ImmutableList;
14import com.google.common.collect.HashMultimap;
15
16import org.json.JSONException;
17import org.json.JSONObject;
18
19import java.util.ArrayList;
20import java.util.Collection;
21import java.util.HashMap;
22import java.util.HashSet;
23import java.util.List;
24import java.util.Map;
25import java.util.Set;
26import java.util.concurrent.CopyOnWriteArraySet;
27
28import eu.siacs.conversations.Config;
29import eu.siacs.conversations.R;
30import eu.siacs.conversations.crypto.PgpDecryptionService;
31import eu.siacs.conversations.crypto.axolotl.AxolotlService;
32import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
33import eu.siacs.conversations.crypto.sasl.ChannelBinding;
34import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
35import eu.siacs.conversations.crypto.sasl.HashedToken;
36import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
37import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
38import eu.siacs.conversations.crypto.sasl.SaslMechanism;
39import eu.siacs.conversations.http.ServiceOutageStatus;
40import eu.siacs.conversations.services.AvatarService;
41import eu.siacs.conversations.utils.Resolver;
42import eu.siacs.conversations.utils.UIHelper;
43import eu.siacs.conversations.utils.XmppUri;
44import eu.siacs.conversations.xml.Element;
45import eu.siacs.conversations.xmpp.Jid;
46import eu.siacs.conversations.xmpp.XmppConnection;
47import eu.siacs.conversations.xmpp.jingle.RtpCapability;
48import eu.siacs.conversations.xmpp.manager.BlockingManager;
49import eu.siacs.conversations.xmpp.manager.DiscoManager;
50import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
51import eu.siacs.conversations.xmpp.manager.RosterManager;
52import java.util.ArrayList;
53import java.util.Collection;
54import java.util.Collections;
55import java.util.HashMap;
56import java.util.HashSet;
57import java.util.List;
58import java.util.Map;
59import java.util.Set;
60import org.json.JSONException;
61import org.json.JSONObject;
62
63public class Account extends AbstractEntity implements AvatarService.Avatarable {
64
65 public static final String TABLENAME = "accounts";
66
67 public static final String USERNAME = "username";
68 public static final String SERVER = "server";
69 public static final String PASSWORD = "password";
70 public static final String OPTIONS = "options";
71 public static final String ROSTERVERSION = "rosterversion";
72 public static final String KEYS = "keys";
73 public static final String AVATAR = "avatar";
74 public static final String DISPLAY_NAME = "display_name";
75 public static final String HOSTNAME = "hostname";
76 public static final String PORT = "port";
77 public static final String STATUS = "status";
78 public static final String STATUS_MESSAGE = "status_message";
79 public static final String RESOURCE = "resource";
80 public static final String PINNED_MECHANISM = "pinned_mechanism";
81 public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding";
82 public static final String FAST_MECHANISM = "fast_mechanism";
83 public static final String FAST_TOKEN = "fast_token";
84
85 public static final int OPTION_DISABLED = 1;
86 public static final int OPTION_REGISTER = 2;
87 public static final int OPTION_MAGIC_CREATE = 4;
88 public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
89 public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
90 public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
91 public static final int OPTION_UNVERIFIED = 8;
92 public static final int OPTION_FIXED_USERNAME = 9;
93 public static final int OPTION_QUICKSTART_AVAILABLE = 10;
94 public static final int OPTION_SOFT_DISABLED = 11;
95
96 private static final String KEY_PGP_SIGNATURE = "pgp_signature";
97 private static final String KEY_PGP_ID = "pgp_id";
98 private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
99 public static final String KEY_SOS_URL = "sos_url";
100 public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
101 protected final JSONObject keys;
102 protected Jid jid;
103 protected String password;
104 protected int options = 0;
105 protected State status = State.OFFLINE;
106 private State lastErrorStatus = State.OFFLINE;
107 protected String resource;
108 protected String avatar;
109 protected String hostname = null;
110 protected int port = 5222;
111 protected boolean online = false;
112 private String rosterVersion;
113 private String displayName = null;
114 private XmppConnection xmppConnection = null;
115 private long mEndGracePeriod = 0L;
116 private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
117 private boolean bookmarksLoaded = false;
118 private im.conversations.android.xmpp.model.stanza.Presence.Availability presenceStatus;
119 private String presenceStatusMessage;
120 private String pinnedMechanism;
121 private String pinnedChannelBinding;
122 private String fastMechanism;
123 private String fastToken;
124 private Integer color = null;
125 private final HashMultimap<String, Contact> gateways = HashMultimap.create();
126 private Element mamPrefs = null;
127 private ServiceOutageStatus serviceOutageStatus;
128
129 public Account(final Jid jid, final String password) {
130 this(
131 java.util.UUID.randomUUID().toString(),
132 jid,
133 password,
134 0,
135 null,
136 "",
137 null,
138 null,
139 null,
140 Resolver.XMPP_PORT_STARTTLS,
141 im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
142 null,
143 null,
144 null,
145 null,
146 null);
147 }
148
149 private Account(
150 final String uuid,
151 final Jid jid,
152 final String password,
153 final int options,
154 final String rosterVersion,
155 final String keys,
156 final String avatar,
157 String displayName,
158 String hostname,
159 int port,
160 final im.conversations.android.xmpp.model.stanza.Presence.Availability status,
161 String statusMessage,
162 final String pinnedMechanism,
163 final String pinnedChannelBinding,
164 final String fastMechanism,
165 final String fastToken) {
166 this.uuid = uuid;
167 this.jid = jid;
168 this.password = password;
169 this.options = options;
170 this.rosterVersion = rosterVersion;
171 this.keys = parseKeys(keys);
172 this.avatar = avatar;
173 this.displayName = displayName;
174 this.hostname = hostname;
175 this.port = port;
176 this.presenceStatus = status;
177 this.presenceStatusMessage = statusMessage;
178 this.pinnedMechanism = pinnedMechanism;
179 this.pinnedChannelBinding = pinnedChannelBinding;
180 this.fastMechanism = fastMechanism;
181 this.fastToken = fastToken;
182 }
183
184 public static JSONObject parseKeys(final String keys) {
185 if (Strings.isNullOrEmpty(keys)) {
186 return new JSONObject();
187 }
188 try {
189 return new JSONObject(keys);
190 } catch (final JSONException e) {
191 return new JSONObject();
192 }
193 }
194
195 public static Account fromCursor(final Cursor cursor) {
196 final Jid jid;
197 try {
198 final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE));
199 jid =
200 Jid.of(
201 cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)),
202 cursor.getString(cursor.getColumnIndexOrThrow(SERVER)),
203 resource == null || resource.trim().isEmpty() ? null : resource);
204 } catch (final IllegalArgumentException e) {
205 Log.d(
206 Config.LOGTAG,
207 cursor.getString(cursor.getColumnIndexOrThrow(USERNAME))
208 + "@"
209 + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)));
210 throw new AssertionError(e);
211 }
212 return new Account(
213 cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
214 jid,
215 cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)),
216 cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)),
217 cursor.getString(cursor.getColumnIndexOrThrow(ROSTERVERSION)),
218 cursor.getString(cursor.getColumnIndexOrThrow(KEYS)),
219 cursor.getString(cursor.getColumnIndexOrThrow(AVATAR)),
220 cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)),
221 cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)),
222 cursor.getInt(cursor.getColumnIndexOrThrow(PORT)),
223 im.conversations.android.xmpp.model.stanza.Presence.Availability.valueOfShown(
224 cursor.getString(cursor.getColumnIndexOrThrow(STATUS))),
225 cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)),
226 cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)),
227 cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)),
228 cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)),
229 cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN)));
230 }
231
232 public void setMamPrefs(Element prefs) {
233 mamPrefs = prefs;
234 }
235
236 public Element mamPrefs() {
237 return mamPrefs;
238 }
239
240 // TODO remove this method and call HttpUploadManager directly i
241 public boolean httpUploadAvailable(final long fileSize) {
242 return xmppConnection.getManager(HttpUploadManager.class).isAvailableForSize(fileSize);
243 }
244
245 public boolean httpUploadAvailable() {
246 return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE)
247 || xmppConnection.getManager(HttpUploadManager.class).isAvailableForSize(0);
248 }
249
250 public String getDisplayName() {
251 return displayName;
252 }
253
254 public void setDisplayName(String displayName) {
255 this.displayName = displayName;
256 }
257
258 public Contact getSelfContact() {
259 return getRoster().getContact(jid);
260 }
261
262 public boolean hasPendingPgpIntent(Conversation conversation) {
263 return getPgpDecryptionService().hasPendingIntent(conversation);
264 }
265
266 public boolean isPgpDecryptionServiceConnected() {
267 return getPgpDecryptionService().isConnected();
268 }
269
270 public void setColor(Integer color) {
271 this.color = color;
272 }
273
274 public int getColor(boolean dark) {
275 if (color != null) return color.intValue();
276
277 return ColorUtils.setAlphaComponent(
278 getAvatarBackgroundColor(),
279 dark ? 25 : 20
280 );
281 }
282
283 public Integer getColorToSave() {
284 return color;
285 }
286
287 public boolean setShowErrorNotification(boolean newValue) {
288 boolean oldValue = showErrorNotification();
289 setKey("show_error", Boolean.toString(newValue));
290 return newValue != oldValue;
291 }
292
293 public boolean showErrorNotification() {
294 String key = getKey("show_error");
295 return key == null || Boolean.parseBoolean(key);
296 }
297
298 public boolean isEnabled() {
299 return !isOptionSet(Account.OPTION_DISABLED);
300 }
301
302 public boolean isConnectionEnabled() {
303 return !isOptionSet(Account.OPTION_DISABLED) && !isOptionSet(Account.OPTION_SOFT_DISABLED);
304 }
305
306 public boolean isOptionSet(final int option) {
307 return ((options & (1 << option)) != 0);
308 }
309
310 public boolean setOption(final int option, final boolean value) {
311 if (value && (option == OPTION_DISABLED || option == OPTION_SOFT_DISABLED)) {
312 this.setStatus(State.OFFLINE);
313 }
314 final int before = this.options;
315 if (value) {
316 this.options |= 1 << option;
317 } else {
318 this.options &= ~(1 << option);
319 }
320 return before != this.options;
321 }
322
323 public String getUsername() {
324 return jid.getLocal();
325 }
326
327 public boolean setJid(final Jid next) {
328 final Jid previousFull = this.jid;
329 final Jid prev = this.jid != null ? this.jid.asBareJid() : null;
330 final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid()));
331 if (changed) {
332 final AxolotlService oldAxolotlService = xmppConnection.getAxolotlService();
333 // TODO check that changing JID and recreating the AxolotlService still works
334 if (oldAxolotlService != null) {
335 oldAxolotlService.destroy();
336 this.jid = next;
337 xmppConnection.setAxolotlService(oldAxolotlService.makeNew());
338 }
339 }
340 this.jid = next;
341 return next != null && !next.equals(previousFull);
342 }
343
344 public Jid getDomain() {
345 return jid.getDomain();
346 }
347
348 public String getServer() {
349 return jid.getDomain().toString();
350 }
351
352 public String getPassword() {
353 return password;
354 }
355
356 public void setPassword(final String password) {
357 this.password = password;
358 }
359
360 @NonNull
361 public String getHostname() {
362 return Strings.nullToEmpty(this.hostname);
363 }
364
365 public void setHostname(final String hostname) {
366 this.hostname = hostname;
367 }
368
369 public boolean isOnion() {
370 final String server = getServer();
371 return server != null && server.endsWith(".onion");
372 }
373
374 public boolean isDirectToOnion() {
375 final var hostname = Strings.nullToEmpty(this.hostname).trim();
376 return isOnion() && (hostname.isEmpty() || hostname.endsWith(".onion"));
377 }
378
379 public int getPort() {
380 return this.port;
381 }
382
383 public void setPort(int port) {
384 this.port = port;
385 }
386
387 public State getStatus() {
388 if (isOptionSet(OPTION_DISABLED)) {
389 return State.DISABLED;
390 } else if (isOptionSet(OPTION_SOFT_DISABLED)) {
391 return State.LOGGED_OUT;
392 } else {
393 return this.status;
394 }
395 }
396
397 public boolean unauthorized() {
398 return this.status == State.UNAUTHORIZED || this.lastErrorStatus == State.UNAUTHORIZED;
399 }
400
401 public State getLastErrorStatus() {
402 return this.lastErrorStatus;
403 }
404
405 public void setStatus(final State status) {
406 this.status = status;
407 if (status.isError || status == State.ONLINE) {
408 this.lastErrorStatus = status;
409 }
410 }
411
412 public void setPinnedMechanism(final SaslMechanism mechanism) {
413 this.pinnedMechanism = mechanism.getMechanism();
414 if (mechanism instanceof ChannelBindingMechanism) {
415 this.pinnedChannelBinding =
416 ((ChannelBindingMechanism) mechanism).getChannelBinding().toString();
417 } else {
418 this.pinnedChannelBinding = null;
419 }
420 }
421
422 public void setFastToken(final HashedToken.Mechanism mechanism, final String token) {
423 this.fastMechanism = mechanism.name();
424 this.fastToken = token;
425 }
426
427 public void resetFastToken() {
428 this.fastMechanism = null;
429 this.fastToken = null;
430 }
431
432 public void resetPinnedMechanism() {
433 this.pinnedMechanism = null;
434 this.pinnedChannelBinding = null;
435 setKey(Account.KEY_PINNED_MECHANISM, String.valueOf(-1));
436 }
437
438 public int getPinnedMechanismPriority() {
439 final int fallback = getKeyAsInt(KEY_PINNED_MECHANISM, -1);
440 if (Strings.isNullOrEmpty(this.pinnedMechanism)) {
441 return fallback;
442 }
443 final SaslMechanism saslMechanism = getPinnedMechanism();
444 if (saslMechanism == null) {
445 return fallback;
446 } else {
447 return saslMechanism.getPriority();
448 }
449 }
450
451 private SaslMechanism getPinnedMechanism() {
452 final String mechanism = Strings.nullToEmpty(this.pinnedMechanism);
453 final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding);
454 return new SaslMechanism.Factory(this).of(mechanism, channelBinding);
455 }
456
457 public HashedToken getFastMechanism() {
458 final HashedToken.Mechanism fastMechanism =
459 HashedToken.Mechanism.ofOrNull(this.fastMechanism);
460 final String token = this.fastToken;
461 if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
462 return null;
463 }
464 if (fastMechanism.hashFunction.equals("SHA-256")) {
465 return new HashedTokenSha256(this, fastMechanism.channelBinding);
466 } else if (fastMechanism.hashFunction.equals("SHA-512")) {
467 return new HashedTokenSha512(this, fastMechanism.channelBinding);
468 } else {
469 return null;
470 }
471 }
472
473 public SaslMechanism getQuickStartMechanism() {
474 final HashedToken hashedTokenMechanism = getFastMechanism();
475 if (hashedTokenMechanism != null) {
476 return hashedTokenMechanism;
477 }
478 return getPinnedMechanism();
479 }
480
481 public String getFastToken() {
482 return this.fastToken;
483 }
484
485 public State getTrueStatus() {
486 return this.status;
487 }
488
489 public boolean errorStatus() {
490 return getStatus().isError();
491 }
492
493 public boolean hasErrorStatus() {
494 return getXmppConnection() != null
495 && (getStatus().isError() || getStatus() == State.CONNECTING)
496 && getXmppConnection().getAttempt() >= 3;
497 }
498
499 public im.conversations.android.xmpp.model.stanza.Presence.Availability getPresenceStatus() {
500 return this.presenceStatus;
501 }
502
503 public void setPresenceStatus(
504 im.conversations.android.xmpp.model.stanza.Presence.Availability status) {
505 this.presenceStatus = status;
506 }
507
508 public String getPresenceStatusMessage() {
509 return this.presenceStatusMessage;
510 }
511
512 public void setPresenceStatusMessage(String message) {
513 this.presenceStatusMessage = message;
514 }
515
516 public String getResource() {
517 return jid.getResource();
518 }
519
520 public void setResource(final String resource) {
521 this.jid = this.jid.withResource(resource);
522 }
523
524 public Jid getJid() {
525 return jid;
526 }
527
528 public JSONObject getKeys() {
529 return keys;
530 }
531
532 public String getKey(final String name) {
533 synchronized (this.keys) {
534 return this.keys.optString(name, null);
535 }
536 }
537
538 public int getKeyAsInt(final String name, int defaultValue) {
539 String key = getKey(name);
540 try {
541 return key == null ? defaultValue : Integer.parseInt(key);
542 } catch (NumberFormatException e) {
543 return defaultValue;
544 }
545 }
546
547 public boolean setKey(final String keyName, final String keyValue) {
548 synchronized (this.keys) {
549 try {
550 this.keys.put(keyName, keyValue);
551 return true;
552 } catch (final JSONException e) {
553 return false;
554 }
555 }
556 }
557
558 public void setPrivateKeyAlias(final String alias) {
559 setKey("private_key_alias", alias);
560 }
561
562 public String getPrivateKeyAlias() {
563 return getKey("private_key_alias");
564 }
565
566 @Override
567 public ContentValues getContentValues() {
568 final ContentValues values = new ContentValues();
569 values.put(UUID, uuid);
570 values.put(USERNAME, jid.getLocal());
571 values.put(SERVER, jid.getDomain().toString());
572 values.put(PASSWORD, password);
573 values.put(OPTIONS, options);
574 synchronized (this.keys) {
575 values.put(KEYS, this.keys.toString());
576 }
577 values.put(ROSTERVERSION, rosterVersion);
578 values.put(AVATAR, avatar);
579 values.put(DISPLAY_NAME, displayName);
580 values.put(HOSTNAME, hostname);
581 values.put(PORT, port);
582 values.put(STATUS, presenceStatus.toShowString());
583 values.put(STATUS_MESSAGE, presenceStatusMessage);
584 values.put(RESOURCE, jid.getResource());
585 values.put(PINNED_MECHANISM, pinnedMechanism);
586 values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding);
587 values.put(FAST_MECHANISM, this.fastMechanism);
588 values.put(FAST_TOKEN, this.fastToken);
589 return values;
590 }
591
592 public AxolotlService getAxolotlService() {
593 return this.xmppConnection.getAxolotlService();
594 }
595
596 public PgpDecryptionService getPgpDecryptionService() {
597 return this.xmppConnection.getPgpDecryptionService();
598 }
599
600 public XmppConnection getXmppConnection() {
601 return this.xmppConnection;
602 }
603
604 public String getRosterVersion() {
605 return Strings.emptyToNull(this.rosterVersion);
606 }
607
608 public void setRosterVersion(final String version) {
609 this.rosterVersion = version;
610 }
611
612 public int countPresences() {
613 return this.getSelfContact().getPresences().size();
614 }
615
616 public int activeDevicesWithRtpCapability() {
617 final var connection = getXmppConnection();
618 if (connection == null) {
619 return 0;
620 }
621 int i = 0;
622 for (String resource : getSelfContact().getPresences().getPresencesMap().keySet()) {
623 final var jid =
624 Strings.isNullOrEmpty(resource)
625 ? getJid().asBareJid()
626 : getJid().withResource(resource);
627 if (RtpCapability.check(connection.getManager(DiscoManager.class).get(jid))
628 != RtpCapability.Capability.NONE) {
629 i++;
630 }
631 }
632 return i;
633 }
634
635 public String getPgpSignature() {
636 return getKey(KEY_PGP_SIGNATURE);
637 }
638
639 public boolean setPgpSignature(String signature) {
640 return setKey(KEY_PGP_SIGNATURE, signature);
641 }
642
643 public boolean unsetPgpSignature() {
644 synchronized (this.keys) {
645 return keys.remove(KEY_PGP_SIGNATURE) != null;
646 }
647 }
648
649 public long getPgpId() {
650 synchronized (this.keys) {
651 if (keys.has(KEY_PGP_ID)) {
652 try {
653 return keys.getLong(KEY_PGP_ID);
654 } catch (JSONException e) {
655 return 0;
656 }
657 } else {
658 return 0;
659 }
660 }
661 }
662
663 public boolean setPgpSignId(long pgpID) {
664 synchronized (this.keys) {
665 try {
666 if (pgpID == 0) {
667 keys.remove(KEY_PGP_ID);
668 } else {
669 keys.put(KEY_PGP_ID, pgpID);
670 }
671 } catch (JSONException e) {
672 return false;
673 }
674 return true;
675 }
676 }
677
678 public Roster getRoster() {
679 return xmppConnection.getManager(RosterManager.class);
680 }
681
682 public void refreshCapsFor(Contact contact) {
683 final var connection = getXmppConnection();
684 if (connection == null) return;
685
686 synchronized (gateways) {
687 for (final var k : new HashSet<>(gateways.keySet())) {
688 gateways.remove(k, contact);
689 }
690 for (final var jid : contact.getPresences().getFullJids()) {
691 final var disco = connection.getManager(DiscoManager.class).get(jid);
692 if (disco == null) continue;
693 for (final var identity : disco.getIdentities()) {
694 if ("gateway".equals(identity.getCategory())) {
695 gateways.put(identity.getType(), contact);
696 }
697 }
698 }
699 }
700 }
701
702 public Collection<Contact> getGateways(final String type) {
703 synchronized (gateways) {
704 return ImmutableList.copyOf(gateways.get(type));
705 }
706 }
707
708 public Collection<Bookmark> getBookmarks() {
709 synchronized (this.bookmarks) {
710 return ImmutableList.copyOf(this.bookmarks.values());
711 }
712 }
713
714 public boolean areBookmarksLoaded() {
715 // No way to tell if old PEP bookmarks are all loaded yet if they are empty
716 // because we don't manually fetch them...
717 if (!getXmppConnection().getFeatures().bookmarks2()) return true;
718
719 return bookmarksLoaded;
720 }
721
722 public void setBookmarks(final Map<Jid, Bookmark> bookmarks) {
723 synchronized (this.bookmarks) {
724 this.bookmarks.clear();
725 this.bookmarks.putAll(bookmarks);
726 this.bookmarksLoaded = true;
727 }
728 }
729
730 public void putBookmark(final Bookmark bookmark) {
731 synchronized (this.bookmarks) {
732 this.bookmarks.put(bookmark.getJid(), bookmark);
733 }
734 }
735
736 public void removeBookmark(Bookmark bookmark) {
737 synchronized (this.bookmarks) {
738 this.bookmarks.remove(bookmark.getJid());
739 }
740 }
741
742 public void removeBookmark(Jid jid) {
743 synchronized (this.bookmarks) {
744 this.bookmarks.remove(jid);
745 }
746 }
747
748 public Set<Jid> getBookmarkedJids() {
749 synchronized (this.bookmarks) {
750 return new HashSet<>(this.bookmarks.keySet());
751 }
752 }
753
754 public Bookmark getBookmark(final Jid jid) {
755 synchronized (this.bookmarks) {
756 return this.bookmarks.get(jid.asBareJid());
757 }
758 }
759
760 public boolean setAvatar(final String filename) {
761 if (this.avatar != null && this.avatar.equals(filename)) {
762 return false;
763 } else {
764 this.avatar = filename;
765 return true;
766 }
767 }
768
769 public String getAvatar() {
770 return this.avatar;
771 }
772
773 public void activateGracePeriod(final long duration) {
774 if (duration > 0) {
775 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
776 }
777 }
778
779 public void deactivateGracePeriod() {
780 this.mEndGracePeriod = 0L;
781 }
782
783 public boolean inGracePeriod() {
784 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
785 }
786
787 public String getShareableUri() {
788 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
789 final String uri = "xmpp:" + Uri.encode(this.getJid().asBareJid().toString(), "@/+");
790 if (fingerprints.isEmpty()) {
791 return uri;
792 } else {
793 return XmppUri.getFingerprintUri(uri, fingerprints, ';');
794 }
795 }
796
797 public String getShareableLink() {
798 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
799 String uri =
800 "https://conversations.im/i/"
801 + XmppUri.lameUrlEncode(this.getJid().asBareJid().toString());
802 if (fingerprints.isEmpty()) {
803 return uri;
804 } else {
805 return XmppUri.getFingerprintUri(uri, fingerprints, '&');
806 }
807 }
808
809 private List<XmppUri.Fingerprint> getFingerprints() {
810 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
811 final var axolotlService = getAxolotlService();
812 fingerprints.add(
813 new XmppUri.Fingerprint(
814 XmppUri.FingerprintType.OMEMO,
815 axolotlService.getOwnFingerprint().substring(2),
816 axolotlService.getOwnDeviceId()));
817 for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
818 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
819 fingerprints.add(
820 new XmppUri.Fingerprint(
821 XmppUri.FingerprintType.OMEMO,
822 session.getFingerprint().substring(2).replaceAll("\\s", ""),
823 session.getRemoteAddress().getDeviceId()));
824 }
825 }
826 return fingerprints;
827 }
828
829 public boolean isBlocked(final ListItem contact) {
830 final Jid jid = contact.getJid();
831 final var blocklist = getBlocklist();
832 return jid != null
833 && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
834 }
835
836 public boolean isBlocked(final Jid jid) {
837 final var blocklist = getBlocklist();
838 return jid != null && blocklist.contains(jid.asBareJid());
839 }
840
841 public Set<Jid> getBlocklist() {
842 final var connection = this.xmppConnection;
843 if (connection == null) {
844 return Collections.emptySet();
845 }
846 return connection.getManager(BlockingManager.class).getBlocklist();
847 }
848
849 public boolean isOnlineAndConnected() {
850 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
851 }
852
853 @Override
854 public int getAvatarBackgroundColor() {
855 return UIHelper.getColorForName(jid.asBareJid().toString());
856 }
857
858 @Override
859 public String getAvatarName() {
860 throw new IllegalStateException("This method should not be called");
861 }
862
863 public void setServiceOutageStatus(final ServiceOutageStatus sos) {
864 this.serviceOutageStatus = sos;
865 }
866
867 public ServiceOutageStatus getServiceOutageStatus() {
868 return this.serviceOutageStatus;
869 }
870
871 public boolean isServiceOutage() {
872 final var sos = this.serviceOutageStatus;
873 if (sos != null
874 && isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
875 && ServiceOutageStatus.isPossibleOutage(this.status)) {
876 return sos.isNow();
877 }
878 return false;
879 }
880
881 public void setXmppConnection(final XmppConnection connection) {
882 this.xmppConnection = connection;
883 }
884
885 public enum State {
886 DISABLED(false, false),
887 LOGGED_OUT(false, false),
888 OFFLINE(false),
889 CONNECTING(false),
890 ONLINE(false),
891 NO_INTERNET(false),
892 CONNECTION_TIMEOUT,
893 UNAUTHORIZED,
894 TEMPORARY_AUTH_FAILURE,
895 SERVER_NOT_FOUND,
896 REGISTRATION_SUCCESSFUL(false),
897 REGISTRATION_FAILED(true, false),
898 REGISTRATION_WEB(true, false),
899 REGISTRATION_CONFLICT(true, false),
900 REGISTRATION_NOT_SUPPORTED(true, false),
901 REGISTRATION_PLEASE_WAIT(true, false),
902 REGISTRATION_INVALID_TOKEN(true, false),
903 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
904 TLS_ERROR,
905 TLS_ERROR_DOMAIN,
906 CHANNEL_BINDING,
907 INCOMPATIBLE_SERVER,
908 INCOMPATIBLE_CLIENT,
909 TOR_NOT_AVAILABLE,
910 DOWNGRADE_ATTACK,
911 SESSION_FAILURE,
912 BIND_FAILURE,
913 HOST_UNKNOWN,
914 STREAM_ERROR,
915 SEE_OTHER_HOST,
916 STREAM_OPENING_ERROR,
917 POLICY_VIOLATION,
918 PAYMENT_REQUIRED,
919 MISSING_INTERNET_PERMISSION(false);
920
921 private final boolean isError;
922 private final boolean attemptReconnect;
923
924 State(final boolean isError) {
925 this(isError, true);
926 }
927
928 State(final boolean isError, final boolean reconnect) {
929 this.isError = isError;
930 this.attemptReconnect = reconnect;
931 }
932
933 State() {
934 this(true, true);
935 }
936
937 public boolean isError() {
938 return this.isError;
939 }
940
941 public boolean isAttemptReconnect() {
942 return this.attemptReconnect;
943 }
944
945 public int getReadableId() {
946 return switch (this) {
947 case DISABLED -> R.string.account_status_disabled;
948 case LOGGED_OUT -> R.string.account_state_logged_out;
949 case ONLINE -> R.string.account_status_online;
950 case CONNECTING -> R.string.account_status_connecting;
951 case OFFLINE -> R.string.account_status_offline;
952 case UNAUTHORIZED -> R.string.account_status_unauthorized;
953 case SERVER_NOT_FOUND -> R.string.account_status_not_found;
954 case NO_INTERNET -> R.string.account_status_no_internet;
955 case CONNECTION_TIMEOUT -> R.string.account_status_connection_timeout;
956 case REGISTRATION_FAILED -> R.string.account_status_regis_fail;
957 case REGISTRATION_WEB -> R.string.account_status_regis_web;
958 case REGISTRATION_CONFLICT -> R.string.account_status_regis_conflict;
959 case REGISTRATION_SUCCESSFUL -> R.string.account_status_regis_success;
960 case REGISTRATION_NOT_SUPPORTED -> R.string.account_status_regis_not_sup;
961 case REGISTRATION_INVALID_TOKEN -> R.string.account_status_regis_invalid_token;
962 case TLS_ERROR -> R.string.account_status_tls_error;
963 case TLS_ERROR_DOMAIN -> R.string.account_status_tls_error_domain;
964 case INCOMPATIBLE_SERVER -> R.string.account_status_incompatible_server;
965 case INCOMPATIBLE_CLIENT -> R.string.account_status_incompatible_client;
966 case CHANNEL_BINDING -> R.string.account_status_channel_binding;
967 case TOR_NOT_AVAILABLE -> R.string.account_status_tor_unavailable;
968 case BIND_FAILURE -> R.string.account_status_bind_failure;
969 case SESSION_FAILURE -> R.string.session_failure;
970 case DOWNGRADE_ATTACK -> R.string.sasl_downgrade;
971 case HOST_UNKNOWN -> R.string.account_status_host_unknown;
972 case POLICY_VIOLATION -> R.string.account_status_policy_violation;
973 case REGISTRATION_PLEASE_WAIT -> R.string.registration_please_wait;
974 case REGISTRATION_PASSWORD_TOO_WEAK -> R.string.registration_password_too_weak;
975 case STREAM_ERROR -> R.string.account_status_stream_error;
976 case STREAM_OPENING_ERROR -> R.string.account_status_stream_opening_error;
977 case PAYMENT_REQUIRED -> R.string.payment_required;
978 case SEE_OTHER_HOST -> R.string.reconnect_on_other_host;
979 case MISSING_INTERNET_PERMISSION -> R.string.missing_internet_permission;
980 case TEMPORARY_AUTH_FAILURE -> R.string.account_status_temporary_auth_failure;
981 default -> R.string.account_status_unknown;
982 };
983 }
984 }
985}