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