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