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