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