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