Contact.java

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