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 void setAvatar(Avatar avatar) {
452 setAvatar(avatar, false);
453 }
454
455 public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
456 if (this.avatar != null && this.avatar.equals(avatar)) {
457 return;
458 }
459 if (!previouslyOmittedPepFetch
460 && this.avatar != null
461 && this.avatar.origin == Avatar.Origin.PEP
462 && 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_ADDRESS_BOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
560 setSystemAccount(null);
561 changed |= setPhotoUri(null);
562 changed |= setSystemName(null);
563 }
564 return changed;
565 }
566
567 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
568 if (clazz == JabberIdContact.class) {
569 return Options.SYNCED_VIA_ADDRESS_BOOK;
570 } else {
571 return Options.SYNCED_VIA_OTHER;
572 }
573 }
574
575 @Override
576 public int getAvatarBackgroundColor() {
577 return UIHelper.getColorForName(
578 jid != null ? jid.asBareJid().toString() : getDisplayName());
579 }
580
581 @Override
582 public String getAvatarName() {
583 return getDisplayName();
584 }
585
586 public boolean hasAvatarOrPresenceName() {
587 return (avatar != null && avatar.getFilename() != null) || presenceName != null;
588 }
589
590 public boolean refreshRtpCapability() {
591 final RtpCapability.Capability previous = this.rtpCapability;
592 this.rtpCapability = RtpCapability.check(this, false);
593 return !Objects.equals(previous, this.rtpCapability);
594 }
595
596 public RtpCapability.Capability getRtpCapability() {
597 return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
598 }
599
600 public static final class Options {
601 public static final int TO = 0;
602 public static final int FROM = 1;
603 public static final int ASKING = 2;
604 public static final int PREEMPTIVE_GRANT = 3;
605 public static final int IN_ROSTER = 4;
606 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
607 public static final int DIRTY_PUSH = 6;
608 public static final int DIRTY_DELETE = 7;
609 private static final int SYNCED_VIA_ADDRESS_BOOK = 8;
610 public static final int SYNCED_VIA_OTHER = 9;
611 }
612}