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