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