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}