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