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.systemName != null && !Config.X509_VERIFICATION) {
109 return this.systemName;
110 } else if (this.serverName != null) {
111 return this.serverName;
112 } else if (this.systemName != null) {
113 return this.systemName;
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 Presences.CHAT:
139 case Presences.ONLINE:
140 tags.add(new Tag("online", 0xff259b24));
141 break;
142 case Presences.AWAY:
143 tags.add(new Tag("away", 0xffff9800));
144 break;
145 case Presences.XA:
146 tags.add(new Tag("not available", 0xfff44336));
147 break;
148 case Presences.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 int status) {
229 this.presences.updatePresence(resource, status);
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 int getMostAvailableStatus() {
242 return this.presences.getMostAvailableStatus();
243 }
244
245 public boolean setPhotoUri(String uri) {
246 if (uri != null && !uri.equals(this.photoUri)) {
247 this.photoUri = uri;
248 return true;
249 } else if (this.photoUri != null && uri == null) {
250 this.photoUri = null;
251 return true;
252 } else {
253 return false;
254 }
255 }
256
257 public void setServerName(String serverName) {
258 this.serverName = serverName;
259 }
260
261 public void setSystemName(String systemName) {
262 this.systemName = systemName;
263 }
264
265 public void setPresenceName(String presenceName) {
266 this.presenceName = presenceName;
267 }
268
269 public String getSystemAccount() {
270 return systemAccount;
271 }
272
273 public void setSystemAccount(String account) {
274 this.systemAccount = account;
275 }
276
277 public List<String> getGroups() {
278 ArrayList<String> groups = new ArrayList<String>();
279 for (int i = 0; i < this.groups.length(); ++i) {
280 try {
281 groups.add(this.groups.getString(i));
282 } catch (final JSONException ignored) {
283 }
284 }
285 return groups;
286 }
287
288 public ArrayList<String> getOtrFingerprints() {
289 synchronized (this.keys) {
290 final ArrayList<String> fingerprints = new ArrayList<String>();
291 try {
292 if (this.keys.has("otr_fingerprints")) {
293 final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
294 for (int i = 0; i < prints.length(); ++i) {
295 final String print = prints.isNull(i) ? null : prints.getString(i);
296 if (print != null && !print.isEmpty()) {
297 fingerprints.add(prints.getString(i));
298 }
299 }
300 }
301 } catch (final JSONException ignored) {
302
303 }
304 return fingerprints;
305 }
306 }
307 public boolean addOtrFingerprint(String print) {
308 synchronized (this.keys) {
309 if (getOtrFingerprints().contains(print)) {
310 return false;
311 }
312 try {
313 JSONArray fingerprints;
314 if (!this.keys.has("otr_fingerprints")) {
315 fingerprints = new JSONArray();
316 } else {
317 fingerprints = this.keys.getJSONArray("otr_fingerprints");
318 }
319 fingerprints.put(print);
320 this.keys.put("otr_fingerprints", fingerprints);
321 return true;
322 } catch (final JSONException ignored) {
323 return false;
324 }
325 }
326 }
327
328 public long getPgpKeyId() {
329 synchronized (this.keys) {
330 if (this.keys.has("pgp_keyid")) {
331 try {
332 return this.keys.getLong("pgp_keyid");
333 } catch (JSONException e) {
334 return 0;
335 }
336 } else {
337 return 0;
338 }
339 }
340 }
341
342 public void setPgpKeyId(long keyId) {
343 synchronized (this.keys) {
344 try {
345 this.keys.put("pgp_keyid", keyId);
346 } catch (final JSONException ignored) {
347 }
348 }
349 }
350
351 public void setOption(int option) {
352 this.subscription |= 1 << option;
353 }
354
355 public void resetOption(int option) {
356 this.subscription &= ~(1 << option);
357 }
358
359 public boolean getOption(int option) {
360 return ((this.subscription & (1 << option)) != 0);
361 }
362
363 public boolean showInRoster() {
364 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
365 .getOption(Contact.Options.DIRTY_DELETE)))
366 || (this.getOption(Contact.Options.DIRTY_PUSH));
367 }
368
369 public void parseSubscriptionFromElement(Element item) {
370 String ask = item.getAttribute("ask");
371 String subscription = item.getAttribute("subscription");
372
373 if (subscription != null) {
374 switch (subscription) {
375 case "to":
376 this.resetOption(Options.FROM);
377 this.setOption(Options.TO);
378 break;
379 case "from":
380 this.resetOption(Options.TO);
381 this.setOption(Options.FROM);
382 this.resetOption(Options.PREEMPTIVE_GRANT);
383 break;
384 case "both":
385 this.setOption(Options.TO);
386 this.setOption(Options.FROM);
387 this.resetOption(Options.PREEMPTIVE_GRANT);
388 break;
389 case "none":
390 this.resetOption(Options.FROM);
391 this.resetOption(Options.TO);
392 break;
393 }
394 }
395
396 // do NOT override asking if pending push request
397 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
398 if ((ask != null) && (ask.equals("subscribe"))) {
399 this.setOption(Contact.Options.ASKING);
400 } else {
401 this.resetOption(Contact.Options.ASKING);
402 }
403 }
404 }
405
406 public void parseGroupsFromElement(Element item) {
407 this.groups = new JSONArray();
408 for (Element element : item.getChildren()) {
409 if (element.getName().equals("group") && element.getContent() != null) {
410 this.groups.put(element.getContent());
411 }
412 }
413 }
414
415 public Element asElement() {
416 final Element item = new Element("item");
417 item.setAttribute("jid", this.jid.toString());
418 if (this.serverName != null) {
419 item.setAttribute("name", this.serverName);
420 }
421 for (String group : getGroups()) {
422 item.addChild("group").setContent(group);
423 }
424 return item;
425 }
426
427 @Override
428 public int compareTo(final ListItem another) {
429 return this.getDisplayName().compareToIgnoreCase(
430 another.getDisplayName());
431 }
432
433 public Jid getServer() {
434 return getJid().toDomainJid();
435 }
436
437 public boolean setAvatar(Avatar avatar) {
438 if (this.avatar != null && this.avatar.equals(avatar)) {
439 return false;
440 } else {
441 if (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 getAvatar() {
450 return avatar == null ? null : avatar.getFilename();
451 }
452
453 public boolean deleteOtrFingerprint(String fingerprint) {
454 synchronized (this.keys) {
455 boolean success = false;
456 try {
457 if (this.keys.has("otr_fingerprints")) {
458 JSONArray newPrints = new JSONArray();
459 JSONArray oldPrints = this.keys
460 .getJSONArray("otr_fingerprints");
461 for (int i = 0; i < oldPrints.length(); ++i) {
462 if (!oldPrints.getString(i).equals(fingerprint)) {
463 newPrints.put(oldPrints.getString(i));
464 } else {
465 success = true;
466 }
467 }
468 this.keys.put("otr_fingerprints", newPrints);
469 }
470 return success;
471 } catch (JSONException e) {
472 return false;
473 }
474 }
475 }
476
477 public boolean trusted() {
478 return getOption(Options.FROM) && getOption(Options.TO);
479 }
480
481 public String getShareableUri() {
482 if (getOtrFingerprints().size() >= 1) {
483 String otr = getOtrFingerprints().get(0);
484 return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
485 } else {
486 return "xmpp:" + getJid().toBareJid().toString();
487 }
488 }
489
490 @Override
491 public boolean isBlocked() {
492 return getAccount().isBlocked(this);
493 }
494
495 @Override
496 public boolean isDomainBlocked() {
497 return getAccount().isBlocked(this.getJid().toDomainJid());
498 }
499
500 @Override
501 public Jid getBlockedJid() {
502 if (isDomainBlocked()) {
503 return getJid().toDomainJid();
504 } else {
505 return getJid();
506 }
507 }
508
509 public boolean isSelf() {
510 return account.getJid().toBareJid().equals(getJid().toBareJid());
511 }
512
513 public static class Lastseen {
514 public long time;
515 public String presence;
516
517 public Lastseen() {
518 this(null, 0);
519 }
520
521 public Lastseen(final String presence, final long time) {
522 this.presence = presence;
523 this.time = time;
524 }
525 }
526
527 public final class Options {
528 public static final int TO = 0;
529 public static final int FROM = 1;
530 public static final int ASKING = 2;
531 public static final int PREEMPTIVE_GRANT = 3;
532 public static final int IN_ROSTER = 4;
533 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
534 public static final int DIRTY_PUSH = 6;
535 public static final int DIRTY_DELETE = 7;
536 }
537}