Contact.java

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