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