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