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