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