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