Contact.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.util.Log;
  6
  7import org.json.JSONArray;
  8import org.json.JSONException;
  9import org.json.JSONObject;
 10import org.whispersystems.libaxolotl.IdentityKey;
 11import org.whispersystems.libaxolotl.InvalidKeyException;
 12
 13import java.util.ArrayList;
 14import java.util.List;
 15import java.util.Locale;
 16
 17import eu.siacs.conversations.Config;
 18import eu.siacs.conversations.utils.UIHelper;
 19import eu.siacs.conversations.xml.Element;
 20import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 21import eu.siacs.conversations.xmpp.jid.Jid;
 22import eu.siacs.conversations.xmpp.pep.Avatar;
 23
 24public class Contact implements ListItem, Blockable {
 25	public static final String TABLENAME = "contacts";
 26
 27	public static final String SYSTEMNAME = "systemname";
 28	public static final String SERVERNAME = "servername";
 29	public static final String JID = "jid";
 30	public static final String OPTIONS = "options";
 31	public static final String SYSTEMACCOUNT = "systemaccount";
 32	public static final String PHOTOURI = "photouri";
 33	public static final String KEYS = "pgpkey";
 34	public static final String ACCOUNT = "accountUuid";
 35	public static final String AVATAR = "avatar";
 36	public static final String LAST_PRESENCE = "last_presence";
 37	public static final String LAST_TIME = "last_time";
 38	public static final String GROUPS = "groups";
 39	public Lastseen lastseen = new Lastseen();
 40	protected String accountUuid;
 41	protected String systemName;
 42	protected String serverName;
 43	protected String presenceName;
 44	protected Jid jid;
 45	protected int subscription = 0;
 46	protected String systemAccount;
 47	protected String photoUri;
 48	protected JSONObject keys = new JSONObject();
 49	protected JSONArray groups = new JSONArray();
 50	protected Presences presences = new Presences();
 51	protected Account account;
 52	protected Avatar avatar;
 53
 54	public Contact(final String account, final String systemName, final String serverName,
 55			final Jid jid, final int subscription, final String photoUri,
 56			final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) {
 57		this.accountUuid = account;
 58		this.systemName = systemName;
 59		this.serverName = serverName;
 60		this.jid = jid;
 61		this.subscription = subscription;
 62		this.photoUri = photoUri;
 63		this.systemAccount = systemAccount;
 64		try {
 65			this.keys = (keys == null ? new JSONObject("") : new JSONObject(keys));
 66		} catch (JSONException e) {
 67			this.keys = new JSONObject();
 68		}
 69		if (avatar != null) {
 70			this.avatar = new Avatar();
 71			this.avatar.sha1sum = avatar;
 72			this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
 73		}
 74		try {
 75			this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
 76		} catch (JSONException e) {
 77			this.groups = new JSONArray();
 78		}
 79		this.lastseen = lastseen;
 80	}
 81
 82	public Contact(final Jid jid) {
 83		this.jid = jid;
 84	}
 85
 86	public static Contact fromCursor(final Cursor cursor) {
 87		final Lastseen lastseen = new Lastseen(
 88				cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
 89				cursor.getLong(cursor.getColumnIndex(LAST_TIME)));
 90		final Jid jid;
 91		try {
 92			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
 93		} catch (final InvalidJidException e) {
 94			// TODO: Borked DB... handle this somehow?
 95			return null;
 96		}
 97		return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 98				cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
 99				cursor.getString(cursor.getColumnIndex(SERVERNAME)),
100				jid,
101				cursor.getInt(cursor.getColumnIndex(OPTIONS)),
102				cursor.getString(cursor.getColumnIndex(PHOTOURI)),
103				cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
104				cursor.getString(cursor.getColumnIndex(KEYS)),
105				cursor.getString(cursor.getColumnIndex(AVATAR)),
106				lastseen,
107				cursor.getString(cursor.getColumnIndex(GROUPS)));
108	}
109
110	public String getDisplayName() {
111		if (this.systemName != null) {
112			return this.systemName;
113		} else if (this.serverName != null) {
114			return this.serverName;
115		} else if (this.presenceName != null) {
116			return this.presenceName;
117		} else if (jid.hasLocalpart()) {
118			return jid.getLocalpart();
119		} else {
120			return jid.getDomainpart();
121		}
122	}
123
124	public String getProfilePhoto() {
125		return this.photoUri;
126	}
127
128	public Jid getJid() {
129		return jid;
130	}
131
132	@Override
133	public List<Tag> getTags() {
134		final ArrayList<Tag> tags = new ArrayList<>();
135		for (final String group : getGroups()) {
136			tags.add(new Tag(group, UIHelper.getColorForName(group)));
137		}
138		switch (getMostAvailableStatus()) {
139			case Presences.CHAT:
140			case Presences.ONLINE:
141				tags.add(new Tag("online", 0xff259b24));
142				break;
143			case Presences.AWAY:
144				tags.add(new Tag("away", 0xffff9800));
145				break;
146			case Presences.XA:
147				tags.add(new Tag("not available", 0xfff44336));
148				break;
149			case Presences.DND:
150				tags.add(new Tag("dnd", 0xfff44336));
151				break;
152		}
153		if (isBlocked()) {
154			tags.add(new Tag("blocked", 0xff2e2f3b));
155		}
156		return tags;
157	}
158
159	public boolean match(String needle) {
160		if (needle == null || needle.isEmpty()) {
161			return true;
162		}
163		needle = needle.toLowerCase(Locale.US).trim();
164		String[] parts = needle.split("\\s+");
165		if (parts.length > 1) {
166			for(int i = 0; i < parts.length; ++i) {
167				if (!match(parts[i])) {
168					return false;
169				}
170			}
171			return true;
172		} else {
173			return jid.toString().contains(needle) ||
174				getDisplayName().toLowerCase(Locale.US).contains(needle) ||
175				matchInTag(needle);
176		}
177	}
178
179	private boolean matchInTag(String needle) {
180		needle = needle.toLowerCase(Locale.US);
181		for (Tag tag : getTags()) {
182			if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
183				return true;
184			}
185		}
186		return false;
187	}
188
189	public ContentValues getContentValues() {
190		synchronized (this.keys) {
191			final ContentValues values = new ContentValues();
192			values.put(ACCOUNT, accountUuid);
193			values.put(SYSTEMNAME, systemName);
194			values.put(SERVERNAME, serverName);
195			values.put(JID, jid.toString());
196			values.put(OPTIONS, subscription);
197			values.put(SYSTEMACCOUNT, systemAccount);
198			values.put(PHOTOURI, photoUri);
199			values.put(KEYS, keys.toString());
200			values.put(AVATAR, avatar == null ? null : avatar.getFilename());
201			values.put(LAST_PRESENCE, lastseen.presence);
202			values.put(LAST_TIME, lastseen.time);
203			values.put(GROUPS, groups.toString());
204			return values;
205		}
206	}
207
208	public int getSubscription() {
209		return this.subscription;
210	}
211
212	public Account getAccount() {
213		return this.account;
214	}
215
216	public void setAccount(Account account) {
217		this.account = account;
218		this.accountUuid = account.getUuid();
219	}
220
221	public Presences getPresences() {
222		return this.presences;
223	}
224
225	public void setPresences(Presences pres) {
226		this.presences = pres;
227	}
228
229	public void updatePresence(final String resource, final int status) {
230		this.presences.updatePresence(resource, status);
231	}
232
233	public void removePresence(final String resource) {
234		this.presences.removePresence(resource);
235	}
236
237	public void clearPresences() {
238		this.presences.clearPresences();
239		this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
240	}
241
242	public int getMostAvailableStatus() {
243		return this.presences.getMostAvailableStatus();
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 List<IdentityKey> getTrustedAxolotlIdentityKeys() {
353		synchronized (this.keys) {
354			JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key");
355			List<IdentityKey> identityKeys = new ArrayList<>();
356			if(serializedKeyItems != null) {
357				for(int i = 0; i<serializedKeyItems.length();++i) {
358					try {
359						String serializedKeyItem = serializedKeyItems.getString(i);
360						IdentityKey identityKey = new IdentityKey(serializedKeyItem.getBytes(), 0);
361						identityKeys.add(identityKey);
362					} catch (InvalidKeyException e) {
363						Log.e(Config.LOGTAG, "Invalid axolotl identity key encountered at" + this.getJid() + ": " + e.getMessage());
364					} catch (JSONException e) {
365						Log.e(Config.LOGTAG, "Error retrieving axolotl identity key at" + this.getJid() + ": " + e.getMessage());
366					}
367				}
368			}
369			return identityKeys;
370		}
371	}
372
373	public boolean addAxolotlIdentityKey(IdentityKey identityKey, boolean trusted) {
374		synchronized (this.keys) {
375			JSONArray keysList;
376			try {
377				keysList = this.keys.getJSONArray("axolotl_identity_key");
378			} catch (JSONException e) {
379				keysList = new JSONArray();
380			}
381			keysList.put(new String(identityKey.serialize()));
382			try {
383				this.keys.put("axolotl_identity_key", keysList);
384			} catch (JSONException e) {
385				Log.e(Config.LOGTAG, "Error adding Identity Key to Contact " + this.getJid() + ": " + e.getMessage());
386				return false;
387			}
388		}
389		return true;
390	}
391
392
393	public void setOption(int option) {
394		this.subscription |= 1 << option;
395	}
396
397	public void resetOption(int option) {
398		this.subscription &= ~(1 << option);
399	}
400
401	public boolean getOption(int option) {
402		return ((this.subscription & (1 << option)) != 0);
403	}
404
405	public boolean showInRoster() {
406		return (this.getOption(Contact.Options.IN_ROSTER) && (!this
407					.getOption(Contact.Options.DIRTY_DELETE)))
408			|| (this.getOption(Contact.Options.DIRTY_PUSH));
409	}
410
411	public void parseSubscriptionFromElement(Element item) {
412		String ask = item.getAttribute("ask");
413		String subscription = item.getAttribute("subscription");
414
415		if (subscription != null) {
416			switch (subscription) {
417				case "to":
418					this.resetOption(Options.FROM);
419					this.setOption(Options.TO);
420					break;
421				case "from":
422					this.resetOption(Options.TO);
423					this.setOption(Options.FROM);
424					this.resetOption(Options.PREEMPTIVE_GRANT);
425					break;
426				case "both":
427					this.setOption(Options.TO);
428					this.setOption(Options.FROM);
429					this.resetOption(Options.PREEMPTIVE_GRANT);
430					break;
431				case "none":
432					this.resetOption(Options.FROM);
433					this.resetOption(Options.TO);
434					break;
435			}
436		}
437
438		// do NOT override asking if pending push request
439		if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
440			if ((ask != null) && (ask.equals("subscribe"))) {
441				this.setOption(Contact.Options.ASKING);
442			} else {
443				this.resetOption(Contact.Options.ASKING);
444			}
445		}
446	}
447
448	public void parseGroupsFromElement(Element item) {
449		this.groups = new JSONArray();
450		for (Element element : item.getChildren()) {
451			if (element.getName().equals("group") && element.getContent() != null) {
452				this.groups.put(element.getContent());
453			}
454		}
455	}
456
457	public Element asElement() {
458		final Element item = new Element("item");
459		item.setAttribute("jid", this.jid.toString());
460		if (this.serverName != null) {
461			item.setAttribute("name", this.serverName);
462		}
463		for (String group : getGroups()) {
464			item.addChild("group").setContent(group);
465		}
466		return item;
467	}
468
469	@Override
470	public int compareTo(final ListItem another) {
471		return this.getDisplayName().compareToIgnoreCase(
472				another.getDisplayName());
473	}
474
475	public Jid getServer() {
476		return getJid().toDomainJid();
477	}
478
479	public boolean setAvatar(Avatar avatar) {
480		if (this.avatar != null && this.avatar.equals(avatar)) {
481			return false;
482		} else {
483			if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
484				return false;
485			}
486			this.avatar = avatar;
487			return true;
488		}
489	}
490
491	public String getAvatar() {
492		return avatar == null ? null : avatar.getFilename();
493	}
494
495	public boolean deleteOtrFingerprint(String fingerprint) {
496		synchronized (this.keys) {
497			boolean success = false;
498			try {
499				if (this.keys.has("otr_fingerprints")) {
500					JSONArray newPrints = new JSONArray();
501					JSONArray oldPrints = this.keys
502							.getJSONArray("otr_fingerprints");
503					for (int i = 0; i < oldPrints.length(); ++i) {
504						if (!oldPrints.getString(i).equals(fingerprint)) {
505							newPrints.put(oldPrints.getString(i));
506						} else {
507							success = true;
508						}
509					}
510					this.keys.put("otr_fingerprints", newPrints);
511				}
512				return success;
513			} catch (JSONException e) {
514				return false;
515			}
516		}
517	}
518
519	public boolean trusted() {
520		return getOption(Options.FROM) && getOption(Options.TO);
521	}
522
523	public String getShareableUri() {
524		if (getOtrFingerprints().size() >= 1) {
525			String otr = getOtrFingerprints().get(0);
526			return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
527		} else {
528			return "xmpp:" + getJid().toBareJid().toString();
529		}
530	}
531
532	@Override
533	public boolean isBlocked() {
534		return getAccount().isBlocked(this);
535	}
536
537	@Override
538	public boolean isDomainBlocked() {
539		return getAccount().isBlocked(this.getJid().toDomainJid());
540	}
541
542	@Override
543	public Jid getBlockedJid() {
544		if (isDomainBlocked()) {
545			return getJid().toDomainJid();
546		} else {
547			return getJid();
548		}
549	}
550
551	public boolean isSelf() {
552		return account.getJid().toBareJid().equals(getJid().toBareJid());
553	}
554
555	public static class Lastseen {
556		public long time;
557		public String presence;
558
559		public Lastseen() {
560			this(null, 0);
561		}
562
563		public Lastseen(final String presence, final long time) {
564			this.presence = presence;
565			this.time = time;
566		}
567	}
568
569	public final class Options {
570		public static final int TO = 0;
571		public static final int FROM = 1;
572		public static final int ASKING = 2;
573		public static final int PREEMPTIVE_GRANT = 3;
574		public static final int IN_ROSTER = 4;
575		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
576		public static final int DIRTY_PUSH = 6;
577		public static final int DIRTY_DELETE = 7;
578	}
579}