MucOptions.java

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