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