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;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.R;
 24import eu.siacs.conversations.android.AbstractPhoneContact;
 25import eu.siacs.conversations.android.JabberIdContact;
 26import eu.siacs.conversations.services.QuickConversationsService;
 27import eu.siacs.conversations.utils.JidHelper;
 28import eu.siacs.conversations.utils.UIHelper;
 29import eu.siacs.conversations.xml.Element;
 30import eu.siacs.conversations.xmpp.pep.Avatar;
 31import eu.siacs.conversations.xmpp.Jid;
 32
 33public class Contact implements ListItem, Blockable {
 34    public static final String TABLENAME = "contacts";
 35
 36    public static final String SYSTEMNAME = "systemname";
 37    public static final String SERVERNAME = "servername";
 38    public static final String PRESENCE_NAME = "presence_name";
 39    public static final String JID = "jid";
 40    public static final String OPTIONS = "options";
 41    public static final String SYSTEMACCOUNT = "systemaccount";
 42    public static final String PHOTOURI = "photouri";
 43    public static final String KEYS = "pgpkey";
 44    public static final String ACCOUNT = "accountUuid";
 45    public static final String AVATAR = "avatar";
 46    public static final String LAST_PRESENCE = "last_presence";
 47    public static final String LAST_TIME = "last_time";
 48    public static final String GROUPS = "groups";
 49    private String accountUuid;
 50    private String systemName;
 51    private String serverName;
 52    private String presenceName;
 53    private String commonName;
 54    protected Jid jid;
 55    private int subscription = 0;
 56    private Uri systemAccount;
 57    private String photoUri;
 58    private final JSONObject keys;
 59    private JSONArray groups = new JSONArray();
 60    private final Presences presences = new Presences();
 61    protected Account account;
 62    protected Avatar avatar;
 63
 64    private boolean mActive = false;
 65    private long mLastseen = 0;
 66    private String mLastPresence = null;
 67
 68    public Contact(final String account, final String systemName, final String serverName, final String presenceName,
 69                   final Jid jid, final int subscription, final String photoUri,
 70                   final Uri systemAccount, final String keys, final String avatar, final long lastseen,
 71                   final String presence, final String groups) {
 72        this.accountUuid = account;
 73        this.systemName = systemName;
 74        this.serverName = serverName;
 75        this.presenceName = presenceName;
 76        this.jid = jid;
 77        this.subscription = subscription;
 78        this.photoUri = photoUri;
 79        this.systemAccount = systemAccount;
 80        JSONObject tmpJsonObject;
 81        try {
 82            tmpJsonObject = (keys == null ? new JSONObject("") : new JSONObject(keys));
 83        } catch (JSONException e) {
 84            tmpJsonObject = new JSONObject();
 85        }
 86        this.keys = tmpJsonObject;
 87        if (avatar != null) {
 88            this.avatar = new Avatar();
 89            this.avatar.sha1sum = avatar;
 90            this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
 91        }
 92        try {
 93            this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
 94        } catch (JSONException e) {
 95            this.groups = new JSONArray();
 96        }
 97        this.mLastseen = lastseen;
 98        this.mLastPresence = presence;
 99    }
100
101    public Contact(final Jid jid) {
102        this.jid = jid;
103        this.keys = new JSONObject();
104    }
105
106    public static Contact fromCursor(final Cursor cursor) {
107        final Jid jid;
108        try {
109            jid = Jid.of(cursor.getString(cursor.getColumnIndex(JID)));
110        } catch (final IllegalArgumentException e) {
111            // TODO: Borked DB... handle this somehow?
112            return null;
113        }
114        Uri systemAccount;
115        try {
116            systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)));
117        } catch (Exception e) {
118            systemAccount = null;
119        }
120        return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
121                cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
122                cursor.getString(cursor.getColumnIndex(SERVERNAME)),
123                cursor.getString(cursor.getColumnIndex(PRESENCE_NAME)),
124                jid,
125                cursor.getInt(cursor.getColumnIndex(OPTIONS)),
126                cursor.getString(cursor.getColumnIndex(PHOTOURI)),
127                systemAccount,
128                cursor.getString(cursor.getColumnIndex(KEYS)),
129                cursor.getString(cursor.getColumnIndex(AVATAR)),
130                cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
131                cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
132                cursor.getString(cursor.getColumnIndex(GROUPS)));
133    }
134
135    public String getDisplayName() {
136        if (isSelf()) {
137            final String displayName = account.getDisplayName();
138            if (!Strings.isNullOrEmpty(displayName)) {
139                return displayName;
140            }
141        }
142        if (Config.X509_VERIFICATION && !TextUtils.isEmpty(this.commonName)) {
143            return this.commonName;
144        } else if (!TextUtils.isEmpty(this.systemName)) {
145            return this.systemName;
146        } else if (!TextUtils.isEmpty(this.serverName)) {
147            return this.serverName;
148        } else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && JidHelper.isQuicksyDomain(jid.getDomain())) || mutualPresenceSubscription())) {
149            return this.presenceName;
150        } else if (jid.getLocal() != null) {
151            return JidHelper.localPartOrFallback(jid);
152        } else {
153            return jid.getDomain().toEscapedString();
154        }
155    }
156
157    public String getPublicDisplayName() {
158        if (!TextUtils.isEmpty(this.presenceName)) {
159            return this.presenceName;
160        } else if (jid.getLocal() != null) {
161            return JidHelper.localPartOrFallback(jid);
162        } else {
163            return jid.getDomain().toEscapedString();
164        }
165    }
166
167    public String getProfilePhoto() {
168        return this.photoUri;
169    }
170
171    public Jid getJid() {
172        return jid;
173    }
174
175    @Override
176    public List<Tag> getTags(Context context) {
177        final ArrayList<Tag> tags = new ArrayList<>();
178        for (final String group : getGroups(true)) {
179            tags.add(new Tag(group, UIHelper.getColorForName(group)));
180        }
181        Presence.Status status = getShownStatus();
182        if (status != Presence.Status.OFFLINE) {
183            tags.add(UIHelper.getTagForStatus(context, status));
184        }
185        if (isBlocked()) {
186            tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
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            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_ADDRESSBOOK) && !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_ADDRESSBOOK;
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 final class Options {
577        public static final int TO = 0;
578        public static final int FROM = 1;
579        public static final int ASKING = 2;
580        public static final int PREEMPTIVE_GRANT = 3;
581        public static final int IN_ROSTER = 4;
582        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
583        public static final int DIRTY_PUSH = 6;
584        public static final int DIRTY_DELETE = 7;
585        private static final int SYNCED_VIA_ADDRESSBOOK = 8;
586        public static final int SYNCED_VIA_OTHER = 9;
587    }
588}