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