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