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