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