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()) {
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 ArrayList<Tag> tags = new ArrayList<>();
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 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    private Collection<String> getSystemTags(final boolean unique) {
373        final Collection<String> tags = unique ? new HashSet<>() : new ArrayList<>();
374        for (int i = 0; i < this.systemTags.length(); ++i) {
375            try {
376                tags.add(this.systemTags.getString(i));
377            } catch (final JSONException ignored) {
378            }
379        }
380        return tags;
381    }
382
383    public long getPgpKeyId() {
384        synchronized (this.keys) {
385            if (this.keys.has("pgp_keyid")) {
386                try {
387                    return this.keys.getLong("pgp_keyid");
388                } catch (JSONException e) {
389                    return 0;
390                }
391            } else {
392                return 0;
393            }
394        }
395    }
396
397    public boolean setPgpKeyId(long keyId) {
398        final long previousKeyId = getPgpKeyId();
399        synchronized (this.keys) {
400            try {
401                this.keys.put("pgp_keyid", keyId);
402                return previousKeyId != keyId;
403            } catch (final JSONException ignored) {
404            }
405        }
406        return false;
407    }
408
409    public void setOption(int option) {
410        this.subscription |= 1 << option;
411    }
412
413    public void resetOption(int option) {
414        this.subscription &= ~(1 << option);
415    }
416
417    public boolean getOption(int option) {
418        return ((this.subscription & (1 << option)) != 0);
419    }
420
421    public boolean canInferPresence() {
422        return showInContactList() || isSelf();
423    }
424
425    public boolean showInRoster() {
426        return (this.getOption(Contact.Options.IN_ROSTER) && (!this
427                .getOption(Contact.Options.DIRTY_DELETE)))
428                || (this.getOption(Contact.Options.DIRTY_PUSH));
429    }
430
431    public boolean showInContactList() {
432        return showInRoster()
433                || getOption(Options.SYNCED_VIA_OTHER)
434                || (QuickConversationsService.isQuicksy() && systemAccount != null);
435    }
436
437    public void parseSubscriptionFromElement(Element item) {
438        String ask = item.getAttribute("ask");
439        String subscription = item.getAttribute("subscription");
440
441        if (subscription == null) {
442            this.resetOption(Options.FROM);
443            this.resetOption(Options.TO);
444        } else {
445            switch (subscription) {
446                case "to":
447                    this.resetOption(Options.FROM);
448                    this.setOption(Options.TO);
449                    break;
450                case "from":
451                    this.resetOption(Options.TO);
452                    this.setOption(Options.FROM);
453                    this.resetOption(Options.PREEMPTIVE_GRANT);
454                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
455                    break;
456                case "both":
457                    this.setOption(Options.TO);
458                    this.setOption(Options.FROM);
459                    this.resetOption(Options.PREEMPTIVE_GRANT);
460                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
461                    break;
462                case "none":
463                    this.resetOption(Options.FROM);
464                    this.resetOption(Options.TO);
465                    break;
466            }
467        }
468
469        // do NOT override asking if pending push request
470        if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
471            if ((ask != null) && (ask.equals("subscribe"))) {
472                this.setOption(Contact.Options.ASKING);
473            } else {
474                this.resetOption(Contact.Options.ASKING);
475            }
476        }
477    }
478
479    public void parseGroupsFromElement(Element item) {
480        this.groups = new JSONArray();
481        for (Element element : item.getChildren()) {
482            if (element.getName().equals("group") && element.getContent() != null) {
483                this.groups.put(element.getContent());
484            }
485        }
486    }
487
488    public Element asElement() {
489        final Element item = new Element("item");
490        item.setAttribute("jid", this.jid);
491        if (this.serverName != null) {
492            item.setAttribute("name", this.serverName);
493        }
494        for (String group : getGroups(false)) {
495            item.addChild("group").setContent(group);
496        }
497        return item;
498    }
499
500    @Override
501    public int compareTo(@NonNull final ListItem another) {
502        if (getJid().isDomainJid() && !another.getJid().isDomainJid()) {
503            return -1;
504        } else if (!getJid().isDomainJid() && another.getJid().isDomainJid()) {
505            return 1;
506        }
507
508        return this.getDisplayName().compareToIgnoreCase(
509                another.getDisplayName());
510    }
511
512    public String getServer() {
513        return getJid().getDomain().toEscapedString();
514    }
515
516    public void setAvatar(Avatar avatar) {
517        setAvatar(avatar, false);
518    }
519
520    public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
521        if (this.avatar != null && this.avatar.equals(avatar)) {
522            return;
523        }
524        if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
525            return;
526        }
527        this.avatar = avatar;
528    }
529
530    public String getAvatarFilename() {
531        return avatar == null ? null : avatar.getFilename();
532    }
533
534    public Avatar getAvatar() {
535        return avatar;
536    }
537
538    public boolean mutualPresenceSubscription() {
539        return getOption(Options.FROM) && getOption(Options.TO);
540    }
541
542    @Override
543    public boolean isBlocked() {
544        return getAccount().isBlocked(this);
545    }
546
547    @Override
548    public boolean isDomainBlocked() {
549        return getAccount().isBlocked(this.getJid().getDomain());
550    }
551
552    @Override
553    public Jid getBlockedJid() {
554        if (isDomainBlocked()) {
555            return getJid().getDomain();
556        } else {
557            return getJid();
558        }
559    }
560
561    public boolean isSelf() {
562        return account.getJid().asBareJid().equals(jid.asBareJid());
563    }
564
565    boolean isOwnServer() {
566        return account.getJid().getDomain().equals(jid.asBareJid());
567    }
568
569    public void setCommonName(String cn) {
570        this.commonName = cn;
571    }
572
573    public void flagActive() {
574        this.mActive = true;
575    }
576
577    public void flagInactive() {
578        this.mActive = false;
579    }
580
581    public boolean isActive() {
582        return this.mActive;
583    }
584
585    public boolean setLastseen(long timestamp) {
586        if (timestamp > this.mLastseen) {
587            this.mLastseen = timestamp;
588            return true;
589        } else {
590            return false;
591        }
592    }
593
594    public long getLastseen() {
595        return this.mLastseen;
596    }
597
598    public void setLastResource(String resource) {
599        this.mLastPresence = resource;
600    }
601
602    public String getLastResource() {
603        return this.mLastPresence;
604    }
605
606    public String getServerName() {
607        return serverName;
608    }
609
610    public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
611        setOption(getOption(phoneContact.getClass()));
612        setSystemAccount(phoneContact.getLookupUri());
613        boolean changed = setSystemName(phoneContact.getDisplayName());
614        changed |= setPhotoUri(phoneContact.getPhotoUri());
615        return changed;
616    }
617
618    public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
619        resetOption(getOption(clazz));
620        boolean changed = false;
621        if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
622            setSystemAccount(null);
623            changed |= setPhotoUri(null);
624            changed |= setSystemName(null);
625        }
626        return changed;
627    }
628
629    protected String phoneAccountLabel() {
630        return account.getJid().asBareJid().toString() +
631            "/" + getJid().asBareJid().toString();
632    }
633
634    public PhoneAccountHandle phoneAccountHandle() {
635        ComponentName componentName = new ComponentName(
636            "com.cheogram.android",
637            "com.cheogram.android.ConnectionService"
638        );
639        return new PhoneAccountHandle(componentName, phoneAccountLabel());
640    }
641
642    // This Contact is a gateway to use for voice calls, register it with OS
643    public void registerAsPhoneAccount(XmppConnectionService ctx) {
644        if (Build.VERSION.SDK_INT < 23) return;
645        if (Build.VERSION.SDK_INT >= 33) {
646            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) return;
647        } else {
648            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
649        }
650
651        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
652
653        PhoneAccount phoneAccount = PhoneAccount.builder(
654            phoneAccountHandle(),
655            account.getJid().asBareJid().toString()
656        ).setAddress(
657            Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
658        ).setIcon(
659            Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
660        ).setHighlightColor(
661            0x7401CF
662        ).setShortDescription(
663            getJid().asBareJid().toString()
664        ).setCapabilities(
665            PhoneAccount.CAPABILITY_CALL_PROVIDER
666        ).build();
667
668        telecomManager.registerPhoneAccount(phoneAccount);
669    }
670
671    // Unregister any associated PSTN gateway integration
672    public void unregisterAsPhoneAccount(Context ctx) {
673        if (Build.VERSION.SDK_INT < 23) return;
674        if (Build.VERSION.SDK_INT >= 33) {
675            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) return;
676        } else {
677            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
678        }
679
680        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
681        telecomManager.unregisterPhoneAccount(phoneAccountHandle());
682    }
683
684    public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
685        if (clazz == JabberIdContact.class) {
686            return Options.SYNCED_VIA_ADDRESSBOOK;
687        } else {
688            return Options.SYNCED_VIA_OTHER;
689        }
690    }
691
692    @Override
693    public int getAvatarBackgroundColor() {
694        return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
695    }
696
697    @Override
698    public String getAvatarName() {
699        return getDisplayName();
700    }
701
702    public boolean hasAvatarOrPresenceName() {
703        return (avatar != null && avatar.getFilename() != null) || presenceName != null;
704    }
705
706    public boolean refreshRtpCapability() {
707        final RtpCapability.Capability previous = this.rtpCapability;
708        this.rtpCapability = RtpCapability.check(this, false);
709        return !Objects.equals(previous, this.rtpCapability);
710    }
711
712    public RtpCapability.Capability getRtpCapability() {
713        return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
714    }
715
716    public static final class Options {
717        public static final int TO = 0;
718        public static final int FROM = 1;
719        public static final int ASKING = 2;
720        public static final int PREEMPTIVE_GRANT = 3;
721        public static final int IN_ROSTER = 4;
722        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
723        public static final int DIRTY_PUSH = 6;
724        public static final int DIRTY_DELETE = 7;
725        private static final int SYNCED_VIA_ADDRESSBOOK = 8;
726        public static final int SYNCED_VIA_OTHER = 9;
727    }
728}