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