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