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.services.QuickConversationsService;
 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) && ((QuickConversationsService.isQuicksy() && Config.QUICKSY_DOMAIN.equals(jid.getDomain())) ||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		return tags;
168	}
169
170	public boolean match(Context context, String needle) {
171		if (TextUtils.isEmpty(needle)) {
172			return true;
173		}
174		needle = needle.toLowerCase(Locale.US).trim();
175		String[] parts = needle.split("\\s+");
176		if (parts.length > 1) {
177			for (String part : parts) {
178				if (!match(context, part)) {
179					return false;
180				}
181			}
182			return true;
183		} else {
184			return jid.toString().contains(needle) ||
185					getDisplayName().toLowerCase(Locale.US).contains(needle) ||
186					matchInTag(context, needle);
187		}
188	}
189
190	private boolean matchInTag(Context context, String needle) {
191		needle = needle.toLowerCase(Locale.US);
192		for (Tag tag : getTags(context)) {
193			if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
194				return true;
195			}
196		}
197		return false;
198	}
199
200	public ContentValues getContentValues() {
201		synchronized (this.keys) {
202			final ContentValues values = new ContentValues();
203			values.put(ACCOUNT, accountUuid);
204			values.put(SYSTEMNAME, systemName);
205			values.put(SERVERNAME, serverName);
206			values.put(JID, jid.toString());
207			values.put(OPTIONS, subscription);
208			values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
209			values.put(PHOTOURI, photoUri);
210			values.put(KEYS, keys.toString());
211			values.put(AVATAR, avatar == null ? null : avatar.getFilename());
212			values.put(LAST_PRESENCE, mLastPresence);
213			values.put(LAST_TIME, mLastseen);
214			values.put(GROUPS, groups.toString());
215			return values;
216		}
217	}
218
219	public Account getAccount() {
220		return this.account;
221	}
222
223	public void setAccount(Account account) {
224		this.account = account;
225		this.accountUuid = account.getUuid();
226	}
227
228	public Presences getPresences() {
229		return this.presences;
230	}
231
232	public void updatePresence(final String resource, final Presence presence) {
233		this.presences.updatePresence(resource, presence);
234	}
235
236	public void removePresence(final String resource) {
237		this.presences.removePresence(resource);
238	}
239
240	public void clearPresences() {
241		this.presences.clearPresences();
242		this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
243	}
244
245	public Presence.Status getShownStatus() {
246		return this.presences.getShownStatus();
247	}
248
249	public boolean setPhotoUri(String uri) {
250		if (uri != null && !uri.equals(this.photoUri)) {
251			this.photoUri = uri;
252			return true;
253		} else if (this.photoUri != null && uri == null) {
254			this.photoUri = null;
255			return true;
256		} else {
257			return false;
258		}
259	}
260
261	public void setServerName(String serverName) {
262		this.serverName = serverName;
263	}
264
265	public boolean setSystemName(String systemName) {
266		final String old = getDisplayName();
267		this.systemName = systemName;
268		return !old.equals(getDisplayName());
269	}
270
271	public boolean setPresenceName(String presenceName) {
272		final String old = getDisplayName();
273		this.presenceName = presenceName;
274		return !old.equals(getDisplayName());
275	}
276
277	public Uri getSystemAccount() {
278		return systemAccount;
279	}
280
281	public void setSystemAccount(Uri lookupUri) {
282		this.systemAccount = lookupUri;
283	}
284
285	private Collection<String> getGroups(final boolean unique) {
286		final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
287		for (int i = 0; i < this.groups.length(); ++i) {
288			try {
289				groups.add(this.groups.getString(i));
290			} catch (final JSONException ignored) {
291			}
292		}
293		return groups;
294	}
295
296	public long getPgpKeyId() {
297		synchronized (this.keys) {
298			if (this.keys.has("pgp_keyid")) {
299				try {
300					return this.keys.getLong("pgp_keyid");
301				} catch (JSONException e) {
302					return 0;
303				}
304			} else {
305				return 0;
306			}
307		}
308	}
309
310	public boolean setPgpKeyId(long keyId) {
311		final long previousKeyId = getPgpKeyId();
312		synchronized (this.keys) {
313			try {
314				this.keys.put("pgp_keyid", keyId);
315				return previousKeyId != keyId;
316			} catch (final JSONException ignored) {
317			}
318		}
319		return false;
320	}
321
322	public void setOption(int option) {
323		this.subscription |= 1 << option;
324	}
325
326	public void resetOption(int option) {
327		this.subscription &= ~(1 << option);
328	}
329
330	public boolean getOption(int option) {
331		return ((this.subscription & (1 << option)) != 0);
332	}
333
334	public boolean showInRoster() {
335		return (this.getOption(Contact.Options.IN_ROSTER) && (!this
336				.getOption(Contact.Options.DIRTY_DELETE)))
337				|| (this.getOption(Contact.Options.DIRTY_PUSH));
338	}
339
340	public boolean showInContactList() {
341		return showInRoster() || getOption(Options.SYNCED_VIA_OTHER);
342	}
343
344	public void parseSubscriptionFromElement(Element item) {
345		String ask = item.getAttribute("ask");
346		String subscription = item.getAttribute("subscription");
347
348		if (subscription == null) {
349			this.resetOption(Options.FROM);
350			this.resetOption(Options.TO);
351		} else {
352			switch (subscription) {
353				case "to":
354					this.resetOption(Options.FROM);
355					this.setOption(Options.TO);
356					break;
357				case "from":
358					this.resetOption(Options.TO);
359					this.setOption(Options.FROM);
360					this.resetOption(Options.PREEMPTIVE_GRANT);
361					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
362					break;
363				case "both":
364					this.setOption(Options.TO);
365					this.setOption(Options.FROM);
366					this.resetOption(Options.PREEMPTIVE_GRANT);
367					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
368					break;
369				case "none":
370					this.resetOption(Options.FROM);
371					this.resetOption(Options.TO);
372					break;
373			}
374		}
375
376		// do NOT override asking if pending push request
377		if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
378			if ((ask != null) && (ask.equals("subscribe"))) {
379				this.setOption(Contact.Options.ASKING);
380			} else {
381				this.resetOption(Contact.Options.ASKING);
382			}
383		}
384	}
385
386	public void parseGroupsFromElement(Element item) {
387		this.groups = new JSONArray();
388		for (Element element : item.getChildren()) {
389			if (element.getName().equals("group") && element.getContent() != null) {
390				this.groups.put(element.getContent());
391			}
392		}
393	}
394
395	public Element asElement() {
396		final Element item = new Element("item");
397		item.setAttribute("jid", this.jid.toString());
398		if (this.serverName != null) {
399			item.setAttribute("name", this.serverName);
400		}
401		for (String group : getGroups(false)) {
402			item.addChild("group").setContent(group);
403		}
404		return item;
405	}
406
407	@Override
408	public int compareTo(@NonNull final ListItem another) {
409		return this.getDisplayName().compareToIgnoreCase(
410				another.getDisplayName());
411	}
412
413	public String getServer() {
414		return getJid().getDomain();
415	}
416
417	public boolean setAvatar(Avatar avatar) {
418		return setAvatar(avatar, false);
419	}
420
421	public boolean setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
422		if (this.avatar != null && this.avatar.equals(avatar)) {
423			return false;
424		} else {
425			if (!previouslyOmittedPepFetch && 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	@Override
541	public int getAvatarBackgroundColor() {
542		return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
543	}
544
545	public final class Options {
546		public static final int TO = 0;
547		public static final int FROM = 1;
548		public static final int ASKING = 2;
549		public static final int PREEMPTIVE_GRANT = 3;
550		public static final int IN_ROSTER = 4;
551		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
552		public static final int DIRTY_PUSH = 6;
553		public static final int DIRTY_DELETE = 7;
554		private static final int SYNCED_VIA_ADDRESSBOOK = 8;
555		public static final int SYNCED_VIA_OTHER = 9;
556	}
557}