Account.java

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