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