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 eu.siacs.conversations.xmpp.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().toEscapedString())) ||mutualPresenceSubscription())) {
138			return this.presenceName;
139		} else if (jid.getLocal() != null) {
140			return JidHelper.localPartOrFallback(jid);
141		} else {
142			return jid.getDomain().toEscapedString();
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()
342				|| getOption(Options.SYNCED_VIA_OTHER)
343				|| (QuickConversationsService.isQuicksy() && systemAccount != null);
344	}
345
346	public void parseSubscriptionFromElement(Element item) {
347		String ask = item.getAttribute("ask");
348		String subscription = item.getAttribute("subscription");
349
350		if (subscription == null) {
351			this.resetOption(Options.FROM);
352			this.resetOption(Options.TO);
353		} else {
354			switch (subscription) {
355				case "to":
356					this.resetOption(Options.FROM);
357					this.setOption(Options.TO);
358					break;
359				case "from":
360					this.resetOption(Options.TO);
361					this.setOption(Options.FROM);
362					this.resetOption(Options.PREEMPTIVE_GRANT);
363					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
364					break;
365				case "both":
366					this.setOption(Options.TO);
367					this.setOption(Options.FROM);
368					this.resetOption(Options.PREEMPTIVE_GRANT);
369					this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
370					break;
371				case "none":
372					this.resetOption(Options.FROM);
373					this.resetOption(Options.TO);
374					break;
375			}
376		}
377
378		// do NOT override asking if pending push request
379		if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
380			if ((ask != null) && (ask.equals("subscribe"))) {
381				this.setOption(Contact.Options.ASKING);
382			} else {
383				this.resetOption(Contact.Options.ASKING);
384			}
385		}
386	}
387
388	public void parseGroupsFromElement(Element item) {
389		this.groups = new JSONArray();
390		for (Element element : item.getChildren()) {
391			if (element.getName().equals("group") && element.getContent() != null) {
392				this.groups.put(element.getContent());
393			}
394		}
395	}
396
397	public Element asElement() {
398		final Element item = new Element("item");
399		item.setAttribute("jid", this.jid);
400		if (this.serverName != null) {
401			item.setAttribute("name", this.serverName);
402		}
403		for (String group : getGroups(false)) {
404			item.addChild("group").setContent(group);
405		}
406		return item;
407	}
408
409	@Override
410	public int compareTo(@NonNull final ListItem another) {
411		return this.getDisplayName().compareToIgnoreCase(
412				another.getDisplayName());
413	}
414
415	public String getServer() {
416		return getJid().getDomain().toEscapedString();
417	}
418
419	public boolean setAvatar(Avatar avatar) {
420		return setAvatar(avatar, false);
421	}
422
423	public boolean setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
424		if (this.avatar != null && this.avatar.equals(avatar)) {
425			return false;
426		} else {
427			if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
428				return false;
429			}
430			this.avatar = avatar;
431			return true;
432		}
433	}
434
435	public String getAvatarFilename() {
436		return avatar == null ? null : avatar.getFilename();
437	}
438
439	public Avatar getAvatar() {
440		return avatar;
441	}
442
443	public boolean mutualPresenceSubscription() {
444		return getOption(Options.FROM) && getOption(Options.TO);
445	}
446
447	@Override
448	public boolean isBlocked() {
449		return getAccount().isBlocked(this);
450	}
451
452	@Override
453	public boolean isDomainBlocked() {
454		return getAccount().isBlocked(this.getJid().getDomain());
455	}
456
457	@Override
458	public Jid getBlockedJid() {
459		if (isDomainBlocked()) {
460			return getJid().getDomain();
461		} else {
462			return getJid();
463		}
464	}
465
466	public boolean isSelf() {
467		return account.getJid().asBareJid().equals(jid.asBareJid());
468	}
469
470	boolean isOwnServer() {
471		return account.getJid().getDomain().equals(jid.asBareJid());
472	}
473
474	public void setCommonName(String cn) {
475		this.commonName = cn;
476	}
477
478	public void flagActive() {
479		this.mActive = true;
480	}
481
482	public void flagInactive() {
483		this.mActive = false;
484	}
485
486	public boolean isActive() {
487		return this.mActive;
488	}
489
490	public boolean setLastseen(long timestamp) {
491		if (timestamp > this.mLastseen) {
492			this.mLastseen = timestamp;
493			return true;
494		} else {
495			return false;
496		}
497	}
498
499	public long getLastseen() {
500		return this.mLastseen;
501	}
502
503	public void setLastResource(String resource) {
504		this.mLastPresence = resource;
505	}
506
507	public String getLastResource() {
508		return this.mLastPresence;
509	}
510
511	public String getServerName() {
512		return serverName;
513	}
514
515	public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
516		setOption(getOption(phoneContact.getClass()));
517		setSystemAccount(phoneContact.getLookupUri());
518		boolean changed = setSystemName(phoneContact.getDisplayName());
519		changed |= setPhotoUri(phoneContact.getPhotoUri());
520		return changed;
521	}
522
523	public synchronized boolean unsetPhoneContact(Class<?extends AbstractPhoneContact> clazz) {
524		resetOption(getOption(clazz));
525		boolean changed = false;
526		if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
527			setSystemAccount(null);
528			changed |= setPhotoUri(null);
529			changed |= setSystemName(null);
530		}
531		return changed;
532	}
533
534	public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
535		if (clazz == JabberIdContact.class) {
536			return Options.SYNCED_VIA_ADDRESSBOOK;
537		} else {
538			return Options.SYNCED_VIA_OTHER;
539		}
540	}
541
542	@Override
543	public int getAvatarBackgroundColor() {
544		return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
545	}
546
547	public final class Options {
548		public static final int TO = 0;
549		public static final int FROM = 1;
550		public static final int ASKING = 2;
551		public static final int PREEMPTIVE_GRANT = 3;
552		public static final int IN_ROSTER = 4;
553		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
554		public static final int DIRTY_PUSH = 6;
555		public static final int DIRTY_DELETE = 7;
556		private static final int SYNCED_VIA_ADDRESSBOOK = 8;
557		public static final int SYNCED_VIA_OTHER = 9;
558	}
559}