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