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 void setPgpKeyId(long keyId) {
316 synchronized (this.keys) {
317 try {
318 this.keys.put("pgp_keyid", keyId);
319 } catch (final JSONException ignored) {
320 }
321 }
322 }
323
324 public void setOption(int option) {
325 this.subscription |= 1 << option;
326 }
327
328 public void resetOption(int option) {
329 this.subscription &= ~(1 << option);
330 }
331
332 public boolean getOption(int option) {
333 return ((this.subscription & (1 << option)) != 0);
334 }
335
336 public boolean showInRoster() {
337 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
338 .getOption(Contact.Options.DIRTY_DELETE)))
339 || (this.getOption(Contact.Options.DIRTY_PUSH));
340 }
341
342 public boolean showInPhoneBook() {
343 return systemAccount != null && !systemAccount.trim().isEmpty();
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.toString());
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();
417 }
418
419 public boolean setAvatar(Avatar avatar) {
420 if (this.avatar != null && this.avatar.equals(avatar)) {
421 return false;
422 } else {
423 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
424 return false;
425 }
426 this.avatar = avatar;
427 return true;
428 }
429 }
430
431 public String getAvatar() {
432 return avatar == null ? null : avatar.getFilename();
433 }
434
435 public boolean mutualPresenceSubscription() {
436 return getOption(Options.FROM) && getOption(Options.TO);
437 }
438
439 @Override
440 public boolean isBlocked() {
441 return getAccount().isBlocked(this);
442 }
443
444 @Override
445 public boolean isDomainBlocked() {
446 return getAccount().isBlocked(Jid.ofDomain(this.getJid().getDomain()));
447 }
448
449 @Override
450 public Jid getBlockedJid() {
451 if (isDomainBlocked()) {
452 return Jid.ofDomain(getJid().getDomain());
453 } else {
454 return getJid();
455 }
456 }
457
458 public boolean isSelf() {
459 return account.getJid().asBareJid().equals(jid.asBareJid());
460 }
461
462 boolean isOwnServer() {
463 return account.getJid().getDomain().equals(jid.asBareJid().toString());
464 }
465
466 public void setCommonName(String cn) {
467 this.commonName = cn;
468 }
469
470 public void flagActive() {
471 this.mActive = true;
472 }
473
474 public void flagInactive() {
475 this.mActive = false;
476 }
477
478 public boolean isActive() {
479 return this.mActive;
480 }
481
482 public boolean setLastseen(long timestamp) {
483 if (timestamp > this.mLastseen) {
484 this.mLastseen = timestamp;
485 return true;
486 } else {
487 return false;
488 }
489 }
490
491 public long getLastseen() {
492 return this.mLastseen;
493 }
494
495 public void setLastResource(String resource) {
496 this.mLastPresence = resource;
497 }
498
499 public String getLastResource() {
500 return this.mLastPresence;
501 }
502
503 public String getServerName() {
504 return serverName;
505 }
506
507 public final class Options {
508 public static final int TO = 0;
509 public static final int FROM = 1;
510 public static final int ASKING = 2;
511 public static final int PREEMPTIVE_GRANT = 3;
512 public static final int IN_ROSTER = 4;
513 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
514 public static final int DIRTY_PUSH = 6;
515 public static final int DIRTY_DELETE = 7;
516 }
517}