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