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