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