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