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