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