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