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 if (this.avatar != null && this.avatar.equals(avatar)) {
442 return false;
443 }
444 if (this.avatar != null
445 && this.avatar.origin == Avatar.Origin.PEP
446 && avatar.origin == Avatar.Origin.VCARD) {
447 return false;
448 }
449 this.avatar = avatar;
450 return true;
451 }
452
453 public String getAvatarFilename() {
454 return avatar == null ? null : avatar.getFilename();
455 }
456
457 public Avatar getAvatar() {
458 return avatar;
459 }
460
461 public boolean mutualPresenceSubscription() {
462 return getOption(Options.FROM) && getOption(Options.TO);
463 }
464
465 @Override
466 public boolean isBlocked() {
467 return getAccount().isBlocked(this);
468 }
469
470 @Override
471 public boolean isDomainBlocked() {
472 return getAccount().isBlocked(this.getJid().getDomain());
473 }
474
475 @Override
476 @NonNull
477 public Jid getBlockedJid() {
478 if (isDomainBlocked()) {
479 return getJid().getDomain();
480 } else {
481 return getJid();
482 }
483 }
484
485 public boolean isSelf() {
486 return account.getJid().asBareJid().equals(jid.asBareJid());
487 }
488
489 boolean isOwnServer() {
490 return account.getJid().getDomain().equals(jid.asBareJid());
491 }
492
493 public void setCommonName(String cn) {
494 this.commonName = cn;
495 }
496
497 public void flagActive() {
498 this.mActive = true;
499 }
500
501 public void flagInactive() {
502 this.mActive = false;
503 }
504
505 public boolean isActive() {
506 return this.mActive;
507 }
508
509 public boolean setLastseen(long timestamp) {
510 if (timestamp > this.mLastseen) {
511 this.mLastseen = timestamp;
512 return true;
513 } else {
514 return false;
515 }
516 }
517
518 public long getLastseen() {
519 return this.mLastseen;
520 }
521
522 public void setLastResource(String resource) {
523 this.mLastPresence = resource;
524 }
525
526 public String getLastResource() {
527 return this.mLastPresence;
528 }
529
530 public String getServerName() {
531 return serverName;
532 }
533
534 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
535 setOption(getOption(phoneContact.getClass()));
536 setSystemAccount(phoneContact.getLookupUri());
537 boolean changed = setSystemName(phoneContact.getDisplayName());
538 changed |= setPhotoUri(phoneContact.getPhotoUri());
539 return changed;
540 }
541
542 public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
543 resetOption(getOption(clazz));
544 boolean changed = false;
545 if (!getOption(Options.SYNCED_VIA_ADDRESS_BOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
546 setSystemAccount(null);
547 changed |= setPhotoUri(null);
548 changed |= setSystemName(null);
549 }
550 return changed;
551 }
552
553 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
554 if (clazz == JabberIdContact.class) {
555 return Options.SYNCED_VIA_ADDRESS_BOOK;
556 } else {
557 return Options.SYNCED_VIA_OTHER;
558 }
559 }
560
561 @Override
562 public int getAvatarBackgroundColor() {
563 return UIHelper.getColorForName(
564 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}