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