Account.java

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