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