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