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