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