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