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