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