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.support.annotation.NonNull;
  8import android.text.TextUtils;
  9
 10import org.json.JSONArray;
 11import org.json.JSONException;
 12import org.json.JSONObject;
 13
 14import java.util.ArrayList;
 15import java.util.Collection;
 16import java.util.HashSet;
 17import java.util.List;
 18import java.util.Locale;
 19
 20import eu.siacs.conversations.Config;
 21import eu.siacs.conversations.R;
 22import eu.siacs.conversations.android.AbstractPhoneContact;
 23import eu.siacs.conversations.android.JabberIdContact;
 24import eu.siacs.conversations.utils.JidHelper;
 25import eu.siacs.conversations.utils.UIHelper;
 26import eu.siacs.conversations.xml.Element;
 27import eu.siacs.conversations.xmpp.pep.Avatar;
 28import rocks.xmpp.addr.Jid;
 29
 30public class Contact implements ListItem, Blockable {
 31	public static final String TABLENAME = "contacts";
 32
 33	public static final String SYSTEMNAME = "systemname";
 34	public static final String SERVERNAME = "servername";
 35	public static final String JID = "jid";
 36	public static final String OPTIONS = "options";
 37	public static final String SYSTEMACCOUNT = "systemaccount";
 38	public static final String PHOTOURI = "photouri";
 39	public static final String KEYS = "pgpkey";
 40	public static final String ACCOUNT = "accountUuid";
 41	public static final String AVATAR = "avatar";
 42	public static final String LAST_PRESENCE = "last_presence";
 43	public static final String LAST_TIME = "last_time";
 44	public static final String GROUPS = "groups";
 45	private String accountUuid;
 46	private String systemName;
 47	private String serverName;
 48	private String presenceName;
 49	private String commonName;
 50	protected Jid jid;
 51	private int subscription = 0;
 52	private Uri systemAccount;
 53	private String photoUri;
 54	private final JSONObject keys;
 55	private JSONArray groups = new JSONArray();
 56	private final Presences presences = new Presences();
 57	protected Account account;
 58	protected Avatar avatar;
 59
 60	private boolean mActive = false;
 61	private long mLastseen = 0;
 62	private String mLastPresence = null;
 63
 64	public Contact(final String account, final String systemName, final String serverName,
 65	               final Jid jid, final int subscription, final String photoUri,
 66	               final Uri systemAccount, final String keys, final String avatar, final long lastseen,
 67	               final String presence, final String groups) {
 68		this.accountUuid = account;
 69		this.systemName = systemName;
 70		this.serverName = serverName;
 71		this.jid = jid;
 72		this.subscription = subscription;
 73		this.photoUri = photoUri;
 74		this.systemAccount = systemAccount;
 75		JSONObject tmpJsonObject;
 76		try {
 77			tmpJsonObject = (keys == null ? new JSONObject("") : new JSONObject(keys));
 78		} catch (JSONException e) {
 79			tmpJsonObject = new JSONObject();
 80		}
 81		this.keys = tmpJsonObject;
 82		if (avatar != null) {
 83			this.avatar = new Avatar();
 84			this.avatar.sha1sum = avatar;
 85			this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
 86		}
 87		try {
 88			this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
 89		} catch (JSONException e) {
 90			this.groups = new JSONArray();
 91		}
 92		this.mLastseen = lastseen;
 93		this.mLastPresence = presence;
 94	}
 95
 96	public Contact(final Jid jid) {
 97		this.jid = jid;
 98		this.keys = new JSONObject();
 99	}
100
101	public static Contact fromCursor(final Cursor cursor) {
102		final Jid jid;
103		try {
104			jid = Jid.of(cursor.getString(cursor.getColumnIndex(JID)));
105		} catch (final IllegalArgumentException e) {
106			// TODO: Borked DB... handle this somehow?
107			return null;
108		}
109		Uri systemAccount;
110		try {
111			systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)));
112		} catch (Exception e) {
113			systemAccount = null;
114		}
115		return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
116				cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
117				cursor.getString(cursor.getColumnIndex(SERVERNAME)),
118				jid,
119				cursor.getInt(cursor.getColumnIndex(OPTIONS)),
120				cursor.getString(cursor.getColumnIndex(PHOTOURI)),
121				systemAccount,
122				cursor.getString(cursor.getColumnIndex(KEYS)),
123				cursor.getString(cursor.getColumnIndex(AVATAR)),
124				cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
125				cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
126				cursor.getString(cursor.getColumnIndex(GROUPS)));
127	}
128
129	public String getDisplayName() {
130		if (Config.X509_VERIFICATION && !TextUtils.isEmpty(this.commonName)) {
131			return this.commonName;
132		} else if (!TextUtils.isEmpty(this.systemName)) {
133			return this.systemName;
134		} else if (!TextUtils.isEmpty(this.serverName)) {
135			return this.serverName;
136		} else if (!TextUtils.isEmpty(this.presenceName) && mutualPresenceSubscription()) {
137			return this.presenceName;
138		} else if (jid.getLocal() != null) {
139			return JidHelper.localPartOrFallback(jid);
140		} else {
141			return jid.getDomain();
142		}
143	}
144
145	public String getProfilePhoto() {
146		return this.photoUri;
147	}
148
149	public Jid getJid() {
150		return jid;
151	}
152
153	@Override
154	public List<Tag> getTags(Context context) {
155		final ArrayList<Tag> tags = new ArrayList<>();
156		for (final String group : getGroups(true)) {
157			tags.add(new Tag(group, UIHelper.getColorForName(group)));
158		}
159		Presence.Status status = getShownStatus();
160		if (status != Presence.Status.OFFLINE) {
161			tags.add(UIHelper.getTagForStatus(context, status));
162		}
163		if (isBlocked()) {
164			tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
165		}
166		return tags;
167	}
168
169	public boolean match(Context context, String needle) {
170		if (TextUtils.isEmpty(needle)) {
171			return true;
172		}
173		needle = needle.toLowerCase(Locale.US).trim();
174		String[] parts = needle.split("\\s+");
175		if (parts.length > 1) {
176			for (String part : parts) {
177				if (!match(context, part)) {
178					return false;
179				}
180			}
181			return true;
182		} else {
183			return jid.toString().contains(needle) ||
184					getDisplayName().toLowerCase(Locale.US).contains(needle) ||
185					matchInTag(context, needle);
186		}
187	}
188
189	private boolean matchInTag(Context context, String needle) {
190		needle = needle.toLowerCase(Locale.US);
191		for (Tag tag : getTags(context)) {
192			if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
193				return true;
194			}
195		}
196		return false;
197	}
198
199	public ContentValues getContentValues() {
200		synchronized (this.keys) {
201			final ContentValues values = new ContentValues();
202			values.put(ACCOUNT, accountUuid);
203			values.put(SYSTEMNAME, systemName);
204			values.put(SERVERNAME, serverName);
205			values.put(JID, jid.toString());
206			values.put(OPTIONS, subscription);
207			values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
208			values.put(PHOTOURI, photoUri);
209			values.put(KEYS, keys.toString());
210			values.put(AVATAR, avatar == null ? null : avatar.getFilename());
211			values.put(LAST_PRESENCE, mLastPresence);
212			values.put(LAST_TIME, mLastseen);
213			values.put(GROUPS, groups.toString());
214			return values;
215		}
216	}
217
218	public Account getAccount() {
219		return this.account;
220	}
221
222	public void setAccount(Account account) {
223		this.account = account;
224		this.accountUuid = account.getUuid();
225	}
226
227	public Presences getPresences() {
228		return this.presences;
229	}
230
231	public void updatePresence(final String resource, final Presence presence) {
232		this.presences.updatePresence(resource, presence);
233	}
234
235	public void removePresence(final String resource) {
236		this.presences.removePresence(resource);
237	}
238
239	public void clearPresences() {
240		this.presences.clearPresences();
241		this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
242	}
243
244	public Presence.Status getShownStatus() {
245		return this.presences.getShownStatus();
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 boolean setSystemName(String systemName) {
265		final String old = getDisplayName();
266		this.systemName = systemName;
267		return !old.equals(getDisplayName());
268	}
269
270	public boolean setPresenceName(String presenceName) {
271		final String old = getDisplayName();
272		this.presenceName = presenceName;
273		return !old.equals(getDisplayName());
274	}
275
276	public Uri getSystemAccount() {
277		return systemAccount;
278	}
279
280	public void setSystemAccount(Uri lookupUri) {
281		this.systemAccount = lookupUri;
282	}
283
284	private Collection<String> getGroups(final boolean unique) {
285		final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
286		for (int i = 0; i < this.groups.length(); ++i) {
287			try {
288				groups.add(this.groups.getString(i));
289			} catch (final JSONException ignored) {
290			}
291		}
292		return groups;
293	}
294
295	public long getPgpKeyId() {
296		synchronized (this.keys) {
297			if (this.keys.has("pgp_keyid")) {
298				try {
299					return this.keys.getLong("pgp_keyid");
300				} catch (JSONException e) {
301					return 0;
302				}
303			} else {
304				return 0;
305			}
306		}
307	}
308
309	public boolean setPgpKeyId(long keyId) {
310		final long previousKeyId = getPgpKeyId();
311		synchronized (this.keys) {
312			try {
313				this.keys.put("pgp_keyid", keyId);
314				return previousKeyId != keyId;
315			} catch (final JSONException ignored) {
316			}
317		}
318		return false;
319	}
320
321	public void setOption(int option) {
322		this.subscription |= 1 << option;
323	}
324
325	public void resetOption(int option) {
326		this.subscription &= ~(1 << option);
327	}
328
329	public boolean getOption(int option) {
330		return ((this.subscription & (1 << option)) != 0);
331	}
332
333	public boolean showInRoster() {
334		return (this.getOption(Contact.Options.IN_ROSTER) && (!this
335				.getOption(Contact.Options.DIRTY_DELETE)))
336				|| (this.getOption(Contact.Options.DIRTY_PUSH));
337	}
338
339	public boolean showInContactList() {
340		return showInRoster() || getOption(Options.SYNCED_VIA_OTHER);
341	}
342
343	public void parseSubscriptionFromElement(Element item) {
344		String ask = item.getAttribute("ask");
345		String subscription = item.getAttribute("subscription");
346
347		if (subscription == null) {
348			this.resetOption(Options.FROM);
349			this.resetOption(Options.TO);
350		} else {
351			switch (subscription) {
352				case "to":
353					this.resetOption(Options.FROM);
354					this.setOption(Options.TO);
355					break;
356				case "from":
357					this.resetOption(Options.TO);
358					this.setOption(Options.FROM);
359					this.resetOption(Options.PREEMPTIVE_GRANT);
360					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
361					break;
362				case "both":
363					this.setOption(Options.TO);
364					this.setOption(Options.FROM);
365					this.resetOption(Options.PREEMPTIVE_GRANT);
366					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
367					break;
368				case "none":
369					this.resetOption(Options.FROM);
370					this.resetOption(Options.TO);
371					break;
372			}
373		}
374
375		// do NOT override asking if pending push request
376		if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
377			if ((ask != null) && (ask.equals("subscribe"))) {
378				this.setOption(Contact.Options.ASKING);
379			} else {
380				this.resetOption(Contact.Options.ASKING);
381			}
382		}
383	}
384
385	public void parseGroupsFromElement(Element item) {
386		this.groups = new JSONArray();
387		for (Element element : item.getChildren()) {
388			if (element.getName().equals("group") && element.getContent() != null) {
389				this.groups.put(element.getContent());
390			}
391		}
392	}
393
394	public Element asElement() {
395		final Element item = new Element("item");
396		item.setAttribute("jid", this.jid.toString());
397		if (this.serverName != null) {
398			item.setAttribute("name", this.serverName);
399		}
400		for (String group : getGroups(false)) {
401			item.addChild("group").setContent(group);
402		}
403		return item;
404	}
405
406	@Override
407	public int compareTo(@NonNull final ListItem another) {
408		return this.getDisplayName().compareToIgnoreCase(
409				another.getDisplayName());
410	}
411
412	public String getServer() {
413		return getJid().getDomain();
414	}
415
416	public boolean setAvatar(Avatar avatar) {
417		if (this.avatar != null && this.avatar.equals(avatar)) {
418			return false;
419		} else {
420			if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
421				return false;
422			}
423			this.avatar = avatar;
424			return true;
425		}
426	}
427
428	public String getAvatarFilename() {
429		return avatar == null ? null : avatar.getFilename();
430	}
431
432	public Avatar getAvatar() {
433		return avatar;
434	}
435
436	public boolean mutualPresenceSubscription() {
437		return getOption(Options.FROM) && getOption(Options.TO);
438	}
439
440	@Override
441	public boolean isBlocked() {
442		return getAccount().isBlocked(this);
443	}
444
445	@Override
446	public boolean isDomainBlocked() {
447		return getAccount().isBlocked(Jid.ofDomain(this.getJid().getDomain()));
448	}
449
450	@Override
451	public Jid getBlockedJid() {
452		if (isDomainBlocked()) {
453			return Jid.ofDomain(getJid().getDomain());
454		} else {
455			return getJid();
456		}
457	}
458
459	public boolean isSelf() {
460		return account.getJid().asBareJid().equals(jid.asBareJid());
461	}
462
463	boolean isOwnServer() {
464		return account.getJid().getDomain().equals(jid.asBareJid().toString());
465	}
466
467	public void setCommonName(String cn) {
468		this.commonName = cn;
469	}
470
471	public void flagActive() {
472		this.mActive = true;
473	}
474
475	public void flagInactive() {
476		this.mActive = false;
477	}
478
479	public boolean isActive() {
480		return this.mActive;
481	}
482
483	public boolean setLastseen(long timestamp) {
484		if (timestamp > this.mLastseen) {
485			this.mLastseen = timestamp;
486			return true;
487		} else {
488			return false;
489		}
490	}
491
492	public long getLastseen() {
493		return this.mLastseen;
494	}
495
496	public void setLastResource(String resource) {
497		this.mLastPresence = resource;
498	}
499
500	public String getLastResource() {
501		return this.mLastPresence;
502	}
503
504	public String getServerName() {
505		return serverName;
506	}
507
508	public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
509		setOption(getOption(phoneContact.getClass()));
510		setSystemAccount(phoneContact.getLookupUri());
511		boolean changed = setSystemName(phoneContact.getDisplayName());
512		changed |= setPhotoUri(phoneContact.getPhotoUri());
513		return changed;
514	}
515
516	public synchronized boolean unsetPhoneContact(Class<?extends AbstractPhoneContact> clazz) {
517		resetOption(getOption(clazz));
518		boolean changed = false;
519		if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
520			setSystemAccount(null);
521			changed |= setPhotoUri(null);
522			changed |= setSystemName(null);
523		}
524		return changed;
525	}
526
527	public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
528		if (clazz == JabberIdContact.class) {
529			return Options.SYNCED_VIA_ADDRESSBOOK;
530		} else {
531			return Options.SYNCED_VIA_OTHER;
532		}
533	}
534
535    public final class Options {
536		public static final int TO = 0;
537		public static final int FROM = 1;
538		public static final int ASKING = 2;
539		public static final int PREEMPTIVE_GRANT = 3;
540		public static final int IN_ROSTER = 4;
541		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
542		public static final int DIRTY_PUSH = 6;
543		public static final int DIRTY_DELETE = 7;
544		private static final int SYNCED_VIA_ADDRESSBOOK = 8;
545		public static final int SYNCED_VIA_OTHER = 9;
546	}
547}