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