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