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