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    public List<Tag> getGroupTags() {
193        final ArrayList<Tag> tags = new ArrayList<>();
194        for (final String group : getGroups(true)) {
195            tags.add(new Tag(group, UIHelper.getColorForName(group)));
196        }
197        return tags;
198    }
199
200    @Override
201    public List<Tag> getTags(Context context) {
202        final ArrayList<Tag> tags = new ArrayList<>();
203        tags.addAll(getGroupTags());
204        for (final String tag : getSystemTags(true)) {
205            tags.add(new Tag(tag, UIHelper.getColorForName(tag)));
206        }
207        Presence.Status status = getShownStatus();
208        if (status != Presence.Status.OFFLINE) {
209            tags.add(UIHelper.getTagForStatus(context, status));
210        }
211        if (isBlocked()) {
212            tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
213        }
214        if (!showInRoster() && getSystemAccount() != null) {
215            tags.add(new Tag("Android", UIHelper.getColorForName("Android")));
216        }
217        return tags;
218    }
219
220    public boolean match(Context context, String needle) {
221        if (TextUtils.isEmpty(needle)) {
222            return true;
223        }
224        needle = needle.toLowerCase(Locale.US).trim();
225        String[] parts = needle.split("[,\\s]+");
226        if (parts.length > 1) {
227            for (String part : parts) {
228                if (!match(context, part)) {
229                    return false;
230                }
231            }
232            return true;
233        } else {
234            return jid.toString().contains(parts[0]) ||
235                    getDisplayName().toLowerCase(Locale.US).contains(parts[0]) ||
236                    matchInTag(context, parts[0]);
237        }
238    }
239
240    private boolean matchInTag(Context context, String needle) {
241        needle = needle.toLowerCase(Locale.US);
242        for (Tag tag : getTags(context)) {
243            if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
244                return true;
245            }
246        }
247        return false;
248    }
249
250    public ContentValues getContentValues() {
251        synchronized (this.keys) {
252            final ContentValues values = new ContentValues();
253            values.put(ACCOUNT, accountUuid);
254            values.put(SYSTEMNAME, systemName);
255            values.put(SERVERNAME, serverName);
256            values.put(PRESENCE_NAME, presenceName);
257            values.put(JID, jid.toString());
258            values.put(OPTIONS, subscription);
259            values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
260            values.put(PHOTOURI, photoUri);
261            values.put(KEYS, keys.toString());
262            values.put(AVATAR, avatar == null ? null : avatar.getFilename());
263            values.put(LAST_PRESENCE, mLastPresence);
264            values.put(LAST_TIME, mLastseen);
265            values.put(GROUPS, groups.toString());
266            values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString());
267            return values;
268        }
269    }
270
271    public Account getAccount() {
272        return this.account;
273    }
274
275    public void setAccount(Account account) {
276        this.account = account;
277        this.accountUuid = account.getUuid();
278    }
279
280    public Presences getPresences() {
281        return this.presences;
282    }
283
284    public void updatePresence(final String resource, final Presence presence) {
285        this.presences.updatePresence(resource, presence);
286    }
287
288    public void removePresence(final String resource) {
289        this.presences.removePresence(resource);
290    }
291
292    public void clearPresences() {
293        this.presences.clearPresences();
294        this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
295    }
296
297    public Presence.Status getShownStatus() {
298        return this.presences.getShownStatus();
299    }
300
301    public Jid resourceWhichSupport(final String namespace) {
302        final String resource = getPresences().firstWhichSupport(namespace);
303        if (resource == null) return null;
304
305        return resource.equals("") ? getJid() : getJid().withResource(resource);
306    }
307
308    public boolean setPhotoUri(String uri) {
309        if (uri != null && !uri.equals(this.photoUri)) {
310            this.photoUri = uri;
311            return true;
312        } else if (this.photoUri != null && uri == null) {
313            this.photoUri = null;
314            return true;
315        } else {
316            return false;
317        }
318    }
319
320    public void setServerName(String serverName) {
321        this.serverName = serverName;
322    }
323
324    public boolean setSystemName(String systemName) {
325        final String old = getDisplayName();
326        this.systemName = systemName;
327        return !old.equals(getDisplayName());
328    }
329
330    public boolean setSystemTags(Collection<String> systemTags) {
331        final JSONArray old = this.systemTags;
332        this.systemTags = new JSONArray();
333        for(String tag : systemTags) {
334            this.systemTags.put(tag);
335        }
336        return !old.equals(this.systemTags);
337    }
338
339    public boolean setPresenceName(String presenceName) {
340        final String old = getDisplayName();
341        this.presenceName = presenceName;
342        return !old.equals(getDisplayName());
343    }
344
345    public Uri getSystemAccount() {
346        return systemAccount;
347    }
348
349    public void setSystemAccount(Uri lookupUri) {
350        this.systemAccount = lookupUri;
351    }
352
353    public void setGroups(List<String> groups) {
354        this.groups = new JSONArray(groups);
355    }
356
357    private Collection<String> getGroups(final boolean unique) {
358        final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
359        for (int i = 0; i < this.groups.length(); ++i) {
360            try {
361                groups.add(this.groups.getString(i));
362            } catch (final JSONException ignored) {
363            }
364        }
365        return groups;
366    }
367
368    private Collection<String> getSystemTags(final boolean unique) {
369        final Collection<String> tags = unique ? new HashSet<>() : new ArrayList<>();
370        for (int i = 0; i < this.systemTags.length(); ++i) {
371            try {
372                tags.add(this.systemTags.getString(i));
373            } catch (final JSONException ignored) {
374            }
375        }
376        return tags;
377    }
378
379    public long getPgpKeyId() {
380        synchronized (this.keys) {
381            if (this.keys.has("pgp_keyid")) {
382                try {
383                    return this.keys.getLong("pgp_keyid");
384                } catch (JSONException e) {
385                    return 0;
386                }
387            } else {
388                return 0;
389            }
390        }
391    }
392
393    public boolean setPgpKeyId(long keyId) {
394        final long previousKeyId = getPgpKeyId();
395        synchronized (this.keys) {
396            try {
397                this.keys.put("pgp_keyid", keyId);
398                return previousKeyId != keyId;
399            } catch (final JSONException ignored) {
400            }
401        }
402        return false;
403    }
404
405    public void setOption(int option) {
406        this.subscription |= 1 << option;
407    }
408
409    public void resetOption(int option) {
410        this.subscription &= ~(1 << option);
411    }
412
413    public boolean getOption(int option) {
414        return ((this.subscription & (1 << option)) != 0);
415    }
416
417    public boolean canInferPresence() {
418        return showInContactList() || isSelf();
419    }
420
421    public boolean showInRoster() {
422        return (this.getOption(Contact.Options.IN_ROSTER) && (!this
423                .getOption(Contact.Options.DIRTY_DELETE)))
424                || (this.getOption(Contact.Options.DIRTY_PUSH));
425    }
426
427    public boolean showInContactList() {
428        return showInRoster()
429                || getOption(Options.SYNCED_VIA_OTHER)
430                || (QuickConversationsService.isQuicksy() && systemAccount != null);
431    }
432
433    public void parseSubscriptionFromElement(Element item) {
434        String ask = item.getAttribute("ask");
435        String subscription = item.getAttribute("subscription");
436
437        if (subscription == null) {
438            this.resetOption(Options.FROM);
439            this.resetOption(Options.TO);
440        } else {
441            switch (subscription) {
442                case "to":
443                    this.resetOption(Options.FROM);
444                    this.setOption(Options.TO);
445                    break;
446                case "from":
447                    this.resetOption(Options.TO);
448                    this.setOption(Options.FROM);
449                    this.resetOption(Options.PREEMPTIVE_GRANT);
450                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
451                    break;
452                case "both":
453                    this.setOption(Options.TO);
454                    this.setOption(Options.FROM);
455                    this.resetOption(Options.PREEMPTIVE_GRANT);
456                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
457                    break;
458                case "none":
459                    this.resetOption(Options.FROM);
460                    this.resetOption(Options.TO);
461                    break;
462            }
463        }
464
465        // do NOT override asking if pending push request
466        if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
467            if ((ask != null) && (ask.equals("subscribe"))) {
468                this.setOption(Contact.Options.ASKING);
469            } else {
470                this.resetOption(Contact.Options.ASKING);
471            }
472        }
473    }
474
475    public void parseGroupsFromElement(Element item) {
476        this.groups = new JSONArray();
477        for (Element element : item.getChildren()) {
478            if (element.getName().equals("group") && element.getContent() != null) {
479                this.groups.put(element.getContent());
480            }
481        }
482    }
483
484    public Element asElement() {
485        final Element item = new Element("item");
486        item.setAttribute("jid", this.jid);
487        if (this.serverName != null) {
488            item.setAttribute("name", this.serverName);
489        }
490        for (String group : getGroups(false)) {
491            item.addChild("group").setContent(group);
492        }
493        return item;
494    }
495
496    @Override
497    public int compareTo(@NonNull final ListItem another) {
498        return this.getDisplayName().compareToIgnoreCase(
499                another.getDisplayName());
500    }
501
502    public String getServer() {
503        return getJid().getDomain().toEscapedString();
504    }
505
506    public void setAvatar(Avatar avatar) {
507        setAvatar(avatar, false);
508    }
509
510    public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
511        if (this.avatar != null && this.avatar.equals(avatar)) {
512            return;
513        }
514        if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
515            return;
516        }
517        this.avatar = avatar;
518    }
519
520    public String getAvatarFilename() {
521        return avatar == null ? null : avatar.getFilename();
522    }
523
524    public Avatar getAvatar() {
525        return avatar;
526    }
527
528    public boolean mutualPresenceSubscription() {
529        return getOption(Options.FROM) && getOption(Options.TO);
530    }
531
532    @Override
533    public boolean isBlocked() {
534        return getAccount().isBlocked(this);
535    }
536
537    @Override
538    public boolean isDomainBlocked() {
539        return getAccount().isBlocked(this.getJid().getDomain());
540    }
541
542    @Override
543    public Jid getBlockedJid() {
544        if (isDomainBlocked()) {
545            return getJid().getDomain();
546        } else {
547            return getJid();
548        }
549    }
550
551    public boolean isSelf() {
552        return account.getJid().asBareJid().equals(jid.asBareJid());
553    }
554
555    boolean isOwnServer() {
556        return account.getJid().getDomain().equals(jid.asBareJid());
557    }
558
559    public void setCommonName(String cn) {
560        this.commonName = cn;
561    }
562
563    public void flagActive() {
564        this.mActive = true;
565    }
566
567    public void flagInactive() {
568        this.mActive = false;
569    }
570
571    public boolean isActive() {
572        return this.mActive;
573    }
574
575    public boolean setLastseen(long timestamp) {
576        if (timestamp > this.mLastseen) {
577            this.mLastseen = timestamp;
578            return true;
579        } else {
580            return false;
581        }
582    }
583
584    public long getLastseen() {
585        return this.mLastseen;
586    }
587
588    public void setLastResource(String resource) {
589        this.mLastPresence = resource;
590    }
591
592    public String getLastResource() {
593        return this.mLastPresence;
594    }
595
596    public String getServerName() {
597        return serverName;
598    }
599
600    public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
601        setOption(getOption(phoneContact.getClass()));
602        setSystemAccount(phoneContact.getLookupUri());
603        boolean changed = setSystemName(phoneContact.getDisplayName());
604        changed |= setPhotoUri(phoneContact.getPhotoUri());
605        return changed;
606    }
607
608    public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
609        resetOption(getOption(clazz));
610        boolean changed = false;
611        if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
612            setSystemAccount(null);
613            changed |= setPhotoUri(null);
614            changed |= setSystemName(null);
615        }
616        return changed;
617    }
618
619    protected String phoneAccountLabel() {
620        return account.getJid().asBareJid().toString() +
621            "/" + getJid().asBareJid().toString();
622    }
623
624    public PhoneAccountHandle phoneAccountHandle() {
625        ComponentName componentName = new ComponentName(
626            "com.cheogram.android",
627            "com.cheogram.android.ConnectionService"
628        );
629        return new PhoneAccountHandle(componentName, phoneAccountLabel());
630    }
631
632    // This Contact is a gateway to use for voice calls, register it with OS
633    public void registerAsPhoneAccount(XmppConnectionService ctx) {
634        if (Build.VERSION.SDK_INT < 23) return;
635
636        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
637
638        PhoneAccount phoneAccount = PhoneAccount.builder(
639            phoneAccountHandle(),
640            account.getJid().asBareJid().toString()
641        ).setAddress(
642            Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
643        ).setIcon(
644            Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
645        ).setHighlightColor(
646            0x7401CF
647        ).setShortDescription(
648            getJid().asBareJid().toString()
649        ).setCapabilities(
650            PhoneAccount.CAPABILITY_CALL_PROVIDER
651        ).build();
652
653        telecomManager.registerPhoneAccount(phoneAccount);
654    }
655
656    // Unregister any associated PSTN gateway integration
657    public void unregisterAsPhoneAccount(Context ctx) {
658        if (Build.VERSION.SDK_INT < 23) return;
659
660        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
661        telecomManager.unregisterPhoneAccount(phoneAccountHandle());
662    }
663
664    public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
665        if (clazz == JabberIdContact.class) {
666            return Options.SYNCED_VIA_ADDRESSBOOK;
667        } else {
668            return Options.SYNCED_VIA_OTHER;
669        }
670    }
671
672    @Override
673    public int getAvatarBackgroundColor() {
674        return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
675    }
676
677    @Override
678    public String getAvatarName() {
679        return getDisplayName();
680    }
681
682    public boolean hasAvatarOrPresenceName() {
683        return (avatar != null && avatar.getFilename() != null) || presenceName != null;
684    }
685
686    public boolean refreshRtpCapability() {
687        final RtpCapability.Capability previous = this.rtpCapability;
688        this.rtpCapability = RtpCapability.check(this, false);
689        return !Objects.equals(previous, this.rtpCapability);
690    }
691
692    public RtpCapability.Capability getRtpCapability() {
693        return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
694    }
695
696    public static final class Options {
697        public static final int TO = 0;
698        public static final int FROM = 1;
699        public static final int ASKING = 2;
700        public static final int PREEMPTIVE_GRANT = 3;
701        public static final int IN_ROSTER = 4;
702        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
703        public static final int DIRTY_PUSH = 6;
704        public static final int DIRTY_DELETE = 7;
705        private static final int SYNCED_VIA_ADDRESSBOOK = 8;
706        public static final int SYNCED_VIA_OTHER = 9;
707    }
708}