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 showInRoster() {
418 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
419 .getOption(Contact.Options.DIRTY_DELETE)))
420 || (this.getOption(Contact.Options.DIRTY_PUSH));
421 }
422
423 public boolean showInContactList() {
424 return showInRoster()
425 || getOption(Options.SYNCED_VIA_OTHER)
426 || (QuickConversationsService.isQuicksy() && systemAccount != null);
427 }
428
429 public void parseSubscriptionFromElement(Element item) {
430 String ask = item.getAttribute("ask");
431 String subscription = item.getAttribute("subscription");
432
433 if (subscription == null) {
434 this.resetOption(Options.FROM);
435 this.resetOption(Options.TO);
436 } else {
437 switch (subscription) {
438 case "to":
439 this.resetOption(Options.FROM);
440 this.setOption(Options.TO);
441 break;
442 case "from":
443 this.resetOption(Options.TO);
444 this.setOption(Options.FROM);
445 this.resetOption(Options.PREEMPTIVE_GRANT);
446 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
447 break;
448 case "both":
449 this.setOption(Options.TO);
450 this.setOption(Options.FROM);
451 this.resetOption(Options.PREEMPTIVE_GRANT);
452 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
453 break;
454 case "none":
455 this.resetOption(Options.FROM);
456 this.resetOption(Options.TO);
457 break;
458 }
459 }
460
461 // do NOT override asking if pending push request
462 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
463 if ((ask != null) && (ask.equals("subscribe"))) {
464 this.setOption(Contact.Options.ASKING);
465 } else {
466 this.resetOption(Contact.Options.ASKING);
467 }
468 }
469 }
470
471 public void parseGroupsFromElement(Element item) {
472 this.groups = new JSONArray();
473 for (Element element : item.getChildren()) {
474 if (element.getName().equals("group") && element.getContent() != null) {
475 this.groups.put(element.getContent());
476 }
477 }
478 }
479
480 public Element asElement() {
481 final Element item = new Element("item");
482 item.setAttribute("jid", this.jid);
483 if (this.serverName != null) {
484 item.setAttribute("name", this.serverName);
485 }
486 for (String group : getGroups(false)) {
487 item.addChild("group").setContent(group);
488 }
489 return item;
490 }
491
492 @Override
493 public int compareTo(@NonNull final ListItem another) {
494 return this.getDisplayName().compareToIgnoreCase(
495 another.getDisplayName());
496 }
497
498 public String getServer() {
499 return getJid().getDomain().toEscapedString();
500 }
501
502 public void setAvatar(Avatar avatar) {
503 setAvatar(avatar, false);
504 }
505
506 public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
507 if (this.avatar != null && this.avatar.equals(avatar)) {
508 return;
509 }
510 if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
511 return;
512 }
513 this.avatar = avatar;
514 }
515
516 public String getAvatarFilename() {
517 return avatar == null ? null : avatar.getFilename();
518 }
519
520 public Avatar getAvatar() {
521 return avatar;
522 }
523
524 public boolean mutualPresenceSubscription() {
525 return getOption(Options.FROM) && getOption(Options.TO);
526 }
527
528 @Override
529 public boolean isBlocked() {
530 return getAccount().isBlocked(this);
531 }
532
533 @Override
534 public boolean isDomainBlocked() {
535 return getAccount().isBlocked(this.getJid().getDomain());
536 }
537
538 @Override
539 public Jid getBlockedJid() {
540 if (isDomainBlocked()) {
541 return getJid().getDomain();
542 } else {
543 return getJid();
544 }
545 }
546
547 public boolean isSelf() {
548 return account.getJid().asBareJid().equals(jid.asBareJid());
549 }
550
551 boolean isOwnServer() {
552 return account.getJid().getDomain().equals(jid.asBareJid());
553 }
554
555 public void setCommonName(String cn) {
556 this.commonName = cn;
557 }
558
559 public void flagActive() {
560 this.mActive = true;
561 }
562
563 public void flagInactive() {
564 this.mActive = false;
565 }
566
567 public boolean isActive() {
568 return this.mActive;
569 }
570
571 public boolean setLastseen(long timestamp) {
572 if (timestamp > this.mLastseen) {
573 this.mLastseen = timestamp;
574 return true;
575 } else {
576 return false;
577 }
578 }
579
580 public long getLastseen() {
581 return this.mLastseen;
582 }
583
584 public void setLastResource(String resource) {
585 this.mLastPresence = resource;
586 }
587
588 public String getLastResource() {
589 return this.mLastPresence;
590 }
591
592 public String getServerName() {
593 return serverName;
594 }
595
596 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
597 setOption(getOption(phoneContact.getClass()));
598 setSystemAccount(phoneContact.getLookupUri());
599 boolean changed = setSystemName(phoneContact.getDisplayName());
600 changed |= setPhotoUri(phoneContact.getPhotoUri());
601 return changed;
602 }
603
604 public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
605 resetOption(getOption(clazz));
606 boolean changed = false;
607 if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
608 setSystemAccount(null);
609 changed |= setPhotoUri(null);
610 changed |= setSystemName(null);
611 }
612 return changed;
613 }
614
615 protected String phoneAccountLabel() {
616 return account.getJid().asBareJid().toString() +
617 "/" + getJid().asBareJid().toString();
618 }
619
620 public PhoneAccountHandle phoneAccountHandle() {
621 ComponentName componentName = new ComponentName(
622 "com.cheogram.android",
623 "com.cheogram.android.ConnectionService"
624 );
625 return new PhoneAccountHandle(componentName, phoneAccountLabel());
626 }
627
628 // This Contact is a gateway to use for voice calls, register it with OS
629 public void registerAsPhoneAccount(XmppConnectionService ctx) {
630 if (Build.VERSION.SDK_INT < 23) return;
631
632 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
633
634 PhoneAccount phoneAccount = PhoneAccount.builder(
635 phoneAccountHandle(),
636 account.getJid().asBareJid().toString()
637 ).setAddress(
638 Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
639 ).setIcon(
640 Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
641 ).setHighlightColor(
642 0x7401CF
643 ).setShortDescription(
644 getJid().asBareJid().toString()
645 ).setCapabilities(
646 PhoneAccount.CAPABILITY_CALL_PROVIDER
647 ).build();
648
649 telecomManager.registerPhoneAccount(phoneAccount);
650 }
651
652 // Unregister any associated PSTN gateway integration
653 public void unregisterAsPhoneAccount(Context ctx) {
654 if (Build.VERSION.SDK_INT < 23) return;
655
656 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
657 telecomManager.unregisterPhoneAccount(phoneAccountHandle());
658 }
659
660 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
661 if (clazz == JabberIdContact.class) {
662 return Options.SYNCED_VIA_ADDRESSBOOK;
663 } else {
664 return Options.SYNCED_VIA_OTHER;
665 }
666 }
667
668 @Override
669 public int getAvatarBackgroundColor() {
670 return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
671 }
672
673 @Override
674 public String getAvatarName() {
675 return getDisplayName();
676 }
677
678 public boolean hasAvatarOrPresenceName() {
679 return (avatar != null && avatar.getFilename() != null) || presenceName != null;
680 }
681
682 public boolean refreshRtpCapability() {
683 final RtpCapability.Capability previous = this.rtpCapability;
684 this.rtpCapability = RtpCapability.check(this, false);
685 return !Objects.equals(previous, this.rtpCapability);
686 }
687
688 public RtpCapability.Capability getRtpCapability() {
689 return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
690 }
691
692 public static final class Options {
693 public static final int TO = 0;
694 public static final int FROM = 1;
695 public static final int ASKING = 2;
696 public static final int PREEMPTIVE_GRANT = 3;
697 public static final int IN_ROSTER = 4;
698 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
699 public static final int DIRTY_PUSH = 6;
700 public static final int DIRTY_DELETE = 7;
701 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
702 public static final int SYNCED_VIA_OTHER = 9;
703 }
704}