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