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