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