1package eu.siacs.conversations.entities;
2
3import android.content.ComponentName;
4import android.content.ContentValues;
5import android.content.Context;
6import android.content.pm.PackageManager;
7import android.database.Cursor;
8import android.graphics.drawable.Icon;
9import android.net.Uri;
10import android.os.Build;
11import android.os.Bundle;
12import android.telecom.PhoneAccount;
13import android.telecom.PhoneAccountHandle;
14import android.telecom.TelecomManager;
15import android.text.TextUtils;
16import android.util.Log;
17
18import androidx.annotation.NonNull;
19import com.google.common.base.Strings;
20
21import org.json.JSONArray;
22import org.json.JSONException;
23import org.json.JSONObject;
24
25import eu.siacs.conversations.Config;
26import eu.siacs.conversations.android.AbstractPhoneContact;
27import eu.siacs.conversations.android.JabberIdContact;
28import eu.siacs.conversations.services.QuickConversationsService;
29import eu.siacs.conversations.utils.JidHelper;
30import eu.siacs.conversations.utils.UIHelper;
31import eu.siacs.conversations.xml.Element;
32import eu.siacs.conversations.xmpp.Jid;
33import eu.siacs.conversations.xmpp.jingle.RtpCapability;
34import im.conversations.android.xmpp.model.stanza.Presence;
35import java.util.ArrayList;
36import java.util.Collection;
37import java.util.HashSet;
38import java.util.List;
39import java.util.Locale;
40import java.util.Objects;
41import java.util.concurrent.atomic.AtomicReference;
42
43import eu.siacs.conversations.BuildConfig;
44import eu.siacs.conversations.Config;
45import eu.siacs.conversations.android.AbstractPhoneContact;
46import eu.siacs.conversations.android.JabberIdContact;
47import eu.siacs.conversations.persistance.FileBackend;
48import eu.siacs.conversations.services.AvatarService;
49import eu.siacs.conversations.services.QuickConversationsService;
50import eu.siacs.conversations.services.XmppConnectionService;
51import eu.siacs.conversations.utils.JidHelper;
52import eu.siacs.conversations.utils.UIHelper;
53import eu.siacs.conversations.xml.Element;
54import eu.siacs.conversations.xmpp.Jid;
55import eu.siacs.conversations.xmpp.jingle.RtpCapability;
56import eu.siacs.conversations.xmpp.pep.Avatar;
57
58public class Contact implements ListItem, Blockable {
59 public static final String TABLENAME = "contacts";
60
61 public static final String SYSTEMNAME = "systemname";
62 public static final String SERVERNAME = "servername";
63 public static final String PRESENCE_NAME = "presence_name";
64 public static final String JID = "jid";
65 public static final String OPTIONS = "options";
66 public static final String SYSTEMACCOUNT = "systemaccount";
67 public static final String PHOTOURI = "photouri";
68 public static final String KEYS = "pgpkey";
69 public static final String ACCOUNT = "accountUuid";
70 public static final String AVATAR = "avatar";
71 public static final String LAST_PRESENCE = "last_presence";
72 public static final String LAST_TIME = "last_time";
73 public static final String GROUPS = "groups";
74 public static final String RTP_CAPABILITY = "rtpCapability";
75 private String accountUuid;
76 private String systemName;
77 private String serverName;
78 private String presenceName;
79 private String commonName;
80 protected Jid jid;
81 private int subscription = 0;
82 private Uri systemAccount;
83 private String photoUri;
84 private final JSONObject keys;
85 private JSONArray groups = new JSONArray();
86 private final AtomicReference<JSONArray> systemTags = new AtomicReference<>(new JSONArray());
87 private final Presences presences = new Presences(this);
88 protected Account account;
89 protected String avatar;
90
91 private boolean mActive = false;
92 private long mLastseen = 0;
93 private String mLastPresence = null;
94 private RtpCapability.Capability rtpCapability;
95
96 public Contact(Contact other) {
97 this(null, other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys == null ? null : other.keys.toString(), other.getAvatar(), other.mLastseen, other.mLastPresence, other.groups == null ? null : other.groups.toString(), other.rtpCapability);
98 setAccount(other.getAccount());
99 }
100
101 public Contact(
102 final String account,
103 final String systemName,
104 final String serverName,
105 final String presenceName,
106 final Jid jid,
107 final int subscription,
108 final String photoUri,
109 final Uri systemAccount,
110 final String keys,
111 final String avatar,
112 final long lastseen,
113 final String presence,
114 final String groups,
115 final RtpCapability.Capability rtpCapability) {
116 this.accountUuid = account;
117 this.systemName = systemName;
118 this.serverName = serverName;
119 this.presenceName = presenceName;
120 this.jid = jid;
121 this.subscription = subscription;
122 this.photoUri = photoUri;
123 this.systemAccount = systemAccount;
124 JSONObject tmpJsonObject;
125 try {
126 tmpJsonObject = (keys == null ? new JSONObject("") : new JSONObject(keys));
127 } catch (JSONException e) {
128 tmpJsonObject = new JSONObject();
129 }
130 this.keys = tmpJsonObject;
131 this.avatar = avatar;
132 try {
133 this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
134 } catch (JSONException e) {
135 this.groups = new JSONArray();
136 }
137 this.mLastseen = lastseen;
138 this.mLastPresence = presence;
139 this.rtpCapability = rtpCapability;
140 }
141
142 public Contact(final Jid jid) {
143 this.jid = jid;
144 this.keys = new JSONObject();
145 }
146
147 public static Contact fromCursor(final Cursor cursor) {
148 final Jid jid;
149 try {
150 jid = Jid.of(cursor.getString(cursor.getColumnIndex(JID)));
151 } catch (final IllegalArgumentException e) {
152 // TODO: Borked DB... handle this somehow?
153 return null;
154 }
155 Uri systemAccount;
156 try {
157 systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)));
158 } catch (Exception e) {
159 systemAccount = null;
160 }
161 return new Contact(
162 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
163 cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
164 cursor.getString(cursor.getColumnIndex(SERVERNAME)),
165 cursor.getString(cursor.getColumnIndex(PRESENCE_NAME)),
166 jid,
167 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
168 cursor.getString(cursor.getColumnIndex(PHOTOURI)),
169 systemAccount,
170 cursor.getString(cursor.getColumnIndex(KEYS)),
171 cursor.getString(cursor.getColumnIndex(AVATAR)),
172 cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
173 cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
174 cursor.getString(cursor.getColumnIndex(GROUPS)),
175 RtpCapability.Capability.of(
176 cursor.getString(cursor.getColumnIndex(RTP_CAPABILITY))));
177 }
178
179 public String getDisplayName() {
180 if (isSelf() && TextUtils.isEmpty(this.systemName)) {
181 final String displayName = account.getDisplayName();
182 if (!Strings.isNullOrEmpty(displayName)) {
183 return displayName;
184 }
185 }
186 if (Config.X509_VERIFICATION && !TextUtils.isEmpty(this.commonName)) {
187 return this.commonName;
188 } else if (!TextUtils.isEmpty(this.systemName)) {
189 return this.systemName;
190 } else if (!TextUtils.isEmpty(this.serverName)) {
191 return this.serverName;
192 }
193
194 ListItem bookmark = account.getBookmark(jid);
195 if (bookmark != null) {
196 return bookmark.getDisplayName();
197 } else if (!TextUtils.isEmpty(this.presenceName) && mutualPresenceSubscription()) {
198 return this.presenceName;
199 } else if (!TextUtils.isEmpty(this.presenceName)) {
200 return this.presenceName + (mutualPresenceSubscription() ? "" : " (" + jid + ")");
201 } else if (jid.getLocal() != null) {
202 return JidHelper.localPartOrFallback(jid);
203 } else {
204 return jid.getDomain().toString();
205 }
206 }
207
208 public String getPublicDisplayName() {
209 if (!TextUtils.isEmpty(this.presenceName)) {
210 return this.presenceName;
211 } else if (jid.getLocal() != null) {
212 return JidHelper.localPartOrFallback(jid);
213 } else {
214 return jid.getDomain().toString();
215 }
216 }
217
218 public String getProfilePhoto() {
219 return this.photoUri;
220 }
221
222 public Jid getJid() {
223 return jid;
224 }
225
226 public List<Tag> getGroupTags() {
227 final ArrayList<Tag> tags = new ArrayList<>();
228 for (final String group : getGroups(true)) {
229 tags.add(new Tag(group));
230 }
231 return tags;
232 }
233
234 @Override
235 public List<Tag> getTags(Context context) {
236 final HashSet<Tag> tags = new HashSet<>();
237 tags.addAll(getGroupTags());
238 for (final String tag : getSystemTags(true)) {
239 tags.add(new Tag(tag));
240 }
241 final var status = getShownStatus();
242 if (!showInRoster() && getSystemAccount() != null) {
243 tags.add(new Tag("Android"));
244 }
245 return new ArrayList<>(tags);
246 }
247
248 public boolean match(Context context, String needle) {
249 if (TextUtils.isEmpty(needle)) {
250 return true;
251 }
252 needle = needle.toLowerCase(Locale.US).trim();
253 String[] parts = needle.split("[,\\s]+");
254 if (parts.length > 1) {
255 for (String part : parts) {
256 if (!match(context, part)) {
257 return false;
258 }
259 }
260 return true;
261 } else if(parts.length > 0) {
262 return jid.toString().contains(parts[0]) ||
263 getDisplayName().toLowerCase(Locale.US).contains(parts[0]) ||
264 matchInTag(context, parts[0]);
265 } else {
266 return jid.toString().contains(needle)
267 || getDisplayName().toLowerCase(Locale.US).contains(needle)
268 || matchInTag(context, needle);
269 }
270 }
271
272 private boolean matchInTag(Context context, String needle) {
273 needle = needle.toLowerCase(Locale.US);
274 for (Tag tag : getTags(context)) {
275 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
276 return true;
277 }
278 }
279 return false;
280 }
281
282 public ContentValues getContentValues() {
283 synchronized (this.keys) {
284 final ContentValues values = new ContentValues();
285 values.put(ACCOUNT, accountUuid);
286 values.put(SYSTEMNAME, systemName);
287 values.put(SERVERNAME, serverName);
288 values.put(PRESENCE_NAME, presenceName);
289 values.put(JID, jid.toString());
290 values.put(OPTIONS, subscription);
291 values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
292 values.put(PHOTOURI, photoUri);
293 values.put(KEYS, keys.toString());
294 values.put(AVATAR, avatar);
295 values.put(LAST_PRESENCE, mLastPresence);
296 values.put(LAST_TIME, mLastseen);
297 values.put(GROUPS, groups.toString());
298 values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString());
299 return values;
300 }
301 }
302
303 public Account getAccount() {
304 return this.account;
305 }
306
307 public void setAccount(Account account) {
308 this.account = account;
309 this.accountUuid = account.getUuid();
310 }
311
312 public Presences getPresences() {
313 return this.presences;
314 }
315
316 public void updatePresence(final String resource, final Presence presence) {
317 this.presences.updatePresence(resource, presence);
318 refreshCaps();
319 }
320
321 public void removePresence(final String resource) {
322 this.presences.removePresence(resource);
323 refreshCaps();
324 }
325
326 public void clearPresences() {
327 this.presences.clearPresences();
328 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
329 refreshCaps();
330 }
331
332 public im.conversations.android.xmpp.model.stanza.Presence.Availability getShownStatus() {
333 return this.presences.getShownStatus();
334 }
335
336 public Jid resourceWhichSupport(final String namespace) {
337 final String resource = getPresences().firstWhichSupport(namespace);
338 if (resource == null) return null;
339
340 return resource.equals("") ? getJid() : getJid().withResource(resource);
341 }
342
343 public boolean setPhotoUri(String uri) {
344 if (uri != null && !uri.equals(this.photoUri)) {
345 this.photoUri = uri;
346 return true;
347 } else if (this.photoUri != null && uri == null) {
348 this.photoUri = null;
349 return true;
350 } else {
351 return false;
352 }
353 }
354
355 public void setServerName(String serverName) {
356 this.serverName = serverName;
357 }
358
359 public boolean setSystemName(String systemName) {
360 final String old = getDisplayName();
361 this.systemName = systemName;
362 return !old.equals(getDisplayName());
363 }
364
365 public boolean setSystemTags(Collection<String> systemTags) {
366 final var newArray = new JSONArray(systemTags);
367 final var oldArray = this.systemTags.getAndSet(newArray);
368 return !oldArray.equals(newArray);
369 }
370
371 public boolean setPresenceName(String presenceName) {
372 final String old = getDisplayName();
373 this.presenceName = presenceName;
374 return !old.equals(getDisplayName());
375 }
376
377 public Uri getSystemAccount() {
378 return systemAccount;
379 }
380
381 public void setSystemAccount(Uri lookupUri) {
382 this.systemAccount = lookupUri;
383 }
384
385 public void setGroups(List<String> groups) {
386 this.groups = new JSONArray(groups);
387 }
388
389 public Collection<String> getGroups(final boolean unique) {
390 final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
391 for (int i = 0; i < this.groups.length(); ++i) {
392 try {
393 groups.add(this.groups.getString(i));
394 } catch (final JSONException ignored) {
395 }
396 }
397 return groups;
398 }
399
400 public void copySystemTagsToGroups() {
401 for (String tag : getSystemTags(true)) {
402 this.groups.put(tag);
403 }
404 }
405
406 private Collection<String> getSystemTags(final boolean unique) {
407 final JSONArray systemTags = this.systemTags.get();
408 final Collection<String> tags = unique ? new HashSet<>() : new ArrayList<>();
409 for (int i = 0; i < systemTags.length(); ++i) {
410 try {
411 tags.add(systemTags.getString(i));
412 } catch (final JSONException ignored) {
413 }
414 }
415 return tags;
416 }
417
418 public long getPgpKeyId() {
419 synchronized (this.keys) {
420 if (this.keys.has("pgp_keyid")) {
421 try {
422 return this.keys.getLong("pgp_keyid");
423 } catch (JSONException e) {
424 return 0;
425 }
426 } else {
427 return 0;
428 }
429 }
430 }
431
432 public boolean setPgpKeyId(long keyId) {
433 final long previousKeyId = getPgpKeyId();
434 synchronized (this.keys) {
435 try {
436 this.keys.put("pgp_keyid", keyId);
437 return previousKeyId != keyId;
438 } catch (final JSONException ignored) {
439 }
440 }
441 return false;
442 }
443
444 public void setOption(int option) {
445 this.subscription |= 1 << option;
446 }
447
448 public void resetOption(int option) {
449 this.subscription &= ~(1 << option);
450 }
451
452 public boolean getOption(int option) {
453 return ((this.subscription & (1 << option)) != 0);
454 }
455
456 public boolean canInferPresence() {
457 return showInContactList() || isSelf();
458 }
459
460 public boolean showInRoster() {
461 return (this.getOption(Contact.Options.IN_ROSTER)
462 && (!this.getOption(Contact.Options.DIRTY_DELETE)))
463 || (this.getOption(Contact.Options.DIRTY_PUSH));
464 }
465
466 public boolean showInContactList() {
467 return showInRoster()
468 || getOption(Options.SYNCED_VIA_OTHER)
469 || systemAccount != null;
470 }
471
472 public void parseSubscriptionFromElement(Element item) {
473 String ask = item.getAttribute("ask");
474 String subscription = item.getAttribute("subscription");
475
476 if (subscription == null) {
477 this.resetOption(Options.FROM);
478 this.resetOption(Options.TO);
479 } else {
480 switch (subscription) {
481 case "to":
482 this.resetOption(Options.FROM);
483 this.setOption(Options.TO);
484 break;
485 case "from":
486 this.resetOption(Options.TO);
487 this.setOption(Options.FROM);
488 this.resetOption(Options.PREEMPTIVE_GRANT);
489 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
490 break;
491 case "both":
492 this.setOption(Options.TO);
493 this.setOption(Options.FROM);
494 this.resetOption(Options.PREEMPTIVE_GRANT);
495 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
496 break;
497 case "none":
498 this.resetOption(Options.FROM);
499 this.resetOption(Options.TO);
500 break;
501 }
502 }
503
504 // do NOT override asking if pending push request
505 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
506 if ((ask != null) && (ask.equals("subscribe"))) {
507 this.setOption(Contact.Options.ASKING);
508 } else {
509 this.resetOption(Contact.Options.ASKING);
510 }
511 }
512 }
513
514 public void parseGroupsFromElement(Element item) {
515 this.groups = new JSONArray();
516 for (Element element : item.getChildren()) {
517 if (element.getName().equals("group") && element.getContent() != null) {
518 this.groups.put(element.getContent());
519 }
520 }
521 }
522
523 public Element asElement() {
524 final Element item = new Element("item");
525 item.setAttribute("jid", this.jid);
526 if (this.serverName != null) {
527 item.setAttribute("name", this.serverName);
528 } else {
529 item.setAttribute("name", getDisplayName());
530 }
531 for (String group : getGroups(false)) {
532 item.addChild("group").setContent(group);
533 }
534 return item;
535 }
536
537 @Override
538 public int compareTo(@NonNull final ListItem another) {
539 if (getJid().isDomainJid() && !another.getJid().isDomainJid()) {
540 return -1;
541 } else if (!getJid().isDomainJid() && another.getJid().isDomainJid()) {
542 return 1;
543 }
544
545 if (getDisplayName().equals(another.getDisplayName())) {
546 return getJid().compareTo(another.getJid());
547 }
548
549 final var anotherName = another.getDisplayName();
550 return this.getDisplayName().compareToIgnoreCase(anotherName == null ? "" : anotherName);
551 }
552
553 public String getServer() {
554 return getJid().getDomain().toString();
555 }
556
557 public boolean setAvatar(final String avatar) {
558 if (this.avatar != null && this.avatar.equals(avatar)) {
559 return false;
560 }
561 this.avatar = avatar;
562 return true;
563 }
564
565 public String getAvatar() {
566 return this.avatar;
567 }
568
569 public boolean mutualPresenceSubscription() {
570 return getOption(Options.FROM) && getOption(Options.TO);
571 }
572
573 @Override
574 public boolean isBlocked() {
575 return getAccount().isBlocked(this);
576 }
577
578 @Override
579 public boolean isDomainBlocked() {
580 return getAccount().isBlocked(this.getJid().getDomain());
581 }
582
583 @Override
584 @NonNull
585 public Jid getBlockedJid() {
586 if (isDomainBlocked()) {
587 return getJid().getDomain();
588 } else {
589 return getJid();
590 }
591 }
592
593 public boolean isSelf() {
594 return account.getJid().asBareJid().equals(jid.asBareJid());
595 }
596
597 boolean isOwnServer() {
598 return account.getJid().getDomain().equals(jid.asBareJid());
599 }
600
601 public void setCommonName(String cn) {
602 this.commonName = cn;
603 }
604
605 public void flagActive() {
606 this.mActive = true;
607 }
608
609 public void flagInactive() {
610 this.mActive = false;
611 }
612
613 public boolean isActive() {
614 return this.mActive;
615 }
616
617 public boolean setLastseen(long timestamp) {
618 if (timestamp > this.mLastseen) {
619 this.mLastseen = timestamp;
620 return true;
621 } else {
622 return false;
623 }
624 }
625
626 public long getLastseen() {
627 return this.mLastseen;
628 }
629
630 public void setLastResource(String resource) {
631 this.mLastPresence = resource;
632 }
633
634 public String getLastResource() {
635 return this.mLastPresence;
636 }
637
638 public String getServerName() {
639 return serverName;
640 }
641
642 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
643 setOption(getOption(phoneContact.getClass()));
644 setSystemAccount(phoneContact.getLookupUri());
645 boolean changed = setSystemName(phoneContact.getDisplayName());
646 changed |= setPhotoUri(phoneContact.getPhotoUri());
647 return changed;
648 }
649
650 public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
651 resetOption(getOption(clazz));
652 boolean changed = false;
653 if (!getOption(Options.SYNCED_VIA_ADDRESS_BOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
654 setSystemAccount(null);
655 changed |= setPhotoUri(null);
656 changed |= setSystemName(null);
657 }
658 return changed;
659 }
660
661 protected String phoneAccountLabel() {
662 return account.getJid().asBareJid().toString() +
663 "/" + getJid().asBareJid().toString();
664 }
665
666 public PhoneAccountHandle phoneAccountHandle() {
667 ComponentName componentName = new ComponentName(
668 BuildConfig.APPLICATION_ID,
669 "com.cheogram.android.ConnectionService"
670 );
671 return new PhoneAccountHandle(componentName, phoneAccountLabel());
672 }
673
674 // This Contact is a gateway to use for voice calls, register it with OS
675 public void registerAsPhoneAccount(XmppConnectionService ctx) {
676 if (Build.VERSION.SDK_INT < 23) return;
677 if (Build.VERSION.SDK_INT >= 33) {
678 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
679 } else {
680 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
681 }
682
683 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
684
685 PhoneAccount phoneAccount = PhoneAccount.builder(
686 phoneAccountHandle(),
687 account.getJid().asBareJid().toString()
688 ).setAddress(
689 Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
690 ).setIcon(
691 Icon.createWithBitmap(FileBackend.drawDrawable(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false)))
692 ).setHighlightColor(
693 0x7401CF
694 ).setShortDescription(
695 getJid().asBareJid().toString()
696 ).setCapabilities(
697 PhoneAccount.CAPABILITY_CALL_PROVIDER
698 ).build();
699
700 try {
701 telecomManager.registerPhoneAccount(phoneAccount);
702 } catch (final Exception e) {
703 Log.w(Config.LOGTAG, "Could not registerPhoneAccount: " + e);
704 }
705 }
706
707 // Unregister any associated PSTN gateway integration
708 public void unregisterAsPhoneAccount(Context ctx) {
709 if (Build.VERSION.SDK_INT < 23) return;
710 if (Build.VERSION.SDK_INT >= 33) {
711 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
712 } else {
713 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
714 }
715
716 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
717
718 try {
719 telecomManager.unregisterPhoneAccount(phoneAccountHandle());
720 } catch (final SecurityException e) {
721 Log.w(Config.LOGTAG, "Could not unregister " + getJid() + " as phone account: " + e);
722 }
723 }
724
725 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
726 if (clazz == JabberIdContact.class) {
727 return Options.SYNCED_VIA_ADDRESS_BOOK;
728 } else {
729 return Options.SYNCED_VIA_OTHER;
730 }
731 }
732
733 @Override
734 public int getAvatarBackgroundColor() {
735 return UIHelper.getColorForName(
736 jid != null ? jid.asBareJid().toString() : getDisplayName());
737 }
738
739 @Override
740 public String getAvatarName() {
741 return getDisplayName();
742 }
743
744 public boolean hasAvatarOrPresenceName() {
745 return avatar != null || presenceName != null;
746 }
747
748 public boolean refreshRtpCapability() {
749 final RtpCapability.Capability previous = this.rtpCapability;
750 this.rtpCapability = RtpCapability.check(this, false);
751 return !Objects.equals(previous, this.rtpCapability);
752 }
753
754 public void refreshCaps() {
755 account.refreshCapsFor(this);
756 }
757
758 public RtpCapability.Capability getRtpCapability() {
759 return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
760 }
761
762 public static final class Options {
763 public static final int TO = 0;
764 public static final int FROM = 1;
765 public static final int ASKING = 2;
766 public static final int PREEMPTIVE_GRANT = 3;
767 public static final int IN_ROSTER = 4;
768 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
769 public static final int DIRTY_PUSH = 6;
770 public static final int DIRTY_DELETE = 7;
771 private static final int SYNCED_VIA_ADDRESS_BOOK = 8;
772 public static final int SYNCED_VIA_OTHER = 9;
773 }
774}