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 Jid resourceWhichSupport(final String namespace) {
296        final String resource = getPresences().firstWhichSupport(namespace);
297        if (resource == null) return null;
298
299        return resource.equals("") ? getJid() : getJid().withResource(resource);
300    }
301
302    public boolean setPhotoUri(String uri) {
303        if (uri != null && !uri.equals(this.photoUri)) {
304            this.photoUri = uri;
305            return true;
306        } else if (this.photoUri != null && uri == null) {
307            this.photoUri = null;
308            return true;
309        } else {
310            return false;
311        }
312    }
313
314    public void setServerName(String serverName) {
315        this.serverName = serverName;
316    }
317
318    public boolean setSystemName(String systemName) {
319        final String old = getDisplayName();
320        this.systemName = systemName;
321        return !old.equals(getDisplayName());
322    }
323
324    public boolean setSystemTags(Collection<String> systemTags) {
325        final JSONArray old = this.systemTags;
326        this.systemTags = new JSONArray();
327        for(String tag : systemTags) {
328            this.systemTags.put(tag);
329        }
330        return !old.equals(this.systemTags);
331    }
332
333    public boolean setPresenceName(String presenceName) {
334        final String old = getDisplayName();
335        this.presenceName = presenceName;
336        return !old.equals(getDisplayName());
337    }
338
339    public Uri getSystemAccount() {
340        return systemAccount;
341    }
342
343    public void setSystemAccount(Uri lookupUri) {
344        this.systemAccount = lookupUri;
345    }
346
347    private Collection<String> getGroups(final boolean unique) {
348        final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
349        for (int i = 0; i < this.groups.length(); ++i) {
350            try {
351                groups.add(this.groups.getString(i));
352            } catch (final JSONException ignored) {
353            }
354        }
355        return groups;
356    }
357
358    private Collection<String> getSystemTags(final boolean unique) {
359        final Collection<String> tags = unique ? new HashSet<>() : new ArrayList<>();
360        for (int i = 0; i < this.systemTags.length(); ++i) {
361            try {
362                tags.add(this.systemTags.getString(i));
363            } catch (final JSONException ignored) {
364            }
365        }
366        return tags;
367    }
368
369    public long getPgpKeyId() {
370        synchronized (this.keys) {
371            if (this.keys.has("pgp_keyid")) {
372                try {
373                    return this.keys.getLong("pgp_keyid");
374                } catch (JSONException e) {
375                    return 0;
376                }
377            } else {
378                return 0;
379            }
380        }
381    }
382
383    public boolean setPgpKeyId(long keyId) {
384        final long previousKeyId = getPgpKeyId();
385        synchronized (this.keys) {
386            try {
387                this.keys.put("pgp_keyid", keyId);
388                return previousKeyId != keyId;
389            } catch (final JSONException ignored) {
390            }
391        }
392        return false;
393    }
394
395    public void setOption(int option) {
396        this.subscription |= 1 << option;
397    }
398
399    public void resetOption(int option) {
400        this.subscription &= ~(1 << option);
401    }
402
403    public boolean getOption(int option) {
404        return ((this.subscription & (1 << option)) != 0);
405    }
406
407    public boolean showInRoster() {
408        return (this.getOption(Contact.Options.IN_ROSTER) && (!this
409                .getOption(Contact.Options.DIRTY_DELETE)))
410                || (this.getOption(Contact.Options.DIRTY_PUSH));
411    }
412
413    public boolean showInContactList() {
414        return showInRoster()
415                || getOption(Options.SYNCED_VIA_OTHER)
416                || (QuickConversationsService.isQuicksy() && systemAccount != null);
417    }
418
419    public void parseSubscriptionFromElement(Element item) {
420        String ask = item.getAttribute("ask");
421        String subscription = item.getAttribute("subscription");
422
423        if (subscription == null) {
424            this.resetOption(Options.FROM);
425            this.resetOption(Options.TO);
426        } else {
427            switch (subscription) {
428                case "to":
429                    this.resetOption(Options.FROM);
430                    this.setOption(Options.TO);
431                    break;
432                case "from":
433                    this.resetOption(Options.TO);
434                    this.setOption(Options.FROM);
435                    this.resetOption(Options.PREEMPTIVE_GRANT);
436                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
437                    break;
438                case "both":
439                    this.setOption(Options.TO);
440                    this.setOption(Options.FROM);
441                    this.resetOption(Options.PREEMPTIVE_GRANT);
442                    this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
443                    break;
444                case "none":
445                    this.resetOption(Options.FROM);
446                    this.resetOption(Options.TO);
447                    break;
448            }
449        }
450
451        // do NOT override asking if pending push request
452        if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
453            if ((ask != null) && (ask.equals("subscribe"))) {
454                this.setOption(Contact.Options.ASKING);
455            } else {
456                this.resetOption(Contact.Options.ASKING);
457            }
458        }
459    }
460
461    public void parseGroupsFromElement(Element item) {
462        this.groups = new JSONArray();
463        for (Element element : item.getChildren()) {
464            if (element.getName().equals("group") && element.getContent() != null) {
465                this.groups.put(element.getContent());
466            }
467        }
468    }
469
470    public Element asElement() {
471        final Element item = new Element("item");
472        item.setAttribute("jid", this.jid);
473        if (this.serverName != null) {
474            item.setAttribute("name", this.serverName);
475        }
476        for (String group : getGroups(false)) {
477            item.addChild("group").setContent(group);
478        }
479        return item;
480    }
481
482    @Override
483    public int compareTo(@NonNull final ListItem another) {
484        return this.getDisplayName().compareToIgnoreCase(
485                another.getDisplayName());
486    }
487
488    public String getServer() {
489        return getJid().getDomain().toEscapedString();
490    }
491
492    public void setAvatar(Avatar avatar) {
493        setAvatar(avatar, false);
494    }
495
496    public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
497        if (this.avatar != null && this.avatar.equals(avatar)) {
498            return;
499        }
500        if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
501            return;
502        }
503        this.avatar = avatar;
504    }
505
506    public String getAvatarFilename() {
507        return avatar == null ? null : avatar.getFilename();
508    }
509
510    public Avatar getAvatar() {
511        return avatar;
512    }
513
514    public boolean mutualPresenceSubscription() {
515        return getOption(Options.FROM) && getOption(Options.TO);
516    }
517
518    @Override
519    public boolean isBlocked() {
520        return getAccount().isBlocked(this);
521    }
522
523    @Override
524    public boolean isDomainBlocked() {
525        return getAccount().isBlocked(this.getJid().getDomain());
526    }
527
528    @Override
529    public Jid getBlockedJid() {
530        if (isDomainBlocked()) {
531            return getJid().getDomain();
532        } else {
533            return getJid();
534        }
535    }
536
537    public boolean isSelf() {
538        return account.getJid().asBareJid().equals(jid.asBareJid());
539    }
540
541    boolean isOwnServer() {
542        return account.getJid().getDomain().equals(jid.asBareJid());
543    }
544
545    public void setCommonName(String cn) {
546        this.commonName = cn;
547    }
548
549    public void flagActive() {
550        this.mActive = true;
551    }
552
553    public void flagInactive() {
554        this.mActive = false;
555    }
556
557    public boolean isActive() {
558        return this.mActive;
559    }
560
561    public boolean setLastseen(long timestamp) {
562        if (timestamp > this.mLastseen) {
563            this.mLastseen = timestamp;
564            return true;
565        } else {
566            return false;
567        }
568    }
569
570    public long getLastseen() {
571        return this.mLastseen;
572    }
573
574    public void setLastResource(String resource) {
575        this.mLastPresence = resource;
576    }
577
578    public String getLastResource() {
579        return this.mLastPresence;
580    }
581
582    public String getServerName() {
583        return serverName;
584    }
585
586    public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
587        setOption(getOption(phoneContact.getClass()));
588        setSystemAccount(phoneContact.getLookupUri());
589        boolean changed = setSystemName(phoneContact.getDisplayName());
590        changed |= setPhotoUri(phoneContact.getPhotoUri());
591        return changed;
592    }
593
594    public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
595        resetOption(getOption(clazz));
596        boolean changed = false;
597        if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
598            setSystemAccount(null);
599            changed |= setPhotoUri(null);
600            changed |= setSystemName(null);
601        }
602        return changed;
603    }
604
605    protected String phoneAccountLabel() {
606        return account.getJid().asBareJid().toString() +
607            "/" + getJid().asBareJid().toString();
608    }
609
610    public PhoneAccountHandle phoneAccountHandle() {
611        ComponentName componentName = new ComponentName(
612            "com.cheogram.android",
613            "com.cheogram.android.ConnectionService"
614        );
615        return new PhoneAccountHandle(componentName, phoneAccountLabel());
616    }
617
618    // This Contact is a gateway to use for voice calls, register it with OS
619    public void registerAsPhoneAccount(XmppConnectionService ctx) {
620        if (Build.VERSION.SDK_INT < 23) return;
621
622        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
623
624        PhoneAccount phoneAccount = PhoneAccount.builder(
625            phoneAccountHandle(),
626            account.getJid().asBareJid().toString()
627        ).setAddress(
628            Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
629        ).setIcon(
630            Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
631        ).setHighlightColor(
632            0x7401CF
633        ).setShortDescription(
634            getJid().asBareJid().toString()
635        ).setCapabilities(
636            PhoneAccount.CAPABILITY_CALL_PROVIDER
637        ).build();
638
639        telecomManager.registerPhoneAccount(phoneAccount);
640    }
641
642    // Unregister any associated PSTN gateway integration
643    public void unregisterAsPhoneAccount(Context ctx) {
644        if (Build.VERSION.SDK_INT < 23) return;
645
646        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
647        telecomManager.unregisterPhoneAccount(phoneAccountHandle());
648    }
649
650    public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
651        if (clazz == JabberIdContact.class) {
652            return Options.SYNCED_VIA_ADDRESSBOOK;
653        } else {
654            return Options.SYNCED_VIA_OTHER;
655        }
656    }
657
658    @Override
659    public int getAvatarBackgroundColor() {
660        return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
661    }
662
663    @Override
664    public String getAvatarName() {
665        return getDisplayName();
666    }
667
668    public boolean hasAvatarOrPresenceName() {
669        return (avatar != null && avatar.getFilename() != null) || presenceName != null;
670    }
671
672    public boolean refreshRtpCapability() {
673        final RtpCapability.Capability previous = this.rtpCapability;
674        this.rtpCapability = RtpCapability.check(this, false);
675        return !Objects.equals(previous, this.rtpCapability);
676    }
677
678    public RtpCapability.Capability getRtpCapability() {
679        return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
680    }
681
682    public static final class Options {
683        public static final int TO = 0;
684        public static final int FROM = 1;
685        public static final int ASKING = 2;
686        public static final int PREEMPTIVE_GRANT = 3;
687        public static final int IN_ROSTER = 4;
688        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
689        public static final int DIRTY_PUSH = 6;
690        public static final int DIRTY_DELETE = 7;
691        private static final int SYNCED_VIA_ADDRESSBOOK = 8;
692        public static final int SYNCED_VIA_OTHER = 9;
693    }
694}