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