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 if (this.avatar != null && this.avatar.equals(avatar)) {
419 return false;
420 } else {
421 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
422 return false;
423 }
424 this.avatar = avatar;
425 return true;
426 }
427 }
428
429 public String getAvatarFilename() {
430 return avatar == null ? null : avatar.getFilename();
431 }
432
433 public Avatar getAvatar() {
434 return avatar;
435 }
436
437 public boolean mutualPresenceSubscription() {
438 return getOption(Options.FROM) && getOption(Options.TO);
439 }
440
441 @Override
442 public boolean isBlocked() {
443 return getAccount().isBlocked(this);
444 }
445
446 @Override
447 public boolean isDomainBlocked() {
448 return getAccount().isBlocked(Jid.ofDomain(this.getJid().getDomain()));
449 }
450
451 @Override
452 public Jid getBlockedJid() {
453 if (isDomainBlocked()) {
454 return Jid.ofDomain(getJid().getDomain());
455 } else {
456 return getJid();
457 }
458 }
459
460 public boolean isSelf() {
461 return account.getJid().asBareJid().equals(jid.asBareJid());
462 }
463
464 boolean isOwnServer() {
465 return account.getJid().getDomain().equals(jid.asBareJid().toString());
466 }
467
468 public void setCommonName(String cn) {
469 this.commonName = cn;
470 }
471
472 public void flagActive() {
473 this.mActive = true;
474 }
475
476 public void flagInactive() {
477 this.mActive = false;
478 }
479
480 public boolean isActive() {
481 return this.mActive;
482 }
483
484 public boolean setLastseen(long timestamp) {
485 if (timestamp > this.mLastseen) {
486 this.mLastseen = timestamp;
487 return true;
488 } else {
489 return false;
490 }
491 }
492
493 public long getLastseen() {
494 return this.mLastseen;
495 }
496
497 public void setLastResource(String resource) {
498 this.mLastPresence = resource;
499 }
500
501 public String getLastResource() {
502 return this.mLastPresence;
503 }
504
505 public String getServerName() {
506 return serverName;
507 }
508
509 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
510 setOption(getOption(phoneContact.getClass()));
511 setSystemAccount(phoneContact.getLookupUri());
512 boolean changed = setSystemName(phoneContact.getDisplayName());
513 changed |= setPhotoUri(phoneContact.getPhotoUri());
514 return changed;
515 }
516
517 public synchronized boolean unsetPhoneContact(Class<?extends AbstractPhoneContact> clazz) {
518 resetOption(getOption(clazz));
519 boolean changed = false;
520 if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
521 setSystemAccount(null);
522 changed |= setPhotoUri(null);
523 changed |= setSystemName(null);
524 }
525 return changed;
526 }
527
528 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
529 if (clazz == JabberIdContact.class) {
530 return Options.SYNCED_VIA_ADDRESSBOOK;
531 } else {
532 return Options.SYNCED_VIA_OTHER;
533 }
534 }
535
536 public final class Options {
537 public static final int TO = 0;
538 public static final int FROM = 1;
539 public static final int ASKING = 2;
540 public static final int PREEMPTIVE_GRANT = 3;
541 public static final int IN_ROSTER = 4;
542 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
543 public static final int DIRTY_PUSH = 6;
544 public static final int DIRTY_DELETE = 7;
545 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
546 public static final int SYNCED_VIA_OTHER = 9;
547 }
548}