Account.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.os.SystemClock;
  6import android.util.Log;
  7
  8import org.json.JSONException;
  9import org.json.JSONObject;
 10
 11import java.util.ArrayList;
 12import java.util.Collection;
 13import java.util.HashMap;
 14import java.util.HashSet;
 15import java.util.List;
 16import java.util.Map;
 17import java.util.Set;
 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.Jid;
 30import eu.siacs.conversations.xmpp.XmppConnection;
 31import eu.siacs.conversations.xmpp.jingle.RtpCapability;
 32
 33public class Account extends AbstractEntity implements AvatarService.Avatarable {
 34
 35    public static final String TABLENAME = "accounts";
 36
 37    public static final String USERNAME = "username";
 38    public static final String SERVER = "server";
 39    public static final String PASSWORD = "password";
 40    public static final String OPTIONS = "options";
 41    public static final String ROSTERVERSION = "rosterversion";
 42    public static final String KEYS = "keys";
 43    public static final String AVATAR = "avatar";
 44    public static final String DISPLAY_NAME = "display_name";
 45    public static final String HOSTNAME = "hostname";
 46    public static final String PORT = "port";
 47    public static final String STATUS = "status";
 48    public static final String STATUS_MESSAGE = "status_message";
 49    public static final String RESOURCE = "resource";
 50
 51    public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
 52    public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
 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    public static final int OPTION_FIXED_USERNAME = 9;
 64    private static final String KEY_PGP_SIGNATURE = "pgp_signature";
 65    private static final String KEY_PGP_ID = "pgp_id";
 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);
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 Jid getDomain() {
234        return jid.getDomain();
235    }
236
237    public String getServer() {
238        return jid.getDomain().toEscapedString();
239    }
240
241    public String getPassword() {
242        return password;
243    }
244
245    public void setPassword(final String password) {
246        this.password = password;
247    }
248
249    public String getHostname() {
250        return this.hostname == null ? "" : this.hostname;
251    }
252
253    public void setHostname(String hostname) {
254        this.hostname = hostname;
255    }
256
257    public boolean isOnion() {
258        final String server = getServer();
259        return server != null && server.endsWith(".onion");
260    }
261
262    public int getPort() {
263        return this.port;
264    }
265
266    public void setPort(int port) {
267        this.port = port;
268    }
269
270    public State getStatus() {
271        if (isOptionSet(OPTION_DISABLED)) {
272            return State.DISABLED;
273        } else {
274            return this.status;
275        }
276    }
277
278    public State getLastErrorStatus() {
279        return this.lastErrorStatus;
280    }
281
282    public void setStatus(final State status) {
283        this.status = status;
284        if (status.isError || status == State.ONLINE) {
285            this.lastErrorStatus = status;
286        }
287    }
288
289    public State getTrueStatus() {
290        return this.status;
291    }
292
293    public boolean errorStatus() {
294        return getStatus().isError();
295    }
296
297    public boolean hasErrorStatus() {
298        return getXmppConnection() != null
299                && (getStatus().isError() || getStatus() == State.CONNECTING)
300                && getXmppConnection().getAttempt() >= 3;
301    }
302
303    public Presence.Status getPresenceStatus() {
304        return this.presenceStatus;
305    }
306
307    public void setPresenceStatus(Presence.Status status) {
308        this.presenceStatus = status;
309    }
310
311    public String getPresenceStatusMessage() {
312        return this.presenceStatusMessage;
313    }
314
315    public void setPresenceStatusMessage(String message) {
316        this.presenceStatusMessage = message;
317    }
318
319    public String getResource() {
320        return jid.getResource();
321    }
322
323    public void setResource(final String resource) {
324        this.jid = this.jid.withResource(resource);
325    }
326
327    public Jid getJid() {
328        return jid;
329    }
330
331    public JSONObject getKeys() {
332        return keys;
333    }
334
335    public String getKey(final String name) {
336        synchronized (this.keys) {
337            return this.keys.optString(name, null);
338        }
339    }
340
341    public int getKeyAsInt(final String name, int defaultValue) {
342        String key = getKey(name);
343        try {
344            return key == null ? defaultValue : Integer.parseInt(key);
345        } catch (NumberFormatException e) {
346            return defaultValue;
347        }
348    }
349
350    public boolean setKey(final String keyName, final String keyValue) {
351        synchronized (this.keys) {
352            try {
353                this.keys.put(keyName, keyValue);
354                return true;
355            } catch (final JSONException e) {
356                return false;
357            }
358        }
359    }
360
361    public boolean setPrivateKeyAlias(String alias) {
362        return setKey("private_key_alias", alias);
363    }
364
365    public String getPrivateKeyAlias() {
366        return getKey("private_key_alias");
367    }
368
369    @Override
370    public ContentValues getContentValues() {
371        final ContentValues values = new ContentValues();
372        values.put(UUID, uuid);
373        values.put(USERNAME, jid.getLocal());
374        values.put(SERVER, jid.getDomain().toEscapedString());
375        values.put(PASSWORD, password);
376        values.put(OPTIONS, options);
377        synchronized (this.keys) {
378            values.put(KEYS, this.keys.toString());
379        }
380        values.put(ROSTERVERSION, rosterVersion);
381        values.put(AVATAR, avatar);
382        values.put(DISPLAY_NAME, displayName);
383        values.put(HOSTNAME, hostname);
384        values.put(PORT, port);
385        values.put(STATUS, presenceStatus.toShowString());
386        values.put(STATUS_MESSAGE, presenceStatusMessage);
387        values.put(RESOURCE, jid.getResource());
388        return values;
389    }
390
391    public AxolotlService getAxolotlService() {
392        return axolotlService;
393    }
394
395    public void initAccountServices(final XmppConnectionService context) {
396        this.axolotlService = new AxolotlService(this, context);
397        this.pgpDecryptionService = new PgpDecryptionService(context);
398        if (xmppConnection != null) {
399            xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
400        }
401    }
402
403    public PgpDecryptionService getPgpDecryptionService() {
404        return this.pgpDecryptionService;
405    }
406
407    public XmppConnection getXmppConnection() {
408        return this.xmppConnection;
409    }
410
411    public void setXmppConnection(final XmppConnection connection) {
412        this.xmppConnection = connection;
413    }
414
415    public String getRosterVersion() {
416        if (this.rosterVersion == null) {
417            return "";
418        } else {
419            return this.rosterVersion;
420        }
421    }
422
423    public void setRosterVersion(final String version) {
424        this.rosterVersion = version;
425    }
426
427    public int countPresences() {
428        return this.getSelfContact().getPresences().size();
429    }
430
431    public int activeDevicesWithRtpCapability() {
432        int i = 0;
433        for(Presence presence : getSelfContact().getPresences().getPresences()) {
434            if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
435                i++;
436            }
437        }
438        return i;
439    }
440
441    public String getPgpSignature() {
442        return getKey(KEY_PGP_SIGNATURE);
443    }
444
445    public boolean setPgpSignature(String signature) {
446        return setKey(KEY_PGP_SIGNATURE, signature);
447    }
448
449    public boolean unsetPgpSignature() {
450        synchronized (this.keys) {
451            return keys.remove(KEY_PGP_SIGNATURE) != null;
452        }
453    }
454
455    public long getPgpId() {
456        synchronized (this.keys) {
457            if (keys.has(KEY_PGP_ID)) {
458                try {
459                    return keys.getLong(KEY_PGP_ID);
460                } catch (JSONException e) {
461                    return 0;
462                }
463            } else {
464                return 0;
465            }
466        }
467    }
468
469    public boolean setPgpSignId(long pgpID) {
470        synchronized (this.keys) {
471            try {
472                if (pgpID == 0) {
473                    keys.remove(KEY_PGP_ID);
474                } else {
475                    keys.put(KEY_PGP_ID, pgpID);
476                }
477            } catch (JSONException e) {
478                return false;
479            }
480            return true;
481        }
482    }
483
484    public Roster getRoster() {
485        return this.roster;
486    }
487
488    public Collection<Bookmark> getBookmarks() {
489        return this.bookmarks.values();
490    }
491
492    public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
493        synchronized (this.bookmarks) {
494            this.bookmarks.clear();
495            this.bookmarks.putAll(bookmarks);
496        }
497    }
498
499    public void putBookmark(Bookmark bookmark) {
500        synchronized (this.bookmarks) {
501            this.bookmarks.put(bookmark.getJid(), bookmark);
502        }
503    }
504
505    public void removeBookmark(Bookmark bookmark) {
506        synchronized (this.bookmarks) {
507            this.bookmarks.remove(bookmark.getJid());
508        }
509    }
510
511    public void removeBookmark(Jid jid) {
512        synchronized (this.bookmarks) {
513            this.bookmarks.remove(jid);
514        }
515    }
516
517    public Set<Jid> getBookmarkedJids() {
518        synchronized (this.bookmarks) {
519            return new HashSet<>(this.bookmarks.keySet());
520        }
521    }
522
523    public Bookmark getBookmark(final Jid jid) {
524        synchronized (this.bookmarks) {
525            return this.bookmarks.get(jid.asBareJid());
526        }
527    }
528
529    public boolean setAvatar(final String filename) {
530        if (this.avatar != null && this.avatar.equals(filename)) {
531            return false;
532        } else {
533            this.avatar = filename;
534            return true;
535        }
536    }
537
538    public String getAvatar() {
539        return this.avatar;
540    }
541
542    public void activateGracePeriod(final long duration) {
543        if (duration > 0) {
544            this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
545        }
546    }
547
548    public void deactivateGracePeriod() {
549        this.mEndGracePeriod = 0L;
550    }
551
552    public boolean inGracePeriod() {
553        return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
554    }
555
556    public String getShareableUri() {
557        List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
558        String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString();
559        if (fingerprints.size() > 0) {
560            return XmppUri.getFingerprintUri(uri, fingerprints, ';');
561        } else {
562            return uri;
563        }
564    }
565
566    public String getShareableLink() {
567        List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
568        String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
569        if (fingerprints.size() > 0) {
570            return XmppUri.getFingerprintUri(uri, fingerprints, '&');
571        } else {
572            return uri;
573        }
574    }
575
576    private List<XmppUri.Fingerprint> getFingerprints() {
577        ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
578        if (axolotlService == null) {
579            return fingerprints;
580        }
581        fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
582        for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
583            if (session.getTrust().isVerified() && session.getTrust().isActive()) {
584                fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
585            }
586        }
587        return fingerprints;
588    }
589
590    public boolean isBlocked(final ListItem contact) {
591        final Jid jid = contact.getJid();
592        return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
593    }
594
595    public boolean isBlocked(final Jid jid) {
596        return jid != null && blocklist.contains(jid.asBareJid());
597    }
598
599    public Collection<Jid> getBlocklist() {
600        return this.blocklist;
601    }
602
603    public void clearBlocklist() {
604        getBlocklist().clear();
605    }
606
607    public boolean isOnlineAndConnected() {
608        return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
609    }
610
611    @Override
612    public int getAvatarBackgroundColor() {
613        return UIHelper.getColorForName(jid.asBareJid().toString());
614    }
615
616    @Override
617    public String getAvatarName() {
618        throw new IllegalStateException("This method should not be called");
619    }
620
621    public enum State {
622        DISABLED(false, false),
623        OFFLINE(false),
624        CONNECTING(false),
625        ONLINE(false),
626        NO_INTERNET(false),
627        UNAUTHORIZED,
628        SERVER_NOT_FOUND,
629        REGISTRATION_SUCCESSFUL(false),
630        REGISTRATION_FAILED(true, false),
631        REGISTRATION_WEB(true, false),
632        REGISTRATION_CONFLICT(true, false),
633        REGISTRATION_NOT_SUPPORTED(true, false),
634        REGISTRATION_PLEASE_WAIT(true, false),
635        REGISTRATION_INVALID_TOKEN(true,false),
636        REGISTRATION_PASSWORD_TOO_WEAK(true, false),
637        TLS_ERROR,
638        INCOMPATIBLE_SERVER,
639        TOR_NOT_AVAILABLE,
640        DOWNGRADE_ATTACK,
641        SESSION_FAILURE,
642        BIND_FAILURE,
643        HOST_UNKNOWN,
644        STREAM_ERROR,
645        STREAM_OPENING_ERROR,
646        POLICY_VIOLATION,
647        PAYMENT_REQUIRED,
648        MISSING_INTERNET_PERMISSION(false);
649
650        private final boolean isError;
651        private final boolean attemptReconnect;
652
653        State(final boolean isError) {
654            this(isError, true);
655        }
656
657        State(final boolean isError, final boolean reconnect) {
658            this.isError = isError;
659            this.attemptReconnect = reconnect;
660        }
661
662        State() {
663            this(true, true);
664        }
665
666        public boolean isError() {
667            return this.isError;
668        }
669
670        public boolean isAttemptReconnect() {
671            return this.attemptReconnect;
672        }
673
674        public int getReadableId() {
675            switch (this) {
676                case DISABLED:
677                    return R.string.account_status_disabled;
678                case ONLINE:
679                    return R.string.account_status_online;
680                case CONNECTING:
681                    return R.string.account_status_connecting;
682                case OFFLINE:
683                    return R.string.account_status_offline;
684                case UNAUTHORIZED:
685                    return R.string.account_status_unauthorized;
686                case SERVER_NOT_FOUND:
687                    return R.string.account_status_not_found;
688                case NO_INTERNET:
689                    return R.string.account_status_no_internet;
690                case REGISTRATION_FAILED:
691                    return R.string.account_status_regis_fail;
692                case REGISTRATION_WEB:
693                    return R.string.account_status_regis_web;
694                case REGISTRATION_CONFLICT:
695                    return R.string.account_status_regis_conflict;
696                case REGISTRATION_SUCCESSFUL:
697                    return R.string.account_status_regis_success;
698                case REGISTRATION_NOT_SUPPORTED:
699                    return R.string.account_status_regis_not_sup;
700                case REGISTRATION_INVALID_TOKEN:
701                    return R.string.account_status_regis_invalid_token;
702                case TLS_ERROR:
703                    return R.string.account_status_tls_error;
704                case INCOMPATIBLE_SERVER:
705                    return R.string.account_status_incompatible_server;
706                case TOR_NOT_AVAILABLE:
707                    return R.string.account_status_tor_unavailable;
708                case BIND_FAILURE:
709                    return R.string.account_status_bind_failure;
710                case SESSION_FAILURE:
711                    return R.string.session_failure;
712                case DOWNGRADE_ATTACK:
713                    return R.string.sasl_downgrade;
714                case HOST_UNKNOWN:
715                    return R.string.account_status_host_unknown;
716                case POLICY_VIOLATION:
717                    return R.string.account_status_policy_violation;
718                case REGISTRATION_PLEASE_WAIT:
719                    return R.string.registration_please_wait;
720                case REGISTRATION_PASSWORD_TOO_WEAK:
721                    return R.string.registration_password_too_weak;
722                case STREAM_ERROR:
723                    return R.string.account_status_stream_error;
724                case STREAM_OPENING_ERROR:
725                    return R.string.account_status_stream_opening_error;
726                case PAYMENT_REQUIRED:
727                    return R.string.payment_required;
728                case MISSING_INTERNET_PERMISSION:
729                    return R.string.missing_internet_permission;
730                default:
731                    return R.string.account_status_unknown;
732            }
733        }
734    }
735}