Account.java

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