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