Contact.java

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