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 (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.DOMAIN_LOCK)) {
128 return jid.getLocalpart();
129 } else if (jid != null) {
130 return jid.toString();
131 } else {
132 return null;
133 }
134 }
135
136 public String getProfilePhoto() {
137 return this.photoUri;
138 }
139
140 public Jid getJid() {
141 return jid;
142 }
143
144 @Override
145 public List<Tag> getTags(Context context) {
146 final ArrayList<Tag> tags = new ArrayList<>();
147 for (final String group : getGroups()) {
148 tags.add(new Tag(group, UIHelper.getColorForName(group)));
149 }
150 Presence.Status status = getMostAvailableStatus();
151 if (status != Presence.Status.OFFLINE) {
152 tags.add(UIHelper.getTagForStatus(context, status));
153 }
154 if (isBlocked()) {
155 tags.add(new Tag("blocked", 0xff2e2f3b));
156 }
157 return tags;
158 }
159
160 public boolean match(Context context, String needle) {
161 if (needle == null || needle.isEmpty()) {
162 return true;
163 }
164 needle = needle.toLowerCase(Locale.US).trim();
165 String[] parts = needle.split("\\s+");
166 if (parts.length > 1) {
167 for(int i = 0; i < parts.length; ++i) {
168 if (!match(context, parts[i])) {
169 return false;
170 }
171 }
172 return true;
173 } else {
174 return jid.toString().contains(needle) ||
175 getDisplayName().toLowerCase(Locale.US).contains(needle) ||
176 matchInTag(context, needle);
177 }
178 }
179
180 private boolean matchInTag(Context context, String needle) {
181 needle = needle.toLowerCase(Locale.US);
182 for (Tag tag : getTags(context)) {
183 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
184 return true;
185 }
186 }
187 return false;
188 }
189
190 public ContentValues getContentValues() {
191 synchronized (this.keys) {
192 final ContentValues values = new ContentValues();
193 values.put(ACCOUNT, accountUuid);
194 values.put(SYSTEMNAME, systemName);
195 values.put(SERVERNAME, serverName);
196 values.put(JID, jid.toString());
197 values.put(OPTIONS, subscription);
198 values.put(SYSTEMACCOUNT, systemAccount);
199 values.put(PHOTOURI, photoUri);
200 values.put(KEYS, keys.toString());
201 values.put(AVATAR, avatar == null ? null : avatar.getFilename());
202 values.put(LAST_PRESENCE, lastseen.presence);
203 values.put(LAST_TIME, lastseen.time);
204 values.put(GROUPS, groups.toString());
205 return values;
206 }
207 }
208
209 public int getSubscription() {
210 return this.subscription;
211 }
212
213 public Account getAccount() {
214 return this.account;
215 }
216
217 public void setAccount(Account account) {
218 this.account = account;
219 this.accountUuid = account.getUuid();
220 }
221
222 public Presences getPresences() {
223 return this.presences;
224 }
225
226 public void updatePresence(final String resource, final Presence presence) {
227 this.presences.updatePresence(resource, presence);
228 }
229
230 public void removePresence(final String resource) {
231 this.presences.removePresence(resource);
232 }
233
234 public void clearPresences() {
235 this.presences.clearPresences();
236 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
237 }
238
239 public Presence.Status getMostAvailableStatus() {
240 Presence p = this.presences.getMostAvailablePresence();
241 if (p == null) {
242 return Presence.Status.OFFLINE;
243 }
244
245 return p.getStatus();
246 }
247
248 public boolean setPhotoUri(String uri) {
249 if (uri != null && !uri.equals(this.photoUri)) {
250 this.photoUri = uri;
251 return true;
252 } else if (this.photoUri != null && uri == null) {
253 this.photoUri = null;
254 return true;
255 } else {
256 return false;
257 }
258 }
259
260 public void setServerName(String serverName) {
261 this.serverName = serverName;
262 }
263
264 public void setSystemName(String systemName) {
265 this.systemName = systemName;
266 }
267
268 public void setPresenceName(String presenceName) {
269 this.presenceName = presenceName;
270 }
271
272 public String getSystemAccount() {
273 return systemAccount;
274 }
275
276 public void setSystemAccount(String account) {
277 this.systemAccount = account;
278 }
279
280 public List<String> getGroups() {
281 ArrayList<String> groups = new ArrayList<String>();
282 for (int i = 0; i < this.groups.length(); ++i) {
283 try {
284 groups.add(this.groups.getString(i));
285 } catch (final JSONException ignored) {
286 }
287 }
288 return groups;
289 }
290
291 public ArrayList<String> getOtrFingerprints() {
292 synchronized (this.keys) {
293 final ArrayList<String> fingerprints = new ArrayList<String>();
294 try {
295 if (this.keys.has("otr_fingerprints")) {
296 final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
297 for (int i = 0; i < prints.length(); ++i) {
298 final String print = prints.isNull(i) ? null : prints.getString(i);
299 if (print != null && !print.isEmpty()) {
300 fingerprints.add(prints.getString(i));
301 }
302 }
303 }
304 } catch (final JSONException ignored) {
305
306 }
307 return fingerprints;
308 }
309 }
310 public boolean addOtrFingerprint(String print) {
311 synchronized (this.keys) {
312 if (getOtrFingerprints().contains(print)) {
313 return false;
314 }
315 try {
316 JSONArray fingerprints;
317 if (!this.keys.has("otr_fingerprints")) {
318 fingerprints = new JSONArray();
319 } else {
320 fingerprints = this.keys.getJSONArray("otr_fingerprints");
321 }
322 fingerprints.put(print);
323 this.keys.put("otr_fingerprints", fingerprints);
324 return true;
325 } catch (final JSONException ignored) {
326 return false;
327 }
328 }
329 }
330
331 public long getPgpKeyId() {
332 synchronized (this.keys) {
333 if (this.keys.has("pgp_keyid")) {
334 try {
335 return this.keys.getLong("pgp_keyid");
336 } catch (JSONException e) {
337 return 0;
338 }
339 } else {
340 return 0;
341 }
342 }
343 }
344
345 public void setPgpKeyId(long keyId) {
346 synchronized (this.keys) {
347 try {
348 this.keys.put("pgp_keyid", keyId);
349 } catch (final JSONException ignored) {
350 }
351 }
352 }
353
354 public void setOption(int option) {
355 this.subscription |= 1 << option;
356 }
357
358 public void resetOption(int option) {
359 this.subscription &= ~(1 << option);
360 }
361
362 public boolean getOption(int option) {
363 return ((this.subscription & (1 << option)) != 0);
364 }
365
366 public boolean showInRoster() {
367 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
368 .getOption(Contact.Options.DIRTY_DELETE)))
369 || (this.getOption(Contact.Options.DIRTY_PUSH));
370 }
371
372 public void parseSubscriptionFromElement(Element item) {
373 String ask = item.getAttribute("ask");
374 String subscription = item.getAttribute("subscription");
375
376 if (subscription != null) {
377 switch (subscription) {
378 case "to":
379 this.resetOption(Options.FROM);
380 this.setOption(Options.TO);
381 break;
382 case "from":
383 this.resetOption(Options.TO);
384 this.setOption(Options.FROM);
385 this.resetOption(Options.PREEMPTIVE_GRANT);
386 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
387 break;
388 case "both":
389 this.setOption(Options.TO);
390 this.setOption(Options.FROM);
391 this.resetOption(Options.PREEMPTIVE_GRANT);
392 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
393 break;
394 case "none":
395 this.resetOption(Options.FROM);
396 this.resetOption(Options.TO);
397 break;
398 }
399 }
400
401 // do NOT override asking if pending push request
402 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
403 if ((ask != null) && (ask.equals("subscribe"))) {
404 this.setOption(Contact.Options.ASKING);
405 } else {
406 this.resetOption(Contact.Options.ASKING);
407 }
408 }
409 }
410
411 public void parseGroupsFromElement(Element item) {
412 this.groups = new JSONArray();
413 for (Element element : item.getChildren()) {
414 if (element.getName().equals("group") && element.getContent() != null) {
415 this.groups.put(element.getContent());
416 }
417 }
418 }
419
420 public Element asElement() {
421 final Element item = new Element("item");
422 item.setAttribute("jid", this.jid.toString());
423 if (this.serverName != null) {
424 item.setAttribute("name", this.serverName);
425 }
426 for (String group : getGroups()) {
427 item.addChild("group").setContent(group);
428 }
429 return item;
430 }
431
432 @Override
433 public int compareTo(final ListItem another) {
434 return this.getDisplayName().compareToIgnoreCase(
435 another.getDisplayName());
436 }
437
438 public Jid getServer() {
439 return getJid().toDomainJid();
440 }
441
442 public boolean setAvatar(Avatar avatar) {
443 if (this.avatar != null && this.avatar.equals(avatar)) {
444 return false;
445 } else {
446 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
447 return false;
448 }
449 this.avatar = avatar;
450 return true;
451 }
452 }
453
454 public String getAvatar() {
455 return avatar == null ? null : avatar.getFilename();
456 }
457
458 public boolean deleteOtrFingerprint(String fingerprint) {
459 synchronized (this.keys) {
460 boolean success = false;
461 try {
462 if (this.keys.has("otr_fingerprints")) {
463 JSONArray newPrints = new JSONArray();
464 JSONArray oldPrints = this.keys
465 .getJSONArray("otr_fingerprints");
466 for (int i = 0; i < oldPrints.length(); ++i) {
467 if (!oldPrints.getString(i).equals(fingerprint)) {
468 newPrints.put(oldPrints.getString(i));
469 } else {
470 success = true;
471 }
472 }
473 this.keys.put("otr_fingerprints", newPrints);
474 }
475 return success;
476 } catch (JSONException e) {
477 return false;
478 }
479 }
480 }
481
482 public boolean trusted() {
483 return getOption(Options.FROM) && getOption(Options.TO);
484 }
485
486 public String getShareableUri() {
487 if (getOtrFingerprints().size() >= 1) {
488 String otr = getOtrFingerprints().get(0);
489 return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
490 } else {
491 return "xmpp:" + getJid().toBareJid().toString();
492 }
493 }
494
495 @Override
496 public boolean isBlocked() {
497 return getAccount().isBlocked(this);
498 }
499
500 @Override
501 public boolean isDomainBlocked() {
502 return getAccount().isBlocked(this.getJid().toDomainJid());
503 }
504
505 @Override
506 public Jid getBlockedJid() {
507 if (isDomainBlocked()) {
508 return getJid().toDomainJid();
509 } else {
510 return getJid();
511 }
512 }
513
514 public boolean isSelf() {
515 return account.getJid().toBareJid().equals(getJid().toBareJid());
516 }
517
518 public void setCommonName(String cn) {
519 this.commonName = cn;
520 }
521
522 public static class Lastseen {
523 public long time;
524 public String presence;
525
526 public Lastseen() {
527 this(null, 0);
528 }
529
530 public Lastseen(final String presence, final long time) {
531 this.presence = presence;
532 this.time = time;
533 }
534 }
535
536 public final class Options {
537 public static final int TO = 0;
538 public static final int FROM = 1;
539 public static final int ASKING = 2;
540 public static final int PREEMPTIVE_GRANT = 3;
541 public static final int IN_ROSTER = 4;
542 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
543 public static final int DIRTY_PUSH = 6;
544 public static final int DIRTY_DELETE = 7;
545 }
546}