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