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