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(final Context context) {
184        final ArrayList<Tag> tags = new ArrayList<>();
185        for (final String group : getGroups(true)) {
186            tags.add(new Tag(group));
187        }
188        return tags;
189    }
190
191    public boolean match(Context context, String needle) {
192        if (TextUtils.isEmpty(needle)) {
193            return true;
194        }
195        needle = needle.toLowerCase(Locale.US).trim();
196        String[] parts = needle.split("\\s+");
197        if (parts.length > 1) {
198            for (String part : parts) {
199                if (!match(context, part)) {
200                    return false;
201                }
202            }
203            return true;
204        } else {
205            return jid.toString().contains(needle) ||
206                    getDisplayName().toLowerCase(Locale.US).contains(needle) ||
207                    matchInTag(context, needle);
208        }
209    }
210
211    private boolean matchInTag(Context context, String needle) {
212        needle = needle.toLowerCase(Locale.US);
213        for (Tag tag : getTags(context)) {
214            if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
215                return true;
216            }
217        }
218        return false;
219    }
220
221    public ContentValues getContentValues() {
222        synchronized (this.keys) {
223            final ContentValues values = new ContentValues();
224            values.put(ACCOUNT, accountUuid);
225            values.put(SYSTEMNAME, systemName);
226            values.put(SERVERNAME, serverName);
227            values.put(PRESENCE_NAME, presenceName);
228            values.put(JID, jid.toString());
229            values.put(OPTIONS, subscription);
230            values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
231            values.put(PHOTOURI, photoUri);
232            values.put(KEYS, keys.toString());
233            values.put(AVATAR, avatar == null ? null : avatar.getFilename());
234            values.put(LAST_PRESENCE, mLastPresence);
235            values.put(LAST_TIME, mLastseen);
236            values.put(GROUPS, groups.toString());
237            values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString());
238            return values;
239        }
240    }
241
242    public Account getAccount() {
243        return this.account;
244    }
245
246    public void setAccount(Account account) {
247        this.account = account;
248        this.accountUuid = account.getUuid();
249    }
250
251    public Presences getPresences() {
252        return this.presences;
253    }
254
255    public void updatePresence(final String resource, final Presence presence) {
256        this.presences.updatePresence(resource, presence);
257    }
258
259    public void removePresence(final String resource) {
260        this.presences.removePresence(resource);
261    }
262
263    public void clearPresences() {
264        this.presences.clearPresences();
265        this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
266    }
267
268    public Presence.Status getShownStatus() {
269        return this.presences.getShownStatus();
270    }
271
272    public boolean setPhotoUri(String uri) {
273        if (uri != null && !uri.equals(this.photoUri)) {
274            this.photoUri = uri;
275            return true;
276        } else if (this.photoUri != null && uri == null) {
277            this.photoUri = null;
278            return true;
279        } else {
280            return false;
281        }
282    }
283
284    public void setServerName(String serverName) {
285        this.serverName = serverName;
286    }
287
288    public boolean setSystemName(String systemName) {
289        final String old = getDisplayName();
290        this.systemName = systemName;
291        return !old.equals(getDisplayName());
292    }
293
294    public boolean setPresenceName(String presenceName) {
295        final String old = getDisplayName();
296        this.presenceName = presenceName;
297        return !old.equals(getDisplayName());
298    }
299
300    public Uri getSystemAccount() {
301        return systemAccount;
302    }
303
304    public void setSystemAccount(Uri lookupUri) {
305        this.systemAccount = lookupUri;
306    }
307
308    private Collection<String> getGroups(final boolean unique) {
309        final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
310        for (int i = 0; i < this.groups.length(); ++i) {
311            try {
312                groups.add(this.groups.getString(i));
313            } catch (final JSONException ignored) {
314            }
315        }
316        return groups;
317    }
318
319    public long getPgpKeyId() {
320        synchronized (this.keys) {
321            if (this.keys.has("pgp_keyid")) {
322                try {
323                    return this.keys.getLong("pgp_keyid");
324                } catch (JSONException e) {
325                    return 0;
326                }
327            } else {
328                return 0;
329            }
330        }
331    }
332
333    public boolean setPgpKeyId(long keyId) {
334        final long previousKeyId = getPgpKeyId();
335        synchronized (this.keys) {
336            try {
337                this.keys.put("pgp_keyid", keyId);
338                return previousKeyId != keyId;
339            } catch (final JSONException ignored) {
340            }
341        }
342        return false;
343    }
344
345    public void setOption(int option) {
346        this.subscription |= 1 << option;
347    }
348
349    public void resetOption(int option) {
350        this.subscription &= ~(1 << option);
351    }
352
353    public boolean getOption(int option) {
354        return ((this.subscription & (1 << option)) != 0);
355    }
356
357    public boolean showInRoster() {
358        return (this.getOption(Contact.Options.IN_ROSTER) && (!this
359                .getOption(Contact.Options.DIRTY_DELETE)))
360                || (this.getOption(Contact.Options.DIRTY_PUSH));
361    }
362
363    public boolean showInContactList() {
364        return showInRoster()
365                || getOption(Options.SYNCED_VIA_OTHER)
366                || (QuickConversationsService.isQuicksy() && systemAccount != null);
367    }
368
369    public void parseSubscriptionFromElement(Element item) {
370        String ask = item.getAttribute("ask");
371        String subscription = item.getAttribute("subscription");
372
373        if (subscription == null) {
374            this.resetOption(Options.FROM);
375            this.resetOption(Options.TO);
376        } else {
377            switch (subscription) {
378                case "to":
379                    this.resetOption(Options.FROM);
380                    this.setOption(Options.TO);
381                    break;
382                case "from":
383                    this.resetOption(Options.TO);
384                    this.setOption(Options.FROM);
385                    this.resetOption(Options.PREEMPTIVE_GRANT);
386                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
387                    break;
388                case "both":
389                    this.setOption(Options.TO);
390                    this.setOption(Options.FROM);
391                    this.resetOption(Options.PREEMPTIVE_GRANT);
392                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
393                    break;
394                case "none":
395                    this.resetOption(Options.FROM);
396                    this.resetOption(Options.TO);
397                    break;
398            }
399        }
400
401        // do NOT override asking if pending push request
402        if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
403            if ((ask != null) && (ask.equals("subscribe"))) {
404                this.setOption(Contact.Options.ASKING);
405            } else {
406                this.resetOption(Contact.Options.ASKING);
407            }
408        }
409    }
410
411    public void parseGroupsFromElement(Element item) {
412        this.groups = new JSONArray();
413        for (Element element : item.getChildren()) {
414            if (element.getName().equals("group") && element.getContent() != null) {
415                this.groups.put(element.getContent());
416            }
417        }
418    }
419
420    public Element asElement() {
421        final Element item = new Element("item");
422        item.setAttribute("jid", this.jid);
423        if (this.serverName != null) {
424            item.setAttribute("name", this.serverName);
425        }
426        for (String group : getGroups(false)) {
427            item.addChild("group").setContent(group);
428        }
429        return item;
430    }
431
432    @Override
433    public int compareTo(@NonNull final ListItem another) {
434        return this.getDisplayName().compareToIgnoreCase(
435                another.getDisplayName());
436    }
437
438    public String getServer() {
439        return getJid().getDomain().toEscapedString();
440    }
441
442    public void setAvatar(Avatar avatar) {
443        setAvatar(avatar, false);
444    }
445
446    public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
447        if (this.avatar != null && this.avatar.equals(avatar)) {
448            return;
449        }
450        if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
451            return;
452        }
453        this.avatar = avatar;
454    }
455
456    public String getAvatarFilename() {
457        return avatar == null ? null : avatar.getFilename();
458    }
459
460    public Avatar getAvatar() {
461        return avatar;
462    }
463
464    public boolean mutualPresenceSubscription() {
465        return getOption(Options.FROM) && getOption(Options.TO);
466    }
467
468    @Override
469    public boolean isBlocked() {
470        return getAccount().isBlocked(this);
471    }
472
473    @Override
474    public boolean isDomainBlocked() {
475        return getAccount().isBlocked(this.getJid().getDomain());
476    }
477
478    @Override
479    public Jid getBlockedJid() {
480        if (isDomainBlocked()) {
481            return getJid().getDomain();
482        } else {
483            return getJid();
484        }
485    }
486
487    public boolean isSelf() {
488        return account.getJid().asBareJid().equals(jid.asBareJid());
489    }
490
491    boolean isOwnServer() {
492        return account.getJid().getDomain().equals(jid.asBareJid());
493    }
494
495    public void setCommonName(String cn) {
496        this.commonName = cn;
497    }
498
499    public void flagActive() {
500        this.mActive = true;
501    }
502
503    public void flagInactive() {
504        this.mActive = false;
505    }
506
507    public boolean isActive() {
508        return this.mActive;
509    }
510
511    public boolean setLastseen(long timestamp) {
512        if (timestamp > this.mLastseen) {
513            this.mLastseen = timestamp;
514            return true;
515        } else {
516            return false;
517        }
518    }
519
520    public long getLastseen() {
521        return this.mLastseen;
522    }
523
524    public void setLastResource(String resource) {
525        this.mLastPresence = resource;
526    }
527
528    public String getLastResource() {
529        return this.mLastPresence;
530    }
531
532    public String getServerName() {
533        return serverName;
534    }
535
536    public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
537        setOption(getOption(phoneContact.getClass()));
538        setSystemAccount(phoneContact.getLookupUri());
539        boolean changed = setSystemName(phoneContact.getDisplayName());
540        changed |= setPhotoUri(phoneContact.getPhotoUri());
541        return changed;
542    }
543
544    public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
545        resetOption(getOption(clazz));
546        boolean changed = false;
547        if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
548            setSystemAccount(null);
549            changed |= setPhotoUri(null);
550            changed |= setSystemName(null);
551        }
552        return changed;
553    }
554
555    public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
556        if (clazz == JabberIdContact.class) {
557            return Options.SYNCED_VIA_ADDRESSBOOK;
558        } else {
559            return Options.SYNCED_VIA_OTHER;
560        }
561    }
562
563    @Override
564    public int getAvatarBackgroundColor() {
565        return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
566    }
567
568    @Override
569    public String getAvatarName() {
570        return getDisplayName();
571    }
572
573    public boolean hasAvatarOrPresenceName() {
574        return (avatar != null && avatar.getFilename() != null) || presenceName != null;
575    }
576
577    public boolean refreshRtpCapability() {
578        final RtpCapability.Capability previous = this.rtpCapability;
579        this.rtpCapability = RtpCapability.check(this, false);
580        return !Objects.equals(previous, this.rtpCapability);
581    }
582
583    public RtpCapability.Capability getRtpCapability() {
584        return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
585    }
586
587    public static final class Options {
588        public static final int TO = 0;
589        public static final int FROM = 1;
590        public static final int ASKING = 2;
591        public static final int PREEMPTIVE_GRANT = 3;
592        public static final int IN_ROSTER = 4;
593        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
594        public static final int DIRTY_PUSH = 6;
595        public static final int DIRTY_DELETE = 7;
596        private static final int SYNCED_VIA_ADDRESSBOOK = 8;
597        public static final int SYNCED_VIA_OTHER = 9;
598    }
599}