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