Contact.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.content.Context;
  5import android.database.Cursor;
  6import android.net.Uri;
  7import android.provider.ContactsContract;
  8
  9import org.json.JSONArray;
 10import org.json.JSONException;
 11import org.json.JSONObject;
 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	protected String accountUuid;
 40	protected String systemName;
 41	protected String serverName;
 42	protected String presenceName;
 43	protected String commonName;
 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 final Presences presences = new Presences();
 51	protected Account account;
 52	protected Avatar avatar;
 53
 54	private boolean mActive = false;
 55	private long mLastseen = 0;
 56	private String mLastPresence = null;
 57
 58	public Contact(final String account, final String systemName, final String serverName,
 59			final Jid jid, final int subscription, final String photoUri,
 60			final String systemAccount, final String keys, final String avatar, final long lastseen,
 61				   final String presence, final String groups) {
 62		this.accountUuid = account;
 63		this.systemName = systemName;
 64		this.serverName = serverName;
 65		this.jid = jid;
 66		this.subscription = subscription;
 67		this.photoUri = photoUri;
 68		this.systemAccount = systemAccount;
 69		try {
 70			this.keys = (keys == null ? new JSONObject("") : new JSONObject(keys));
 71		} catch (JSONException e) {
 72			this.keys = new JSONObject();
 73		}
 74		if (avatar != null) {
 75			this.avatar = new Avatar();
 76			this.avatar.sha1sum = avatar;
 77			this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
 78		}
 79		try {
 80			this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
 81		} catch (JSONException e) {
 82			this.groups = new JSONArray();
 83		}
 84		this.mLastseen = lastseen;
 85		this.mLastPresence = presence;
 86	}
 87
 88	public Contact(final Jid jid) {
 89		this.jid = jid;
 90	}
 91
 92	public static Contact fromCursor(final Cursor cursor) {
 93		final Jid jid;
 94		try {
 95			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
 96		} catch (final InvalidJidException e) {
 97			// TODO: Borked DB... handle this somehow?
 98			return null;
 99		}
100		return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
101				cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
102				cursor.getString(cursor.getColumnIndex(SERVERNAME)),
103				jid,
104				cursor.getInt(cursor.getColumnIndex(OPTIONS)),
105				cursor.getString(cursor.getColumnIndex(PHOTOURI)),
106				cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
107				cursor.getString(cursor.getColumnIndex(KEYS)),
108				cursor.getString(cursor.getColumnIndex(AVATAR)),
109				cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
110				cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
111				cursor.getString(cursor.getColumnIndex(GROUPS)));
112	}
113
114	public String getDisplayName() {
115		if (Config.X509_VERIFICATION && this.commonName != null && !this.commonName.isEmpty()) {
116			return this.commonName;
117		} else if (this.systemName != null && !this.systemName.isEmpty()) {
118			return this.systemName;
119		} else if (this.serverName != null && !this.serverName.isEmpty()) {
120			return this.serverName;
121		} else if (this.presenceName != null && !this.presenceName.isEmpty() && mutualPresenceSubscription() ) {
122			return this.presenceName;
123		} else if (jid.hasLocalpart()) {
124			return jid.getUnescapedLocalpart();
125		} else {
126			return jid.getDomainpart();
127		}
128	}
129
130	@Override
131	public String getDisplayJid() {
132		if (jid != null) {
133			return jid.toString();
134		} else {
135			return null;
136		}
137	}
138
139	public String getProfilePhoto() {
140		return this.photoUri;
141	}
142
143	public Jid getJid() {
144		return jid;
145	}
146
147	@Override
148	public List<Tag> getTags(Context context) {
149		final ArrayList<Tag> tags = new ArrayList<>();
150		for (final String group : getGroups()) {
151			tags.add(new Tag(group, UIHelper.getColorForName(group)));
152		}
153		Presence.Status status = getShownStatus();
154		if (status != Presence.Status.OFFLINE) {
155			tags.add(UIHelper.getTagForStatus(context, status));
156		}
157		if (isBlocked()) {
158			tags.add(new Tag("blocked", 0xff2e2f3b));
159		}
160		return tags;
161	}
162
163	public boolean match(Context context, String needle) {
164		if (needle == null || needle.isEmpty()) {
165			return true;
166		}
167		needle = needle.toLowerCase(Locale.US).trim();
168		String[] parts = needle.split("\\s+");
169		if (parts.length > 1) {
170			for(int i = 0; i < parts.length; ++i) {
171				if (!match(context, parts[i])) {
172					return false;
173				}
174			}
175			return true;
176		} else {
177			return jid.toString().contains(needle) ||
178				getDisplayName().toLowerCase(Locale.US).contains(needle) ||
179				matchInTag(context, needle);
180		}
181	}
182
183	private boolean matchInTag(Context context, String needle) {
184		needle = needle.toLowerCase(Locale.US);
185		for (Tag tag : getTags(context)) {
186			if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
187				return true;
188			}
189		}
190		return false;
191	}
192
193	public ContentValues getContentValues() {
194		synchronized (this.keys) {
195			final ContentValues values = new ContentValues();
196			values.put(ACCOUNT, accountUuid);
197			values.put(SYSTEMNAME, systemName);
198			values.put(SERVERNAME, serverName);
199			values.put(JID, jid.toPreppedString());
200			values.put(OPTIONS, subscription);
201			values.put(SYSTEMACCOUNT, systemAccount);
202			values.put(PHOTOURI, photoUri);
203			values.put(KEYS, keys.toString());
204			values.put(AVATAR, avatar == null ? null : avatar.getFilename());
205			values.put(LAST_PRESENCE, mLastPresence);
206			values.put(LAST_TIME, mLastseen);
207			values.put(GROUPS, groups.toString());
208			return values;
209		}
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 updatePresence(final String resource, final Presence presence) {
226		this.presences.updatePresence(resource, presence);
227	}
228
229	public void removePresence(final String resource) {
230		this.presences.removePresence(resource);
231	}
232
233	public void clearPresences() {
234		this.presences.clearPresences();
235		this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
236	}
237
238	public Presence.Status getShownStatus() {
239		return this.presences.getShownStatus();
240	}
241
242	public boolean setPhotoUri(String uri) {
243		if (uri != null && !uri.equals(this.photoUri)) {
244			this.photoUri = uri;
245			return true;
246		} else if (this.photoUri != null && uri == null) {
247			this.photoUri = null;
248			return true;
249		} else {
250			return false;
251		}
252	}
253
254	public void setServerName(String serverName) {
255		this.serverName = serverName;
256	}
257
258	public boolean setSystemName(String systemName) {
259		String old = this.systemName;
260		this.systemName = systemName;
261		return (old == null && systemName != null) || (old != null && !old.equals(systemName));
262	}
263
264	public void setPresenceName(String presenceName) {
265		this.presenceName = presenceName;
266	}
267
268	public Uri getSystemAccount() {
269		if (systemAccount == null) {
270			return null;
271		} else {
272			String[] parts = systemAccount.split("#");
273			if (parts.length != 2) {
274				return null;
275			} else {
276				long id = Long.parseLong(parts[0]);
277				return ContactsContract.Contacts.getLookupUri(id, parts[1]);
278			}
279		}
280	}
281
282	public void setSystemAccount(String account) {
283		this.systemAccount = account;
284	}
285
286	public List<String> getGroups() {
287		ArrayList<String> groups = new ArrayList<String>();
288		for (int i = 0; i < this.groups.length(); ++i) {
289			try {
290				groups.add(this.groups.getString(i));
291			} catch (final JSONException ignored) {
292			}
293		}
294		return groups;
295	}
296
297	public ArrayList<String> getOtrFingerprints() {
298		synchronized (this.keys) {
299			final ArrayList<String> fingerprints = new ArrayList<String>();
300			try {
301				if (this.keys.has("otr_fingerprints")) {
302					final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
303					for (int i = 0; i < prints.length(); ++i) {
304						final String print = prints.isNull(i) ? null : prints.getString(i);
305						if (print != null && !print.isEmpty()) {
306							fingerprints.add(prints.getString(i).toLowerCase(Locale.US));
307						}
308					}
309				}
310			} catch (final JSONException ignored) {
311
312			}
313			return fingerprints;
314		}
315	}
316	public boolean addOtrFingerprint(String print) {
317		synchronized (this.keys) {
318			if (getOtrFingerprints().contains(print)) {
319				return false;
320			}
321			try {
322				JSONArray fingerprints;
323				if (!this.keys.has("otr_fingerprints")) {
324					fingerprints = new JSONArray();
325				} else {
326					fingerprints = this.keys.getJSONArray("otr_fingerprints");
327				}
328				fingerprints.put(print);
329				this.keys.put("otr_fingerprints", fingerprints);
330				return true;
331			} catch (final JSONException ignored) {
332				return false;
333			}
334		}
335	}
336
337	public long getPgpKeyId() {
338		synchronized (this.keys) {
339			if (this.keys.has("pgp_keyid")) {
340				try {
341					return this.keys.getLong("pgp_keyid");
342				} catch (JSONException e) {
343					return 0;
344				}
345			} else {
346				return 0;
347			}
348		}
349	}
350
351	public void setPgpKeyId(long keyId) {
352		synchronized (this.keys) {
353			try {
354				this.keys.put("pgp_keyid", keyId);
355			} catch (final JSONException ignored) {
356			}
357		}
358	}
359
360	public void setOption(int option) {
361		this.subscription |= 1 << option;
362	}
363
364	public void resetOption(int option) {
365		this.subscription &= ~(1 << option);
366	}
367
368	public boolean getOption(int option) {
369		return ((this.subscription & (1 << option)) != 0);
370	}
371
372	public boolean showInRoster() {
373		return (this.getOption(Contact.Options.IN_ROSTER) && (!this
374					.getOption(Contact.Options.DIRTY_DELETE)))
375			|| (this.getOption(Contact.Options.DIRTY_PUSH));
376	}
377
378	public void parseSubscriptionFromElement(Element item) {
379		String ask = item.getAttribute("ask");
380		String subscription = item.getAttribute("subscription");
381
382		if (subscription != null) {
383			switch (subscription) {
384				case "to":
385					this.resetOption(Options.FROM);
386					this.setOption(Options.TO);
387					break;
388				case "from":
389					this.resetOption(Options.TO);
390					this.setOption(Options.FROM);
391					this.resetOption(Options.PREEMPTIVE_GRANT);
392					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
393					break;
394				case "both":
395					this.setOption(Options.TO);
396					this.setOption(Options.FROM);
397					this.resetOption(Options.PREEMPTIVE_GRANT);
398					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
399					break;
400				case "none":
401					this.resetOption(Options.FROM);
402					this.resetOption(Options.TO);
403					break;
404			}
405		}
406
407		// do NOT override asking if pending push request
408		if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
409			if ((ask != null) && (ask.equals("subscribe"))) {
410				this.setOption(Contact.Options.ASKING);
411			} else {
412				this.resetOption(Contact.Options.ASKING);
413			}
414		}
415	}
416
417	public void parseGroupsFromElement(Element item) {
418		this.groups = new JSONArray();
419		for (Element element : item.getChildren()) {
420			if (element.getName().equals("group") && element.getContent() != null) {
421				this.groups.put(element.getContent());
422			}
423		}
424	}
425
426	public Element asElement() {
427		final Element item = new Element("item");
428		item.setAttribute("jid", this.jid.toString());
429		if (this.serverName != null) {
430			item.setAttribute("name", this.serverName);
431		}
432		for (String group : getGroups()) {
433			item.addChild("group").setContent(group);
434		}
435		return item;
436	}
437
438	@Override
439	public int compareTo(final ListItem another) {
440		return this.getDisplayName().compareToIgnoreCase(
441				another.getDisplayName());
442	}
443
444	public Jid getServer() {
445		return getJid().toDomainJid();
446	}
447
448	public boolean setAvatar(Avatar avatar) {
449		if (this.avatar != null && this.avatar.equals(avatar)) {
450			return false;
451		} else {
452			if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
453				return false;
454			}
455			this.avatar = avatar;
456			return true;
457		}
458	}
459
460	public String getAvatar() {
461		return avatar == null ? null : avatar.getFilename();
462	}
463
464	public boolean deleteOtrFingerprint(String fingerprint) {
465		synchronized (this.keys) {
466			boolean success = false;
467			try {
468				if (this.keys.has("otr_fingerprints")) {
469					JSONArray newPrints = new JSONArray();
470					JSONArray oldPrints = this.keys
471							.getJSONArray("otr_fingerprints");
472					for (int i = 0; i < oldPrints.length(); ++i) {
473						if (!oldPrints.getString(i).equals(fingerprint)) {
474							newPrints.put(oldPrints.getString(i));
475						} else {
476							success = true;
477						}
478					}
479					this.keys.put("otr_fingerprints", newPrints);
480				}
481				return success;
482			} catch (JSONException e) {
483				return false;
484			}
485		}
486	}
487
488	public boolean mutualPresenceSubscription() {
489		return getOption(Options.FROM) && getOption(Options.TO);
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 void setCommonName(String cn) {
516		this.commonName = cn;
517	}
518
519	public void flagActive() {
520		this.mActive = true;
521	}
522
523	public void flagInactive() {
524		this.mActive = false;
525	}
526
527	public boolean isActive() {
528		return this.mActive;
529	}
530
531	public boolean setLastseen(long timestamp) {
532		if (timestamp > this.mLastseen) {
533			this.mLastseen = timestamp;
534			return true;
535		} else {
536			return false;
537		}
538	}
539
540	public long getLastseen() {
541		return this.mLastseen;
542	}
543
544	public void setLastResource(String resource) {
545		this.mLastPresence = resource;
546	}
547
548	public String getLastResource() {
549		return this.mLastPresence;
550	}
551
552	public final class Options {
553		public static final int TO = 0;
554		public static final int FROM = 1;
555		public static final int ASKING = 2;
556		public static final int PREEMPTIVE_GRANT = 3;
557		public static final int IN_ROSTER = 4;
558		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
559		public static final int DIRTY_PUSH = 6;
560		public static final int DIRTY_DELETE = 7;
561	}
562}