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