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