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 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) && ((QuickConversationsService.isQuicksy() && Config.QUICKSY_DOMAIN.equals(jid.getDomain())) ||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 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() || getOption(Options.SYNCED_VIA_OTHER);
342 }
343
344 public void parseSubscriptionFromElement(Element item) {
345 String ask = item.getAttribute("ask");
346 String subscription = item.getAttribute("subscription");
347
348 if (subscription == null) {
349 this.resetOption(Options.FROM);
350 this.resetOption(Options.TO);
351 } else {
352 switch (subscription) {
353 case "to":
354 this.resetOption(Options.FROM);
355 this.setOption(Options.TO);
356 break;
357 case "from":
358 this.resetOption(Options.TO);
359 this.setOption(Options.FROM);
360 this.resetOption(Options.PREEMPTIVE_GRANT);
361 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
362 break;
363 case "both":
364 this.setOption(Options.TO);
365 this.setOption(Options.FROM);
366 this.resetOption(Options.PREEMPTIVE_GRANT);
367 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
368 break;
369 case "none":
370 this.resetOption(Options.FROM);
371 this.resetOption(Options.TO);
372 break;
373 }
374 }
375
376 // do NOT override asking if pending push request
377 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
378 if ((ask != null) && (ask.equals("subscribe"))) {
379 this.setOption(Contact.Options.ASKING);
380 } else {
381 this.resetOption(Contact.Options.ASKING);
382 }
383 }
384 }
385
386 public void parseGroupsFromElement(Element item) {
387 this.groups = new JSONArray();
388 for (Element element : item.getChildren()) {
389 if (element.getName().equals("group") && element.getContent() != null) {
390 this.groups.put(element.getContent());
391 }
392 }
393 }
394
395 public Element asElement() {
396 final Element item = new Element("item");
397 item.setAttribute("jid", this.jid.toString());
398 if (this.serverName != null) {
399 item.setAttribute("name", this.serverName);
400 }
401 for (String group : getGroups(false)) {
402 item.addChild("group").setContent(group);
403 }
404 return item;
405 }
406
407 @Override
408 public int compareTo(@NonNull final ListItem another) {
409 return this.getDisplayName().compareToIgnoreCase(
410 another.getDisplayName());
411 }
412
413 public String getServer() {
414 return getJid().getDomain();
415 }
416
417 public boolean setAvatar(Avatar avatar) {
418 return setAvatar(avatar, false);
419 }
420
421 public boolean setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
422 if (this.avatar != null && this.avatar.equals(avatar)) {
423 return false;
424 } else {
425 if (!previouslyOmittedPepFetch && 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 @Override
541 public int getAvatarBackgroundColor() {
542 return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
543 }
544
545 public final class Options {
546 public static final int TO = 0;
547 public static final int FROM = 1;
548 public static final int ASKING = 2;
549 public static final int PREEMPTIVE_GRANT = 3;
550 public static final int IN_ROSTER = 4;
551 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
552 public static final int DIRTY_PUSH = 6;
553 public static final int DIRTY_DELETE = 7;
554 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
555 public static final int SYNCED_VIA_OTHER = 9;
556 }
557}