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