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