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