Contact.java

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