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}