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 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 public String getProfilePhoto() {
125 return this.photoUri;
126 }
127
128 public Jid getJid() {
129 return jid;
130 }
131
132 @Override
133 public List<Tag> getTags() {
134 final ArrayList<Tag> tags = new ArrayList<>();
135 for (final String group : getGroups()) {
136 tags.add(new Tag(group, UIHelper.getColorForName(group)));
137 }
138 switch (getMostAvailableStatus()) {
139 case CHAT:
140 case ONLINE:
141 tags.add(new Tag("online", 0xff259b24));
142 break;
143 case AWAY:
144 tags.add(new Tag("away", 0xffff9800));
145 break;
146 case XA:
147 tags.add(new Tag("not available", 0xfff44336));
148 break;
149 case DND:
150 tags.add(new Tag("dnd", 0xfff44336));
151 break;
152 }
153 if (isBlocked()) {
154 tags.add(new Tag("blocked", 0xff2e2f3b));
155 }
156 return tags;
157 }
158
159 public boolean match(String needle) {
160 if (needle == null || needle.isEmpty()) {
161 return true;
162 }
163 needle = needle.toLowerCase(Locale.US).trim();
164 String[] parts = needle.split("\\s+");
165 if (parts.length > 1) {
166 for(int i = 0; i < parts.length; ++i) {
167 if (!match(parts[i])) {
168 return false;
169 }
170 }
171 return true;
172 } else {
173 return jid.toString().contains(needle) ||
174 getDisplayName().toLowerCase(Locale.US).contains(needle) ||
175 matchInTag(needle);
176 }
177 }
178
179 private boolean matchInTag(String needle) {
180 needle = needle.toLowerCase(Locale.US);
181 for (Tag tag : getTags()) {
182 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
183 return true;
184 }
185 }
186 return false;
187 }
188
189 public ContentValues getContentValues() {
190 synchronized (this.keys) {
191 final ContentValues values = new ContentValues();
192 values.put(ACCOUNT, accountUuid);
193 values.put(SYSTEMNAME, systemName);
194 values.put(SERVERNAME, serverName);
195 values.put(JID, jid.toString());
196 values.put(OPTIONS, subscription);
197 values.put(SYSTEMACCOUNT, systemAccount);
198 values.put(PHOTOURI, photoUri);
199 values.put(KEYS, keys.toString());
200 values.put(AVATAR, avatar == null ? null : avatar.getFilename());
201 values.put(LAST_PRESENCE, lastseen.presence);
202 values.put(LAST_TIME, lastseen.time);
203 values.put(GROUPS, groups.toString());
204 return values;
205 }
206 }
207
208 public int getSubscription() {
209 return this.subscription;
210 }
211
212 public Account getAccount() {
213 return this.account;
214 }
215
216 public void setAccount(Account account) {
217 this.account = account;
218 this.accountUuid = account.getUuid();
219 }
220
221 public Presences getPresences() {
222 return this.presences;
223 }
224
225 public void setPresences(Presences pres) {
226 this.presences = pres;
227 }
228
229 public void updatePresence(final String resource, final Presence presence) {
230 this.presences.updatePresence(resource, presence);
231 }
232
233 public void removePresence(final String resource) {
234 this.presences.removePresence(resource);
235 }
236
237 public void clearPresences() {
238 this.presences.clearPresences();
239 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
240 }
241
242 public Presence.Status getMostAvailableStatus() {
243 Presence p = this.presences.getMostAvailablePresence();
244 if (p == null) {
245 return Presence.Status.OFFLINE;
246 }
247
248 return p.getStatus();
249 }
250
251 public boolean setPhotoUri(String uri) {
252 if (uri != null && !uri.equals(this.photoUri)) {
253 this.photoUri = uri;
254 return true;
255 } else if (this.photoUri != null && uri == null) {
256 this.photoUri = null;
257 return true;
258 } else {
259 return false;
260 }
261 }
262
263 public void setServerName(String serverName) {
264 this.serverName = serverName;
265 }
266
267 public void setSystemName(String systemName) {
268 this.systemName = systemName;
269 }
270
271 public void setPresenceName(String presenceName) {
272 this.presenceName = presenceName;
273 }
274
275 public String getSystemAccount() {
276 return systemAccount;
277 }
278
279 public void setSystemAccount(String account) {
280 this.systemAccount = account;
281 }
282
283 public List<String> getGroups() {
284 ArrayList<String> groups = new ArrayList<String>();
285 for (int i = 0; i < this.groups.length(); ++i) {
286 try {
287 groups.add(this.groups.getString(i));
288 } catch (final JSONException ignored) {
289 }
290 }
291 return groups;
292 }
293
294 public ArrayList<String> getOtrFingerprints() {
295 synchronized (this.keys) {
296 final ArrayList<String> fingerprints = new ArrayList<String>();
297 try {
298 if (this.keys.has("otr_fingerprints")) {
299 final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
300 for (int i = 0; i < prints.length(); ++i) {
301 final String print = prints.isNull(i) ? null : prints.getString(i);
302 if (print != null && !print.isEmpty()) {
303 fingerprints.add(prints.getString(i));
304 }
305 }
306 }
307 } catch (final JSONException ignored) {
308
309 }
310 return fingerprints;
311 }
312 }
313 public boolean addOtrFingerprint(String print) {
314 synchronized (this.keys) {
315 if (getOtrFingerprints().contains(print)) {
316 return false;
317 }
318 try {
319 JSONArray fingerprints;
320 if (!this.keys.has("otr_fingerprints")) {
321 fingerprints = new JSONArray();
322 } else {
323 fingerprints = this.keys.getJSONArray("otr_fingerprints");
324 }
325 fingerprints.put(print);
326 this.keys.put("otr_fingerprints", fingerprints);
327 return true;
328 } catch (final JSONException ignored) {
329 return false;
330 }
331 }
332 }
333
334 public long getPgpKeyId() {
335 synchronized (this.keys) {
336 if (this.keys.has("pgp_keyid")) {
337 try {
338 return this.keys.getLong("pgp_keyid");
339 } catch (JSONException e) {
340 return 0;
341 }
342 } else {
343 return 0;
344 }
345 }
346 }
347
348 public void setPgpKeyId(long keyId) {
349 synchronized (this.keys) {
350 try {
351 this.keys.put("pgp_keyid", keyId);
352 } catch (final JSONException ignored) {
353 }
354 }
355 }
356
357 public void setOption(int option) {
358 this.subscription |= 1 << option;
359 }
360
361 public void resetOption(int option) {
362 this.subscription &= ~(1 << option);
363 }
364
365 public boolean getOption(int option) {
366 return ((this.subscription & (1 << option)) != 0);
367 }
368
369 public boolean showInRoster() {
370 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
371 .getOption(Contact.Options.DIRTY_DELETE)))
372 || (this.getOption(Contact.Options.DIRTY_PUSH));
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 switch (subscription) {
381 case "to":
382 this.resetOption(Options.FROM);
383 this.setOption(Options.TO);
384 break;
385 case "from":
386 this.resetOption(Options.TO);
387 this.setOption(Options.FROM);
388 this.resetOption(Options.PREEMPTIVE_GRANT);
389 break;
390 case "both":
391 this.setOption(Options.TO);
392 this.setOption(Options.FROM);
393 this.resetOption(Options.PREEMPTIVE_GRANT);
394 break;
395 case "none":
396 this.resetOption(Options.FROM);
397 this.resetOption(Options.TO);
398 break;
399 }
400 }
401
402 // do NOT override asking if pending push request
403 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
404 if ((ask != null) && (ask.equals("subscribe"))) {
405 this.setOption(Contact.Options.ASKING);
406 } else {
407 this.resetOption(Contact.Options.ASKING);
408 }
409 }
410 }
411
412 public void parseGroupsFromElement(Element item) {
413 this.groups = new JSONArray();
414 for (Element element : item.getChildren()) {
415 if (element.getName().equals("group") && element.getContent() != null) {
416 this.groups.put(element.getContent());
417 }
418 }
419 }
420
421 public Element asElement() {
422 final Element item = new Element("item");
423 item.setAttribute("jid", this.jid.toString());
424 if (this.serverName != null) {
425 item.setAttribute("name", this.serverName);
426 }
427 for (String group : getGroups()) {
428 item.addChild("group").setContent(group);
429 }
430 return item;
431 }
432
433 @Override
434 public int compareTo(final ListItem another) {
435 return this.getDisplayName().compareToIgnoreCase(
436 another.getDisplayName());
437 }
438
439 public Jid getServer() {
440 return getJid().toDomainJid();
441 }
442
443 public boolean setAvatar(Avatar avatar) {
444 if (this.avatar != null && this.avatar.equals(avatar)) {
445 return false;
446 } else {
447 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
448 return false;
449 }
450 this.avatar = avatar;
451 return true;
452 }
453 }
454
455 public String getAvatar() {
456 return avatar == null ? null : avatar.getFilename();
457 }
458
459 public boolean deleteOtrFingerprint(String fingerprint) {
460 synchronized (this.keys) {
461 boolean success = false;
462 try {
463 if (this.keys.has("otr_fingerprints")) {
464 JSONArray newPrints = new JSONArray();
465 JSONArray oldPrints = this.keys
466 .getJSONArray("otr_fingerprints");
467 for (int i = 0; i < oldPrints.length(); ++i) {
468 if (!oldPrints.getString(i).equals(fingerprint)) {
469 newPrints.put(oldPrints.getString(i));
470 } else {
471 success = true;
472 }
473 }
474 this.keys.put("otr_fingerprints", newPrints);
475 }
476 return success;
477 } catch (JSONException e) {
478 return false;
479 }
480 }
481 }
482
483 public boolean trusted() {
484 return getOption(Options.FROM) && getOption(Options.TO);
485 }
486
487 public String getShareableUri() {
488 if (getOtrFingerprints().size() >= 1) {
489 String otr = getOtrFingerprints().get(0);
490 return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
491 } else {
492 return "xmpp:" + getJid().toBareJid().toString();
493 }
494 }
495
496 @Override
497 public boolean isBlocked() {
498 return getAccount().isBlocked(this);
499 }
500
501 @Override
502 public boolean isDomainBlocked() {
503 return getAccount().isBlocked(this.getJid().toDomainJid());
504 }
505
506 @Override
507 public Jid getBlockedJid() {
508 if (isDomainBlocked()) {
509 return getJid().toDomainJid();
510 } else {
511 return getJid();
512 }
513 }
514
515 public boolean isSelf() {
516 return account.getJid().toBareJid().equals(getJid().toBareJid());
517 }
518
519 public void setCommonName(String cn) {
520 this.commonName = cn;
521 }
522
523 public static class Lastseen {
524 public long time;
525 public String presence;
526
527 public Lastseen() {
528 this(null, 0);
529 }
530
531 public Lastseen(final String presence, final long time) {
532 this.presence = presence;
533 this.time = time;
534 }
535 }
536
537 public final class Options {
538 public static final int TO = 0;
539 public static final int FROM = 1;
540 public static final int ASKING = 2;
541 public static final int PREEMPTIVE_GRANT = 3;
542 public static final int IN_ROSTER = 4;
543 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
544 public static final int DIRTY_PUSH = 6;
545 public static final int DIRTY_DELETE = 7;
546 }
547}