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 if (!showInRoster() && getSystemAccount() != null) {
204 tags.add(new Tag("Android", UIHelper.getColorForName("Android")));
205 }
206 return tags;
207 }
208
209 public boolean match(Context context, String needle) {
210 if (TextUtils.isEmpty(needle)) {
211 return true;
212 }
213 needle = needle.toLowerCase(Locale.US).trim();
214 String[] parts = needle.split("\\s+");
215 if (parts.length > 1) {
216 for (String part : parts) {
217 if (!match(context, part)) {
218 return false;
219 }
220 }
221 return true;
222 } else {
223 return jid.toString().contains(needle) ||
224 getDisplayName().toLowerCase(Locale.US).contains(needle) ||
225 matchInTag(context, needle);
226 }
227 }
228
229 private boolean matchInTag(Context context, String needle) {
230 needle = needle.toLowerCase(Locale.US);
231 for (Tag tag : getTags(context)) {
232 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
233 return true;
234 }
235 }
236 return false;
237 }
238
239 public ContentValues getContentValues() {
240 synchronized (this.keys) {
241 final ContentValues values = new ContentValues();
242 values.put(ACCOUNT, accountUuid);
243 values.put(SYSTEMNAME, systemName);
244 values.put(SERVERNAME, serverName);
245 values.put(PRESENCE_NAME, presenceName);
246 values.put(JID, jid.toString());
247 values.put(OPTIONS, subscription);
248 values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
249 values.put(PHOTOURI, photoUri);
250 values.put(KEYS, keys.toString());
251 values.put(AVATAR, avatar == null ? null : avatar.getFilename());
252 values.put(LAST_PRESENCE, mLastPresence);
253 values.put(LAST_TIME, mLastseen);
254 values.put(GROUPS, groups.toString());
255 values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString());
256 return values;
257 }
258 }
259
260 public Account getAccount() {
261 return this.account;
262 }
263
264 public void setAccount(Account account) {
265 this.account = account;
266 this.accountUuid = account.getUuid();
267 }
268
269 public Presences getPresences() {
270 return this.presences;
271 }
272
273 public void updatePresence(final String resource, final Presence presence) {
274 this.presences.updatePresence(resource, presence);
275 }
276
277 public void removePresence(final String resource) {
278 this.presences.removePresence(resource);
279 }
280
281 public void clearPresences() {
282 this.presences.clearPresences();
283 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
284 }
285
286 public Presence.Status getShownStatus() {
287 return this.presences.getShownStatus();
288 }
289
290 public boolean setPhotoUri(String uri) {
291 if (uri != null && !uri.equals(this.photoUri)) {
292 this.photoUri = uri;
293 return true;
294 } else if (this.photoUri != null && uri == null) {
295 this.photoUri = null;
296 return true;
297 } else {
298 return false;
299 }
300 }
301
302 public void setServerName(String serverName) {
303 this.serverName = serverName;
304 }
305
306 public boolean setSystemName(String systemName) {
307 final String old = getDisplayName();
308 this.systemName = systemName;
309 return !old.equals(getDisplayName());
310 }
311
312 public boolean setPresenceName(String presenceName) {
313 final String old = getDisplayName();
314 this.presenceName = presenceName;
315 return !old.equals(getDisplayName());
316 }
317
318 public Uri getSystemAccount() {
319 return systemAccount;
320 }
321
322 public void setSystemAccount(Uri lookupUri) {
323 this.systemAccount = lookupUri;
324 }
325
326 private Collection<String> getGroups(final boolean unique) {
327 final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
328 for (int i = 0; i < this.groups.length(); ++i) {
329 try {
330 groups.add(this.groups.getString(i));
331 } catch (final JSONException ignored) {
332 }
333 }
334 return groups;
335 }
336
337 public long getPgpKeyId() {
338 synchronized (this.keys) {
339 if (this.keys.has("pgp_keyid")) {
340 try {
341 return this.keys.getLong("pgp_keyid");
342 } catch (JSONException e) {
343 return 0;
344 }
345 } else {
346 return 0;
347 }
348 }
349 }
350
351 public boolean setPgpKeyId(long keyId) {
352 final long previousKeyId = getPgpKeyId();
353 synchronized (this.keys) {
354 try {
355 this.keys.put("pgp_keyid", keyId);
356 return previousKeyId != keyId;
357 } catch (final JSONException ignored) {
358 }
359 }
360 return false;
361 }
362
363 public void setOption(int option) {
364 this.subscription |= 1 << option;
365 }
366
367 public void resetOption(int option) {
368 this.subscription &= ~(1 << option);
369 }
370
371 public boolean getOption(int option) {
372 return ((this.subscription & (1 << option)) != 0);
373 }
374
375 public boolean showInRoster() {
376 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
377 .getOption(Contact.Options.DIRTY_DELETE)))
378 || (this.getOption(Contact.Options.DIRTY_PUSH));
379 }
380
381 public boolean showInContactList() {
382 return showInRoster()
383 || getOption(Options.SYNCED_VIA_OTHER)
384 || (QuickConversationsService.isQuicksy() && systemAccount != null);
385 }
386
387 public void parseSubscriptionFromElement(Element item) {
388 String ask = item.getAttribute("ask");
389 String subscription = item.getAttribute("subscription");
390
391 if (subscription == null) {
392 this.resetOption(Options.FROM);
393 this.resetOption(Options.TO);
394 } else {
395 switch (subscription) {
396 case "to":
397 this.resetOption(Options.FROM);
398 this.setOption(Options.TO);
399 break;
400 case "from":
401 this.resetOption(Options.TO);
402 this.setOption(Options.FROM);
403 this.resetOption(Options.PREEMPTIVE_GRANT);
404 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
405 break;
406 case "both":
407 this.setOption(Options.TO);
408 this.setOption(Options.FROM);
409 this.resetOption(Options.PREEMPTIVE_GRANT);
410 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
411 break;
412 case "none":
413 this.resetOption(Options.FROM);
414 this.resetOption(Options.TO);
415 break;
416 }
417 }
418
419 // do NOT override asking if pending push request
420 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
421 if ((ask != null) && (ask.equals("subscribe"))) {
422 this.setOption(Contact.Options.ASKING);
423 } else {
424 this.resetOption(Contact.Options.ASKING);
425 }
426 }
427 }
428
429 public void parseGroupsFromElement(Element item) {
430 this.groups = new JSONArray();
431 for (Element element : item.getChildren()) {
432 if (element.getName().equals("group") && element.getContent() != null) {
433 this.groups.put(element.getContent());
434 }
435 }
436 }
437
438 public Element asElement() {
439 final Element item = new Element("item");
440 item.setAttribute("jid", this.jid);
441 if (this.serverName != null) {
442 item.setAttribute("name", this.serverName);
443 }
444 for (String group : getGroups(false)) {
445 item.addChild("group").setContent(group);
446 }
447 return item;
448 }
449
450 @Override
451 public int compareTo(@NonNull final ListItem another) {
452 return this.getDisplayName().compareToIgnoreCase(
453 another.getDisplayName());
454 }
455
456 public String getServer() {
457 return getJid().getDomain().toEscapedString();
458 }
459
460 public void setAvatar(Avatar avatar) {
461 setAvatar(avatar, false);
462 }
463
464 public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
465 if (this.avatar != null && this.avatar.equals(avatar)) {
466 return;
467 }
468 if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
469 return;
470 }
471 this.avatar = avatar;
472 }
473
474 public String getAvatarFilename() {
475 return avatar == null ? null : avatar.getFilename();
476 }
477
478 public Avatar getAvatar() {
479 return avatar;
480 }
481
482 public boolean mutualPresenceSubscription() {
483 return getOption(Options.FROM) && getOption(Options.TO);
484 }
485
486 @Override
487 public boolean isBlocked() {
488 return getAccount().isBlocked(this);
489 }
490
491 @Override
492 public boolean isDomainBlocked() {
493 return getAccount().isBlocked(this.getJid().getDomain());
494 }
495
496 @Override
497 public Jid getBlockedJid() {
498 if (isDomainBlocked()) {
499 return getJid().getDomain();
500 } else {
501 return getJid();
502 }
503 }
504
505 public boolean isSelf() {
506 return account.getJid().asBareJid().equals(jid.asBareJid());
507 }
508
509 boolean isOwnServer() {
510 return account.getJid().getDomain().equals(jid.asBareJid());
511 }
512
513 public void setCommonName(String cn) {
514 this.commonName = cn;
515 }
516
517 public void flagActive() {
518 this.mActive = true;
519 }
520
521 public void flagInactive() {
522 this.mActive = false;
523 }
524
525 public boolean isActive() {
526 return this.mActive;
527 }
528
529 public boolean setLastseen(long timestamp) {
530 if (timestamp > this.mLastseen) {
531 this.mLastseen = timestamp;
532 return true;
533 } else {
534 return false;
535 }
536 }
537
538 public long getLastseen() {
539 return this.mLastseen;
540 }
541
542 public void setLastResource(String resource) {
543 this.mLastPresence = resource;
544 }
545
546 public String getLastResource() {
547 return this.mLastPresence;
548 }
549
550 public String getServerName() {
551 return serverName;
552 }
553
554 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
555 setOption(getOption(phoneContact.getClass()));
556 setSystemAccount(phoneContact.getLookupUri());
557 boolean changed = setSystemName(phoneContact.getDisplayName());
558 changed |= setPhotoUri(phoneContact.getPhotoUri());
559 return changed;
560 }
561
562 public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
563 resetOption(getOption(clazz));
564 boolean changed = false;
565 if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
566 setSystemAccount(null);
567 changed |= setPhotoUri(null);
568 changed |= setSystemName(null);
569 }
570 return changed;
571 }
572
573 protected String phoneAccountLabel() {
574 return account.getJid().asBareJid().toString() +
575 "/" + getJid().asBareJid().toString();
576 }
577
578 protected PhoneAccountHandle phoneAccountHandle() {
579 ComponentName componentName = new ComponentName(
580 "com.cheogram.android",
581 "com.cheogram.android.ConnectionService"
582 );
583 return new PhoneAccountHandle(componentName, phoneAccountLabel());
584 }
585
586 // This Contact is a gateway to use for voice calls, register it with OS
587 public void registerAsPhoneAccount(XmppConnectionService ctx) {
588 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
589
590 PhoneAccount phoneAccount = PhoneAccount.builder(
591 phoneAccountHandle(),
592 account.getJid().asBareJid().toString()
593 ).setAddress(
594 Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
595 ).setIcon(
596 Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
597 ).setHighlightColor(
598 0x7401CF
599 ).setShortDescription(
600 getJid().asBareJid().toString()
601 ).setCapabilities(
602 PhoneAccount.CAPABILITY_CALL_PROVIDER
603 ).build();
604
605 telecomManager.registerPhoneAccount(phoneAccount);
606 }
607
608 // Unregister any associated PSTN gateway integration
609 public void unregisterAsPhoneAccount(Context ctx) {
610 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
611 telecomManager.unregisterPhoneAccount(phoneAccountHandle());
612 }
613
614 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
615 if (clazz == JabberIdContact.class) {
616 return Options.SYNCED_VIA_ADDRESSBOOK;
617 } else {
618 return Options.SYNCED_VIA_OTHER;
619 }
620 }
621
622 @Override
623 public int getAvatarBackgroundColor() {
624 return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
625 }
626
627 @Override
628 public String getAvatarName() {
629 return getDisplayName();
630 }
631
632 public boolean hasAvatarOrPresenceName() {
633 return (avatar != null && avatar.getFilename() != null) || presenceName != null;
634 }
635
636 public boolean refreshRtpCapability() {
637 final RtpCapability.Capability previous = this.rtpCapability;
638 this.rtpCapability = RtpCapability.check(this, false);
639 return !Objects.equals(previous, this.rtpCapability);
640 }
641
642 public RtpCapability.Capability getRtpCapability() {
643 return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
644 }
645
646 public static final class Options {
647 public static final int TO = 0;
648 public static final int FROM = 1;
649 public static final int ASKING = 2;
650 public static final int PREEMPTIVE_GRANT = 3;
651 public static final int IN_ROSTER = 4;
652 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
653 public static final int DIRTY_PUSH = 6;
654 public static final int DIRTY_DELETE = 7;
655 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
656 public static final int SYNCED_VIA_OTHER = 9;
657 }
658}