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 final 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 updatePresence(final String resource, final Presence presence) {
226 this.presences.updatePresence(resource, presence);
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 Presence.Status getMostAvailableStatus() {
239 Presence p = this.presences.getMostAvailablePresence();
240 if (p == null) {
241 return Presence.Status.OFFLINE;
242 }
243
244 return p.getStatus();
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 void setOption(int option) {
354 this.subscription |= 1 << option;
355 }
356
357 public void resetOption(int option) {
358 this.subscription &= ~(1 << option);
359 }
360
361 public boolean getOption(int option) {
362 return ((this.subscription & (1 << option)) != 0);
363 }
364
365 public boolean showInRoster() {
366 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
367 .getOption(Contact.Options.DIRTY_DELETE)))
368 || (this.getOption(Contact.Options.DIRTY_PUSH));
369 }
370
371 public void parseSubscriptionFromElement(Element item) {
372 String ask = item.getAttribute("ask");
373 String subscription = item.getAttribute("subscription");
374
375 if (subscription != null) {
376 switch (subscription) {
377 case "to":
378 this.resetOption(Options.FROM);
379 this.setOption(Options.TO);
380 break;
381 case "from":
382 this.resetOption(Options.TO);
383 this.setOption(Options.FROM);
384 this.resetOption(Options.PREEMPTIVE_GRANT);
385 break;
386 case "both":
387 this.setOption(Options.TO);
388 this.setOption(Options.FROM);
389 this.resetOption(Options.PREEMPTIVE_GRANT);
390 break;
391 case "none":
392 this.resetOption(Options.FROM);
393 this.resetOption(Options.TO);
394 break;
395 }
396 }
397
398 // do NOT override asking if pending push request
399 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
400 if ((ask != null) && (ask.equals("subscribe"))) {
401 this.setOption(Contact.Options.ASKING);
402 } else {
403 this.resetOption(Contact.Options.ASKING);
404 }
405 }
406 }
407
408 public void parseGroupsFromElement(Element item) {
409 this.groups = new JSONArray();
410 for (Element element : item.getChildren()) {
411 if (element.getName().equals("group") && element.getContent() != null) {
412 this.groups.put(element.getContent());
413 }
414 }
415 }
416
417 public Element asElement() {
418 final Element item = new Element("item");
419 item.setAttribute("jid", this.jid.toString());
420 if (this.serverName != null) {
421 item.setAttribute("name", this.serverName);
422 }
423 for (String group : getGroups()) {
424 item.addChild("group").setContent(group);
425 }
426 return item;
427 }
428
429 @Override
430 public int compareTo(final ListItem another) {
431 return this.getDisplayName().compareToIgnoreCase(
432 another.getDisplayName());
433 }
434
435 public Jid getServer() {
436 return getJid().toDomainJid();
437 }
438
439 public boolean setAvatar(Avatar avatar) {
440 if (this.avatar != null && this.avatar.equals(avatar)) {
441 return false;
442 } else {
443 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
444 return false;
445 }
446 this.avatar = avatar;
447 return true;
448 }
449 }
450
451 public String getAvatar() {
452 return avatar == null ? null : avatar.getFilename();
453 }
454
455 public boolean deleteOtrFingerprint(String fingerprint) {
456 synchronized (this.keys) {
457 boolean success = false;
458 try {
459 if (this.keys.has("otr_fingerprints")) {
460 JSONArray newPrints = new JSONArray();
461 JSONArray oldPrints = this.keys
462 .getJSONArray("otr_fingerprints");
463 for (int i = 0; i < oldPrints.length(); ++i) {
464 if (!oldPrints.getString(i).equals(fingerprint)) {
465 newPrints.put(oldPrints.getString(i));
466 } else {
467 success = true;
468 }
469 }
470 this.keys.put("otr_fingerprints", newPrints);
471 }
472 return success;
473 } catch (JSONException e) {
474 return false;
475 }
476 }
477 }
478
479 public boolean trusted() {
480 return getOption(Options.FROM) && getOption(Options.TO);
481 }
482
483 public String getShareableUri() {
484 if (getOtrFingerprints().size() >= 1) {
485 String otr = getOtrFingerprints().get(0);
486 return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
487 } else {
488 return "xmpp:" + getJid().toBareJid().toString();
489 }
490 }
491
492 @Override
493 public boolean isBlocked() {
494 return getAccount().isBlocked(this);
495 }
496
497 @Override
498 public boolean isDomainBlocked() {
499 return getAccount().isBlocked(this.getJid().toDomainJid());
500 }
501
502 @Override
503 public Jid getBlockedJid() {
504 if (isDomainBlocked()) {
505 return getJid().toDomainJid();
506 } else {
507 return getJid();
508 }
509 }
510
511 public boolean isSelf() {
512 return account.getJid().toBareJid().equals(getJid().toBareJid());
513 }
514
515 public void setCommonName(String cn) {
516 this.commonName = cn;
517 }
518
519 public static class Lastseen {
520 public long time;
521 public String presence;
522
523 public Lastseen() {
524 this(null, 0);
525 }
526
527 public Lastseen(final String presence, final long time) {
528 this.presence = presence;
529 this.time = time;
530 }
531 }
532
533 public final class Options {
534 public static final int TO = 0;
535 public static final int FROM = 1;
536 public static final int ASKING = 2;
537 public static final int PREEMPTIVE_GRANT = 3;
538 public static final int IN_ROSTER = 4;
539 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
540 public static final int DIRTY_PUSH = 6;
541 public static final int DIRTY_DELETE = 7;
542 }
543}