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