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