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 List<IdentityKey> getAxolotlIdentityKeys() {
354		synchronized (this.keys) {
355			JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key");
356			List<IdentityKey> identityKeys = new ArrayList<>();
357			List<Integer> toDelete = new ArrayList<>();
358			if(serializedKeyItems != null) {
359				for(int i = 0; i<serializedKeyItems.length();++i) {
360					try {
361						String serializedKeyItem = serializedKeyItems.getString(i);
362						IdentityKey identityKey = new IdentityKey(Base64.decode(serializedKeyItem, Base64.DEFAULT), 0);
363						identityKeys.add(identityKey);
364					} catch (InvalidKeyException e) {
365						Log.e(Config.LOGTAG, "Invalid axolotl identity key encountered at contact" + this.getJid() + ": " + e.getMessage() + ", marking for deletion...");
366						toDelete.add(i);
367					} catch (JSONException e) {
368						Log.e(Config.LOGTAG, "Error retrieving axolotl identity key at contact " + this.getJid() + ": " + e.getMessage());
369					} catch (IllegalArgumentException e) {
370						Log.e(Config.LOGTAG, "Encountered malformed identity key for contact" + this.getJid() + ": " + e.getMessage() + ", marking for deletion... ");
371						toDelete.add(i);
372					}
373				}
374				if(!toDelete.isEmpty()) {
375					try {
376						JSONArray filteredKeyItems = new JSONArray();
377						for (int i = 0; i < serializedKeyItems.length(); ++i) {
378							if (!toDelete.contains(i)) {
379								filteredKeyItems.put(serializedKeyItems.get(i));
380							}
381						}
382						this.keys.put("axolotl_identity_key", filteredKeyItems);
383					} catch (JSONException e) {
384						//should never happen
385					}
386				}
387			}
388			return identityKeys;
389		}
390	}
391
392	public boolean addAxolotlIdentityKey(IdentityKey identityKey) {
393		synchronized (this.keys) {
394			if(!getAxolotlIdentityKeys().contains(identityKey)) {
395				JSONArray keysList;
396				try {
397					keysList = this.keys.getJSONArray("axolotl_identity_key");
398				} catch (JSONException e) {
399					keysList = new JSONArray();
400				}
401
402				keysList.put(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
403				try {
404					this.keys.put("axolotl_identity_key", keysList);
405				} catch (JSONException e) {
406					Log.e(Config.LOGTAG, "Error adding Identity Key to Contact " + this.getJid() + ": " + e.getMessage());
407					return false;
408				}
409				return true;
410			} else {
411				return false;
412			}
413		}
414	}
415
416
417	public void setOption(int option) {
418		this.subscription |= 1 << option;
419	}
420
421	public void resetOption(int option) {
422		this.subscription &= ~(1 << option);
423	}
424
425	public boolean getOption(int option) {
426		return ((this.subscription & (1 << option)) != 0);
427	}
428
429	public boolean showInRoster() {
430		return (this.getOption(Contact.Options.IN_ROSTER) && (!this
431					.getOption(Contact.Options.DIRTY_DELETE)))
432			|| (this.getOption(Contact.Options.DIRTY_PUSH));
433	}
434
435	public void parseSubscriptionFromElement(Element item) {
436		String ask = item.getAttribute("ask");
437		String subscription = item.getAttribute("subscription");
438
439		if (subscription != null) {
440			switch (subscription) {
441				case "to":
442					this.resetOption(Options.FROM);
443					this.setOption(Options.TO);
444					break;
445				case "from":
446					this.resetOption(Options.TO);
447					this.setOption(Options.FROM);
448					this.resetOption(Options.PREEMPTIVE_GRANT);
449					break;
450				case "both":
451					this.setOption(Options.TO);
452					this.setOption(Options.FROM);
453					this.resetOption(Options.PREEMPTIVE_GRANT);
454					break;
455				case "none":
456					this.resetOption(Options.FROM);
457					this.resetOption(Options.TO);
458					break;
459			}
460		}
461
462		// do NOT override asking if pending push request
463		if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
464			if ((ask != null) && (ask.equals("subscribe"))) {
465				this.setOption(Contact.Options.ASKING);
466			} else {
467				this.resetOption(Contact.Options.ASKING);
468			}
469		}
470	}
471
472	public void parseGroupsFromElement(Element item) {
473		this.groups = new JSONArray();
474		for (Element element : item.getChildren()) {
475			if (element.getName().equals("group") && element.getContent() != null) {
476				this.groups.put(element.getContent());
477			}
478		}
479	}
480
481	public Element asElement() {
482		final Element item = new Element("item");
483		item.setAttribute("jid", this.jid.toString());
484		if (this.serverName != null) {
485			item.setAttribute("name", this.serverName);
486		}
487		for (String group : getGroups()) {
488			item.addChild("group").setContent(group);
489		}
490		return item;
491	}
492
493	@Override
494	public int compareTo(final ListItem another) {
495		return this.getDisplayName().compareToIgnoreCase(
496				another.getDisplayName());
497	}
498
499	public Jid getServer() {
500		return getJid().toDomainJid();
501	}
502
503	public boolean setAvatar(Avatar avatar) {
504		if (this.avatar != null && this.avatar.equals(avatar)) {
505			return false;
506		} else {
507			if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
508				return false;
509			}
510			this.avatar = avatar;
511			return true;
512		}
513	}
514
515	public String getAvatar() {
516		return avatar == null ? null : avatar.getFilename();
517	}
518
519	public boolean deleteOtrFingerprint(String fingerprint) {
520		synchronized (this.keys) {
521			boolean success = false;
522			try {
523				if (this.keys.has("otr_fingerprints")) {
524					JSONArray newPrints = new JSONArray();
525					JSONArray oldPrints = this.keys
526							.getJSONArray("otr_fingerprints");
527					for (int i = 0; i < oldPrints.length(); ++i) {
528						if (!oldPrints.getString(i).equals(fingerprint)) {
529							newPrints.put(oldPrints.getString(i));
530						} else {
531							success = true;
532						}
533					}
534					this.keys.put("otr_fingerprints", newPrints);
535				}
536				return success;
537			} catch (JSONException e) {
538				return false;
539			}
540		}
541	}
542
543	public boolean trusted() {
544		return getOption(Options.FROM) && getOption(Options.TO);
545	}
546
547	public String getShareableUri() {
548		if (getOtrFingerprints().size() >= 1) {
549			String otr = getOtrFingerprints().get(0);
550			return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
551		} else {
552			return "xmpp:" + getJid().toBareJid().toString();
553		}
554	}
555
556	@Override
557	public boolean isBlocked() {
558		return getAccount().isBlocked(this);
559	}
560
561	@Override
562	public boolean isDomainBlocked() {
563		return getAccount().isBlocked(this.getJid().toDomainJid());
564	}
565
566	@Override
567	public Jid getBlockedJid() {
568		if (isDomainBlocked()) {
569			return getJid().toDomainJid();
570		} else {
571			return getJid();
572		}
573	}
574
575	public boolean isSelf() {
576		return account.getJid().toBareJid().equals(getJid().toBareJid());
577	}
578
579	public static class Lastseen {
580		public long time;
581		public String presence;
582
583		public Lastseen() {
584			this(null, 0);
585		}
586
587		public Lastseen(final String presence, final long time) {
588			this.presence = presence;
589			this.time = time;
590		}
591	}
592
593	public final class Options {
594		public static final int TO = 0;
595		public static final int FROM = 1;
596		public static final int ASKING = 2;
597		public static final int PREEMPTIVE_GRANT = 3;
598		public static final int IN_ROSTER = 4;
599		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
600		public static final int DIRTY_PUSH = 6;
601		public static final int DIRTY_DELETE = 7;
602	}
603}