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