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