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