1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.database.Cursor;
6import android.net.Uri;
7import androidx.annotation.NonNull;
8import android.text.TextUtils;
9
10import com.google.common.base.Strings;
11
12import org.json.JSONArray;
13import org.json.JSONException;
14import org.json.JSONObject;
15
16import java.util.ArrayList;
17import java.util.Collection;
18import java.util.HashSet;
19import java.util.List;
20import java.util.Locale;
21import java.util.Objects;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.R;
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.jingle.RtpCapability;
32import eu.siacs.conversations.xmpp.pep.Avatar;
33import eu.siacs.conversations.xmpp.Jid;
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(Context context) {
183 final ArrayList<Tag> tags = new ArrayList<>();
184 for (final String group : getGroups(true)) {
185 tags.add(new Tag(group, UIHelper.getColorForName(group)));
186 }
187 Presence.Status status = getShownStatus();
188 if (status != Presence.Status.OFFLINE) {
189 tags.add(UIHelper.getTagForStatus(context, status));
190 }
191 if (isBlocked()) {
192 tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
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 == null ? null : avatar.getFilename());
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 Presence.Status 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 private 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) && (!this
365 .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 public Element asElement() {
427 final Element item = new Element("item");
428 item.setAttribute("jid", this.jid);
429 if (this.serverName != null) {
430 item.setAttribute("name", this.serverName);
431 }
432 for (String group : getGroups(false)) {
433 item.addChild("group").setContent(group);
434 }
435 return item;
436 }
437
438 @Override
439 public int compareTo(@NonNull final ListItem another) {
440 return this.getDisplayName().compareToIgnoreCase(
441 another.getDisplayName());
442 }
443
444 public String getServer() {
445 return getJid().getDomain().toEscapedString();
446 }
447
448 public void setAvatar(Avatar avatar) {
449 setAvatar(avatar, false);
450 }
451
452 public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
453 if (this.avatar != null && this.avatar.equals(avatar)) {
454 return;
455 }
456 if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
457 return;
458 }
459 this.avatar = avatar;
460 }
461
462 public String getAvatarFilename() {
463 return avatar == null ? null : avatar.getFilename();
464 }
465
466 public Avatar getAvatar() {
467 return avatar;
468 }
469
470 public boolean mutualPresenceSubscription() {
471 return getOption(Options.FROM) && getOption(Options.TO);
472 }
473
474 @Override
475 public boolean isBlocked() {
476 return getAccount().isBlocked(this);
477 }
478
479 @Override
480 public boolean isDomainBlocked() {
481 return getAccount().isBlocked(this.getJid().getDomain());
482 }
483
484 @Override
485 public Jid getBlockedJid() {
486 if (isDomainBlocked()) {
487 return getJid().getDomain();
488 } else {
489 return getJid();
490 }
491 }
492
493 public boolean isSelf() {
494 return account.getJid().asBareJid().equals(jid.asBareJid());
495 }
496
497 boolean isOwnServer() {
498 return account.getJid().getDomain().equals(jid.asBareJid());
499 }
500
501 public void setCommonName(String cn) {
502 this.commonName = cn;
503 }
504
505 public void flagActive() {
506 this.mActive = true;
507 }
508
509 public void flagInactive() {
510 this.mActive = false;
511 }
512
513 public boolean isActive() {
514 return this.mActive;
515 }
516
517 public boolean setLastseen(long timestamp) {
518 if (timestamp > this.mLastseen) {
519 this.mLastseen = timestamp;
520 return true;
521 } else {
522 return false;
523 }
524 }
525
526 public long getLastseen() {
527 return this.mLastseen;
528 }
529
530 public void setLastResource(String resource) {
531 this.mLastPresence = resource;
532 }
533
534 public String getLastResource() {
535 return this.mLastPresence;
536 }
537
538 public String getServerName() {
539 return serverName;
540 }
541
542 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
543 setOption(getOption(phoneContact.getClass()));
544 setSystemAccount(phoneContact.getLookupUri());
545 boolean changed = setSystemName(phoneContact.getDisplayName());
546 changed |= setPhotoUri(phoneContact.getPhotoUri());
547 return changed;
548 }
549
550 public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
551 resetOption(getOption(clazz));
552 boolean changed = false;
553 if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
554 setSystemAccount(null);
555 changed |= setPhotoUri(null);
556 changed |= setSystemName(null);
557 }
558 return changed;
559 }
560
561 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
562 if (clazz == JabberIdContact.class) {
563 return Options.SYNCED_VIA_ADDRESSBOOK;
564 } else {
565 return Options.SYNCED_VIA_OTHER;
566 }
567 }
568
569 @Override
570 public int getAvatarBackgroundColor() {
571 return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
572 }
573
574 @Override
575 public String getAvatarName() {
576 return getDisplayName();
577 }
578
579 public boolean hasAvatarOrPresenceName() {
580 return (avatar != null && avatar.getFilename() != null) || presenceName != null;
581 }
582
583 public boolean refreshRtpCapability() {
584 final RtpCapability.Capability previous = this.rtpCapability;
585 this.rtpCapability = RtpCapability.check(this, false);
586 return !Objects.equals(previous, this.rtpCapability);
587 }
588
589 public RtpCapability.Capability getRtpCapability() {
590
591 return 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}