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        return setAvatar(avatar, false);
442    }
443
444    public boolean setAvatar(final Avatar avatar, final boolean previouslyOmittedPepFetch) {
445        if (this.avatar != null && this.avatar.equals(avatar)) {
446            return false;
447        }
448        if (!previouslyOmittedPepFetch
449                && this.avatar != null
450                && this.avatar.origin == Avatar.Origin.PEP
451                && avatar.origin == Avatar.Origin.VCARD) {
452            return false;
453        }
454        this.avatar = avatar;
455        return true;
456    }
457
458    public String getAvatarFilename() {
459        return avatar == null ? null : avatar.getFilename();
460    }
461
462    public Avatar getAvatar() {
463        return avatar;
464    }
465
466    public boolean mutualPresenceSubscription() {
467        return getOption(Options.FROM) && getOption(Options.TO);
468    }
469
470    @Override
471    public boolean isBlocked() {
472        return getAccount().isBlocked(this);
473    }
474
475    @Override
476    public boolean isDomainBlocked() {
477        return getAccount().isBlocked(this.getJid().getDomain());
478    }
479
480    @Override
481    @NonNull
482    public Jid getBlockedJid() {
483        if (isDomainBlocked()) {
484            return getJid().getDomain();
485        } else {
486            return getJid();
487        }
488    }
489
490    public boolean isSelf() {
491        return account.getJid().asBareJid().equals(jid.asBareJid());
492    }
493
494    boolean isOwnServer() {
495        return account.getJid().getDomain().equals(jid.asBareJid());
496    }
497
498    public void setCommonName(String cn) {
499        this.commonName = cn;
500    }
501
502    public void flagActive() {
503        this.mActive = true;
504    }
505
506    public void flagInactive() {
507        this.mActive = false;
508    }
509
510    public boolean isActive() {
511        return this.mActive;
512    }
513
514    public boolean setLastseen(long timestamp) {
515        if (timestamp > this.mLastseen) {
516            this.mLastseen = timestamp;
517            return true;
518        } else {
519            return false;
520        }
521    }
522
523    public long getLastseen() {
524        return this.mLastseen;
525    }
526
527    public void setLastResource(String resource) {
528        this.mLastPresence = resource;
529    }
530
531    public String getLastResource() {
532        return this.mLastPresence;
533    }
534
535    public String getServerName() {
536        return serverName;
537    }
538
539    public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
540        setOption(getOption(phoneContact.getClass()));
541        setSystemAccount(phoneContact.getLookupUri());
542        boolean changed = setSystemName(phoneContact.getDisplayName());
543        changed |= setPhotoUri(phoneContact.getPhotoUri());
544        return changed;
545    }
546
547    public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
548        resetOption(getOption(clazz));
549        boolean changed = false;
550        if (!getOption(Options.SYNCED_VIA_ADDRESS_BOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
551            setSystemAccount(null);
552            changed |= setPhotoUri(null);
553            changed |= setSystemName(null);
554        }
555        return changed;
556    }
557
558    public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
559        if (clazz == JabberIdContact.class) {
560            return Options.SYNCED_VIA_ADDRESS_BOOK;
561        } else {
562            return Options.SYNCED_VIA_OTHER;
563        }
564    }
565
566    @Override
567    public int getAvatarBackgroundColor() {
568        return UIHelper.getColorForName(
569                jid != null ? jid.asBareJid().toString() : getDisplayName());
570    }
571
572    @Override
573    public String getAvatarName() {
574        return getDisplayName();
575    }
576
577    public boolean hasAvatarOrPresenceName() {
578        return (avatar != null && avatar.getFilename() != null) || presenceName != null;
579    }
580
581    public boolean refreshRtpCapability() {
582        final RtpCapability.Capability previous = this.rtpCapability;
583        this.rtpCapability = RtpCapability.check(this, false);
584        return !Objects.equals(previous, this.rtpCapability);
585    }
586
587    public RtpCapability.Capability getRtpCapability() {
588        return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
589    }
590
591    public static final class Options {
592        public static final int TO = 0;
593        public static final int FROM = 1;
594        public static final int ASKING = 2;
595        public static final int PREEMPTIVE_GRANT = 3;
596        public static final int IN_ROSTER = 4;
597        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
598        public static final int DIRTY_PUSH = 6;
599        public static final int DIRTY_DELETE = 7;
600        private static final int SYNCED_VIA_ADDRESS_BOOK = 8;
601        public static final int SYNCED_VIA_OTHER = 9;
602    }
603}