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        TLS_ERROR_DOMAIN,
639        INCOMPATIBLE_SERVER,
640        TOR_NOT_AVAILABLE,
641        DOWNGRADE_ATTACK,
642        SESSION_FAILURE,
643        BIND_FAILURE,
644        HOST_UNKNOWN,
645        STREAM_ERROR,
646        STREAM_OPENING_ERROR,
647        POLICY_VIOLATION,
648        PAYMENT_REQUIRED,
649        MISSING_INTERNET_PERMISSION(false);
650
651        private final boolean isError;
652        private final boolean attemptReconnect;
653
654        State(final boolean isError) {
655            this(isError, true);
656        }
657
658        State(final boolean isError, final boolean reconnect) {
659            this.isError = isError;
660            this.attemptReconnect = reconnect;
661        }
662
663        State() {
664            this(true, true);
665        }
666
667        public boolean isError() {
668            return this.isError;
669        }
670
671        public boolean isAttemptReconnect() {
672            return this.attemptReconnect;
673        }
674
675        public int getReadableId() {
676            switch (this) {
677                case DISABLED:
678                    return R.string.account_status_disabled;
679                case ONLINE:
680                    return R.string.account_status_online;
681                case CONNECTING:
682                    return R.string.account_status_connecting;
683                case OFFLINE:
684                    return R.string.account_status_offline;
685                case UNAUTHORIZED:
686                    return R.string.account_status_unauthorized;
687                case SERVER_NOT_FOUND:
688                    return R.string.account_status_not_found;
689                case NO_INTERNET:
690                    return R.string.account_status_no_internet;
691                case REGISTRATION_FAILED:
692                    return R.string.account_status_regis_fail;
693                case REGISTRATION_WEB:
694                    return R.string.account_status_regis_web;
695                case REGISTRATION_CONFLICT:
696                    return R.string.account_status_regis_conflict;
697                case REGISTRATION_SUCCESSFUL:
698                    return R.string.account_status_regis_success;
699                case REGISTRATION_NOT_SUPPORTED:
700                    return R.string.account_status_regis_not_sup;
701                case REGISTRATION_INVALID_TOKEN:
702                    return R.string.account_status_regis_invalid_token;
703                case TLS_ERROR:
704                    return R.string.account_status_tls_error;
705                case TLS_ERROR_DOMAIN:
706                    return R.string.account_status_tls_error_domain;
707                case INCOMPATIBLE_SERVER:
708                    return R.string.account_status_incompatible_server;
709                case TOR_NOT_AVAILABLE:
710                    return R.string.account_status_tor_unavailable;
711                case BIND_FAILURE:
712                    return R.string.account_status_bind_failure;
713                case SESSION_FAILURE:
714                    return R.string.session_failure;
715                case DOWNGRADE_ATTACK:
716                    return R.string.sasl_downgrade;
717                case HOST_UNKNOWN:
718                    return R.string.account_status_host_unknown;
719                case POLICY_VIOLATION:
720                    return R.string.account_status_policy_violation;
721                case REGISTRATION_PLEASE_WAIT:
722                    return R.string.registration_please_wait;
723                case REGISTRATION_PASSWORD_TOO_WEAK:
724                    return R.string.registration_password_too_weak;
725                case STREAM_ERROR:
726                    return R.string.account_status_stream_error;
727                case STREAM_OPENING_ERROR:
728                    return R.string.account_status_stream_opening_error;
729                case PAYMENT_REQUIRED:
730                    return R.string.payment_required;
731                case MISSING_INTERNET_PERMISSION:
732                    return R.string.missing_internet_permission;
733                default:
734                    return R.string.account_status_unknown;
735            }
736        }
737    }
738}