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        return this.getDisplayName().compareToIgnoreCase(
500                another.getDisplayName());
501    }
502
503    public String getServer() {
504        return getJid().getDomain().toEscapedString();
505    }
506
507    public void setAvatar(Avatar avatar) {
508        setAvatar(avatar, false);
509    }
510
511    public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
512        if (this.avatar != null && this.avatar.equals(avatar)) {
513            return;
514        }
515        if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
516            return;
517        }
518        this.avatar = avatar;
519    }
520
521    public String getAvatarFilename() {
522        return avatar == null ? null : avatar.getFilename();
523    }
524
525    public Avatar getAvatar() {
526        return avatar;
527    }
528
529    public boolean mutualPresenceSubscription() {
530        return getOption(Options.FROM) && getOption(Options.TO);
531    }
532
533    @Override
534    public boolean isBlocked() {
535        return getAccount().isBlocked(this);
536    }
537
538    @Override
539    public boolean isDomainBlocked() {
540        return getAccount().isBlocked(this.getJid().getDomain());
541    }
542
543    @Override
544    public Jid getBlockedJid() {
545        if (isDomainBlocked()) {
546            return getJid().getDomain();
547        } else {
548            return getJid();
549        }
550    }
551
552    public boolean isSelf() {
553        return account.getJid().asBareJid().equals(jid.asBareJid());
554    }
555
556    boolean isOwnServer() {
557        return account.getJid().getDomain().equals(jid.asBareJid());
558    }
559
560    public void setCommonName(String cn) {
561        this.commonName = cn;
562    }
563
564    public void flagActive() {
565        this.mActive = true;
566    }
567
568    public void flagInactive() {
569        this.mActive = false;
570    }
571
572    public boolean isActive() {
573        return this.mActive;
574    }
575
576    public boolean setLastseen(long timestamp) {
577        if (timestamp > this.mLastseen) {
578            this.mLastseen = timestamp;
579            return true;
580        } else {
581            return false;
582        }
583    }
584
585    public long getLastseen() {
586        return this.mLastseen;
587    }
588
589    public void setLastResource(String resource) {
590        this.mLastPresence = resource;
591    }
592
593    public String getLastResource() {
594        return this.mLastPresence;
595    }
596
597    public String getServerName() {
598        return serverName;
599    }
600
601    public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
602        setOption(getOption(phoneContact.getClass()));
603        setSystemAccount(phoneContact.getLookupUri());
604        boolean changed = setSystemName(phoneContact.getDisplayName());
605        changed |= setPhotoUri(phoneContact.getPhotoUri());
606        return changed;
607    }
608
609    public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
610        resetOption(getOption(clazz));
611        boolean changed = false;
612        if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
613            setSystemAccount(null);
614            changed |= setPhotoUri(null);
615            changed |= setSystemName(null);
616        }
617        return changed;
618    }
619
620    protected String phoneAccountLabel() {
621        return account.getJid().asBareJid().toString() +
622            "/" + getJid().asBareJid().toString();
623    }
624
625    public PhoneAccountHandle phoneAccountHandle() {
626        ComponentName componentName = new ComponentName(
627            "com.cheogram.android",
628            "com.cheogram.android.ConnectionService"
629        );
630        return new PhoneAccountHandle(componentName, phoneAccountLabel());
631    }
632
633    // This Contact is a gateway to use for voice calls, register it with OS
634    public void registerAsPhoneAccount(XmppConnectionService ctx) {
635        if (Build.VERSION.SDK_INT < 23) return;
636        if (Build.VERSION.SDK_INT >= 33) {
637            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) return;
638        } else {
639            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
640        }
641
642        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
643
644        PhoneAccount phoneAccount = PhoneAccount.builder(
645            phoneAccountHandle(),
646            account.getJid().asBareJid().toString()
647        ).setAddress(
648            Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
649        ).setIcon(
650            Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
651        ).setHighlightColor(
652            0x7401CF
653        ).setShortDescription(
654            getJid().asBareJid().toString()
655        ).setCapabilities(
656            PhoneAccount.CAPABILITY_CALL_PROVIDER
657        ).build();
658
659        telecomManager.registerPhoneAccount(phoneAccount);
660    }
661
662    // Unregister any associated PSTN gateway integration
663    public void unregisterAsPhoneAccount(Context ctx) {
664        if (Build.VERSION.SDK_INT < 23) return;
665        if (Build.VERSION.SDK_INT >= 33) {
666            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) return;
667        } else {
668            if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
669        }
670
671        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
672        telecomManager.unregisterPhoneAccount(phoneAccountHandle());
673    }
674
675    public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
676        if (clazz == JabberIdContact.class) {
677            return Options.SYNCED_VIA_ADDRESSBOOK;
678        } else {
679            return Options.SYNCED_VIA_OTHER;
680        }
681    }
682
683    @Override
684    public int getAvatarBackgroundColor() {
685        return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
686    }
687
688    @Override
689    public String getAvatarName() {
690        return getDisplayName();
691    }
692
693    public boolean hasAvatarOrPresenceName() {
694        return (avatar != null && avatar.getFilename() != null) || presenceName != null;
695    }
696
697    public boolean refreshRtpCapability() {
698        final RtpCapability.Capability previous = this.rtpCapability;
699        this.rtpCapability = RtpCapability.check(this, false);
700        return !Objects.equals(previous, this.rtpCapability);
701    }
702
703    public RtpCapability.Capability getRtpCapability() {
704        return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
705    }
706
707    public static final class Options {
708        public static final int TO = 0;
709        public static final int FROM = 1;
710        public static final int ASKING = 2;
711        public static final int PREEMPTIVE_GRANT = 3;
712        public static final int IN_ROSTER = 4;
713        public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
714        public static final int DIRTY_PUSH = 6;
715        public static final int DIRTY_DELETE = 7;
716        private static final int SYNCED_VIA_ADDRESSBOOK = 8;
717        public static final int SYNCED_VIA_OTHER = 9;
718    }
719}