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