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 this.resetOption(Options.FROM);
345 this.resetOption(Options.TO);
346 } else {
347 switch (subscription) {
348 case "to":
349 this.resetOption(Options.FROM);
350 this.setOption(Options.TO);
351 break;
352 case "from":
353 this.resetOption(Options.TO);
354 this.setOption(Options.FROM);
355 this.resetOption(Options.PREEMPTIVE_GRANT);
356 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
357 break;
358 case "both":
359 this.setOption(Options.TO);
360 this.setOption(Options.FROM);
361 this.resetOption(Options.PREEMPTIVE_GRANT);
362 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
363 break;
364 case "none":
365 this.resetOption(Options.FROM);
366 this.resetOption(Options.TO);
367 break;
368 }
369 }
370
371 // do NOT override asking if pending push request
372 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
373 if ((ask != null) && (ask.equals("subscribe"))) {
374 this.setOption(Contact.Options.ASKING);
375 } else {
376 this.resetOption(Contact.Options.ASKING);
377 }
378 }
379 }
380
381 public void parseGroupsFromElement(Element item) {
382 this.groups = new JSONArray();
383 for (Element element : item.getChildren()) {
384 if (element.getName().equals("group") && element.getContent() != null) {
385 this.groups.put(element.getContent());
386 }
387 }
388 }
389
390 public Element asElement() {
391 final Element item = new Element("item");
392 item.setAttribute("jid", this.jid.toString());
393 if (this.serverName != null) {
394 item.setAttribute("name", this.serverName);
395 }
396 for (String group : getGroups(false)) {
397 item.addChild("group").setContent(group);
398 }
399 return item;
400 }
401
402 @Override
403 public int compareTo(@NonNull final ListItem another) {
404 return this.getDisplayName().compareToIgnoreCase(
405 another.getDisplayName());
406 }
407
408 public String getServer() {
409 return getJid().getDomain();
410 }
411
412 public boolean setAvatar(Avatar avatar) {
413 if (this.avatar != null && this.avatar.equals(avatar)) {
414 return false;
415 } else {
416 if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
417 return false;
418 }
419 this.avatar = avatar;
420 return true;
421 }
422 }
423
424 public String getAvatar() {
425 return avatar == null ? null : avatar.getFilename();
426 }
427
428 public boolean mutualPresenceSubscription() {
429 return getOption(Options.FROM) && getOption(Options.TO);
430 }
431
432 @Override
433 public boolean isBlocked() {
434 return getAccount().isBlocked(this);
435 }
436
437 @Override
438 public boolean isDomainBlocked() {
439 return getAccount().isBlocked(Jid.ofDomain(this.getJid().getDomain()));
440 }
441
442 @Override
443 public Jid getBlockedJid() {
444 if (isDomainBlocked()) {
445 return Jid.ofDomain(getJid().getDomain());
446 } else {
447 return getJid();
448 }
449 }
450
451 public boolean isSelf() {
452 return account.getJid().asBareJid().equals(jid.asBareJid());
453 }
454
455 boolean isOwnServer() {
456 return account.getJid().getDomain().equals(jid.asBareJid().toString());
457 }
458
459 public void setCommonName(String cn) {
460 this.commonName = cn;
461 }
462
463 public void flagActive() {
464 this.mActive = true;
465 }
466
467 public void flagInactive() {
468 this.mActive = false;
469 }
470
471 public boolean isActive() {
472 return this.mActive;
473 }
474
475 public boolean setLastseen(long timestamp) {
476 if (timestamp > this.mLastseen) {
477 this.mLastseen = timestamp;
478 return true;
479 } else {
480 return false;
481 }
482 }
483
484 public long getLastseen() {
485 return this.mLastseen;
486 }
487
488 public void setLastResource(String resource) {
489 this.mLastPresence = resource;
490 }
491
492 public String getLastResource() {
493 return this.mLastPresence;
494 }
495
496 public String getServerName() {
497 return serverName;
498 }
499
500 public final class Options {
501 public static final int TO = 0;
502 public static final int FROM = 1;
503 public static final int ASKING = 2;
504 public static final int PREEMPTIVE_GRANT = 3;
505 public static final int IN_ROSTER = 4;
506 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
507 public static final int DIRTY_PUSH = 6;
508 public static final int DIRTY_DELETE = 7;
509 }
510}