MucOptions.java

  1package eu.siacs.conversations.entities;
  2
  3import android.text.TextUtils;
  4
  5import androidx.annotation.NonNull;
  6import androidx.annotation.Nullable;
  7
  8import java.util.ArrayList;
  9import java.util.Collections;
 10import java.util.HashSet;
 11import java.util.List;
 12import java.util.Locale;
 13import java.util.Set;
 14
 15import eu.siacs.conversations.Config;
 16import eu.siacs.conversations.R;
 17import eu.siacs.conversations.services.AvatarService;
 18import eu.siacs.conversations.services.MessageArchiveService;
 19import eu.siacs.conversations.utils.JidHelper;
 20import eu.siacs.conversations.utils.UIHelper;
 21import eu.siacs.conversations.xmpp.Jid;
 22import eu.siacs.conversations.xmpp.chatstate.ChatState;
 23import eu.siacs.conversations.xmpp.forms.Data;
 24import eu.siacs.conversations.xmpp.forms.Field;
 25import eu.siacs.conversations.xmpp.pep.Avatar;
 26
 27public class MucOptions {
 28
 29    public static final String STATUS_CODE_SELF_PRESENCE = "110";
 30    public static final String STATUS_CODE_ROOM_CREATED = "201";
 31    public static final String STATUS_CODE_BANNED = "301";
 32    public static final String STATUS_CODE_CHANGED_NICK = "303";
 33    public static final String STATUS_CODE_KICKED = "307";
 34    public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
 35    public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
 36    public static final String STATUS_CODE_SHUTDOWN = "332";
 37    private final Set<User> users = new HashSet<>();
 38    private final Conversation conversation;
 39    public OnRenameListener onRenameListener = null;
 40    private boolean mAutoPushConfiguration = true;
 41    private final Account account;
 42    private ServiceDiscoveryResult serviceDiscoveryResult;
 43    private boolean isOnline = false;
 44    private Error error = Error.NONE;
 45    private User self;
 46    private String password = null;
 47
 48    private boolean tookProposedNickFromBookmark = false;
 49
 50    public MucOptions(Conversation conversation) {
 51        this.account = conversation.getAccount();
 52        this.conversation = conversation;
 53        this.self = new User(this, createJoinJid(getProposedNick()));
 54        this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
 55        this.self.role = Role.of(conversation.getAttribute("role"));
 56    }
 57
 58    public Account getAccount() {
 59        return this.conversation.getAccount();
 60    }
 61
 62    public boolean setSelf(User user) {
 63        this.self = user;
 64        final boolean roleChanged = this.conversation.setAttribute("role", user.role.toString());
 65        final boolean affiliationChanged = this.conversation.setAttribute("affiliation", user.affiliation.toString());
 66        return roleChanged || affiliationChanged;
 67    }
 68
 69    public void changeAffiliation(Jid jid, Affiliation affiliation) {
 70        User user = findUserByRealJid(jid);
 71        synchronized (users) {
 72            if (user != null && user.getRole() == Role.NONE) {
 73                users.remove(user);
 74                if (affiliation.ranks(Affiliation.MEMBER)) {
 75                    user.affiliation = affiliation;
 76                    users.add(user);
 77                }
 78            }
 79        }
 80    }
 81
 82    public void flagNoAutoPushConfiguration() {
 83        mAutoPushConfiguration = false;
 84    }
 85
 86    public boolean autoPushConfiguration() {
 87        return mAutoPushConfiguration;
 88    }
 89
 90    public boolean isSelf(Jid counterpart) {
 91        return counterpart.equals(self.getFullJid());
 92    }
 93
 94    public void resetChatState() {
 95        synchronized (users) {
 96            for (User user : users) {
 97                user.chatState = Config.DEFAULT_CHAT_STATE;
 98            }
 99        }
100    }
101
102    public boolean isTookProposedNickFromBookmark() {
103        return tookProposedNickFromBookmark;
104    }
105
106    void notifyOfBookmarkNick(final String nick) {
107        final String normalized = normalize(account.getJid(),nick);
108        if (normalized != null && normalized.equals(getSelf().getFullJid().getResource())) {
109            this.tookProposedNickFromBookmark = true;
110        }
111    }
112
113    public boolean mamSupport() {
114        return MessageArchiveService.Version.has(getFeatures());
115    }
116
117    public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
118        this.serviceDiscoveryResult = serviceDiscoveryResult;
119        String name;
120        Field roomConfigName = getRoomInfoForm().getFieldByName("muc#roomconfig_roomname");
121        if (roomConfigName != null) {
122            name = roomConfigName.getValue();
123        } else {
124            List<ServiceDiscoveryResult.Identity> identities = serviceDiscoveryResult.getIdentities();
125            String identityName = identities.size() > 0 ? identities.get(0).getName() : null;
126            final Jid jid = conversation.getJid();
127            if (identityName != null && !identityName.equals(jid == null ? null : jid.getEscapedLocal())) {
128                name = identityName;
129            } else {
130                name = null;
131            }
132        }
133        boolean changed = conversation.setAttribute("muc_name", name);
134        changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
135        changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
136        changed |= conversation.setAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
137        return changed;
138    }
139
140    private Data getRoomInfoForm() {
141        final List<Data> forms = serviceDiscoveryResult == null ? Collections.emptyList() : serviceDiscoveryResult.forms;
142        return forms.size() == 0 ? new Data() : forms.get(0);
143    }
144
145    public String getAvatar() {
146        return account.getRoster().getContact(conversation.getJid()).getAvatarFilename();
147    }
148
149    public boolean hasFeature(String feature) {
150        return this.serviceDiscoveryResult != null && this.serviceDiscoveryResult.features.contains(feature);
151    }
152
153    public boolean hasVCards() {
154        return hasFeature("vcard-temp");
155    }
156
157    public boolean canInvite() {
158        return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites();
159    }
160
161    public boolean allowInvites() {
162        final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
163        return field != null && "1".equals(field.getValue());
164    }
165
166    public boolean canChangeSubject() {
167        return self.getRole().ranks(Role.MODERATOR) || participantsCanChangeSubject();
168    }
169
170    public boolean participantsCanChangeSubject() {
171        final Field field = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
172        return field != null && "1".equals(field.getValue());
173    }
174
175    public boolean allowPm() {
176        final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
177        if (field == null) {
178            return true; //fall back if field does not exists
179        }
180        if ("anyone".equals(field.getValue())) {
181            return true;
182        } else if ("participants".equals(field.getValue())) {
183            return self.getRole().ranks(Role.PARTICIPANT);
184        } else if ("moderators".equals(field.getValue())) {
185            return self.getRole().ranks(Role.MODERATOR);
186        } else {
187            return false;
188        }
189    }
190
191    public boolean participating() {
192        return self.getRole().ranks(Role.PARTICIPANT) || !moderated();
193    }
194
195    public boolean membersOnly() {
196        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
197    }
198
199    public List<String> getFeatures() {
200        return this.serviceDiscoveryResult != null ? this.serviceDiscoveryResult.features : Collections.emptyList();
201    }
202
203    public boolean nonanonymous() {
204        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false);
205    }
206
207    public boolean isPrivateAndNonAnonymous() {
208        return membersOnly() && nonanonymous();
209    }
210
211    public boolean moderated() {
212        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
213    }
214
215    public boolean stableId() {
216        return getFeatures().contains("http://jabber.org/protocol/muc#stable_id");
217    }
218
219    public User deleteUser(Jid jid) {
220        User user = findUserByFullJid(jid);
221        if (user != null) {
222            synchronized (users) {
223                users.remove(user);
224                boolean realJidInMuc = false;
225                for (User u : users) {
226                    if (user.realJid != null && user.realJid.equals(u.realJid)) {
227                        realJidInMuc = true;
228                        break;
229                    }
230                }
231                boolean self = user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
232                if (membersOnly()
233                        && nonanonymous()
234                        && user.affiliation.ranks(Affiliation.MEMBER)
235                        && user.realJid != null
236                        && !realJidInMuc
237                        && !self) {
238                    user.role = Role.NONE;
239                    user.avatar = null;
240                    user.fullJid = null;
241                    users.add(user);
242                }
243            }
244        }
245        return user;
246    }
247
248    //returns true if real jid was new;
249    public boolean updateUser(User user) {
250        User old;
251        boolean realJidFound = false;
252        if (user.fullJid == null && user.realJid != null) {
253            old = findUserByRealJid(user.realJid);
254            realJidFound = old != null;
255            if (old != null) {
256                if (old.fullJid != null) {
257                    return false; //don't add. user already exists
258                } else {
259                    synchronized (users) {
260                        users.remove(old);
261                    }
262                }
263            }
264        } else if (user.realJid != null) {
265            old = findUserByRealJid(user.realJid);
266            realJidFound = old != null;
267            synchronized (users) {
268                if (old != null && (old.fullJid == null || old.role == Role.NONE)) {
269                    users.remove(old);
270                }
271            }
272        }
273        old = findUserByFullJid(user.getFullJid());
274
275        synchronized (this.users) {
276            if (old != null) {
277                users.remove(old);
278            }
279            boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
280            if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
281                    && user.getAffiliation().outranks(Affiliation.OUTCAST)
282                    && !fullJidIsSelf) {
283                this.users.add(user);
284                return !realJidFound && user.realJid != null;
285            }
286        }
287        return false;
288    }
289
290    public User findUserByFullJid(Jid jid) {
291        if (jid == null) {
292            return null;
293        }
294        synchronized (users) {
295            for (User user : users) {
296                if (jid.equals(user.getFullJid())) {
297                    return user;
298                }
299            }
300        }
301        return null;
302    }
303
304    public User findUserByRealJid(Jid jid) {
305        if (jid == null) {
306            return null;
307        }
308        synchronized (users) {
309            for (User user : users) {
310                if (jid.equals(user.realJid)) {
311                    return user;
312                }
313            }
314        }
315        return null;
316    }
317
318    public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) {
319        User user = findUserByRealJid(jid);
320        if (user == null) {
321            user = new User(this, fullJid);
322            user.setRealJid(jid);
323        }
324        return user;
325    }
326
327    public User findUser(ReadByMarker readByMarker) {
328        if (readByMarker.getRealJid() != null) {
329            return findOrCreateUserByRealJid(readByMarker.getRealJid().asBareJid(), readByMarker.getFullJid());
330        } else if (readByMarker.getFullJid() != null) {
331            return findUserByFullJid(readByMarker.getFullJid());
332        } else {
333            return null;
334        }
335    }
336
337    public boolean isContactInRoom(Contact contact) {
338        return contact != null && findUserByRealJid(contact.getJid().asBareJid()) != null;
339    }
340
341    public boolean isUserInRoom(Jid jid) {
342        return findUserByFullJid(jid) != null;
343    }
344
345    public boolean setOnline() {
346        boolean before = this.isOnline;
347        this.isOnline = true;
348        return !before;
349    }
350
351    public ArrayList<User> getUsers() {
352        return getUsers(true);
353    }
354
355    public ArrayList<User> getUsers(boolean includeOffline) {
356        synchronized (users) {
357            ArrayList<User> users = new ArrayList<>();
358            for (User user : this.users) {
359                if (!user.isDomain() && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) {
360                    users.add(user);
361                }
362            }
363            return users;
364        }
365    }
366
367    public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
368        synchronized (users) {
369            ArrayList<User> list = new ArrayList<>();
370            for (User user : users) {
371                if (user.chatState == state) {
372                    list.add(user);
373                    if (list.size() >= max) {
374                        break;
375                    }
376                }
377            }
378            return list;
379        }
380    }
381
382    public List<User> getUsers(int max) {
383        ArrayList<User> subset = new ArrayList<>();
384        HashSet<Jid> jids = new HashSet<>();
385        jids.add(account.getJid().asBareJid());
386        synchronized (users) {
387            for (User user : users) {
388                if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
389                    subset.add(user);
390                }
391                if (subset.size() >= max) {
392                    break;
393                }
394            }
395        }
396        return subset;
397    }
398
399    public static List<User> sub(List<User> users, int max) {
400        ArrayList<User> subset = new ArrayList<>();
401        HashSet<Jid> jids = new HashSet<>();
402        for (User user : users) {
403            jids.add(user.getAccount().getJid().asBareJid());
404            if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
405                subset.add(user);
406            }
407            if (subset.size() >= max) {
408                break;
409            }
410        }
411        return subset;
412    }
413
414    public int getUserCount() {
415        synchronized (users) {
416            return users.size();
417        }
418    }
419
420    public String getProposedNick() {
421        final Bookmark bookmark = this.conversation.getBookmark();
422        final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
423        if (bookmarkedNick != null) {
424            this.tookProposedNickFromBookmark = true;
425            return bookmarkedNick;
426        } else if (!conversation.getJid().isBareJid()) {
427            return conversation.getJid().getResource();
428        } else {
429            return defaultNick(account);
430        }
431    }
432
433    public static String defaultNick(final Account account) {
434        final String displayName = normalize(account.getJid(), account.getDisplayName());
435        if (displayName == null) {
436            return JidHelper.localPartOrFallback(account.getJid());
437        } else {
438            return displayName;
439        }
440    }
441
442    private static String normalize(Jid account, String nick) {
443        if (account == null || TextUtils.isEmpty(nick)) {
444            return null;
445        }
446        try {
447            return account.withResource(nick).getResource();
448        } catch (IllegalArgumentException e) {
449            return null;
450        }
451
452    }
453
454    public String getActualNick() {
455        if (this.self.getName() != null) {
456            return this.self.getName();
457        } else {
458            return this.getProposedNick();
459        }
460    }
461
462    public boolean online() {
463        return this.isOnline;
464    }
465
466    public Error getError() {
467        return this.error;
468    }
469
470    public void setError(Error error) {
471        this.isOnline = isOnline && error == Error.NONE;
472        this.error = error;
473    }
474
475    public void setOnRenameListener(OnRenameListener listener) {
476        this.onRenameListener = listener;
477    }
478
479    public void setOffline() {
480        synchronized (users) {
481            this.users.clear();
482        }
483        this.error = Error.NO_RESPONSE;
484        this.isOnline = false;
485    }
486
487    public User getSelf() {
488        return self;
489    }
490
491    public boolean setSubject(String subject) {
492        return this.conversation.setAttribute("subject", subject);
493    }
494
495    public String getSubject() {
496        return this.conversation.getAttribute("subject");
497    }
498
499    public String getName() {
500        return this.conversation.getAttribute("muc_name");
501    }
502
503    private List<User> getFallbackUsersFromCryptoTargets() {
504        List<User> users = new ArrayList<>();
505        for (Jid jid : conversation.getAcceptedCryptoTargets()) {
506            User user = new User(this, null);
507            user.setRealJid(jid);
508            users.add(user);
509        }
510        return users;
511    }
512
513    public List<User> getUsersRelevantForNameAndAvatar() {
514        final List<User> users;
515        if (isOnline) {
516            users = getUsers(5);
517        } else {
518            users = getFallbackUsersFromCryptoTargets();
519        }
520        return users;
521    }
522
523    String createNameFromParticipants() {
524        List<User> users = getUsersRelevantForNameAndAvatar();
525        if (users.size() >= 2) {
526            StringBuilder builder = new StringBuilder();
527            for (User user : users) {
528                if (builder.length() != 0) {
529                    builder.append(", ");
530                }
531                String name = UIHelper.getDisplayName(user);
532                if (name != null) {
533                    builder.append(name.split("\\s+")[0]);
534                }
535            }
536            return builder.toString();
537        } else {
538            return null;
539        }
540    }
541
542    public long[] getPgpKeyIds() {
543        List<Long> ids = new ArrayList<>();
544        for (User user : this.users) {
545            if (user.getPgpKeyId() != 0) {
546                ids.add(user.getPgpKeyId());
547            }
548        }
549        ids.add(account.getPgpId());
550        long[] primitiveLongArray = new long[ids.size()];
551        for (int i = 0; i < ids.size(); ++i) {
552            primitiveLongArray[i] = ids.get(i);
553        }
554        return primitiveLongArray;
555    }
556
557    public boolean pgpKeysInUse() {
558        synchronized (users) {
559            for (User user : users) {
560                if (user.getPgpKeyId() != 0) {
561                    return true;
562                }
563            }
564        }
565        return false;
566    }
567
568    public boolean everybodyHasKeys() {
569        synchronized (users) {
570            for (User user : users) {
571                if (user.getPgpKeyId() == 0) {
572                    return false;
573                }
574            }
575        }
576        return true;
577    }
578
579    public Jid createJoinJid(String nick) {
580        try {
581            return conversation.getJid().withResource(nick);
582        } catch (final IllegalArgumentException e) {
583            return null;
584        }
585    }
586
587    public Jid getTrueCounterpart(Jid jid) {
588        if (jid.equals(getSelf().getFullJid())) {
589            return account.getJid().asBareJid();
590        }
591        User user = findUserByFullJid(jid);
592        return user == null ? null : user.realJid;
593    }
594
595    public String getPassword() {
596        this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
597        if (this.password == null && conversation.getBookmark() != null
598                && conversation.getBookmark().getPassword() != null) {
599            return conversation.getBookmark().getPassword();
600        } else {
601            return this.password;
602        }
603    }
604
605    public void setPassword(String password) {
606        if (conversation.getBookmark() != null) {
607            conversation.getBookmark().setPassword(password);
608        } else {
609            this.password = password;
610        }
611        conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
612    }
613
614    public Conversation getConversation() {
615        return this.conversation;
616    }
617
618    public List<Jid> getMembers(final boolean includeDomains) {
619        ArrayList<Jid> members = new ArrayList<>();
620        synchronized (users) {
621            for (User user : users) {
622                if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && !user.realJid.asBareJid().equals(conversation.account.getJid().asBareJid()) && (!user.isDomain() || includeDomains)) {
623                    members.add(user.realJid);
624                }
625            }
626        }
627        return members;
628    }
629
630    public enum Affiliation {
631        OWNER(4, R.string.owner),
632        ADMIN(3, R.string.admin),
633        MEMBER(2, R.string.member),
634        OUTCAST(0, R.string.outcast),
635        NONE(1, R.string.no_affiliation);
636
637        private final int resId;
638        private final int rank;
639
640        Affiliation(int rank, int resId) {
641            this.resId = resId;
642            this.rank = rank;
643        }
644
645        public static Affiliation of(@Nullable String value) {
646            if (value == null) {
647                return NONE;
648            }
649            try {
650                return Affiliation.valueOf(value.toUpperCase(Locale.US));
651            } catch (IllegalArgumentException e) {
652                return NONE;
653            }
654        }
655
656        public int getResId() {
657            return resId;
658        }
659
660        @Override
661        public String toString() {
662            return name().toLowerCase(Locale.US);
663        }
664
665        public boolean outranks(Affiliation affiliation) {
666            return rank > affiliation.rank;
667        }
668
669        public boolean ranks(Affiliation affiliation) {
670            return rank >= affiliation.rank;
671        }
672    }
673
674    public enum Role {
675        MODERATOR(R.string.moderator, 3),
676        VISITOR(R.string.visitor, 1),
677        PARTICIPANT(R.string.participant, 2),
678        NONE(R.string.no_role, 0);
679
680        private final int resId;
681        private final int rank;
682
683        Role(int resId, int rank) {
684            this.resId = resId;
685            this.rank = rank;
686        }
687
688        public static Role of(@Nullable String value) {
689            if (value == null) {
690                return NONE;
691            }
692            try {
693                return Role.valueOf(value.toUpperCase(Locale.US));
694            } catch (IllegalArgumentException e) {
695                return NONE;
696            }
697        }
698
699        public int getResId() {
700            return resId;
701        }
702
703        @Override
704        public String toString() {
705            return name().toLowerCase(Locale.US);
706        }
707
708        public boolean ranks(Role role) {
709            return rank >= role.rank;
710        }
711    }
712
713    public enum Error {
714        NO_RESPONSE,
715        SERVER_NOT_FOUND,
716        REMOTE_SERVER_TIMEOUT,
717        NONE,
718        NICK_IN_USE,
719        PASSWORD_REQUIRED,
720        BANNED,
721        MEMBERS_ONLY,
722        RESOURCE_CONSTRAINT,
723        KICKED,
724        SHUTDOWN,
725        DESTROYED,
726        INVALID_NICK,
727        UNKNOWN,
728        NON_ANONYMOUS
729    }
730
731    private interface OnEventListener {
732        void onSuccess();
733
734        void onFailure();
735    }
736
737    public interface OnRenameListener extends OnEventListener {
738
739    }
740
741    public static class User implements Comparable<User>, AvatarService.Avatarable {
742        private Role role = Role.NONE;
743        private Affiliation affiliation = Affiliation.NONE;
744        private Jid realJid;
745        private Jid fullJid;
746        private long pgpKeyId = 0;
747        private Avatar avatar;
748        private final MucOptions options;
749        private ChatState chatState = Config.DEFAULT_CHAT_STATE;
750
751        public User(MucOptions options, Jid fullJid) {
752            this.options = options;
753            this.fullJid = fullJid;
754        }
755
756        public String getName() {
757            return fullJid == null ? null : fullJid.getResource();
758        }
759
760        public Role getRole() {
761            return this.role;
762        }
763
764        public void setRole(String role) {
765            this.role = Role.of(role);
766        }
767
768        public Affiliation getAffiliation() {
769            return this.affiliation;
770        }
771
772        public void setAffiliation(String affiliation) {
773            this.affiliation = Affiliation.of(affiliation);
774        }
775
776        public long getPgpKeyId() {
777            if (this.pgpKeyId != 0) {
778                return this.pgpKeyId;
779            } else if (realJid != null) {
780                return getAccount().getRoster().getContact(realJid).getPgpKeyId();
781            } else {
782                return 0;
783            }
784        }
785
786        public void setPgpKeyId(long id) {
787            this.pgpKeyId = id;
788        }
789
790        public Contact getContact() {
791            if (fullJid != null) {
792                return getAccount().getRoster().getContactFromContactList(realJid);
793            } else if (realJid != null) {
794                return getAccount().getRoster().getContact(realJid);
795            } else {
796                return null;
797            }
798        }
799
800        public boolean setAvatar(Avatar avatar) {
801            if (this.avatar != null && this.avatar.equals(avatar)) {
802                return false;
803            } else {
804                this.avatar = avatar;
805                return true;
806            }
807        }
808
809        public String getAvatar() {
810            if (avatar != null) {
811                return avatar.getFilename();
812            }
813            Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
814            return avatar == null ? null : avatar.getFilename();
815        }
816
817        public Account getAccount() {
818            return options.getAccount();
819        }
820
821        public Conversation getConversation() {
822            return options.getConversation();
823        }
824
825        public Jid getFullJid() {
826            return fullJid;
827        }
828
829        @Override
830        public boolean equals(Object o) {
831            if (this == o) return true;
832            if (o == null || getClass() != o.getClass()) return false;
833
834            User user = (User) o;
835
836            if (role != user.role) return false;
837            if (affiliation != user.affiliation) return false;
838            if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
839                return false;
840            return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
841
842        }
843
844        public boolean isDomain() {
845            return realJid != null && realJid.getLocal() == null && role == Role.NONE;
846        }
847
848        @Override
849        public int hashCode() {
850            int result = role != null ? role.hashCode() : 0;
851            result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
852            result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
853            result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
854            return result;
855        }
856
857        @Override
858        public String toString() {
859            return "[fulljid:" + fullJid + ",realjid:" + realJid + ",affiliation" + affiliation.toString() + "]";
860        }
861
862        public boolean realJidMatchesAccount() {
863            return realJid != null && realJid.equals(options.account.getJid().asBareJid());
864        }
865
866        @Override
867        public int compareTo(@NonNull User another) {
868            if (another.getAffiliation().outranks(getAffiliation())) {
869                return 1;
870            } else if (getAffiliation().outranks(another.getAffiliation())) {
871                return -1;
872            } else {
873                return getComparableName().compareToIgnoreCase(another.getComparableName());
874            }
875        }
876
877        public String getComparableName() {
878            Contact contact = getContact();
879            if (contact != null) {
880                return contact.getDisplayName();
881            } else {
882                String name = getName();
883                return name == null ? "" : name;
884            }
885        }
886
887        public Jid getRealJid() {
888            return realJid;
889        }
890
891        public void setRealJid(Jid jid) {
892            this.realJid = jid != null ? jid.asBareJid() : null;
893        }
894
895        public boolean setChatState(ChatState chatState) {
896            if (this.chatState == chatState) {
897                return false;
898            }
899            this.chatState = chatState;
900            return true;
901        }
902
903        @Override
904        public int getAvatarBackgroundColor() {
905            final String seed = realJid != null ? realJid.asBareJid().toString() : null;
906            return UIHelper.getColorForName(seed == null ? getName() : seed);
907        }
908
909        @Override
910        public String getAvatarName() {
911            return getConversation().getName().toString();
912        }
913    }
914}