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