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