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