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 (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.DOMAIN_LOCK)) {
128			return jid.getLocalpart();
129		} else if (jid != null) {
130			return jid.toString();
131		} else {
132			return null;
133		}
134	}
135
136	public String getProfilePhoto() {
137		return this.photoUri;
138	}
139
140	public Jid getJid() {
141		return jid;
142	}
143
144	@Override
145	public List<Tag> getTags(Context context) {
146		final ArrayList<Tag> tags = new ArrayList<>();
147		for (final String group : getGroups()) {
148			tags.add(new Tag(group, UIHelper.getColorForName(group)));
149		}
150		Presence.Status status = getMostAvailableStatus();
151		if (status != Presence.Status.OFFLINE) {
152			tags.add(UIHelper.getTagForStatus(context, status));
153		}
154		if (isBlocked()) {
155			tags.add(new Tag("blocked", 0xff2e2f3b));
156		}
157		return tags;
158	}
159
160	public boolean match(Context context, 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(context, 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(context, needle);
177		}
178	}
179
180	private boolean matchInTag(Context context, String needle) {
181		needle = needle.toLowerCase(Locale.US);
182		for (Tag tag : getTags(context)) {
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 updatePresence(final String resource, final Presence presence) {
227		this.presences.updatePresence(resource, presence);
228	}
229
230	public void removePresence(final String resource) {
231		this.presences.removePresence(resource);
232	}
233
234	public void clearPresences() {
235		this.presences.clearPresences();
236		this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
237	}
238
239	public Presence.Status getMostAvailableStatus() {
240		Presence p = this.presences.getMostAvailablePresence();
241		if (p == null) {
242			return Presence.Status.OFFLINE;
243		}
244
245		return p.getStatus();
246	}
247
248	public boolean setPhotoUri(String uri) {
249		if (uri != null && !uri.equals(this.photoUri)) {
250			this.photoUri = uri;
251			return true;
252		} else if (this.photoUri != null && uri == null) {
253			this.photoUri = null;
254			return true;
255		} else {
256			return false;
257		}
258	}
259
260	public void setServerName(String serverName) {
261		this.serverName = serverName;
262	}
263
264	public void setSystemName(String systemName) {
265		this.systemName = systemName;
266	}
267
268	public void setPresenceName(String presenceName) {
269		this.presenceName = presenceName;
270	}
271
272	public String getSystemAccount() {
273		return systemAccount;
274	}
275
276	public void setSystemAccount(String account) {
277		this.systemAccount = account;
278	}
279
280	public List<String> getGroups() {
281		ArrayList<String> groups = new ArrayList<String>();
282		for (int i = 0; i < this.groups.length(); ++i) {
283			try {
284				groups.add(this.groups.getString(i));
285			} catch (final JSONException ignored) {
286			}
287		}
288		return groups;
289	}
290
291	public ArrayList<String> getOtrFingerprints() {
292		synchronized (this.keys) {
293			final ArrayList<String> fingerprints = new ArrayList<String>();
294			try {
295				if (this.keys.has("otr_fingerprints")) {
296					final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
297					for (int i = 0; i < prints.length(); ++i) {
298						final String print = prints.isNull(i) ? null : prints.getString(i);
299						if (print != null && !print.isEmpty()) {
300							fingerprints.add(prints.getString(i));
301						}
302					}
303				}
304			} catch (final JSONException ignored) {
305
306			}
307			return fingerprints;
308		}
309	}
310	public boolean addOtrFingerprint(String print) {
311		synchronized (this.keys) {
312			if (getOtrFingerprints().contains(print)) {
313				return false;
314			}
315			try {
316				JSONArray fingerprints;
317				if (!this.keys.has("otr_fingerprints")) {
318					fingerprints = new JSONArray();
319				} else {
320					fingerprints = this.keys.getJSONArray("otr_fingerprints");
321				}
322				fingerprints.put(print);
323				this.keys.put("otr_fingerprints", fingerprints);
324				return true;
325			} catch (final JSONException ignored) {
326				return false;
327			}
328		}
329	}
330
331	public long getPgpKeyId() {
332		synchronized (this.keys) {
333			if (this.keys.has("pgp_keyid")) {
334				try {
335					return this.keys.getLong("pgp_keyid");
336				} catch (JSONException e) {
337					return 0;
338				}
339			} else {
340				return 0;
341			}
342		}
343	}
344
345	public void setPgpKeyId(long keyId) {
346		synchronized (this.keys) {
347			try {
348				this.keys.put("pgp_keyid", keyId);
349			} catch (final JSONException ignored) {
350			}
351		}
352	}
353
354	public void setOption(int option) {
355		this.subscription |= 1 << option;
356	}
357
358	public void resetOption(int option) {
359		this.subscription &= ~(1 << option);
360	}
361
362	public boolean getOption(int option) {
363		return ((this.subscription & (1 << option)) != 0);
364	}
365
366	public boolean showInRoster() {
367		return (this.getOption(Contact.Options.IN_ROSTER) && (!this
368					.getOption(Contact.Options.DIRTY_DELETE)))
369			|| (this.getOption(Contact.Options.DIRTY_PUSH));
370	}
371
372	public void parseSubscriptionFromElement(Element item) {
373		String ask = item.getAttribute("ask");
374		String subscription = item.getAttribute("subscription");
375
376		if (subscription != null) {
377			switch (subscription) {
378				case "to":
379					this.resetOption(Options.FROM);
380					this.setOption(Options.TO);
381					break;
382				case "from":
383					this.resetOption(Options.TO);
384					this.setOption(Options.FROM);
385					this.resetOption(Options.PREEMPTIVE_GRANT);
386					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
387					break;
388				case "both":
389					this.setOption(Options.TO);
390					this.setOption(Options.FROM);
391					this.resetOption(Options.PREEMPTIVE_GRANT);
392					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
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 void setCommonName(String cn) {
519		this.commonName = cn;
520	}
521
522	public static class Lastseen {
523		public long time;
524		public String presence;
525
526		public Lastseen() {
527			this(null, 0);
528		}
529
530		public Lastseen(final String presence, final long time) {
531			this.presence = presence;
532			this.time = time;
533		}
534	}
535
536	public final class Options {
537		public static final int TO = 0;
538		public static final int FROM = 1;
539		public static final int ASKING = 2;
540		public static final int PREEMPTIVE_GRANT = 3;
541		public static final int IN_ROSTER = 4;
542		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
543		public static final int DIRTY_PUSH = 6;
544		public static final int DIRTY_DELETE = 7;
545	}
546}