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