1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.os.SystemClock;
6import android.util.Log;
7import androidx.annotation.NonNull;
8import com.google.common.base.Strings;
9import com.google.common.collect.ImmutableList;
10import eu.siacs.conversations.Config;
11import eu.siacs.conversations.R;
12import eu.siacs.conversations.crypto.PgpDecryptionService;
13import eu.siacs.conversations.crypto.axolotl.AxolotlService;
14import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
15import eu.siacs.conversations.crypto.sasl.ChannelBinding;
16import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
17import eu.siacs.conversations.crypto.sasl.HashedToken;
18import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
19import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
20import eu.siacs.conversations.crypto.sasl.SaslMechanism;
21import eu.siacs.conversations.http.ServiceOutageStatus;
22import eu.siacs.conversations.services.AvatarService;
23import eu.siacs.conversations.services.XmppConnectionService;
24import eu.siacs.conversations.utils.Resolver;
25import eu.siacs.conversations.utils.UIHelper;
26import eu.siacs.conversations.utils.XmppUri;
27import eu.siacs.conversations.xmpp.Jid;
28import eu.siacs.conversations.xmpp.XmppConnection;
29import eu.siacs.conversations.xmpp.jingle.RtpCapability;
30import eu.siacs.conversations.xmpp.manager.DiscoManager;
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.HashMap;
34import java.util.HashSet;
35import java.util.List;
36import java.util.Map;
37import java.util.Set;
38import java.util.concurrent.CopyOnWriteArraySet;
39import org.json.JSONException;
40import org.json.JSONObject;
41
42public class Account extends AbstractEntity implements AvatarService.Avatarable {
43
44 public static final String TABLENAME = "accounts";
45
46 public static final String USERNAME = "username";
47 public static final String SERVER = "server";
48 public static final String PASSWORD = "password";
49 public static final String OPTIONS = "options";
50 public static final String ROSTERVERSION = "rosterversion";
51 public static final String KEYS = "keys";
52 public static final String AVATAR = "avatar";
53 public static final String DISPLAY_NAME = "display_name";
54 public static final String HOSTNAME = "hostname";
55 public static final String PORT = "port";
56 public static final String STATUS = "status";
57 public static final String STATUS_MESSAGE = "status_message";
58 public static final String RESOURCE = "resource";
59 public static final String PINNED_MECHANISM = "pinned_mechanism";
60 public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding";
61 public static final String FAST_MECHANISM = "fast_mechanism";
62 public static final String FAST_TOKEN = "fast_token";
63
64 public static final int OPTION_DISABLED = 1;
65 public static final int OPTION_REGISTER = 2;
66 public static final int OPTION_MAGIC_CREATE = 4;
67 public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
68 public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
69 public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
70 public static final int OPTION_UNVERIFIED = 8;
71 public static final int OPTION_FIXED_USERNAME = 9;
72 public static final int OPTION_QUICKSTART_AVAILABLE = 10;
73 public static final int OPTION_SOFT_DISABLED = 11;
74
75 private static final String KEY_PGP_SIGNATURE = "pgp_signature";
76 private static final String KEY_PGP_ID = "pgp_id";
77 private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
78 public static final String KEY_SOS_URL = "sos_url";
79 public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
80
81 protected final JSONObject keys;
82 private final Roster roster = new Roster(this);
83 private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
84 public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
85 public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
86 public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
87 public final Set<Conversation> inProgressConferencePings = new HashSet<>();
88 protected Jid jid;
89 protected String password;
90 protected int options = 0;
91 protected State status = State.OFFLINE;
92 private State lastErrorStatus = State.OFFLINE;
93 protected String resource;
94 protected String avatar;
95 protected String hostname = null;
96 protected int port = 5222;
97 protected boolean online = false;
98 private String rosterVersion;
99 private String displayName = null;
100 private AxolotlService axolotlService = null;
101 private PgpDecryptionService pgpDecryptionService = null;
102 private XmppConnection xmppConnection = null;
103 private long mEndGracePeriod = 0L;
104 private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
105 private im.conversations.android.xmpp.model.stanza.Presence.Availability presenceStatus;
106 private String presenceStatusMessage;
107 private String pinnedMechanism;
108 private String pinnedChannelBinding;
109 private String fastMechanism;
110 private String fastToken;
111 private ServiceOutageStatus serviceOutageStatus;
112
113 public Account(final Jid jid, final String password) {
114 this(
115 java.util.UUID.randomUUID().toString(),
116 jid,
117 password,
118 0,
119 null,
120 "",
121 null,
122 null,
123 null,
124 Resolver.XMPP_PORT_STARTTLS,
125 im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
126 null,
127 null,
128 null,
129 null,
130 null);
131 }
132
133 private Account(
134 final String uuid,
135 final Jid jid,
136 final String password,
137 final int options,
138 final String rosterVersion,
139 final String keys,
140 final String avatar,
141 String displayName,
142 String hostname,
143 int port,
144 final im.conversations.android.xmpp.model.stanza.Presence.Availability status,
145 String statusMessage,
146 final String pinnedMechanism,
147 final String pinnedChannelBinding,
148 final String fastMechanism,
149 final String fastToken) {
150 this.uuid = uuid;
151 this.jid = jid;
152 this.password = password;
153 this.options = options;
154 this.rosterVersion = rosterVersion;
155 this.keys = parseKeys(keys);
156 this.avatar = avatar;
157 this.displayName = displayName;
158 this.hostname = hostname;
159 this.port = port;
160 this.presenceStatus = status;
161 this.presenceStatusMessage = statusMessage;
162 this.pinnedMechanism = pinnedMechanism;
163 this.pinnedChannelBinding = pinnedChannelBinding;
164 this.fastMechanism = fastMechanism;
165 this.fastToken = fastToken;
166 }
167
168 public static JSONObject parseKeys(final String keys) {
169 if (Strings.isNullOrEmpty(keys)) {
170 return new JSONObject();
171 }
172 try {
173 return new JSONObject(keys);
174 } catch (final JSONException e) {
175 return new JSONObject();
176 }
177 }
178
179 public static Account fromCursor(final Cursor cursor) {
180 final Jid jid;
181 try {
182 final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE));
183 jid =
184 Jid.of(
185 cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)),
186 cursor.getString(cursor.getColumnIndexOrThrow(SERVER)),
187 resource == null || resource.trim().isEmpty() ? null : resource);
188 } catch (final IllegalArgumentException e) {
189 Log.d(
190 Config.LOGTAG,
191 cursor.getString(cursor.getColumnIndexOrThrow(USERNAME))
192 + "@"
193 + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)));
194 throw new AssertionError(e);
195 }
196 return new Account(
197 cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
198 jid,
199 cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)),
200 cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)),
201 cursor.getString(cursor.getColumnIndexOrThrow(ROSTERVERSION)),
202 cursor.getString(cursor.getColumnIndexOrThrow(KEYS)),
203 cursor.getString(cursor.getColumnIndexOrThrow(AVATAR)),
204 cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)),
205 cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)),
206 cursor.getInt(cursor.getColumnIndexOrThrow(PORT)),
207 im.conversations.android.xmpp.model.stanza.Presence.Availability.valueOfShown(
208 cursor.getString(cursor.getColumnIndexOrThrow(STATUS))),
209 cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)),
210 cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)),
211 cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)),
212 cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)),
213 cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN)));
214 }
215
216 public boolean httpUploadAvailable(long size) {
217 return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size);
218 }
219
220 public boolean httpUploadAvailable() {
221 return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE) || httpUploadAvailable(0);
222 }
223
224 public String getDisplayName() {
225 return displayName;
226 }
227
228 public void setDisplayName(String displayName) {
229 this.displayName = displayName;
230 }
231
232 public Contact getSelfContact() {
233 return getRoster().getContact(jid);
234 }
235
236 public boolean hasPendingPgpIntent(Conversation conversation) {
237 return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
238 }
239
240 public boolean isPgpDecryptionServiceConnected() {
241 return pgpDecryptionService != null && pgpDecryptionService.isConnected();
242 }
243
244 public boolean setShowErrorNotification(boolean newValue) {
245 boolean oldValue = showErrorNotification();
246 setKey("show_error", Boolean.toString(newValue));
247 return newValue != oldValue;
248 }
249
250 public boolean showErrorNotification() {
251 String key = getKey("show_error");
252 return key == null || Boolean.parseBoolean(key);
253 }
254
255 public boolean isEnabled() {
256 return !isOptionSet(Account.OPTION_DISABLED);
257 }
258
259 public boolean isConnectionEnabled() {
260 return !isOptionSet(Account.OPTION_DISABLED) && !isOptionSet(Account.OPTION_SOFT_DISABLED);
261 }
262
263 public boolean isOptionSet(final int option) {
264 return ((options & (1 << option)) != 0);
265 }
266
267 public boolean setOption(final int option, final boolean value) {
268 if (value && (option == OPTION_DISABLED || option == OPTION_SOFT_DISABLED)) {
269 this.setStatus(State.OFFLINE);
270 }
271 final int before = this.options;
272 if (value) {
273 this.options |= 1 << option;
274 } else {
275 this.options &= ~(1 << option);
276 }
277 return before != this.options;
278 }
279
280 public String getUsername() {
281 return jid.getLocal();
282 }
283
284 public boolean setJid(final Jid next) {
285 final Jid previousFull = this.jid;
286 final Jid prev = this.jid != null ? this.jid.asBareJid() : null;
287 final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid()));
288 if (changed) {
289 final AxolotlService oldAxolotlService = this.axolotlService;
290 if (oldAxolotlService != null) {
291 oldAxolotlService.destroy();
292 this.jid = next;
293 this.axolotlService = oldAxolotlService.makeNew();
294 }
295 }
296 this.jid = next;
297 return next != null && !next.equals(previousFull);
298 }
299
300 public Jid getDomain() {
301 return jid.getDomain();
302 }
303
304 public String getServer() {
305 return jid.getDomain().toString();
306 }
307
308 public String getPassword() {
309 return password;
310 }
311
312 public void setPassword(final String password) {
313 this.password = password;
314 }
315
316 @NonNull
317 public String getHostname() {
318 return Strings.nullToEmpty(this.hostname);
319 }
320
321 public void setHostname(final String hostname) {
322 this.hostname = hostname;
323 }
324
325 public boolean isOnion() {
326 final String server = getServer();
327 return server != null && server.endsWith(".onion");
328 }
329
330 public boolean isDirectToOnion() {
331 final var hostname = Strings.nullToEmpty(this.hostname).trim();
332 return isOnion() && (hostname.isEmpty() || hostname.endsWith(".onion"));
333 }
334
335 public int getPort() {
336 return this.port;
337 }
338
339 public void setPort(int port) {
340 this.port = port;
341 }
342
343 public State getStatus() {
344 if (isOptionSet(OPTION_DISABLED)) {
345 return State.DISABLED;
346 } else if (isOptionSet(OPTION_SOFT_DISABLED)) {
347 return State.LOGGED_OUT;
348 } else {
349 return this.status;
350 }
351 }
352
353 public boolean unauthorized() {
354 return this.status == State.UNAUTHORIZED || this.lastErrorStatus == State.UNAUTHORIZED;
355 }
356
357 public State getLastErrorStatus() {
358 return this.lastErrorStatus;
359 }
360
361 public void setStatus(final State status) {
362 this.status = status;
363 if (status.isError || status == State.ONLINE) {
364 this.lastErrorStatus = status;
365 }
366 }
367
368 public void setPinnedMechanism(final SaslMechanism mechanism) {
369 this.pinnedMechanism = mechanism.getMechanism();
370 if (mechanism instanceof ChannelBindingMechanism) {
371 this.pinnedChannelBinding =
372 ((ChannelBindingMechanism) mechanism).getChannelBinding().toString();
373 } else {
374 this.pinnedChannelBinding = null;
375 }
376 }
377
378 public void setFastToken(final HashedToken.Mechanism mechanism, final String token) {
379 this.fastMechanism = mechanism.name();
380 this.fastToken = token;
381 }
382
383 public void resetFastToken() {
384 this.fastMechanism = null;
385 this.fastToken = null;
386 }
387
388 public void resetPinnedMechanism() {
389 this.pinnedMechanism = null;
390 this.pinnedChannelBinding = null;
391 setKey(Account.KEY_PINNED_MECHANISM, String.valueOf(-1));
392 }
393
394 public int getPinnedMechanismPriority() {
395 final int fallback = getKeyAsInt(KEY_PINNED_MECHANISM, -1);
396 if (Strings.isNullOrEmpty(this.pinnedMechanism)) {
397 return fallback;
398 }
399 final SaslMechanism saslMechanism = getPinnedMechanism();
400 if (saslMechanism == null) {
401 return fallback;
402 } else {
403 return saslMechanism.getPriority();
404 }
405 }
406
407 private SaslMechanism getPinnedMechanism() {
408 final String mechanism = Strings.nullToEmpty(this.pinnedMechanism);
409 final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding);
410 return new SaslMechanism.Factory(this).of(mechanism, channelBinding);
411 }
412
413 public HashedToken getFastMechanism() {
414 final HashedToken.Mechanism fastMechanism =
415 HashedToken.Mechanism.ofOrNull(this.fastMechanism);
416 final String token = this.fastToken;
417 if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
418 return null;
419 }
420 if (fastMechanism.hashFunction.equals("SHA-256")) {
421 return new HashedTokenSha256(this, fastMechanism.channelBinding);
422 } else if (fastMechanism.hashFunction.equals("SHA-512")) {
423 return new HashedTokenSha512(this, fastMechanism.channelBinding);
424 } else {
425 return null;
426 }
427 }
428
429 public SaslMechanism getQuickStartMechanism() {
430 final HashedToken hashedTokenMechanism = getFastMechanism();
431 if (hashedTokenMechanism != null) {
432 return hashedTokenMechanism;
433 }
434 return getPinnedMechanism();
435 }
436
437 public String getFastToken() {
438 return this.fastToken;
439 }
440
441 public State getTrueStatus() {
442 return this.status;
443 }
444
445 public boolean errorStatus() {
446 return getStatus().isError();
447 }
448
449 public boolean hasErrorStatus() {
450 return getXmppConnection() != null
451 && (getStatus().isError() || getStatus() == State.CONNECTING)
452 && getXmppConnection().getAttempt() >= 3;
453 }
454
455 public im.conversations.android.xmpp.model.stanza.Presence.Availability getPresenceStatus() {
456 return this.presenceStatus;
457 }
458
459 public void setPresenceStatus(
460 im.conversations.android.xmpp.model.stanza.Presence.Availability status) {
461 this.presenceStatus = status;
462 }
463
464 public String getPresenceStatusMessage() {
465 return this.presenceStatusMessage;
466 }
467
468 public void setPresenceStatusMessage(String message) {
469 this.presenceStatusMessage = message;
470 }
471
472 public String getResource() {
473 return jid.getResource();
474 }
475
476 public void setResource(final String resource) {
477 this.jid = this.jid.withResource(resource);
478 }
479
480 public Jid getJid() {
481 return jid;
482 }
483
484 public JSONObject getKeys() {
485 return keys;
486 }
487
488 public String getKey(final String name) {
489 synchronized (this.keys) {
490 return this.keys.optString(name, null);
491 }
492 }
493
494 public int getKeyAsInt(final String name, int defaultValue) {
495 String key = getKey(name);
496 try {
497 return key == null ? defaultValue : Integer.parseInt(key);
498 } catch (NumberFormatException e) {
499 return defaultValue;
500 }
501 }
502
503 public boolean setKey(final String keyName, final String keyValue) {
504 synchronized (this.keys) {
505 try {
506 this.keys.put(keyName, keyValue);
507 return true;
508 } catch (final JSONException e) {
509 return false;
510 }
511 }
512 }
513
514 public void setPrivateKeyAlias(final String alias) {
515 setKey("private_key_alias", alias);
516 }
517
518 public String getPrivateKeyAlias() {
519 return getKey("private_key_alias");
520 }
521
522 @Override
523 public ContentValues getContentValues() {
524 final ContentValues values = new ContentValues();
525 values.put(UUID, uuid);
526 values.put(USERNAME, jid.getLocal());
527 values.put(SERVER, jid.getDomain().toString());
528 values.put(PASSWORD, password);
529 values.put(OPTIONS, options);
530 synchronized (this.keys) {
531 values.put(KEYS, this.keys.toString());
532 }
533 values.put(ROSTERVERSION, rosterVersion);
534 values.put(AVATAR, avatar);
535 values.put(DISPLAY_NAME, displayName);
536 values.put(HOSTNAME, hostname);
537 values.put(PORT, port);
538 values.put(STATUS, presenceStatus.toShowString());
539 values.put(STATUS_MESSAGE, presenceStatusMessage);
540 values.put(RESOURCE, jid.getResource());
541 values.put(PINNED_MECHANISM, pinnedMechanism);
542 values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding);
543 values.put(FAST_MECHANISM, this.fastMechanism);
544 values.put(FAST_TOKEN, this.fastToken);
545 return values;
546 }
547
548 public AxolotlService getAxolotlService() {
549 return axolotlService;
550 }
551
552 public void initAccountServices(final XmppConnectionService context) {
553 this.axolotlService = new AxolotlService(this, context);
554 this.pgpDecryptionService = new PgpDecryptionService(context);
555 if (xmppConnection != null) {
556 xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
557 }
558 }
559
560 public PgpDecryptionService getPgpDecryptionService() {
561 return this.pgpDecryptionService;
562 }
563
564 public XmppConnection getXmppConnection() {
565 return this.xmppConnection;
566 }
567
568 public void setXmppConnection(final XmppConnection connection) {
569 this.xmppConnection = connection;
570 }
571
572 public String getRosterVersion() {
573 if (this.rosterVersion == null) {
574 return "";
575 } else {
576 return this.rosterVersion;
577 }
578 }
579
580 public void setRosterVersion(final String version) {
581 this.rosterVersion = version;
582 }
583
584 public int countPresences() {
585 return this.getSelfContact().getPresences().size();
586 }
587
588 public int activeDevicesWithRtpCapability() {
589 final var connection = getXmppConnection();
590 if (connection == null) {
591 return 0;
592 }
593 int i = 0;
594 for (String resource : getSelfContact().getPresences().getPresencesMap().keySet()) {
595 final var jid =
596 Strings.isNullOrEmpty(resource)
597 ? getJid().asBareJid()
598 : getJid().withResource(resource);
599 if (RtpCapability.check(connection.getManager(DiscoManager.class).get(jid))
600 != RtpCapability.Capability.NONE) {
601 i++;
602 }
603 }
604 return i;
605 }
606
607 public String getPgpSignature() {
608 return getKey(KEY_PGP_SIGNATURE);
609 }
610
611 public boolean setPgpSignature(String signature) {
612 return setKey(KEY_PGP_SIGNATURE, signature);
613 }
614
615 public boolean unsetPgpSignature() {
616 synchronized (this.keys) {
617 return keys.remove(KEY_PGP_SIGNATURE) != null;
618 }
619 }
620
621 public long getPgpId() {
622 synchronized (this.keys) {
623 if (keys.has(KEY_PGP_ID)) {
624 try {
625 return keys.getLong(KEY_PGP_ID);
626 } catch (JSONException e) {
627 return 0;
628 }
629 } else {
630 return 0;
631 }
632 }
633 }
634
635 public boolean setPgpSignId(long pgpID) {
636 synchronized (this.keys) {
637 try {
638 if (pgpID == 0) {
639 keys.remove(KEY_PGP_ID);
640 } else {
641 keys.put(KEY_PGP_ID, pgpID);
642 }
643 } catch (JSONException e) {
644 return false;
645 }
646 return true;
647 }
648 }
649
650 public Roster getRoster() {
651 return this.roster;
652 }
653
654 public Collection<Bookmark> getBookmarks() {
655 synchronized (this.bookmarks) {
656 return ImmutableList.copyOf(this.bookmarks.values());
657 }
658 }
659
660 public void setBookmarks(final Map<Jid, Bookmark> bookmarks) {
661 synchronized (this.bookmarks) {
662 this.bookmarks.clear();
663 this.bookmarks.putAll(bookmarks);
664 }
665 }
666
667 public void putBookmark(final Bookmark bookmark) {
668 synchronized (this.bookmarks) {
669 this.bookmarks.put(bookmark.getJid(), bookmark);
670 }
671 }
672
673 public void removeBookmark(Bookmark bookmark) {
674 synchronized (this.bookmarks) {
675 this.bookmarks.remove(bookmark.getJid());
676 }
677 }
678
679 public void removeBookmark(Jid jid) {
680 synchronized (this.bookmarks) {
681 this.bookmarks.remove(jid);
682 }
683 }
684
685 public Set<Jid> getBookmarkedJids() {
686 synchronized (this.bookmarks) {
687 return new HashSet<>(this.bookmarks.keySet());
688 }
689 }
690
691 public Bookmark getBookmark(final Jid jid) {
692 synchronized (this.bookmarks) {
693 return this.bookmarks.get(jid.asBareJid());
694 }
695 }
696
697 public boolean setAvatar(final String filename) {
698 if (this.avatar != null && this.avatar.equals(filename)) {
699 return false;
700 } else {
701 this.avatar = filename;
702 return true;
703 }
704 }
705
706 public String getAvatar() {
707 return this.avatar;
708 }
709
710 public void activateGracePeriod(final long duration) {
711 if (duration > 0) {
712 this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
713 }
714 }
715
716 public void deactivateGracePeriod() {
717 this.mEndGracePeriod = 0L;
718 }
719
720 public boolean inGracePeriod() {
721 return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
722 }
723
724 public String getShareableUri() {
725 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
726 final String uri = "xmpp:" + this.getJid().asBareJid().toString();
727 if (fingerprints.isEmpty()) {
728 return uri;
729 } else {
730 return XmppUri.getFingerprintUri(uri, fingerprints, ';');
731 }
732 }
733
734 public String getShareableLink() {
735 List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
736 String uri =
737 "https://conversations.im/i/"
738 + XmppUri.lameUrlEncode(this.getJid().asBareJid().toString());
739 if (fingerprints.isEmpty()) {
740 return uri;
741 } else {
742 return XmppUri.getFingerprintUri(uri, fingerprints, '&');
743 }
744 }
745
746 private List<XmppUri.Fingerprint> getFingerprints() {
747 ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
748 if (axolotlService == null) {
749 return fingerprints;
750 }
751 fingerprints.add(
752 new XmppUri.Fingerprint(
753 XmppUri.FingerprintType.OMEMO,
754 axolotlService.getOwnFingerprint().substring(2),
755 axolotlService.getOwnDeviceId()));
756 for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
757 if (session.getTrust().isVerified() && session.getTrust().isActive()) {
758 fingerprints.add(
759 new XmppUri.Fingerprint(
760 XmppUri.FingerprintType.OMEMO,
761 session.getFingerprint().substring(2).replaceAll("\\s", ""),
762 session.getRemoteAddress().getDeviceId()));
763 }
764 }
765 return fingerprints;
766 }
767
768 public boolean isBlocked(final ListItem contact) {
769 final Jid jid = contact.getJid();
770 return jid != null
771 && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
772 }
773
774 public boolean isBlocked(final Jid jid) {
775 return jid != null && blocklist.contains(jid.asBareJid());
776 }
777
778 public Collection<Jid> getBlocklist() {
779 return this.blocklist;
780 }
781
782 public void clearBlocklist() {
783 getBlocklist().clear();
784 }
785
786 public boolean isOnlineAndConnected() {
787 return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
788 }
789
790 @Override
791 public int getAvatarBackgroundColor() {
792 return UIHelper.getColorForName(jid.asBareJid().toString());
793 }
794
795 @Override
796 public String getAvatarName() {
797 throw new IllegalStateException("This method should not be called");
798 }
799
800 public void setServiceOutageStatus(final ServiceOutageStatus sos) {
801 this.serviceOutageStatus = sos;
802 }
803
804 public ServiceOutageStatus getServiceOutageStatus() {
805 return this.serviceOutageStatus;
806 }
807
808 public boolean isServiceOutage() {
809 final var sos = this.serviceOutageStatus;
810 if (sos != null
811 && isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
812 && ServiceOutageStatus.isPossibleOutage(this.status)) {
813 return sos.isNow();
814 }
815 return false;
816 }
817
818 public enum State {
819 DISABLED(false, false),
820 LOGGED_OUT(false, false),
821 OFFLINE(false),
822 CONNECTING(false),
823 ONLINE(false),
824 NO_INTERNET(false),
825 CONNECTION_TIMEOUT,
826 UNAUTHORIZED,
827 TEMPORARY_AUTH_FAILURE,
828 SERVER_NOT_FOUND,
829 REGISTRATION_SUCCESSFUL(false),
830 REGISTRATION_FAILED(true, false),
831 REGISTRATION_WEB(true, false),
832 REGISTRATION_CONFLICT(true, false),
833 REGISTRATION_NOT_SUPPORTED(true, false),
834 REGISTRATION_PLEASE_WAIT(true, false),
835 REGISTRATION_INVALID_TOKEN(true, false),
836 REGISTRATION_PASSWORD_TOO_WEAK(true, false),
837 TLS_ERROR,
838 TLS_ERROR_DOMAIN,
839 CHANNEL_BINDING,
840 INCOMPATIBLE_SERVER,
841 INCOMPATIBLE_CLIENT,
842 TOR_NOT_AVAILABLE,
843 DOWNGRADE_ATTACK,
844 SESSION_FAILURE,
845 BIND_FAILURE,
846 HOST_UNKNOWN,
847 STREAM_ERROR,
848 SEE_OTHER_HOST,
849 STREAM_OPENING_ERROR,
850 POLICY_VIOLATION,
851 PAYMENT_REQUIRED,
852 MISSING_INTERNET_PERMISSION(false);
853
854 private final boolean isError;
855 private final boolean attemptReconnect;
856
857 State(final boolean isError) {
858 this(isError, true);
859 }
860
861 State(final boolean isError, final boolean reconnect) {
862 this.isError = isError;
863 this.attemptReconnect = reconnect;
864 }
865
866 State() {
867 this(true, true);
868 }
869
870 public boolean isError() {
871 return this.isError;
872 }
873
874 public boolean isAttemptReconnect() {
875 return this.attemptReconnect;
876 }
877
878 public int getReadableId() {
879 return switch (this) {
880 case DISABLED -> R.string.account_status_disabled;
881 case LOGGED_OUT -> R.string.account_state_logged_out;
882 case ONLINE -> R.string.account_status_online;
883 case CONNECTING -> R.string.account_status_connecting;
884 case OFFLINE -> R.string.account_status_offline;
885 case UNAUTHORIZED -> R.string.account_status_unauthorized;
886 case SERVER_NOT_FOUND -> R.string.account_status_not_found;
887 case NO_INTERNET -> R.string.account_status_no_internet;
888 case CONNECTION_TIMEOUT -> R.string.account_status_connection_timeout;
889 case REGISTRATION_FAILED -> R.string.account_status_regis_fail;
890 case REGISTRATION_WEB -> R.string.account_status_regis_web;
891 case REGISTRATION_CONFLICT -> R.string.account_status_regis_conflict;
892 case REGISTRATION_SUCCESSFUL -> R.string.account_status_regis_success;
893 case REGISTRATION_NOT_SUPPORTED -> R.string.account_status_regis_not_sup;
894 case REGISTRATION_INVALID_TOKEN -> R.string.account_status_regis_invalid_token;
895 case TLS_ERROR -> R.string.account_status_tls_error;
896 case TLS_ERROR_DOMAIN -> R.string.account_status_tls_error_domain;
897 case INCOMPATIBLE_SERVER -> R.string.account_status_incompatible_server;
898 case INCOMPATIBLE_CLIENT -> R.string.account_status_incompatible_client;
899 case CHANNEL_BINDING -> R.string.account_status_channel_binding;
900 case TOR_NOT_AVAILABLE -> R.string.account_status_tor_unavailable;
901 case BIND_FAILURE -> R.string.account_status_bind_failure;
902 case SESSION_FAILURE -> R.string.session_failure;
903 case DOWNGRADE_ATTACK -> R.string.sasl_downgrade;
904 case HOST_UNKNOWN -> R.string.account_status_host_unknown;
905 case POLICY_VIOLATION -> R.string.account_status_policy_violation;
906 case REGISTRATION_PLEASE_WAIT -> R.string.registration_please_wait;
907 case REGISTRATION_PASSWORD_TOO_WEAK -> R.string.registration_password_too_weak;
908 case STREAM_ERROR -> R.string.account_status_stream_error;
909 case STREAM_OPENING_ERROR -> R.string.account_status_stream_opening_error;
910 case PAYMENT_REQUIRED -> R.string.payment_required;
911 case SEE_OTHER_HOST -> R.string.reconnect_on_other_host;
912 case MISSING_INTERNET_PERMISSION -> R.string.missing_internet_permission;
913 case TEMPORARY_AUTH_FAILURE -> R.string.account_status_temporary_auth_failure;
914 default -> R.string.account_status_unknown;
915 };
916 }
917 }
918}