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