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