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