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