Contact.java

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