Contact.java

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