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.DiscoManager;
 31import java.util.ArrayList;
 32import java.util.Collection;
 33import java.util.HashMap;
 34import java.util.HashSet;
 35import java.util.List;
 36import java.util.Map;
 37import java.util.Set;
 38import java.util.concurrent.CopyOnWriteArraySet;
 39import org.json.JSONException;
 40import org.json.JSONObject;
 41
 42public class Account extends AbstractEntity implements AvatarService.Avatarable {
 43
 44    public static final String TABLENAME = "accounts";
 45
 46    public static final String USERNAME = "username";
 47    public static final String SERVER = "server";
 48    public static final String PASSWORD = "password";
 49    public static final String OPTIONS = "options";
 50    public static final String ROSTERVERSION = "rosterversion";
 51    public static final String KEYS = "keys";
 52    public static final String AVATAR = "avatar";
 53    public static final String DISPLAY_NAME = "display_name";
 54    public static final String HOSTNAME = "hostname";
 55    public static final String PORT = "port";
 56    public static final String STATUS = "status";
 57    public static final String STATUS_MESSAGE = "status_message";
 58    public static final String RESOURCE = "resource";
 59    public static final String PINNED_MECHANISM = "pinned_mechanism";
 60    public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding";
 61    public static final String FAST_MECHANISM = "fast_mechanism";
 62    public static final String FAST_TOKEN = "fast_token";
 63
 64    public static final int OPTION_DISABLED = 1;
 65    public static final int OPTION_REGISTER = 2;
 66    public static final int OPTION_MAGIC_CREATE = 4;
 67    public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
 68    public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
 69    public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
 70    public static final int OPTION_UNVERIFIED = 8;
 71    public static final int OPTION_FIXED_USERNAME = 9;
 72    public static final int OPTION_QUICKSTART_AVAILABLE = 10;
 73    public static final int OPTION_SOFT_DISABLED = 11;
 74
 75    private static final String KEY_PGP_SIGNATURE = "pgp_signature";
 76    private static final String KEY_PGP_ID = "pgp_id";
 77    private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
 78    public static final String KEY_SOS_URL = "sos_url";
 79    public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
 80
 81    protected final JSONObject keys;
 82    private final Roster roster = new Roster(this);
 83    private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
 84    public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
 85    public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
 86    public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
 87    public final Set<Conversation> inProgressConferencePings = new HashSet<>();
 88    protected Jid jid;
 89    protected String password;
 90    protected int options = 0;
 91    protected State status = State.OFFLINE;
 92    private State lastErrorStatus = State.OFFLINE;
 93    protected String resource;
 94    protected String avatar;
 95    protected String hostname = null;
 96    protected int port = 5222;
 97    protected boolean online = false;
 98    private String rosterVersion;
 99    private String displayName = null;
100    private AxolotlService axolotlService = null;
101    private PgpDecryptionService pgpDecryptionService = null;
102    private XmppConnection xmppConnection = null;
103    private long mEndGracePeriod = 0L;
104    private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
105    private im.conversations.android.xmpp.model.stanza.Presence.Availability presenceStatus;
106    private String presenceStatusMessage;
107    private String pinnedMechanism;
108    private String pinnedChannelBinding;
109    private String fastMechanism;
110    private String fastToken;
111    private ServiceOutageStatus serviceOutageStatus;
112
113    public Account(final Jid jid, final String password) {
114        this(
115                java.util.UUID.randomUUID().toString(),
116                jid,
117                password,
118                0,
119                null,
120                "",
121                null,
122                null,
123                null,
124                Resolver.XMPP_PORT_STARTTLS,
125                im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
126                null,
127                null,
128                null,
129                null,
130                null);
131    }
132
133    private Account(
134            final String uuid,
135            final Jid jid,
136            final String password,
137            final int options,
138            final String rosterVersion,
139            final String keys,
140            final String avatar,
141            String displayName,
142            String hostname,
143            int port,
144            final im.conversations.android.xmpp.model.stanza.Presence.Availability status,
145            String statusMessage,
146            final String pinnedMechanism,
147            final String pinnedChannelBinding,
148            final String fastMechanism,
149            final String fastToken) {
150        this.uuid = uuid;
151        this.jid = jid;
152        this.password = password;
153        this.options = options;
154        this.rosterVersion = rosterVersion;
155        this.keys = parseKeys(keys);
156        this.avatar = avatar;
157        this.displayName = displayName;
158        this.hostname = hostname;
159        this.port = port;
160        this.presenceStatus = status;
161        this.presenceStatusMessage = statusMessage;
162        this.pinnedMechanism = pinnedMechanism;
163        this.pinnedChannelBinding = pinnedChannelBinding;
164        this.fastMechanism = fastMechanism;
165        this.fastToken = fastToken;
166    }
167
168    public static JSONObject parseKeys(final String keys) {
169        if (Strings.isNullOrEmpty(keys)) {
170            return new JSONObject();
171        }
172        try {
173            return new JSONObject(keys);
174        } catch (final JSONException e) {
175            return new JSONObject();
176        }
177    }
178
179    public static Account fromCursor(final Cursor cursor) {
180        final Jid jid;
181        try {
182            final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE));
183            jid =
184                    Jid.of(
185                            cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)),
186                            cursor.getString(cursor.getColumnIndexOrThrow(SERVER)),
187                            resource == null || resource.trim().isEmpty() ? null : resource);
188        } catch (final IllegalArgumentException e) {
189            Log.d(
190                    Config.LOGTAG,
191                    cursor.getString(cursor.getColumnIndexOrThrow(USERNAME))
192                            + "@"
193                            + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)));
194            throw new AssertionError(e);
195        }
196        return new Account(
197                cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
198                jid,
199                cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)),
200                cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)),
201                cursor.getString(cursor.getColumnIndexOrThrow(ROSTERVERSION)),
202                cursor.getString(cursor.getColumnIndexOrThrow(KEYS)),
203                cursor.getString(cursor.getColumnIndexOrThrow(AVATAR)),
204                cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)),
205                cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)),
206                cursor.getInt(cursor.getColumnIndexOrThrow(PORT)),
207                im.conversations.android.xmpp.model.stanza.Presence.Availability.valueOfShown(
208                        cursor.getString(cursor.getColumnIndexOrThrow(STATUS))),
209                cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)),
210                cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)),
211                cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)),
212                cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)),
213                cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN)));
214    }
215
216    public boolean httpUploadAvailable(long size) {
217        return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size);
218    }
219
220    public boolean httpUploadAvailable() {
221        return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE) || httpUploadAvailable(0);
222    }
223
224    public String getDisplayName() {
225        return displayName;
226    }
227
228    public void setDisplayName(String displayName) {
229        this.displayName = displayName;
230    }
231
232    public Contact getSelfContact() {
233        return getRoster().getContact(jid);
234    }
235
236    public boolean hasPendingPgpIntent(Conversation conversation) {
237        return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
238    }
239
240    public boolean isPgpDecryptionServiceConnected() {
241        return pgpDecryptionService != null && pgpDecryptionService.isConnected();
242    }
243
244    public boolean setShowErrorNotification(boolean newValue) {
245        boolean oldValue = showErrorNotification();
246        setKey("show_error", Boolean.toString(newValue));
247        return newValue != oldValue;
248    }
249
250    public boolean showErrorNotification() {
251        String key = getKey("show_error");
252        return key == null || Boolean.parseBoolean(key);
253    }
254
255    public boolean isEnabled() {
256        return !isOptionSet(Account.OPTION_DISABLED);
257    }
258
259    public boolean isConnectionEnabled() {
260        return !isOptionSet(Account.OPTION_DISABLED) && !isOptionSet(Account.OPTION_SOFT_DISABLED);
261    }
262
263    public boolean isOptionSet(final int option) {
264        return ((options & (1 << option)) != 0);
265    }
266
267    public boolean setOption(final int option, final boolean value) {
268        if (value && (option == OPTION_DISABLED || option == OPTION_SOFT_DISABLED)) {
269            this.setStatus(State.OFFLINE);
270        }
271        final int before = this.options;
272        if (value) {
273            this.options |= 1 << option;
274        } else {
275            this.options &= ~(1 << option);
276        }
277        return before != this.options;
278    }
279
280    public String getUsername() {
281        return jid.getLocal();
282    }
283
284    public boolean setJid(final Jid next) {
285        final Jid previousFull = this.jid;
286        final Jid prev = this.jid != null ? this.jid.asBareJid() : null;
287        final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid()));
288        if (changed) {
289            final AxolotlService oldAxolotlService = this.axolotlService;
290            if (oldAxolotlService != null) {
291                oldAxolotlService.destroy();
292                this.jid = next;
293                this.axolotlService = 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 axolotlService;
550    }
551
552    public void initAccountServices(final XmppConnectionService context) {
553        this.axolotlService = new AxolotlService(this, context);
554        this.pgpDecryptionService = new PgpDecryptionService(context);
555        if (xmppConnection != null) {
556            xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
557        }
558    }
559
560    public PgpDecryptionService getPgpDecryptionService() {
561        return this.pgpDecryptionService;
562    }
563
564    public XmppConnection getXmppConnection() {
565        return this.xmppConnection;
566    }
567
568    public void setXmppConnection(final XmppConnection connection) {
569        this.xmppConnection = connection;
570    }
571
572    public String getRosterVersion() {
573        if (this.rosterVersion == null) {
574            return "";
575        } else {
576            return this.rosterVersion;
577        }
578    }
579
580    public void setRosterVersion(final String version) {
581        this.rosterVersion = version;
582    }
583
584    public int countPresences() {
585        return this.getSelfContact().getPresences().size();
586    }
587
588    public int activeDevicesWithRtpCapability() {
589        final var connection = getXmppConnection();
590        if (connection == null) {
591            return 0;
592        }
593        int i = 0;
594        for (String resource : getSelfContact().getPresences().getPresencesMap().keySet()) {
595            final var jid =
596                    Strings.isNullOrEmpty(resource)
597                            ? getJid().asBareJid()
598                            : getJid().withResource(resource);
599            if (RtpCapability.check(connection.getManager(DiscoManager.class).get(jid))
600                    != RtpCapability.Capability.NONE) {
601                i++;
602            }
603        }
604        return i;
605    }
606
607    public String getPgpSignature() {
608        return getKey(KEY_PGP_SIGNATURE);
609    }
610
611    public boolean setPgpSignature(String signature) {
612        return setKey(KEY_PGP_SIGNATURE, signature);
613    }
614
615    public boolean unsetPgpSignature() {
616        synchronized (this.keys) {
617            return keys.remove(KEY_PGP_SIGNATURE) != null;
618        }
619    }
620
621    public long getPgpId() {
622        synchronized (this.keys) {
623            if (keys.has(KEY_PGP_ID)) {
624                try {
625                    return keys.getLong(KEY_PGP_ID);
626                } catch (JSONException e) {
627                    return 0;
628                }
629            } else {
630                return 0;
631            }
632        }
633    }
634
635    public boolean setPgpSignId(long pgpID) {
636        synchronized (this.keys) {
637            try {
638                if (pgpID == 0) {
639                    keys.remove(KEY_PGP_ID);
640                } else {
641                    keys.put(KEY_PGP_ID, pgpID);
642                }
643            } catch (JSONException e) {
644                return false;
645            }
646            return true;
647        }
648    }
649
650    public Roster getRoster() {
651        return this.roster;
652    }
653
654    public Collection<Bookmark> getBookmarks() {
655        synchronized (this.bookmarks) {
656            return ImmutableList.copyOf(this.bookmarks.values());
657        }
658    }
659
660    public void setBookmarks(final Map<Jid, Bookmark> bookmarks) {
661        synchronized (this.bookmarks) {
662            this.bookmarks.clear();
663            this.bookmarks.putAll(bookmarks);
664        }
665    }
666
667    public void putBookmark(final Bookmark bookmark) {
668        synchronized (this.bookmarks) {
669            this.bookmarks.put(bookmark.getJid(), bookmark);
670        }
671    }
672
673    public void removeBookmark(Bookmark bookmark) {
674        synchronized (this.bookmarks) {
675            this.bookmarks.remove(bookmark.getJid());
676        }
677    }
678
679    public void removeBookmark(Jid jid) {
680        synchronized (this.bookmarks) {
681            this.bookmarks.remove(jid);
682        }
683    }
684
685    public Set<Jid> getBookmarkedJids() {
686        synchronized (this.bookmarks) {
687            return new HashSet<>(this.bookmarks.keySet());
688        }
689    }
690
691    public Bookmark getBookmark(final Jid jid) {
692        synchronized (this.bookmarks) {
693            return this.bookmarks.get(jid.asBareJid());
694        }
695    }
696
697    public boolean setAvatar(final String filename) {
698        if (this.avatar != null && this.avatar.equals(filename)) {
699            return false;
700        } else {
701            this.avatar = filename;
702            return true;
703        }
704    }
705
706    public String getAvatar() {
707        return this.avatar;
708    }
709
710    public void activateGracePeriod(final long duration) {
711        if (duration > 0) {
712            this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
713        }
714    }
715
716    public void deactivateGracePeriod() {
717        this.mEndGracePeriod = 0L;
718    }
719
720    public boolean inGracePeriod() {
721        return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
722    }
723
724    public String getShareableUri() {
725        List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
726        final String uri = "xmpp:" + this.getJid().asBareJid().toString();
727        if (fingerprints.isEmpty()) {
728            return uri;
729        } else {
730            return XmppUri.getFingerprintUri(uri, fingerprints, ';');
731        }
732    }
733
734    public String getShareableLink() {
735        List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
736        String uri =
737                "https://conversations.im/i/"
738                        + XmppUri.lameUrlEncode(this.getJid().asBareJid().toString());
739        if (fingerprints.isEmpty()) {
740            return uri;
741        } else {
742            return XmppUri.getFingerprintUri(uri, fingerprints, '&');
743        }
744    }
745
746    private List<XmppUri.Fingerprint> getFingerprints() {
747        ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
748        if (axolotlService == null) {
749            return fingerprints;
750        }
751        fingerprints.add(
752                new XmppUri.Fingerprint(
753                        XmppUri.FingerprintType.OMEMO,
754                        axolotlService.getOwnFingerprint().substring(2),
755                        axolotlService.getOwnDeviceId()));
756        for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
757            if (session.getTrust().isVerified() && session.getTrust().isActive()) {
758                fingerprints.add(
759                        new XmppUri.Fingerprint(
760                                XmppUri.FingerprintType.OMEMO,
761                                session.getFingerprint().substring(2).replaceAll("\\s", ""),
762                                session.getRemoteAddress().getDeviceId()));
763            }
764        }
765        return fingerprints;
766    }
767
768    public boolean isBlocked(final ListItem contact) {
769        final Jid jid = contact.getJid();
770        return jid != null
771                && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
772    }
773
774    public boolean isBlocked(final Jid jid) {
775        return jid != null && blocklist.contains(jid.asBareJid());
776    }
777
778    public Collection<Jid> getBlocklist() {
779        return this.blocklist;
780    }
781
782    public void clearBlocklist() {
783        getBlocklist().clear();
784    }
785
786    public boolean isOnlineAndConnected() {
787        return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
788    }
789
790    @Override
791    public int getAvatarBackgroundColor() {
792        return UIHelper.getColorForName(jid.asBareJid().toString());
793    }
794
795    @Override
796    public String getAvatarName() {
797        throw new IllegalStateException("This method should not be called");
798    }
799
800    public void setServiceOutageStatus(final ServiceOutageStatus sos) {
801        this.serviceOutageStatus = sos;
802    }
803
804    public ServiceOutageStatus getServiceOutageStatus() {
805        return this.serviceOutageStatus;
806    }
807
808    public boolean isServiceOutage() {
809        final var sos = this.serviceOutageStatus;
810        if (sos != null
811                && isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
812                && ServiceOutageStatus.isPossibleOutage(this.status)) {
813            return sos.isNow();
814        }
815        return false;
816    }
817
818    public enum State {
819        DISABLED(false, false),
820        LOGGED_OUT(false, false),
821        OFFLINE(false),
822        CONNECTING(false),
823        ONLINE(false),
824        NO_INTERNET(false),
825        CONNECTION_TIMEOUT,
826        UNAUTHORIZED,
827        TEMPORARY_AUTH_FAILURE,
828        SERVER_NOT_FOUND,
829        REGISTRATION_SUCCESSFUL(false),
830        REGISTRATION_FAILED(true, false),
831        REGISTRATION_WEB(true, false),
832        REGISTRATION_CONFLICT(true, false),
833        REGISTRATION_NOT_SUPPORTED(true, false),
834        REGISTRATION_PLEASE_WAIT(true, false),
835        REGISTRATION_INVALID_TOKEN(true, false),
836        REGISTRATION_PASSWORD_TOO_WEAK(true, false),
837        TLS_ERROR,
838        TLS_ERROR_DOMAIN,
839        CHANNEL_BINDING,
840        INCOMPATIBLE_SERVER,
841        INCOMPATIBLE_CLIENT,
842        TOR_NOT_AVAILABLE,
843        DOWNGRADE_ATTACK,
844        SESSION_FAILURE,
845        BIND_FAILURE,
846        HOST_UNKNOWN,
847        STREAM_ERROR,
848        SEE_OTHER_HOST,
849        STREAM_OPENING_ERROR,
850        POLICY_VIOLATION,
851        PAYMENT_REQUIRED,
852        MISSING_INTERNET_PERMISSION(false);
853
854        private final boolean isError;
855        private final boolean attemptReconnect;
856
857        State(final boolean isError) {
858            this(isError, true);
859        }
860
861        State(final boolean isError, final boolean reconnect) {
862            this.isError = isError;
863            this.attemptReconnect = reconnect;
864        }
865
866        State() {
867            this(true, true);
868        }
869
870        public boolean isError() {
871            return this.isError;
872        }
873
874        public boolean isAttemptReconnect() {
875            return this.attemptReconnect;
876        }
877
878        public int getReadableId() {
879            return switch (this) {
880                case DISABLED -> R.string.account_status_disabled;
881                case LOGGED_OUT -> R.string.account_state_logged_out;
882                case ONLINE -> R.string.account_status_online;
883                case CONNECTING -> R.string.account_status_connecting;
884                case OFFLINE -> R.string.account_status_offline;
885                case UNAUTHORIZED -> R.string.account_status_unauthorized;
886                case SERVER_NOT_FOUND -> R.string.account_status_not_found;
887                case NO_INTERNET -> R.string.account_status_no_internet;
888                case CONNECTION_TIMEOUT -> R.string.account_status_connection_timeout;
889                case REGISTRATION_FAILED -> R.string.account_status_regis_fail;
890                case REGISTRATION_WEB -> R.string.account_status_regis_web;
891                case REGISTRATION_CONFLICT -> R.string.account_status_regis_conflict;
892                case REGISTRATION_SUCCESSFUL -> R.string.account_status_regis_success;
893                case REGISTRATION_NOT_SUPPORTED -> R.string.account_status_regis_not_sup;
894                case REGISTRATION_INVALID_TOKEN -> R.string.account_status_regis_invalid_token;
895                case TLS_ERROR -> R.string.account_status_tls_error;
896                case TLS_ERROR_DOMAIN -> R.string.account_status_tls_error_domain;
897                case INCOMPATIBLE_SERVER -> R.string.account_status_incompatible_server;
898                case INCOMPATIBLE_CLIENT -> R.string.account_status_incompatible_client;
899                case CHANNEL_BINDING -> R.string.account_status_channel_binding;
900                case TOR_NOT_AVAILABLE -> R.string.account_status_tor_unavailable;
901                case BIND_FAILURE -> R.string.account_status_bind_failure;
902                case SESSION_FAILURE -> R.string.session_failure;
903                case DOWNGRADE_ATTACK -> R.string.sasl_downgrade;
904                case HOST_UNKNOWN -> R.string.account_status_host_unknown;
905                case POLICY_VIOLATION -> R.string.account_status_policy_violation;
906                case REGISTRATION_PLEASE_WAIT -> R.string.registration_please_wait;
907                case REGISTRATION_PASSWORD_TOO_WEAK -> R.string.registration_password_too_weak;
908                case STREAM_ERROR -> R.string.account_status_stream_error;
909                case STREAM_OPENING_ERROR -> R.string.account_status_stream_opening_error;
910                case PAYMENT_REQUIRED -> R.string.payment_required;
911                case SEE_OTHER_HOST -> R.string.reconnect_on_other_host;
912                case MISSING_INTERNET_PERMISSION -> R.string.missing_internet_permission;
913                case TEMPORARY_AUTH_FAILURE -> R.string.account_status_temporary_auth_failure;
914                default -> R.string.account_status_unknown;
915            };
916        }
917    }
918}