Contact.java

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