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