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;
21
22import eu.siacs.conversations.Config;
23import eu.siacs.conversations.R;
24import eu.siacs.conversations.android.AbstractPhoneContact;
25import eu.siacs.conversations.android.JabberIdContact;
26import eu.siacs.conversations.services.QuickConversationsService;
27import eu.siacs.conversations.utils.JidHelper;
28import eu.siacs.conversations.utils.UIHelper;
29import eu.siacs.conversations.xml.Element;
30import eu.siacs.conversations.xmpp.pep.Avatar;
31import eu.siacs.conversations.xmpp.Jid;
32
33public class Contact implements ListItem, Blockable {
34 public static final String TABLENAME = "contacts";
35
36 public static final String SYSTEMNAME = "systemname";
37 public static final String SERVERNAME = "servername";
38 public static final String PRESENCE_NAME = "presence_name";
39 public static final String JID = "jid";
40 public static final String OPTIONS = "options";
41 public static final String SYSTEMACCOUNT = "systemaccount";
42 public static final String PHOTOURI = "photouri";
43 public static final String KEYS = "pgpkey";
44 public static final String ACCOUNT = "accountUuid";
45 public static final String AVATAR = "avatar";
46 public static final String LAST_PRESENCE = "last_presence";
47 public static final String LAST_TIME = "last_time";
48 public static final String GROUPS = "groups";
49 private String accountUuid;
50 private String systemName;
51 private String serverName;
52 private String presenceName;
53 private String commonName;
54 protected Jid jid;
55 private int subscription = 0;
56 private Uri systemAccount;
57 private String photoUri;
58 private final JSONObject keys;
59 private JSONArray groups = new JSONArray();
60 private final Presences presences = new Presences();
61 protected Account account;
62 protected Avatar avatar;
63
64 private boolean mActive = false;
65 private long mLastseen = 0;
66 private String mLastPresence = null;
67
68 public Contact(final String account, final String systemName, final String serverName, final String presenceName,
69 final Jid jid, final int subscription, final String photoUri,
70 final Uri systemAccount, final String keys, final String avatar, final long lastseen,
71 final String presence, final String groups) {
72 this.accountUuid = account;
73 this.systemName = systemName;
74 this.serverName = serverName;
75 this.presenceName = presenceName;
76 this.jid = jid;
77 this.subscription = subscription;
78 this.photoUri = photoUri;
79 this.systemAccount = systemAccount;
80 JSONObject tmpJsonObject;
81 try {
82 tmpJsonObject = (keys == null ? new JSONObject("") : new JSONObject(keys));
83 } catch (JSONException e) {
84 tmpJsonObject = new JSONObject();
85 }
86 this.keys = tmpJsonObject;
87 if (avatar != null) {
88 this.avatar = new Avatar();
89 this.avatar.sha1sum = avatar;
90 this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
91 }
92 try {
93 this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
94 } catch (JSONException e) {
95 this.groups = new JSONArray();
96 }
97 this.mLastseen = lastseen;
98 this.mLastPresence = presence;
99 }
100
101 public Contact(final Jid jid) {
102 this.jid = jid;
103 this.keys = new JSONObject();
104 }
105
106 public static Contact fromCursor(final Cursor cursor) {
107 final Jid jid;
108 try {
109 jid = Jid.of(cursor.getString(cursor.getColumnIndex(JID)));
110 } catch (final IllegalArgumentException e) {
111 // TODO: Borked DB... handle this somehow?
112 return null;
113 }
114 Uri systemAccount;
115 try {
116 systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)));
117 } catch (Exception e) {
118 systemAccount = null;
119 }
120 return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
121 cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
122 cursor.getString(cursor.getColumnIndex(SERVERNAME)),
123 cursor.getString(cursor.getColumnIndex(PRESENCE_NAME)),
124 jid,
125 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
126 cursor.getString(cursor.getColumnIndex(PHOTOURI)),
127 systemAccount,
128 cursor.getString(cursor.getColumnIndex(KEYS)),
129 cursor.getString(cursor.getColumnIndex(AVATAR)),
130 cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
131 cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
132 cursor.getString(cursor.getColumnIndex(GROUPS)));
133 }
134
135 public String getDisplayName() {
136 if (isSelf()) {
137 final String displayName = account.getDisplayName();
138 if (!Strings.isNullOrEmpty(displayName)) {
139 return displayName;
140 }
141 }
142 if (Config.X509_VERIFICATION && !TextUtils.isEmpty(this.commonName)) {
143 return this.commonName;
144 } else if (!TextUtils.isEmpty(this.systemName)) {
145 return this.systemName;
146 } else if (!TextUtils.isEmpty(this.serverName)) {
147 return this.serverName;
148 } else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && JidHelper.isQuicksyDomain(jid.getDomain())) || mutualPresenceSubscription())) {
149 return this.presenceName;
150 } else if (jid.getLocal() != null) {
151 return JidHelper.localPartOrFallback(jid);
152 } else {
153 return jid.getDomain().toEscapedString();
154 }
155 }
156
157 public String getPublicDisplayName() {
158 if (!TextUtils.isEmpty(this.presenceName)) {
159 return this.presenceName;
160 } else if (jid.getLocal() != null) {
161 return JidHelper.localPartOrFallback(jid);
162 } else {
163 return jid.getDomain().toEscapedString();
164 }
165 }
166
167 public String getProfilePhoto() {
168 return this.photoUri;
169 }
170
171 public Jid getJid() {
172 return jid;
173 }
174
175 @Override
176 public List<Tag> getTags(Context context) {
177 final ArrayList<Tag> tags = new ArrayList<>();
178 for (final String group : getGroups(true)) {
179 tags.add(new Tag(group, UIHelper.getColorForName(group)));
180 }
181 Presence.Status status = getShownStatus();
182 if (status != Presence.Status.OFFLINE) {
183 tags.add(UIHelper.getTagForStatus(context, status));
184 }
185 if (isBlocked()) {
186 tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
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 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_ADDRESSBOOK) && !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_ADDRESSBOOK;
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 final class Options {
577 public static final int TO = 0;
578 public static final int FROM = 1;
579 public static final int ASKING = 2;
580 public static final int PREEMPTIVE_GRANT = 3;
581 public static final int IN_ROSTER = 4;
582 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
583 public static final int DIRTY_PUSH = 6;
584 public static final int DIRTY_DELETE = 7;
585 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
586 public static final int SYNCED_VIA_OTHER = 9;
587 }
588}