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