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}