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