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