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(final Context context) {
184 final ArrayList<Tag> tags = new ArrayList<>();
185 for (final String group : getGroups(true)) {
186 tags.add(new Tag(group));
187 }
188 return tags;
189 }
190
191 public boolean match(Context context, String needle) {
192 if (TextUtils.isEmpty(needle)) {
193 return true;
194 }
195 needle = needle.toLowerCase(Locale.US).trim();
196 String[] parts = needle.split("\\s+");
197 if (parts.length > 1) {
198 for (String part : parts) {
199 if (!match(context, part)) {
200 return false;
201 }
202 }
203 return true;
204 } else {
205 return jid.toString().contains(needle) ||
206 getDisplayName().toLowerCase(Locale.US).contains(needle) ||
207 matchInTag(context, needle);
208 }
209 }
210
211 private boolean matchInTag(Context context, String needle) {
212 needle = needle.toLowerCase(Locale.US);
213 for (Tag tag : getTags(context)) {
214 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
215 return true;
216 }
217 }
218 return false;
219 }
220
221 public ContentValues getContentValues() {
222 synchronized (this.keys) {
223 final ContentValues values = new ContentValues();
224 values.put(ACCOUNT, accountUuid);
225 values.put(SYSTEMNAME, systemName);
226 values.put(SERVERNAME, serverName);
227 values.put(PRESENCE_NAME, presenceName);
228 values.put(JID, jid.toString());
229 values.put(OPTIONS, subscription);
230 values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
231 values.put(PHOTOURI, photoUri);
232 values.put(KEYS, keys.toString());
233 values.put(AVATAR, avatar == null ? null : avatar.getFilename());
234 values.put(LAST_PRESENCE, mLastPresence);
235 values.put(LAST_TIME, mLastseen);
236 values.put(GROUPS, groups.toString());
237 values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString());
238 return values;
239 }
240 }
241
242 public Account getAccount() {
243 return this.account;
244 }
245
246 public void setAccount(Account account) {
247 this.account = account;
248 this.accountUuid = account.getUuid();
249 }
250
251 public Presences getPresences() {
252 return this.presences;
253 }
254
255 public void updatePresence(final String resource, final Presence presence) {
256 this.presences.updatePresence(resource, presence);
257 }
258
259 public void removePresence(final String resource) {
260 this.presences.removePresence(resource);
261 }
262
263 public void clearPresences() {
264 this.presences.clearPresences();
265 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
266 }
267
268 public Presence.Status getShownStatus() {
269 return this.presences.getShownStatus();
270 }
271
272 public boolean setPhotoUri(String uri) {
273 if (uri != null && !uri.equals(this.photoUri)) {
274 this.photoUri = uri;
275 return true;
276 } else if (this.photoUri != null && uri == null) {
277 this.photoUri = null;
278 return true;
279 } else {
280 return false;
281 }
282 }
283
284 public void setServerName(String serverName) {
285 this.serverName = serverName;
286 }
287
288 public boolean setSystemName(String systemName) {
289 final String old = getDisplayName();
290 this.systemName = systemName;
291 return !old.equals(getDisplayName());
292 }
293
294 public boolean setPresenceName(String presenceName) {
295 final String old = getDisplayName();
296 this.presenceName = presenceName;
297 return !old.equals(getDisplayName());
298 }
299
300 public Uri getSystemAccount() {
301 return systemAccount;
302 }
303
304 public void setSystemAccount(Uri lookupUri) {
305 this.systemAccount = lookupUri;
306 }
307
308 private Collection<String> getGroups(final boolean unique) {
309 final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
310 for (int i = 0; i < this.groups.length(); ++i) {
311 try {
312 groups.add(this.groups.getString(i));
313 } catch (final JSONException ignored) {
314 }
315 }
316 return groups;
317 }
318
319 public long getPgpKeyId() {
320 synchronized (this.keys) {
321 if (this.keys.has("pgp_keyid")) {
322 try {
323 return this.keys.getLong("pgp_keyid");
324 } catch (JSONException e) {
325 return 0;
326 }
327 } else {
328 return 0;
329 }
330 }
331 }
332
333 public boolean setPgpKeyId(long keyId) {
334 final long previousKeyId = getPgpKeyId();
335 synchronized (this.keys) {
336 try {
337 this.keys.put("pgp_keyid", keyId);
338 return previousKeyId != keyId;
339 } catch (final JSONException ignored) {
340 }
341 }
342 return false;
343 }
344
345 public void setOption(int option) {
346 this.subscription |= 1 << option;
347 }
348
349 public void resetOption(int option) {
350 this.subscription &= ~(1 << option);
351 }
352
353 public boolean getOption(int option) {
354 return ((this.subscription & (1 << option)) != 0);
355 }
356
357 public boolean showInRoster() {
358 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
359 .getOption(Contact.Options.DIRTY_DELETE)))
360 || (this.getOption(Contact.Options.DIRTY_PUSH));
361 }
362
363 public boolean showInContactList() {
364 return showInRoster()
365 || getOption(Options.SYNCED_VIA_OTHER)
366 || (QuickConversationsService.isQuicksy() && systemAccount != null);
367 }
368
369 public void parseSubscriptionFromElement(Element item) {
370 String ask = item.getAttribute("ask");
371 String subscription = item.getAttribute("subscription");
372
373 if (subscription == null) {
374 this.resetOption(Options.FROM);
375 this.resetOption(Options.TO);
376 } else {
377 switch (subscription) {
378 case "to":
379 this.resetOption(Options.FROM);
380 this.setOption(Options.TO);
381 break;
382 case "from":
383 this.resetOption(Options.TO);
384 this.setOption(Options.FROM);
385 this.resetOption(Options.PREEMPTIVE_GRANT);
386 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
387 break;
388 case "both":
389 this.setOption(Options.TO);
390 this.setOption(Options.FROM);
391 this.resetOption(Options.PREEMPTIVE_GRANT);
392 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
393 break;
394 case "none":
395 this.resetOption(Options.FROM);
396 this.resetOption(Options.TO);
397 break;
398 }
399 }
400
401 // do NOT override asking if pending push request
402 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
403 if ((ask != null) && (ask.equals("subscribe"))) {
404 this.setOption(Contact.Options.ASKING);
405 } else {
406 this.resetOption(Contact.Options.ASKING);
407 }
408 }
409 }
410
411 public void parseGroupsFromElement(Element item) {
412 this.groups = new JSONArray();
413 for (Element element : item.getChildren()) {
414 if (element.getName().equals("group") && element.getContent() != null) {
415 this.groups.put(element.getContent());
416 }
417 }
418 }
419
420 public Element asElement() {
421 final Element item = new Element("item");
422 item.setAttribute("jid", this.jid);
423 if (this.serverName != null) {
424 item.setAttribute("name", this.serverName);
425 }
426 for (String group : getGroups(false)) {
427 item.addChild("group").setContent(group);
428 }
429 return item;
430 }
431
432 @Override
433 public int compareTo(@NonNull final ListItem another) {
434 return this.getDisplayName().compareToIgnoreCase(
435 another.getDisplayName());
436 }
437
438 public String getServer() {
439 return getJid().getDomain().toEscapedString();
440 }
441
442 public void setAvatar(Avatar avatar) {
443 setAvatar(avatar, false);
444 }
445
446 public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
447 if (this.avatar != null && this.avatar.equals(avatar)) {
448 return;
449 }
450 if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
451 return;
452 }
453 this.avatar = avatar;
454 }
455
456 public String getAvatarFilename() {
457 return avatar == null ? null : avatar.getFilename();
458 }
459
460 public Avatar getAvatar() {
461 return avatar;
462 }
463
464 public boolean mutualPresenceSubscription() {
465 return getOption(Options.FROM) && getOption(Options.TO);
466 }
467
468 @Override
469 public boolean isBlocked() {
470 return getAccount().isBlocked(this);
471 }
472
473 @Override
474 public boolean isDomainBlocked() {
475 return getAccount().isBlocked(this.getJid().getDomain());
476 }
477
478 @Override
479 public Jid getBlockedJid() {
480 if (isDomainBlocked()) {
481 return getJid().getDomain();
482 } else {
483 return getJid();
484 }
485 }
486
487 public boolean isSelf() {
488 return account.getJid().asBareJid().equals(jid.asBareJid());
489 }
490
491 boolean isOwnServer() {
492 return account.getJid().getDomain().equals(jid.asBareJid());
493 }
494
495 public void setCommonName(String cn) {
496 this.commonName = cn;
497 }
498
499 public void flagActive() {
500 this.mActive = true;
501 }
502
503 public void flagInactive() {
504 this.mActive = false;
505 }
506
507 public boolean isActive() {
508 return this.mActive;
509 }
510
511 public boolean setLastseen(long timestamp) {
512 if (timestamp > this.mLastseen) {
513 this.mLastseen = timestamp;
514 return true;
515 } else {
516 return false;
517 }
518 }
519
520 public long getLastseen() {
521 return this.mLastseen;
522 }
523
524 public void setLastResource(String resource) {
525 this.mLastPresence = resource;
526 }
527
528 public String getLastResource() {
529 return this.mLastPresence;
530 }
531
532 public String getServerName() {
533 return serverName;
534 }
535
536 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
537 setOption(getOption(phoneContact.getClass()));
538 setSystemAccount(phoneContact.getLookupUri());
539 boolean changed = setSystemName(phoneContact.getDisplayName());
540 changed |= setPhotoUri(phoneContact.getPhotoUri());
541 return changed;
542 }
543
544 public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
545 resetOption(getOption(clazz));
546 boolean changed = false;
547 if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
548 setSystemAccount(null);
549 changed |= setPhotoUri(null);
550 changed |= setSystemName(null);
551 }
552 return changed;
553 }
554
555 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
556 if (clazz == JabberIdContact.class) {
557 return Options.SYNCED_VIA_ADDRESSBOOK;
558 } else {
559 return Options.SYNCED_VIA_OTHER;
560 }
561 }
562
563 @Override
564 public int getAvatarBackgroundColor() {
565 return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
566 }
567
568 @Override
569 public String getAvatarName() {
570 return getDisplayName();
571 }
572
573 public boolean hasAvatarOrPresenceName() {
574 return (avatar != null && avatar.getFilename() != null) || presenceName != null;
575 }
576
577 public boolean refreshRtpCapability() {
578 final RtpCapability.Capability previous = this.rtpCapability;
579 this.rtpCapability = RtpCapability.check(this, false);
580 return !Objects.equals(previous, this.rtpCapability);
581 }
582
583 public RtpCapability.Capability getRtpCapability() {
584 return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
585 }
586
587 public static final class Options {
588 public static final int TO = 0;
589 public static final int FROM = 1;
590 public static final int ASKING = 2;
591 public static final int PREEMPTIVE_GRANT = 3;
592 public static final int IN_ROSTER = 4;
593 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
594 public static final int DIRTY_PUSH = 6;
595 public static final int DIRTY_DELETE = 7;
596 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
597 public static final int SYNCED_VIA_OTHER = 9;
598 }
599}