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