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