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 = getShownStatus();
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 getShownStatus() {
241 return this.presences.getShownStatus();
242 }
243
244 public boolean setPhotoUri(String uri) {
245 if (uri != null && !uri.equals(this.photoUri)) {
246 this.photoUri = uri;
247 return true;
248 } else if (this.photoUri != null && uri == null) {
249 this.photoUri = null;
250 return true;
251 } else {
252 return false;
253 }
254 }
255
256 public void setServerName(String serverName) {
257 this.serverName = serverName;
258 }
259
260 public void setSystemName(String systemName) {
261 this.systemName = systemName;
262 }
263
264 public void setPresenceName(String presenceName) {
265 this.presenceName = presenceName;
266 }
267
268 public String getSystemAccount() {
269 return systemAccount;
270 }
271
272 public void setSystemAccount(String account) {
273 this.systemAccount = account;
274 }
275
276 public List<String> getGroups() {
277 ArrayList<String> groups = new ArrayList<String>();
278 for (int i = 0; i < this.groups.length(); ++i) {
279 try {
280 groups.add(this.groups.getString(i));
281 } catch (final JSONException ignored) {
282 }
283 }
284 return groups;
285 }
286
287 public ArrayList<String> getOtrFingerprints() {
288 synchronized (this.keys) {
289 final ArrayList<String> fingerprints = new ArrayList<String>();
290 try {
291 if (this.keys.has("otr_fingerprints")) {
292 final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
293 for (int i = 0; i < prints.length(); ++i) {
294 final String print = prints.isNull(i) ? null : prints.getString(i);
295 if (print != null && !print.isEmpty()) {
296 fingerprints.add(prints.getString(i));
297 }
298 }
299 }
300 } catch (final JSONException ignored) {
301
302 }
303 return fingerprints;
304 }
305 }
306 public boolean addOtrFingerprint(String print) {
307 synchronized (this.keys) {
308 if (getOtrFingerprints().contains(print)) {
309 return false;
310 }
311 try {
312 JSONArray fingerprints;
313 if (!this.keys.has("otr_fingerprints")) {
314 fingerprints = new JSONArray();
315 } else {
316 fingerprints = this.keys.getJSONArray("otr_fingerprints");
317 }
318 fingerprints.put(print);
319 this.keys.put("otr_fingerprints", fingerprints);
320 return true;
321 } catch (final JSONException ignored) {
322 return false;
323 }
324 }
325 }
326
327 public long getPgpKeyId() {
328 synchronized (this.keys) {
329 if (this.keys.has("pgp_keyid")) {
330 try {
331 return this.keys.getLong("pgp_keyid");
332 } catch (JSONException e) {
333 return 0;
334 }
335 } else {
336 return 0;
337 }
338 }
339 }
340
341 public void setPgpKeyId(long keyId) {
342 synchronized (this.keys) {
343 try {
344 this.keys.put("pgp_keyid", keyId);
345 } catch (final JSONException ignored) {
346 }
347 }
348 }
349
350 public void setOption(int option) {
351 this.subscription |= 1 << option;
352 }
353
354 public void resetOption(int option) {
355 this.subscription &= ~(1 << option);
356 }
357
358 public boolean getOption(int option) {
359 return ((this.subscription & (1 << option)) != 0);
360 }
361
362 public boolean showInRoster() {
363 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
364 .getOption(Contact.Options.DIRTY_DELETE)))
365 || (this.getOption(Contact.Options.DIRTY_PUSH));
366 }
367
368 public void parseSubscriptionFromElement(Element item) {
369 String ask = item.getAttribute("ask");
370 String subscription = item.getAttribute("subscription");
371
372 if (subscription != null) {
373 switch (subscription) {
374 case "to":
375 this.resetOption(Options.FROM);
376 this.setOption(Options.TO);
377 break;
378 case "from":
379 this.resetOption(Options.TO);
380 this.setOption(Options.FROM);
381 this.resetOption(Options.PREEMPTIVE_GRANT);
382 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
383 break;
384 case "both":
385 this.setOption(Options.TO);
386 this.setOption(Options.FROM);
387 this.resetOption(Options.PREEMPTIVE_GRANT);
388 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
389 break;
390 case "none":
391 this.resetOption(Options.FROM);
392 this.resetOption(Options.TO);
393 break;
394 }
395 }
396
397 // do NOT override asking if pending push request
398 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
399 if ((ask != null) && (ask.equals("subscribe"))) {
400 this.setOption(Contact.Options.ASKING);
401 } else {
402 this.resetOption(Contact.Options.ASKING);
403 }
404 }
405 }
406
407 public void parseGroupsFromElement(Element item) {
408 this.groups = new JSONArray();
409 for (Element element : item.getChildren()) {
410 if (element.getName().equals("group") && element.getContent() != null) {
411 this.groups.put(element.getContent());
412 }
413 }
414 }
415
416 public Element asElement() {
417 final Element item = new Element("item");
418 item.setAttribute("jid", this.jid.toString());
419 if (this.serverName != null) {
420 item.setAttribute("name", this.serverName);
421 }
422 for (String group : getGroups()) {
423 item.addChild("group").setContent(group);
424 }
425 return item;
426 }
427
428 @Override
429 public int compareTo(final ListItem another) {
430 return this.getDisplayName().compareToIgnoreCase(
431 another.getDisplayName());
432 }
433
434 public Jid getServer() {
435 return getJid().toDomainJid();
436 }
437
438 public boolean setAvatar(Avatar avatar) {
439 if (this.avatar != null && this.avatar.equals(avatar)) {
440 return false;
441 } else {
442 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
443 return false;
444 }
445 this.avatar = avatar;
446 return true;
447 }
448 }
449
450 public String getAvatar() {
451 return avatar == null ? null : avatar.getFilename();
452 }
453
454 public boolean deleteOtrFingerprint(String fingerprint) {
455 synchronized (this.keys) {
456 boolean success = false;
457 try {
458 if (this.keys.has("otr_fingerprints")) {
459 JSONArray newPrints = new JSONArray();
460 JSONArray oldPrints = this.keys
461 .getJSONArray("otr_fingerprints");
462 for (int i = 0; i < oldPrints.length(); ++i) {
463 if (!oldPrints.getString(i).equals(fingerprint)) {
464 newPrints.put(oldPrints.getString(i));
465 } else {
466 success = true;
467 }
468 }
469 this.keys.put("otr_fingerprints", newPrints);
470 }
471 return success;
472 } catch (JSONException e) {
473 return false;
474 }
475 }
476 }
477
478 public boolean trusted() {
479 return getOption(Options.FROM) && getOption(Options.TO);
480 }
481
482 @Override
483 public boolean isBlocked() {
484 return getAccount().isBlocked(this);
485 }
486
487 @Override
488 public boolean isDomainBlocked() {
489 return getAccount().isBlocked(this.getJid().toDomainJid());
490 }
491
492 @Override
493 public Jid getBlockedJid() {
494 if (isDomainBlocked()) {
495 return getJid().toDomainJid();
496 } else {
497 return getJid();
498 }
499 }
500
501 public boolean isSelf() {
502 return account.getJid().toBareJid().equals(getJid().toBareJid());
503 }
504
505 public void setCommonName(String cn) {
506 this.commonName = cn;
507 }
508
509 public void flagActive() {
510 this.mActive = true;
511 }
512
513 public void flagInactive() {
514 this.mActive = false;
515 }
516
517 public boolean isActive() {
518 return this.mActive;
519 }
520
521 public void setLastseen(long timestamp) {
522 this.mLastseen = Math.max(timestamp, mLastseen);
523 }
524
525 public long getLastseen() {
526 return this.mLastseen;
527 }
528
529 public void setLastResource(String resource) {
530 this.mLastPresence = resource;
531 }
532
533 public String getLastResource() {
534 return this.mLastPresence;
535 }
536
537 public final class Options {
538 public static final int TO = 0;
539 public static final int FROM = 1;
540 public static final int ASKING = 2;
541 public static final int PREEMPTIVE_GRANT = 3;
542 public static final int IN_ROSTER = 4;
543 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
544 public static final int DIRTY_PUSH = 6;
545 public static final int DIRTY_DELETE = 7;
546 }
547}