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