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        final var anotherName =
569            another.getDisplayName() == null ? "" : another.getDisplayName();
570        final var displayNameScore = getDisplayName().compareToIgnoreCase(anotherName);
571
572        if (displayNameScore == 0) {
573            return getJid().compareTo(another.getJid());
574        }
575
576        return displayNameScore;
577    }
578
579    public String getServer() {
580        return getJid().getDomain().toString();
581    }
582
583    public boolean setAvatar(final String avatar) {
584        if (this.avatar != null && this.avatar.equals(avatar)) {
585            return false;
586        }
587        this.avatar = avatar;
588        return true;
589    }
590
591    public String getAvatar() {
592        return this.avatar;
593    }
594
595    public boolean mutualPresenceSubscription() {
596        return getOption(Options.FROM) && getOption(Options.TO);
597    }
598
599    @Override
600    public boolean isBlocked() {
601        return getAccount().isBlocked(this);
602    }
603
604    @Override
605    public boolean isDomainBlocked() {
606        return getAccount().isBlocked(this.getJid().getDomain());
607    }
608
609    @Override
610    @NonNull
611    public Jid getBlockedJid() {
612        if (isDomainBlocked()) {
613            return getJid().getDomain();
614        } else {
615            return getJid();
616        }
617    }
618
619    public boolean isSelf() {
620        return account.getJid().asBareJid().equals(jid.asBareJid());
621    }
622
623    boolean isOwnServer() {
624        return account.getJid().getDomain().equals(jid.asBareJid());
625    }
626
627    public void setCommonName(String cn) {
628        this.commonName = cn;
629    }
630
631    public void flagActive() {
632        this.mActive = true;
633    }
634
635    public void flagInactive() {
636        this.mActive = false;
637    }
638
639    public boolean isActive() {
640        return this.mActive;
641    }
642
643    public boolean setLastseen(long timestamp) {
644        if (timestamp > this.mLastseen) {
645            this.mLastseen = timestamp;
646            return true;
647        } else {
648            return false;
649        }
650    }
651
652    public long getLastseen() {
653        return this.mLastseen;
654    }
655
656    public void setLastResource(String resource) {
657        this.mLastPresence = resource;
658    }
659
660    public String getLastResource() {
661        return this.mLastPresence;
662    }
663
664    public String getServerName() {
665        return serverName;
666    }
667
668    public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
669        setOption(getOption(phoneContact.getClass()));
670        setSystemAccount(phoneContact.getLookupUri());
671        boolean changed = setSystemName(phoneContact.getDisplayName());
672        changed |= setPhotoUri(phoneContact.getPhotoUri());
673        return changed;
674    }
675
676    public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
677        resetOption(getOption(clazz));
678        boolean changed = false;
679        if (!getOption(Options.SYNCED_VIA_ADDRESS_BOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
680            setSystemAccount(null);
681            changed |= setPhotoUri(null);
682            changed |= setSystemName(null);
683        }
684        return changed;
685    }
686
687    protected String phoneAccountLabel() {
688        return account.getJid().asBareJid().toString() +
689            "/" + getJid().asBareJid().toString();
690    }
691
692    public PhoneAccountHandle phoneAccountHandle() {
693        ComponentName componentName = new ComponentName(
694            BuildConfig.APPLICATION_ID,
695            "com.cheogram.android.ConnectionService"
696        );
697        return new PhoneAccountHandle(componentName, phoneAccountLabel());
698    }
699
700    // This Contact is a gateway to use for voice calls, register it with OS
701    public void registerAsPhoneAccount(XmppConnectionService ctx) {
702        if (Build.VERSION.SDK_INT < 23) return;
703        if (Build.VERSION.SDK_INT >= 33) {
704            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
705        } else {
706            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
707        }
708
709        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
710
711        PhoneAccount phoneAccount = PhoneAccount.builder(
712            phoneAccountHandle(),
713            account.getJid().asBareJid().toString()
714        ).setAddress(
715            Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
716        ).setIcon(
717            Icon.createWithBitmap(FileBackend.drawDrawable(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false)))
718        ).setHighlightColor(
719            0x7401CF
720        ).setShortDescription(
721            getJid().asBareJid().toString()
722        ).setCapabilities(
723            PhoneAccount.CAPABILITY_CALL_PROVIDER
724        ).build();
725
726        try {
727            telecomManager.registerPhoneAccount(phoneAccount);
728        } catch (final Exception e) {
729            Log.w(Config.LOGTAG, "Could not registerPhoneAccount: " + e);
730        }
731    }
732
733    // Unregister any associated PSTN gateway integration
734    public void unregisterAsPhoneAccount(Context ctx) {
735        if (Build.VERSION.SDK_INT < 23) return;
736        if (Build.VERSION.SDK_INT >= 33) {
737            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
738        } else {
739            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
740        }
741
742        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
743
744        try {
745            telecomManager.unregisterPhoneAccount(phoneAccountHandle());
746        } catch (final SecurityException e) {
747            Log.w(Config.LOGTAG, "Could not unregister " + getJid() + " as phone account: " + e);
748        }
749    }
750
751    public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
752        if (clazz == JabberIdContact.class) {
753            return Options.SYNCED_VIA_ADDRESS_BOOK;
754        } else {
755            return Options.SYNCED_VIA_OTHER;
756        }
757    }
758
759    @Override
760    public int getAvatarBackgroundColor() {
761        return UIHelper.getColorForName(
762                jid != null ? jid.asBareJid().toString() : getDisplayName());
763    }
764
765    @Override
766    public String getAvatarName() {
767        return getDisplayName();
768    }
769
770    public boolean hasAvatarOrPresenceName() {
771        return avatar != null || presenceName != null;
772    }
773
774    public boolean refreshRtpCapability() {
775        final RtpCapability.Capability previous = this.rtpCapability;
776        this.rtpCapability = RtpCapability.check(this, false);
777        return !Objects.equals(previous, this.rtpCapability);
778    }
779
780    public void refreshCaps() {
781        account.refreshCapsFor(this);
782    }
783
784    public RtpCapability.Capability getRtpCapability() {
785        return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
786    }
787
788    public static final class Options {
789        public static final int TO = 0;
790        public static final int FROM = 1;
791        public static final int ASKING = 2;
792        public static final int PREEMPTIVE_GRANT = 3;
793        public static final int IN_ROSTER = 4;
794        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
795        public static final int DIRTY_PUSH = 6;
796        public static final int DIRTY_DELETE = 7;
797        private static final int SYNCED_VIA_ADDRESS_BOOK = 8;
798        public static final int SYNCED_VIA_OTHER = 9;
799    }
800}