1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.database.Cursor;
6
7import org.json.JSONArray;
8import org.json.JSONException;
9import org.json.JSONObject;
10
11import java.util.ArrayList;
12import java.util.List;
13import java.util.Locale;
14
15import eu.siacs.conversations.Config;
16import eu.siacs.conversations.utils.UIHelper;
17import eu.siacs.conversations.xml.Element;
18import eu.siacs.conversations.xmpp.jid.InvalidJidException;
19import eu.siacs.conversations.xmpp.jid.Jid;
20import eu.siacs.conversations.xmpp.pep.Avatar;
21
22public class Contact implements ListItem, Blockable {
23 public static final String TABLENAME = "contacts";
24
25 public static final String SYSTEMNAME = "systemname";
26 public static final String SERVERNAME = "servername";
27 public static final String JID = "jid";
28 public static final String OPTIONS = "options";
29 public static final String SYSTEMACCOUNT = "systemaccount";
30 public static final String PHOTOURI = "photouri";
31 public static final String KEYS = "pgpkey";
32 public static final String ACCOUNT = "accountUuid";
33 public static final String AVATAR = "avatar";
34 public static final String LAST_PRESENCE = "last_presence";
35 public static final String LAST_TIME = "last_time";
36 public static final String GROUPS = "groups";
37 public Lastseen lastseen = new Lastseen();
38 protected String accountUuid;
39 protected String systemName;
40 protected String serverName;
41 protected String presenceName;
42 protected String commonName;
43 protected Jid jid;
44 protected int subscription = 0;
45 protected String systemAccount;
46 protected String photoUri;
47 protected JSONObject keys = new JSONObject();
48 protected JSONArray groups = new JSONArray();
49 protected final Presences presences = new Presences();
50 protected Account account;
51 protected Avatar avatar;
52
53 public Contact(final String account, final String systemName, final String serverName,
54 final Jid jid, final int subscription, final String photoUri,
55 final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) {
56 this.accountUuid = account;
57 this.systemName = systemName;
58 this.serverName = serverName;
59 this.jid = jid;
60 this.subscription = subscription;
61 this.photoUri = photoUri;
62 this.systemAccount = systemAccount;
63 try {
64 this.keys = (keys == null ? new JSONObject("") : new JSONObject(keys));
65 } catch (JSONException e) {
66 this.keys = new JSONObject();
67 }
68 if (avatar != null) {
69 this.avatar = new Avatar();
70 this.avatar.sha1sum = avatar;
71 this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
72 }
73 try {
74 this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
75 } catch (JSONException e) {
76 this.groups = new JSONArray();
77 }
78 this.lastseen = lastseen;
79 }
80
81 public Contact(final Jid jid) {
82 this.jid = jid;
83 }
84
85 public static Contact fromCursor(final Cursor cursor) {
86 final Lastseen lastseen = new Lastseen(
87 cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
88 cursor.getLong(cursor.getColumnIndex(LAST_TIME)));
89 final Jid jid;
90 try {
91 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
92 } catch (final InvalidJidException e) {
93 // TODO: Borked DB... handle this somehow?
94 return null;
95 }
96 return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
97 cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
98 cursor.getString(cursor.getColumnIndex(SERVERNAME)),
99 jid,
100 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
101 cursor.getString(cursor.getColumnIndex(PHOTOURI)),
102 cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
103 cursor.getString(cursor.getColumnIndex(KEYS)),
104 cursor.getString(cursor.getColumnIndex(AVATAR)),
105 lastseen,
106 cursor.getString(cursor.getColumnIndex(GROUPS)));
107 }
108
109 public String getDisplayName() {
110 if (this.commonName != null && Config.X509_VERIFICATION) {
111 return this.commonName;
112 } else if (this.systemName != null) {
113 return this.systemName;
114 } else if (this.serverName != null) {
115 return this.serverName;
116 } else if (this.presenceName != null) {
117 return this.presenceName;
118 } else if (jid.hasLocalpart()) {
119 return jid.getLocalpart();
120 } else {
121 return jid.getDomainpart();
122 }
123 }
124
125 @Override
126 public String getDisplayJid() {
127 if (jid != null) {
128 return jid.toString();
129 } else {
130 return null;
131 }
132 }
133
134 public String getProfilePhoto() {
135 return this.photoUri;
136 }
137
138 public Jid getJid() {
139 return jid;
140 }
141
142 @Override
143 public List<Tag> getTags(Context context) {
144 final ArrayList<Tag> tags = new ArrayList<>();
145 for (final String group : getGroups()) {
146 tags.add(new Tag(group, UIHelper.getColorForName(group)));
147 }
148 Presence.Status status = getMostAvailableStatus();
149 if (status != Presence.Status.OFFLINE) {
150 tags.add(UIHelper.getTagForStatus(context, status));
151 }
152 if (isBlocked()) {
153 tags.add(new Tag("blocked", 0xff2e2f3b));
154 }
155 return tags;
156 }
157
158 public boolean match(Context context, String needle) {
159 if (needle == null || needle.isEmpty()) {
160 return true;
161 }
162 needle = needle.toLowerCase(Locale.US).trim();
163 String[] parts = needle.split("\\s+");
164 if (parts.length > 1) {
165 for(int i = 0; i < parts.length; ++i) {
166 if (!match(context, parts[i])) {
167 return false;
168 }
169 }
170 return true;
171 } else {
172 return jid.toString().contains(needle) ||
173 getDisplayName().toLowerCase(Locale.US).contains(needle) ||
174 matchInTag(context, needle);
175 }
176 }
177
178 private boolean matchInTag(Context context, String needle) {
179 needle = needle.toLowerCase(Locale.US);
180 for (Tag tag : getTags(context)) {
181 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
182 return true;
183 }
184 }
185 return false;
186 }
187
188 public ContentValues getContentValues() {
189 synchronized (this.keys) {
190 final ContentValues values = new ContentValues();
191 values.put(ACCOUNT, accountUuid);
192 values.put(SYSTEMNAME, systemName);
193 values.put(SERVERNAME, serverName);
194 values.put(JID, jid.toString());
195 values.put(OPTIONS, subscription);
196 values.put(SYSTEMACCOUNT, systemAccount);
197 values.put(PHOTOURI, photoUri);
198 values.put(KEYS, keys.toString());
199 values.put(AVATAR, avatar == null ? null : avatar.getFilename());
200 values.put(LAST_PRESENCE, lastseen.presence);
201 values.put(LAST_TIME, lastseen.time);
202 values.put(GROUPS, groups.toString());
203 return values;
204 }
205 }
206
207 public int getSubscription() {
208 return this.subscription;
209 }
210
211 public Account getAccount() {
212 return this.account;
213 }
214
215 public void setAccount(Account account) {
216 this.account = account;
217 this.accountUuid = account.getUuid();
218 }
219
220 public Presences getPresences() {
221 return this.presences;
222 }
223
224 public void updatePresence(final String resource, final Presence presence) {
225 this.presences.updatePresence(resource, presence);
226 }
227
228 public void removePresence(final String resource) {
229 this.presences.removePresence(resource);
230 }
231
232 public void clearPresences() {
233 this.presences.clearPresences();
234 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
235 }
236
237 public Presence.Status getMostAvailableStatus() {
238 Presence p = this.presences.getMostAvailablePresence();
239 if (p == null) {
240 return Presence.Status.OFFLINE;
241 }
242
243 return p.getStatus();
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 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
385 break;
386 case "both":
387 this.setOption(Options.TO);
388 this.setOption(Options.FROM);
389 this.resetOption(Options.PREEMPTIVE_GRANT);
390 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
391 break;
392 case "none":
393 this.resetOption(Options.FROM);
394 this.resetOption(Options.TO);
395 break;
396 }
397 }
398
399 // do NOT override asking if pending push request
400 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
401 if ((ask != null) && (ask.equals("subscribe"))) {
402 this.setOption(Contact.Options.ASKING);
403 } else {
404 this.resetOption(Contact.Options.ASKING);
405 }
406 }
407 }
408
409 public void parseGroupsFromElement(Element item) {
410 this.groups = new JSONArray();
411 for (Element element : item.getChildren()) {
412 if (element.getName().equals("group") && element.getContent() != null) {
413 this.groups.put(element.getContent());
414 }
415 }
416 }
417
418 public Element asElement() {
419 final Element item = new Element("item");
420 item.setAttribute("jid", this.jid.toString());
421 if (this.serverName != null) {
422 item.setAttribute("name", this.serverName);
423 }
424 for (String group : getGroups()) {
425 item.addChild("group").setContent(group);
426 }
427 return item;
428 }
429
430 @Override
431 public int compareTo(final ListItem another) {
432 return this.getDisplayName().compareToIgnoreCase(
433 another.getDisplayName());
434 }
435
436 public Jid getServer() {
437 return getJid().toDomainJid();
438 }
439
440 public boolean setAvatar(Avatar avatar) {
441 if (this.avatar != null && this.avatar.equals(avatar)) {
442 return false;
443 } else {
444 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
445 return false;
446 }
447 this.avatar = avatar;
448 return true;
449 }
450 }
451
452 public String getAvatar() {
453 return avatar == null ? null : avatar.getFilename();
454 }
455
456 public boolean deleteOtrFingerprint(String fingerprint) {
457 synchronized (this.keys) {
458 boolean success = false;
459 try {
460 if (this.keys.has("otr_fingerprints")) {
461 JSONArray newPrints = new JSONArray();
462 JSONArray oldPrints = this.keys
463 .getJSONArray("otr_fingerprints");
464 for (int i = 0; i < oldPrints.length(); ++i) {
465 if (!oldPrints.getString(i).equals(fingerprint)) {
466 newPrints.put(oldPrints.getString(i));
467 } else {
468 success = true;
469 }
470 }
471 this.keys.put("otr_fingerprints", newPrints);
472 }
473 return success;
474 } catch (JSONException e) {
475 return false;
476 }
477 }
478 }
479
480 public boolean trusted() {
481 return getOption(Options.FROM) && getOption(Options.TO);
482 }
483
484 public String getShareableUri() {
485 if (getOtrFingerprints().size() >= 1) {
486 String otr = getOtrFingerprints().get(0);
487 return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
488 } else {
489 return "xmpp:" + getJid().toBareJid().toString();
490 }
491 }
492
493 @Override
494 public boolean isBlocked() {
495 return getAccount().isBlocked(this);
496 }
497
498 @Override
499 public boolean isDomainBlocked() {
500 return getAccount().isBlocked(this.getJid().toDomainJid());
501 }
502
503 @Override
504 public Jid getBlockedJid() {
505 if (isDomainBlocked()) {
506 return getJid().toDomainJid();
507 } else {
508 return getJid();
509 }
510 }
511
512 public boolean isSelf() {
513 return account.getJid().toBareJid().equals(getJid().toBareJid());
514 }
515
516 public void setCommonName(String cn) {
517 this.commonName = cn;
518 }
519
520 public static class Lastseen {
521 public long time;
522 public String presence;
523
524 public Lastseen() {
525 this(null, 0);
526 }
527
528 public Lastseen(final String presence, final long time) {
529 this.presence = presence;
530 this.time = time;
531 }
532 }
533
534 public final class Options {
535 public static final int TO = 0;
536 public static final int FROM = 1;
537 public static final int ASKING = 2;
538 public static final int PREEMPTIVE_GRANT = 3;
539 public static final int IN_ROSTER = 4;
540 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
541 public static final int DIRTY_PUSH = 6;
542 public static final int DIRTY_DELETE = 7;
543 }
544}