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.services.QuickConversationsService;
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 eu.siacs.conversations.xmpp.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) && ((QuickConversationsService.isQuicksy() && Config.QUICKSY_DOMAIN.equals(jid.getDomain().toEscapedString())) ||mutualPresenceSubscription())) {
138 return this.presenceName;
139 } else if (jid.getLocal() != null) {
140 return JidHelper.localPartOrFallback(jid);
141 } else {
142 return jid.getDomain().toEscapedString();
143 }
144 }
145
146 public String getPublicDisplayName() {
147 if (!TextUtils.isEmpty(this.presenceName)) {
148 return this.presenceName;
149 } else if (jid.getLocal() != null) {
150 return JidHelper.localPartOrFallback(jid);
151 } else {
152 return jid.getDomain().toEscapedString();
153 }
154 }
155
156 public String getProfilePhoto() {
157 return this.photoUri;
158 }
159
160 public Jid getJid() {
161 return jid;
162 }
163
164 @Override
165 public List<Tag> getTags(Context context) {
166 final ArrayList<Tag> tags = new ArrayList<>();
167 for (final String group : getGroups(true)) {
168 tags.add(new Tag(group, UIHelper.getColorForName(group)));
169 }
170 Presence.Status status = getShownStatus();
171 if (status != Presence.Status.OFFLINE) {
172 tags.add(UIHelper.getTagForStatus(context, status));
173 }
174 if (isBlocked()) {
175 tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
176 }
177 return tags;
178 }
179
180 public boolean match(Context context, String needle) {
181 if (TextUtils.isEmpty(needle)) {
182 return true;
183 }
184 needle = needle.toLowerCase(Locale.US).trim();
185 String[] parts = needle.split("\\s+");
186 if (parts.length > 1) {
187 for (String part : parts) {
188 if (!match(context, part)) {
189 return false;
190 }
191 }
192 return true;
193 } else {
194 return jid.toString().contains(needle) ||
195 getDisplayName().toLowerCase(Locale.US).contains(needle) ||
196 matchInTag(context, needle);
197 }
198 }
199
200 private boolean matchInTag(Context context, String needle) {
201 needle = needle.toLowerCase(Locale.US);
202 for (Tag tag : getTags(context)) {
203 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
204 return true;
205 }
206 }
207 return false;
208 }
209
210 public ContentValues getContentValues() {
211 synchronized (this.keys) {
212 final ContentValues values = new ContentValues();
213 values.put(ACCOUNT, accountUuid);
214 values.put(SYSTEMNAME, systemName);
215 values.put(SERVERNAME, serverName);
216 values.put(JID, jid.toString());
217 values.put(OPTIONS, subscription);
218 values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
219 values.put(PHOTOURI, photoUri);
220 values.put(KEYS, keys.toString());
221 values.put(AVATAR, avatar == null ? null : avatar.getFilename());
222 values.put(LAST_PRESENCE, mLastPresence);
223 values.put(LAST_TIME, mLastseen);
224 values.put(GROUPS, groups.toString());
225 return values;
226 }
227 }
228
229 public Account getAccount() {
230 return this.account;
231 }
232
233 public void setAccount(Account account) {
234 this.account = account;
235 this.accountUuid = account.getUuid();
236 }
237
238 public Presences getPresences() {
239 return this.presences;
240 }
241
242 public void updatePresence(final String resource, final Presence presence) {
243 this.presences.updatePresence(resource, presence);
244 }
245
246 public void removePresence(final String resource) {
247 this.presences.removePresence(resource);
248 }
249
250 public void clearPresences() {
251 this.presences.clearPresences();
252 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
253 }
254
255 public Presence.Status getShownStatus() {
256 return this.presences.getShownStatus();
257 }
258
259 public boolean setPhotoUri(String uri) {
260 if (uri != null && !uri.equals(this.photoUri)) {
261 this.photoUri = uri;
262 return true;
263 } else if (this.photoUri != null && uri == null) {
264 this.photoUri = null;
265 return true;
266 } else {
267 return false;
268 }
269 }
270
271 public void setServerName(String serverName) {
272 this.serverName = serverName;
273 }
274
275 public boolean setSystemName(String systemName) {
276 final String old = getDisplayName();
277 this.systemName = systemName;
278 return !old.equals(getDisplayName());
279 }
280
281 public boolean setPresenceName(String presenceName) {
282 final String old = getDisplayName();
283 this.presenceName = presenceName;
284 return !old.equals(getDisplayName());
285 }
286
287 public Uri getSystemAccount() {
288 return systemAccount;
289 }
290
291 public void setSystemAccount(Uri lookupUri) {
292 this.systemAccount = lookupUri;
293 }
294
295 private Collection<String> getGroups(final boolean unique) {
296 final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
297 for (int i = 0; i < this.groups.length(); ++i) {
298 try {
299 groups.add(this.groups.getString(i));
300 } catch (final JSONException ignored) {
301 }
302 }
303 return groups;
304 }
305
306 public long getPgpKeyId() {
307 synchronized (this.keys) {
308 if (this.keys.has("pgp_keyid")) {
309 try {
310 return this.keys.getLong("pgp_keyid");
311 } catch (JSONException e) {
312 return 0;
313 }
314 } else {
315 return 0;
316 }
317 }
318 }
319
320 public boolean setPgpKeyId(long keyId) {
321 final long previousKeyId = getPgpKeyId();
322 synchronized (this.keys) {
323 try {
324 this.keys.put("pgp_keyid", keyId);
325 return previousKeyId != keyId;
326 } catch (final JSONException ignored) {
327 }
328 }
329 return false;
330 }
331
332 public void setOption(int option) {
333 this.subscription |= 1 << option;
334 }
335
336 public void resetOption(int option) {
337 this.subscription &= ~(1 << option);
338 }
339
340 public boolean getOption(int option) {
341 return ((this.subscription & (1 << option)) != 0);
342 }
343
344 public boolean showInRoster() {
345 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
346 .getOption(Contact.Options.DIRTY_DELETE)))
347 || (this.getOption(Contact.Options.DIRTY_PUSH));
348 }
349
350 public boolean showInContactList() {
351 return showInRoster()
352 || getOption(Options.SYNCED_VIA_OTHER)
353 || (QuickConversationsService.isQuicksy() && systemAccount != null);
354 }
355
356 public void parseSubscriptionFromElement(Element item) {
357 String ask = item.getAttribute("ask");
358 String subscription = item.getAttribute("subscription");
359
360 if (subscription == null) {
361 this.resetOption(Options.FROM);
362 this.resetOption(Options.TO);
363 } else {
364 switch (subscription) {
365 case "to":
366 this.resetOption(Options.FROM);
367 this.setOption(Options.TO);
368 break;
369 case "from":
370 this.resetOption(Options.TO);
371 this.setOption(Options.FROM);
372 this.resetOption(Options.PREEMPTIVE_GRANT);
373 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
374 break;
375 case "both":
376 this.setOption(Options.TO);
377 this.setOption(Options.FROM);
378 this.resetOption(Options.PREEMPTIVE_GRANT);
379 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
380 break;
381 case "none":
382 this.resetOption(Options.FROM);
383 this.resetOption(Options.TO);
384 break;
385 }
386 }
387
388 // do NOT override asking if pending push request
389 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
390 if ((ask != null) && (ask.equals("subscribe"))) {
391 this.setOption(Contact.Options.ASKING);
392 } else {
393 this.resetOption(Contact.Options.ASKING);
394 }
395 }
396 }
397
398 public void parseGroupsFromElement(Element item) {
399 this.groups = new JSONArray();
400 for (Element element : item.getChildren()) {
401 if (element.getName().equals("group") && element.getContent() != null) {
402 this.groups.put(element.getContent());
403 }
404 }
405 }
406
407 public Element asElement() {
408 final Element item = new Element("item");
409 item.setAttribute("jid", this.jid);
410 if (this.serverName != null) {
411 item.setAttribute("name", this.serverName);
412 }
413 for (String group : getGroups(false)) {
414 item.addChild("group").setContent(group);
415 }
416 return item;
417 }
418
419 @Override
420 public int compareTo(@NonNull final ListItem another) {
421 return this.getDisplayName().compareToIgnoreCase(
422 another.getDisplayName());
423 }
424
425 public String getServer() {
426 return getJid().getDomain().toEscapedString();
427 }
428
429 public boolean setAvatar(Avatar avatar) {
430 return setAvatar(avatar, false);
431 }
432
433 public boolean setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
434 if (this.avatar != null && this.avatar.equals(avatar)) {
435 return false;
436 } else {
437 if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
438 return false;
439 }
440 this.avatar = avatar;
441 return true;
442 }
443 }
444
445 public String getAvatarFilename() {
446 return avatar == null ? null : avatar.getFilename();
447 }
448
449 public Avatar getAvatar() {
450 return avatar;
451 }
452
453 public boolean mutualPresenceSubscription() {
454 return getOption(Options.FROM) && getOption(Options.TO);
455 }
456
457 @Override
458 public boolean isBlocked() {
459 return getAccount().isBlocked(this);
460 }
461
462 @Override
463 public boolean isDomainBlocked() {
464 return getAccount().isBlocked(this.getJid().getDomain());
465 }
466
467 @Override
468 public Jid getBlockedJid() {
469 if (isDomainBlocked()) {
470 return getJid().getDomain();
471 } else {
472 return getJid();
473 }
474 }
475
476 public boolean isSelf() {
477 return account.getJid().asBareJid().equals(jid.asBareJid());
478 }
479
480 boolean isOwnServer() {
481 return account.getJid().getDomain().equals(jid.asBareJid());
482 }
483
484 public void setCommonName(String cn) {
485 this.commonName = cn;
486 }
487
488 public void flagActive() {
489 this.mActive = true;
490 }
491
492 public void flagInactive() {
493 this.mActive = false;
494 }
495
496 public boolean isActive() {
497 return this.mActive;
498 }
499
500 public boolean setLastseen(long timestamp) {
501 if (timestamp > this.mLastseen) {
502 this.mLastseen = timestamp;
503 return true;
504 } else {
505 return false;
506 }
507 }
508
509 public long getLastseen() {
510 return this.mLastseen;
511 }
512
513 public void setLastResource(String resource) {
514 this.mLastPresence = resource;
515 }
516
517 public String getLastResource() {
518 return this.mLastPresence;
519 }
520
521 public String getServerName() {
522 return serverName;
523 }
524
525 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
526 setOption(getOption(phoneContact.getClass()));
527 setSystemAccount(phoneContact.getLookupUri());
528 boolean changed = setSystemName(phoneContact.getDisplayName());
529 changed |= setPhotoUri(phoneContact.getPhotoUri());
530 return changed;
531 }
532
533 public synchronized boolean unsetPhoneContact(Class<?extends AbstractPhoneContact> clazz) {
534 resetOption(getOption(clazz));
535 boolean changed = false;
536 if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
537 setSystemAccount(null);
538 changed |= setPhotoUri(null);
539 changed |= setSystemName(null);
540 }
541 return changed;
542 }
543
544 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
545 if (clazz == JabberIdContact.class) {
546 return Options.SYNCED_VIA_ADDRESSBOOK;
547 } else {
548 return Options.SYNCED_VIA_OTHER;
549 }
550 }
551
552 @Override
553 public int getAvatarBackgroundColor() {
554 return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
555 }
556
557 public final class Options {
558 public static final int TO = 0;
559 public static final int FROM = 1;
560 public static final int ASKING = 2;
561 public static final int PREEMPTIVE_GRANT = 3;
562 public static final int IN_ROSTER = 4;
563 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
564 public static final int DIRTY_PUSH = 6;
565 public static final int DIRTY_DELETE = 7;
566 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
567 public static final int SYNCED_VIA_OTHER = 9;
568 }
569}