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