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