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