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