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