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