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}