Account.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.os.SystemClock;
  6import android.util.Log;
  7import android.util.Pair;
  8
  9import org.json.JSONException;
 10import org.json.JSONObject;
 11
 12import java.util.ArrayList;
 13import java.util.Collection;
 14import java.util.HashSet;
 15import java.util.List;
 16import java.util.concurrent.CopyOnWriteArrayList;
 17import java.util.concurrent.CopyOnWriteArraySet;
 18
 19import eu.siacs.conversations.Config;
 20import eu.siacs.conversations.R;
 21import eu.siacs.conversations.crypto.PgpDecryptionService;
 22import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 23import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 24import eu.siacs.conversations.services.XmppConnectionService;
 25import eu.siacs.conversations.utils.XmppUri;
 26import eu.siacs.conversations.xmpp.XmppConnection;
 27import rocks.xmpp.addr.Jid;
 28
 29public class Account extends AbstractEntity {
 30
 31    public static final String TABLENAME = "accounts";
 32
 33    public static final String USERNAME = "username";
 34    public static final String SERVER = "server";
 35    public static final String PASSWORD = "password";
 36    public static final String OPTIONS = "options";
 37    public static final String ROSTERVERSION = "rosterversion";
 38    public static final String KEYS = "keys";
 39    public static final String AVATAR = "avatar";
 40    public static final String DISPLAY_NAME = "display_name";
 41    public static final String HOSTNAME = "hostname";
 42    public static final String PORT = "port";
 43    public static final String STATUS = "status";
 44    public static final String STATUS_MESSAGE = "status_message";
 45    public static final String RESOURCE = "resource";
 46
 47    public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
 48
 49    public static final int OPTION_USETLS = 0;
 50    public static final int OPTION_DISABLED = 1;
 51    public static final int OPTION_REGISTER = 2;
 52    public static final int OPTION_USECOMPRESSION = 3;
 53    public static final int OPTION_MAGIC_CREATE = 4;
 54    public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
 55    public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
 56    public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
 57    private static final String KEY_PGP_SIGNATURE = "pgp_signature";
 58    private static final String KEY_PGP_ID = "pgp_id";
 59    public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
 60    protected final JSONObject keys;
 61    private final Roster roster = new Roster(this);
 62    private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
 63    public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>();
 64    public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>();
 65    protected Jid jid;
 66    protected String password;
 67    protected int options = 0;
 68    protected State status = State.OFFLINE;
 69    protected String resource;
 70    protected String avatar;
 71    protected String hostname = null;
 72    protected int port = 5222;
 73    protected boolean online = false;
 74    private String rosterVersion;
 75    private String displayName = null;
 76    private AxolotlService axolotlService = null;
 77    private PgpDecryptionService pgpDecryptionService = null;
 78    private XmppConnection xmppConnection = null;
 79    private long mEndGracePeriod = 0L;
 80    private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
 81    private Presence.Status presenceStatus = Presence.Status.ONLINE;
 82    private String presenceStatusMessage = null;
 83
 84    public Account(final Jid jid, final String password) {
 85        this(java.util.UUID.randomUUID().toString(), jid,
 86                password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
 87    }
 88
 89    private Account(final String uuid, final Jid jid,
 90                    final String password, final int options, final String rosterVersion, final String keys,
 91                    final String avatar, String displayName, String hostname, int port,
 92                    final Presence.Status status, String statusMessage) {
 93        this.uuid = uuid;
 94        this.jid = jid;
 95        this.password = password;
 96        this.options = options;
 97        this.rosterVersion = rosterVersion;
 98        JSONObject tmp;
 99        try {
100            tmp = new JSONObject(keys);
101        } catch (JSONException e) {
102            tmp = new JSONObject();
103        }
104        this.keys = tmp;
105        this.avatar = avatar;
106        this.displayName = displayName;
107        this.hostname = hostname;
108        this.port = port;
109        this.presenceStatus = status;
110        this.presenceStatusMessage = statusMessage;
111    }
112
113    public static Account fromCursor(final Cursor cursor) {
114        final Jid jid;
115        try {
116            String resource = cursor.getString(cursor.getColumnIndex(RESOURCE));
117            jid = Jid.of(
118                    cursor.getString(cursor.getColumnIndex(USERNAME)),
119                    cursor.getString(cursor.getColumnIndex(SERVER)),
120                    resource == null || resource.trim().isEmpty() ? null : resource);
121        } catch (final IllegalArgumentException ignored) {
122            Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndex(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndex(SERVER)));
123            throw new AssertionError(ignored);
124        }
125        return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
126                jid,
127                cursor.getString(cursor.getColumnIndex(PASSWORD)),
128                cursor.getInt(cursor.getColumnIndex(OPTIONS)),
129                cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
130                cursor.getString(cursor.getColumnIndex(KEYS)),
131                cursor.getString(cursor.getColumnIndex(AVATAR)),
132                cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
133                cursor.getString(cursor.getColumnIndex(HOSTNAME)),
134                cursor.getInt(cursor.getColumnIndex(PORT)),
135                Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
136                cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
137    }
138
139    public boolean httpUploadAvailable(long filesize) {
140        return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
141    }
142
143    public boolean httpUploadAvailable() {
144        return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE) || httpUploadAvailable(0);
145    }
146
147    public String getDisplayName() {
148        return displayName;
149    }
150
151    public void setDisplayName(String displayName) {
152        this.displayName = displayName;
153    }
154
155    public XmppConnection.Identity getServerIdentity() {
156        if (xmppConnection == null) {
157            return XmppConnection.Identity.UNKNOWN;
158        } else {
159            return xmppConnection.getServerIdentity();
160        }
161    }
162
163    public Contact getSelfContact() {
164        return getRoster().getContact(jid);
165    }
166
167    public boolean hasPendingPgpIntent(Conversation conversation) {
168        return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
169    }
170
171    public boolean isPgpDecryptionServiceConnected() {
172        return pgpDecryptionService != null && pgpDecryptionService.isConnected();
173    }
174
175    public boolean setShowErrorNotification(boolean newValue) {
176        boolean oldValue = showErrorNotification();
177        setKey("show_error", Boolean.toString(newValue));
178        return newValue != oldValue;
179    }
180
181    public boolean showErrorNotification() {
182        String key = getKey("show_error");
183        return key == null || Boolean.parseBoolean(key);
184    }
185
186    public boolean isEnabled() {
187        return !isOptionSet(Account.OPTION_DISABLED);
188    }
189
190    public boolean isOptionSet(final int option) {
191        return ((options & (1 << option)) != 0);
192    }
193
194    public boolean setOption(final int option, final boolean value) {
195        final int before = this.options;
196        if (value) {
197            this.options |= 1 << option;
198        } else {
199            this.options &= ~(1 << option);
200        }
201        return before != this.options;
202    }
203
204    public String getUsername() {
205        return jid.getEscapedLocal();
206    }
207
208    public boolean setJid(final Jid next) {
209        final Jid previousFull = this.jid;
210        final Jid prev = this.jid != null ? this.jid.asBareJid() : null;
211        final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid()));
212        if (changed) {
213            final AxolotlService oldAxolotlService = this.axolotlService;
214            if (oldAxolotlService != null) {
215                oldAxolotlService.destroy();
216                this.jid = next;
217                this.axolotlService = oldAxolotlService.makeNew();
218            }
219        }
220        this.jid = next;
221        return next != null && !next.equals(previousFull);
222    }
223
224    public String getServer() {
225        return jid.getDomain();
226    }
227
228    public String getPassword() {
229        return password;
230    }
231
232    public void setPassword(final String password) {
233        this.password = password;
234    }
235
236    public String getHostname() {
237        return this.hostname == null ? "" : this.hostname;
238    }
239
240    public void setHostname(String hostname) {
241        this.hostname = hostname;
242    }
243
244    public boolean isOnion() {
245        final String server = getServer();
246        return server != null && server.endsWith(".onion");
247    }
248
249    public int getPort() {
250        return this.port;
251    }
252
253    public void setPort(int port) {
254        this.port = port;
255    }
256
257    public State getStatus() {
258        if (isOptionSet(OPTION_DISABLED)) {
259            return State.DISABLED;
260        } else {
261            return this.status;
262        }
263    }
264
265    public void setStatus(final State status) {
266        this.status = status;
267    }
268
269    public State getTrueStatus() {
270        return this.status;
271    }
272
273    public boolean errorStatus() {
274        return getStatus().isError();
275    }
276
277    public boolean hasErrorStatus() {
278        return getXmppConnection() != null
279                && (getStatus().isError() || getStatus() == State.CONNECTING)
280                && getXmppConnection().getAttempt() >= 3;
281    }
282
283    public Presence.Status getPresenceStatus() {
284        return this.presenceStatus;
285    }
286
287    public void setPresenceStatus(Presence.Status status) {
288        this.presenceStatus = status;
289    }
290
291    public String getPresenceStatusMessage() {
292        return this.presenceStatusMessage;
293    }
294
295    public void setPresenceStatusMessage(String message) {
296        this.presenceStatusMessage = message;
297    }
298
299    public String getResource() {
300        return jid.getResource();
301    }
302
303    public void setResource(final String resource) {
304        this.jid = this.jid.withResource(resource);
305    }
306
307    public Jid getJid() {
308        return jid;
309    }
310
311    public JSONObject getKeys() {
312        return keys;
313    }
314
315    public String getKey(final String name) {
316        synchronized (this.keys) {
317            return this.keys.optString(name, null);
318        }
319    }
320
321    public int getKeyAsInt(final String name, int defaultValue) {
322        String key = getKey(name);
323        try {
324            return key == null ? defaultValue : Integer.parseInt(key);
325        } catch (NumberFormatException e) {
326            return defaultValue;
327        }
328    }
329
330    public boolean setKey(final String keyName, final String keyValue) {
331        synchronized (this.keys) {
332            try {
333                this.keys.put(keyName, keyValue);
334                return true;
335            } catch (final JSONException e) {
336                return false;
337            }
338        }
339    }
340
341    public boolean setPrivateKeyAlias(String alias) {
342        return setKey("private_key_alias", alias);
343    }
344
345    public String getPrivateKeyAlias() {
346        return getKey("private_key_alias");
347    }
348
349    @Override
350    public ContentValues getContentValues() {
351        final ContentValues values = new ContentValues();
352        values.put(UUID, uuid);
353        values.put(USERNAME, jid.getLocal());
354        values.put(SERVER, jid.getDomain());
355        values.put(PASSWORD, password);
356        values.put(OPTIONS, options);
357        synchronized (this.keys) {
358            values.put(KEYS, this.keys.toString());
359        }
360        values.put(ROSTERVERSION, rosterVersion);
361        values.put(AVATAR, avatar);
362        values.put(DISPLAY_NAME, displayName);
363        values.put(HOSTNAME, hostname);
364        values.put(PORT, port);
365        values.put(STATUS, presenceStatus.toShowString());
366        values.put(STATUS_MESSAGE, presenceStatusMessage);
367        values.put(RESOURCE, jid.getResource());
368        return values;
369    }
370
371    public AxolotlService getAxolotlService() {
372        return axolotlService;
373    }
374
375    public void initAccountServices(final XmppConnectionService context) {
376        this.axolotlService = new AxolotlService(this, context);
377        this.pgpDecryptionService = new PgpDecryptionService(context);
378        if (xmppConnection != null) {
379            xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
380        }
381    }
382
383    public PgpDecryptionService getPgpDecryptionService() {
384        return this.pgpDecryptionService;
385    }
386
387    public XmppConnection getXmppConnection() {
388        return this.xmppConnection;
389    }
390
391    public void setXmppConnection(final XmppConnection connection) {
392        this.xmppConnection = connection;
393    }
394
395    public String getRosterVersion() {
396        if (this.rosterVersion == null) {
397            return "";
398        } else {
399            return this.rosterVersion;
400        }
401    }
402
403    public void setRosterVersion(final String version) {
404        this.rosterVersion = version;
405    }
406
407    public int countPresences() {
408        return this.getSelfContact().getPresences().size();
409    }
410
411    public String getPgpSignature() {
412        return getKey(KEY_PGP_SIGNATURE);
413    }
414
415    public boolean setPgpSignature(String signature) {
416        return setKey(KEY_PGP_SIGNATURE, signature);
417    }
418
419    public boolean unsetPgpSignature() {
420        synchronized (this.keys) {
421            return keys.remove(KEY_PGP_SIGNATURE) != null;
422        }
423    }
424
425    public long getPgpId() {
426        synchronized (this.keys) {
427            if (keys.has(KEY_PGP_ID)) {
428                try {
429                    return keys.getLong(KEY_PGP_ID);
430                } catch (JSONException e) {
431                    return 0;
432                }
433            } else {
434                return 0;
435            }
436        }
437    }
438
439    public boolean setPgpSignId(long pgpID) {
440        synchronized (this.keys) {
441            try {
442                if (pgpID == 0) {
443                    keys.remove(KEY_PGP_ID);
444                } else {
445                    keys.put(KEY_PGP_ID, pgpID);
446                }
447            } catch (JSONException e) {
448                return false;
449            }
450            return true;
451        }
452    }
453
454    public Roster getRoster() {
455        return this.roster;
456    }
457
458    public List<Bookmark> getBookmarks() {
459        return this.bookmarks;
460    }
461
462    public void setBookmarks(final CopyOnWriteArrayList<Bookmark> bookmarks) {
463        this.bookmarks = bookmarks;
464    }
465
466    public boolean hasBookmarkFor(final Jid conferenceJid) {
467        return getBookmark(conferenceJid) != null;
468    }
469
470    Bookmark getBookmark(final Jid jid) {
471        for (final Bookmark bookmark : this.bookmarks) {
472            if (bookmark.getJid() != null && jid.asBareJid().equals(bookmark.getJid().asBareJid())) {
473                return bookmark;
474            }
475        }
476        return null;
477    }
478
479    public boolean setAvatar(final String filename) {
480        if (this.avatar != null && this.avatar.equals(filename)) {
481            return false;
482        } else {
483            this.avatar = filename;
484            return true;
485        }
486    }
487
488    public String getAvatar() {
489        return this.avatar;
490    }
491
492    public void activateGracePeriod(final long duration) {
493        if (duration > 0) {
494            this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
495        }
496    }
497
498    public void deactivateGracePeriod() {
499        this.mEndGracePeriod = 0L;
500    }
501
502    public boolean inGracePeriod() {
503        return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
504    }
505
506    public String getShareableUri() {
507        List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
508        String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString();
509        if (fingerprints.size() > 0) {
510            return XmppUri.getFingerprintUri(uri, fingerprints, ';');
511        } else {
512            return uri;
513        }
514    }
515
516    public String getShareableLink() {
517        List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
518        String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
519        if (fingerprints.size() > 0) {
520            return XmppUri.getFingerprintUri(uri, fingerprints, '&');
521        } else {
522            return uri;
523        }
524    }
525
526    private List<XmppUri.Fingerprint> getFingerprints() {
527        ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
528        if (axolotlService == null) {
529            return fingerprints;
530        }
531        fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
532        for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
533            if (session.getTrust().isVerified() && session.getTrust().isActive()) {
534                fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
535            }
536        }
537        return fingerprints;
538    }
539
540    public boolean isBlocked(final ListItem contact) {
541        final Jid jid = contact.getJid();
542        return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(Jid.ofDomain(jid.getDomain())));
543    }
544
545    public boolean isBlocked(final Jid jid) {
546        return jid != null && blocklist.contains(jid.asBareJid());
547    }
548
549    public Collection<Jid> getBlocklist() {
550        return this.blocklist;
551    }
552
553    public void clearBlocklist() {
554        getBlocklist().clear();
555    }
556
557    public boolean isOnlineAndConnected() {
558        return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
559    }
560
561    public enum State {
562        DISABLED(false, false),
563        OFFLINE(false),
564        CONNECTING(false),
565        ONLINE(false),
566        NO_INTERNET(false),
567        UNAUTHORIZED,
568        SERVER_NOT_FOUND,
569        REGISTRATION_SUCCESSFUL(false),
570        REGISTRATION_FAILED(true, false),
571        REGISTRATION_WEB(true, false),
572        REGISTRATION_CONFLICT(true, false),
573        REGISTRATION_NOT_SUPPORTED(true, false),
574        REGISTRATION_PLEASE_WAIT(true, false),
575        REGISTRATION_PASSWORD_TOO_WEAK(true, false),
576        TLS_ERROR,
577        INCOMPATIBLE_SERVER,
578        TOR_NOT_AVAILABLE,
579        DOWNGRADE_ATTACK,
580        SESSION_FAILURE,
581        BIND_FAILURE,
582        HOST_UNKNOWN,
583        STREAM_ERROR,
584        STREAM_OPENING_ERROR,
585        POLICY_VIOLATION,
586        PAYMENT_REQUIRED,
587        MISSING_INTERNET_PERMISSION(false);
588
589        private final boolean isError;
590        private final boolean attemptReconnect;
591
592        State(final boolean isError) {
593            this(isError, true);
594        }
595
596        State(final boolean isError, final boolean reconnect) {
597            this.isError = isError;
598            this.attemptReconnect = reconnect;
599        }
600
601        State() {
602            this(true, true);
603        }
604
605        public boolean isError() {
606            return this.isError;
607        }
608
609        public boolean isAttemptReconnect() {
610            return this.attemptReconnect;
611        }
612
613        public int getReadableId() {
614            switch (this) {
615                case DISABLED:
616                    return R.string.account_status_disabled;
617                case ONLINE:
618                    return R.string.account_status_online;
619                case CONNECTING:
620                    return R.string.account_status_connecting;
621                case OFFLINE:
622                    return R.string.account_status_offline;
623                case UNAUTHORIZED:
624                    return R.string.account_status_unauthorized;
625                case SERVER_NOT_FOUND:
626                    return R.string.account_status_not_found;
627                case NO_INTERNET:
628                    return R.string.account_status_no_internet;
629                case REGISTRATION_FAILED:
630                    return R.string.account_status_regis_fail;
631                case REGISTRATION_WEB:
632                    return R.string.account_status_regis_web;
633                case REGISTRATION_CONFLICT:
634                    return R.string.account_status_regis_conflict;
635                case REGISTRATION_SUCCESSFUL:
636                    return R.string.account_status_regis_success;
637                case REGISTRATION_NOT_SUPPORTED:
638                    return R.string.account_status_regis_not_sup;
639                case TLS_ERROR:
640                    return R.string.account_status_tls_error;
641                case INCOMPATIBLE_SERVER:
642                    return R.string.account_status_incompatible_server;
643                case TOR_NOT_AVAILABLE:
644                    return R.string.account_status_tor_unavailable;
645                case BIND_FAILURE:
646                    return R.string.account_status_bind_failure;
647                case SESSION_FAILURE:
648                    return R.string.session_failure;
649                case DOWNGRADE_ATTACK:
650                    return R.string.sasl_downgrade;
651                case HOST_UNKNOWN:
652                    return R.string.account_status_host_unknown;
653                case POLICY_VIOLATION:
654                    return R.string.account_status_policy_violation;
655                case REGISTRATION_PLEASE_WAIT:
656                    return R.string.registration_please_wait;
657                case REGISTRATION_PASSWORD_TOO_WEAK:
658                    return R.string.registration_password_too_weak;
659                case STREAM_ERROR:
660                    return R.string.account_status_stream_error;
661                case STREAM_OPENING_ERROR:
662                    return R.string.account_status_stream_opening_error;
663                case PAYMENT_REQUIRED:
664                    return R.string.payment_required;
665                case MISSING_INTERNET_PERMISSION:
666                    return R.string.missing_internet_permission;
667                default:
668                    return R.string.account_status_unknown;
669            }
670        }
671    }
672}